Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion cmd/app/options/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ package options
import (
"fmt"

"k8s.io/apiserver/pkg/server/options"

"github.com/spf13/pflag"

cliflag "k8s.io/component-base/cli/flag"
Expand All @@ -19,17 +21,27 @@ type OIDCAuthenticationOptions struct {
GroupsPrefix string
SigningAlgs []string
RequiredClaims map[string]string
ClientCertKey options.CertKey
}

func NewOIDCAuthenticationOptions(nfs *cliflag.NamedFlagSets) *OIDCAuthenticationOptions {
return new(OIDCAuthenticationOptions).AddFlags(nfs.FlagSet("OIDC"))
}

func (o *OIDCAuthenticationOptions) Validate() error {
if o != nil && (len(o.IssuerURL) > 0) != (len(o.ClientID) > 0) {
if o == nil {
return nil
}

if (len(o.IssuerURL) > 0) != (len(o.ClientID) > 0) {
return fmt.Errorf("oidc-issuer-url and oidc-client-id should be specified together")
}

if ((o.ClientCertKey.CertFile != "") && (o.ClientCertKey.KeyFile == "")) ||
(o.ClientCertKey.CertFile == "" && o.ClientCertKey.KeyFile != "") {
return fmt.Errorf("oidc-tls-client-cert-file and oidc-tls-client-cert-key must be specified together")
}

return nil
}

Expand Down Expand Up @@ -61,6 +73,14 @@ func (o *OIDCAuthenticationOptions) AddFlags(fs *pflag.FlagSet) *OIDCAuthenticat
"If provided, all groups will be prefixed with this value to prevent conflicts with "+
"other authentication strategies.")

fs.StringVar(&o.ClientCertKey.CertFile, "oidc-tls-client-cert-file", "", ""+
"The absolute path to a X.509 client certificate. If provided, HTTPS requests made to the OIDC issuer will "+
"use mTLS. Also requires --oidc-tls-client-key-file.")

fs.StringVar(&o.ClientCertKey.KeyFile, "oidc-tls-client-key-file", "", ""+
"The absolute path to a X.509 private key. If provided, HTTPS requests made to the OIDC issuer will use mTLS."+
"Also requires --oidc-tls-client-cert-file.")

fs.StringSliceVar(&o.SigningAlgs, "oidc-signing-algs", []string{"RS256"}, ""+
"Comma-separated list of allowed JOSE asymmetric signing algorithms. JWTs with a "+
"'alg' header value not in this list will be rejected. "+
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/jstemmer/go-junit-report v1.0.0 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v1.0.0 h1:8X1gzZpR+nVQLAht+L/foqOeX2l9DTZoaIPbEQHxsds=
github.com/jstemmer/go-junit-report v1.0.0/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
Expand Down
92 changes: 89 additions & 3 deletions pkg/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package proxy

import (
ctx "context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
Expand All @@ -11,6 +13,9 @@ import (
"net/url"
"time"

"k8s.io/apimachinery/pkg/util/net"
"k8s.io/apiserver/pkg/server/dynamiccertificates"

"k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/request/bearertoken"
Expand Down Expand Up @@ -60,6 +65,7 @@ type Proxy struct {
subjectAccessReviewer *subjectaccessreview.SubjectAccessReview
secureServingInfo *server.SecureServingInfo
auditor *audit.Audit
dynamicClientCert *DynamicCertificate

restConfig *rest.Config
clientTransport http.RoundTripper
Expand All @@ -82,13 +88,60 @@ func (caFromFile CAFromFile) CurrentCABundleContent() []byte {
return res
}

// DynamicCertificate wraps DynamicCertKeyPairContent so that we can attach a function to it that can be used by
// the TLS client config to load the client certificate dynamically
type DynamicCertificate struct {
*dynamiccertificates.DynamicCertKeyPairContent
}

// GetClientCertificate returns a client certificate based on the most recent certificate and key data loaded from the
// file system.
func (c *DynamicCertificate) GetClientCertificate(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) {
certBytes, keyBytes := c.CurrentCertKeyContent()
cert, err := tls.X509KeyPair(certBytes, keyBytes)
if err != nil {
return nil, fmt.Errorf("failed to load OIDC client certificate: %w", err)
}
return &cert, nil
}

// NewDynamicCertificate create a new instance of DynamicCertificate using the supplied certificate and key files
func NewDynamicCertificate(purpose, certFile, keyFile string) (*DynamicCertificate, error) {
content, err := dynamiccertificates.NewDynamicServingContentFromFiles(purpose, certFile, keyFile)
if err != nil {
return nil, fmt.Errorf("failed to create new dynamic serving content for '%s': %w", purpose, err)
}
return &DynamicCertificate{content}, nil
}

// setupClient creates an HTTP client using the dynamic content provided for the client certificate and the CA bundle.
// This function is based on how the setup would have been set up by the underlying OIDC code had we not passed in our
// own client.
func setupClient(dynamicClientCert *DynamicCertificate, caFromFile oidc.CAContentProvider) (*http.Client, error) {
var roots *x509.CertPool
if caFromFile != nil {
roots = x509.NewCertPool()
if !roots.AppendCertsFromPEM(caFromFile.CurrentCABundleContent()) {
return nil, fmt.Errorf("failed to append OIDC ca bundle to pool")
}
}

// Copied from http.DefaultTransport.
tr := net.SetTransportDefaults(&http.Transport{
TLSClientConfig: &tls.Config{RootCAs: roots, GetClientCertificate: dynamicClientCert.GetClientCertificate},
})

return &http.Client{Transport: tr, Timeout: 30 * time.Second}, nil
}

func New(restConfig *rest.Config,
oidcOptions *options.OIDCAuthenticationOptions,
auditOptions *options.AuditOptions,
tokenReviewer *tokenreview.TokenReview,
subjectAccessReviewer *subjectaccessreview.SubjectAccessReview,
ssinfo *server.SecureServingInfo,
config *Config) (*Proxy, error) {
var err error

// load the CA from the file listed in the options
caFromFile := CAFromFile{
Expand All @@ -115,13 +168,32 @@ func New(restConfig *rest.Config,
},
}

// generate tokenAuther from oidc config
tokenAuther, err := oidc.New(ctx.TODO(), oidc.Options{
tokenAutherOptions := oidc.Options{
CAContentProvider: caFromFile,
//RequiredClaims: oidcOptions.RequiredClaims,
SupportedSigningAlgs: oidcOptions.SigningAlgs,
JWTAuthenticator: jwtConfig,
})
}

var dyCert *DynamicCertificate
if oidcOptions.ClientCertKey.CertFile != "" {
// Use the client certificate and key to enable mTLS
dyCert, err = NewDynamicCertificate("oidc-client", oidcOptions.ClientCertKey.CertFile, oidcOptions.ClientCertKey.KeyFile)
if err != nil {
return nil, fmt.Errorf("failed to initialize OIDC client certificate loader: %v", err)
}

client, err := setupClient(dyCert, caFromFile)
if err != nil {
return nil, err
}

tokenAutherOptions.Client = client
tokenAutherOptions.CAContentProvider = nil
}

// generate tokenAuther from oidc config
tokenAuther, err := oidc.New(ctx.TODO(), tokenAutherOptions)
if err != nil {
return nil, err
}
Expand All @@ -141,6 +213,7 @@ func New(restConfig *rest.Config,
oidcRequestAuther: bearertoken.New(tokenAuther),
tokenAuther: tokenAuther,
auditor: auditor,
dynamicClientCert: dyCert,
}, nil
}

Expand All @@ -152,6 +225,19 @@ func (p *Proxy) Run(stopCh <-chan struct{}) (<-chan struct{}, <-chan struct{}, e
}
p.clientTransport = clientRT

if p.dynamicClientCert != nil {
// Start monitoring the OIDC client TLS certificate
c, cancel := ctx.WithCancel(ctx.Background())
go func() {
select {
case <-stopCh:
cancel()
case <-c.Done():
}
}()
go p.dynamicClientCert.Run(c, 1)
}

// No auth round tripper for no impersonation
if p.config.DisableImpersonation || p.config.TokenReview {
noAuthClientRT, err := p.roundTripperForRestConfig(&rest.Config{
Expand Down
23 changes: 22 additions & 1 deletion test/e2e/framework/framework.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func (f *Framework) BeforeEach() {
f.helper.KubeClient = f.KubeClientSet

By("Deploying mock OIDC Issuer")
issuerKeyBundle, issuerURL, err := f.helper.DeployIssuer(f.Namespace.Name)
issuerKeyBundle, issuerURL, err := f.helper.DeployIssuer(f.Namespace.Name, nil)
Expect(err).NotTo(HaveOccurred())

By("Deploying kube-oidc-proxy")
Expand All @@ -93,11 +93,19 @@ func (f *Framework) BeforeEach() {
// AfterEach deletes the namespace, after reading its events.
func (f *Framework) AfterEach() {
// Output logs from proxy of test case.
By("Gathering kube-oidc-proxy logs")
err := f.Helper().Kubectl(f.Namespace.Name).Run("logs", "-lapp=kube-oidc-proxy-e2e")
if err != nil {
By("Failed to gather logs from kube-oidc-proxy: " + err.Error())
}

// Output logs from the issuer of test case.
By("Gathering oidc-issuer logs")
err = f.Helper().Kubectl(f.Namespace.Name).Run("logs", "-lapp=oidc-issuer-e2e")
if err != nil {
By("Failed to gather logs from oidc-issuer: " + err.Error())
}

By("Deleting kube-oidc-proxy deployment")
err = f.Helper().DeleteProxy(f.Namespace.Name)
Expect(err).NotTo(HaveOccurred())
Expand Down Expand Up @@ -125,6 +133,19 @@ func (f *Framework) DeployProxyWith(extraVolumes []corev1.Volume, extraArgs ...s
Expect(err).NotTo(HaveOccurred())
}

func (f *Framework) DeployIssuerWith(extraVolumes []corev1.Volume, extraArgs ...string) {
By("Deleting oidc-issuer deployment")
err := f.Helper().DeleteIssuer(f.Namespace.Name)
Expect(err).NotTo(HaveOccurred())

err = f.Helper().WaitForDeploymentToDelete(f.Namespace.Name, kind.IssuerImageName, time.Second*30)
Expect(err).NotTo(HaveOccurred())

By(fmt.Sprintf("Deploying oidc-issuer with extra args %s", extraArgs))
f.issuerKeyBundle, f.issuerURL, err = f.helper.DeployIssuer(f.Namespace.Name, extraVolumes, extraArgs...)
Expect(err).NotTo(HaveOccurred())
}

func (f *Framework) Helper() *helper.Helper {
return f.helper
}
Expand Down
16 changes: 12 additions & 4 deletions test/e2e/framework/helper/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,18 +244,18 @@ func (h *Helper) DeployProxy(ns *corev1.Namespace, issuerURL *url.URL, clientID
return bundle, appURL, nil
}

func (h *Helper) DeployIssuer(ns string) (*util.KeyBundle, *url.URL, error) {
func (h *Helper) DeployIssuer(ns string, extraVolumes []corev1.Volume, extraArgs ...string) (*util.KeyBundle, *url.URL, error) {
cnt := corev1.Container{
Name: kind.IssuerImageName,
Image: kind.IssuerImageName,
ImagePullPolicy: corev1.PullNever,
Args: []string{
Args: append([]string{
"oidc-issuer",
"--secure-port=6443",
fmt.Sprintf("--issuer-url=https://oidc-issuer-e2e.%s.svc.cluster.local:6443", ns),
"--tls-cert-file=/tls/cert.pem",
"--tls-private-key-file=/tls/key.pem",
},
}, extraArgs...),
VolumeMounts: []corev1.VolumeMount{
corev1.VolumeMount{
MountPath: "/tls",
Expand All @@ -270,7 +270,15 @@ func (h *Helper) DeployIssuer(ns string) (*util.KeyBundle, *url.URL, error) {
},
}

bundle, appURL, err := h.deployApp(ns, kind.IssuerImageName, corev1.ServiceTypeClusterIP, cnt)
for _, v := range extraVolumes {
cnt.VolumeMounts = append(cnt.VolumeMounts, corev1.VolumeMount{
MountPath: fmt.Sprintf("/%s", v.Name),
Name: v.Name,
ReadOnly: true,
})
}

bundle, appURL, err := h.deployApp(ns, kind.IssuerImageName, corev1.ServiceTypeClusterIP, cnt, extraVolumes...)
if err != nil {
return nil, nil, err
}
Expand Down
1 change: 1 addition & 0 deletions test/e2e/suite/cases/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
_ "github.com/jetstack/kube-oidc-proxy/test/e2e/suite/cases/audit"
_ "github.com/jetstack/kube-oidc-proxy/test/e2e/suite/cases/headers"
_ "github.com/jetstack/kube-oidc-proxy/test/e2e/suite/cases/impersonation"
_ "github.com/jetstack/kube-oidc-proxy/test/e2e/suite/cases/mtls"
_ "github.com/jetstack/kube-oidc-proxy/test/e2e/suite/cases/passthrough"
_ "github.com/jetstack/kube-oidc-proxy/test/e2e/suite/cases/probe"
_ "github.com/jetstack/kube-oidc-proxy/test/e2e/suite/cases/rbac"
Expand Down
Loading