From 48cccaeded6efcb1780a8c48c6de35ca99b0ae82 Mon Sep 17 00:00:00 2001 From: kaedwen Date: Mon, 1 Jul 2024 23:31:10 +0200 Subject: [PATCH] add AuthRef to kubernetes provider fixes #3627 (#3628) * add AuthRef to kubernetes provider fixes #3627 Signed-off-by: kaedwen * run make reviewable Signed-off-by: kaedwen * fix validation for given authRef Signed-off-by: kaedwen * refactor kubernetes provider auth Signed-off-by: kaedwen * satisfy linter Signed-off-by: kaedwen * add URL for kubernetes provider tests Signed-off-by: kaedwen --------- Signed-off-by: kaedwen --- .../v1beta1/secretstore_kubernetes_types.go | 6 + .../v1beta1/zz_generated.deepcopy.go | 5 + ...ternal-secrets.io_clustersecretstores.yaml | 21 +- .../external-secrets.io_secretstores.yaml | 21 +- deploy/crds/bundle.yaml | 38 ++- docs/api/spec.md | 16 ++ pkg/provider/kubernetes/auth.go | 83 ++++--- pkg/provider/kubernetes/auth_test.go | 223 +++++++++++++++--- pkg/provider/kubernetes/provider.go | 25 +- pkg/provider/kubernetes/provider_test.go | 55 +++++ pkg/provider/kubernetes/validate.go | 2 +- 11 files changed, 401 insertions(+), 94 deletions(-) diff --git a/apis/externalsecrets/v1beta1/secretstore_kubernetes_types.go b/apis/externalsecrets/v1beta1/secretstore_kubernetes_types.go index fefd6d07b..2dc83cabc 100644 --- a/apis/externalsecrets/v1beta1/secretstore_kubernetes_types.go +++ b/apis/externalsecrets/v1beta1/secretstore_kubernetes_types.go @@ -37,11 +37,17 @@ type KubernetesServer struct { // Configures a store to sync secrets with a Kubernetes instance. type KubernetesProvider struct { // configures the Kubernetes server Address. + // +optional Server KubernetesServer `json:"server,omitempty"` // Auth configures how secret-manager authenticates with a Kubernetes instance. + // +optional Auth KubernetesAuth `json:"auth"` + // A reference to a secret that contains the auth information. + // +optional + AuthRef *esmeta.SecretKeySelector `json:"authRef,omitempty"` + // Remote namespace to fetch the secrets from // +kubebuilder:default= default // +optional diff --git a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go index 31c591118..2fc04558c 100644 --- a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go +++ b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go @@ -1858,6 +1858,11 @@ func (in *KubernetesProvider) DeepCopyInto(out *KubernetesProvider) { *out = *in in.Server.DeepCopyInto(&out.Server) in.Auth.DeepCopyInto(&out.Auth) + if in.AuthRef != nil { + in, out := &in.AuthRef, &out.AuthRef + *out = new(metav1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesProvider. diff --git a/config/crds/bases/external-secrets.io_clustersecretstores.yaml b/config/crds/bases/external-secrets.io_clustersecretstores.yaml index 5463ecaff..cba6e4b8d 100644 --- a/config/crds/bases/external-secrets.io_clustersecretstores.yaml +++ b/config/crds/bases/external-secrets.io_clustersecretstores.yaml @@ -3199,6 +3199,25 @@ spec: type: object type: object type: object + authRef: + description: A reference to a secret that contains the auth + information. + properties: + key: + description: |- + The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be + defaulted, in others it may be required. + type: string + name: + description: The name of the Secret resource being referred + to. + type: string + namespace: + description: |- + Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults + to the namespace of the referent. + type: string + type: object remoteNamespace: default: default description: Remote namespace to fetch the secrets from @@ -3242,8 +3261,6 @@ spec: description: configures the Kubernetes server Address. type: string type: object - required: - - auth type: object onboardbase: description: Onboardbase configures this store to sync secrets diff --git a/config/crds/bases/external-secrets.io_secretstores.yaml b/config/crds/bases/external-secrets.io_secretstores.yaml index eaba10eff..e9ff3e815 100644 --- a/config/crds/bases/external-secrets.io_secretstores.yaml +++ b/config/crds/bases/external-secrets.io_secretstores.yaml @@ -3199,6 +3199,25 @@ spec: type: object type: object type: object + authRef: + description: A reference to a secret that contains the auth + information. + properties: + key: + description: |- + The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be + defaulted, in others it may be required. + type: string + name: + description: The name of the Secret resource being referred + to. + type: string + namespace: + description: |- + Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults + to the namespace of the referent. + type: string + type: object remoteNamespace: default: default description: Remote namespace to fetch the secrets from @@ -3242,8 +3261,6 @@ spec: description: configures the Kubernetes server Address. type: string type: object - required: - - auth type: object onboardbase: description: Onboardbase configures this store to sync secrets diff --git a/deploy/crds/bundle.yaml b/deploy/crds/bundle.yaml index 7b9073e72..c4c9a04b5 100644 --- a/deploy/crds/bundle.yaml +++ b/deploy/crds/bundle.yaml @@ -3634,6 +3634,23 @@ spec: type: object type: object type: object + authRef: + description: A reference to a secret that contains the auth information. + properties: + key: + description: |- + The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be + defaulted, in others it may be required. + type: string + name: + description: The name of the Secret resource being referred to. + type: string + namespace: + description: |- + Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults + to the namespace of the referent. + type: string + type: object remoteNamespace: default: default description: Remote namespace to fetch the secrets from @@ -3674,8 +3691,6 @@ spec: description: configures the Kubernetes server Address. type: string type: object - required: - - auth type: object onboardbase: description: Onboardbase configures this store to sync secrets using the Onboardbase provider @@ -9182,6 +9197,23 @@ spec: type: object type: object type: object + authRef: + description: A reference to a secret that contains the auth information. + properties: + key: + description: |- + The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be + defaulted, in others it may be required. + type: string + name: + description: The name of the Secret resource being referred to. + type: string + namespace: + description: |- + Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults + to the namespace of the referent. + type: string + type: object remoteNamespace: default: default description: Remote namespace to fetch the secrets from @@ -9222,8 +9254,6 @@ spec: description: configures the Kubernetes server Address. type: string type: object - required: - - auth type: object onboardbase: description: Onboardbase configures this store to sync secrets using the Onboardbase provider diff --git a/docs/api/spec.md b/docs/api/spec.md index 163e43741..2613780dc 100644 --- a/docs/api/spec.md +++ b/docs/api/spec.md @@ -4864,6 +4864,7 @@ KubernetesServer +(Optional)

configures the Kubernetes server Address.

@@ -4877,11 +4878,26 @@ KubernetesAuth +(Optional)

Auth configures how secret-manager authenticates with a Kubernetes instance.

+authRef
+ + +External Secrets meta/v1.SecretKeySelector + + + + +(Optional) +

A reference to a secret that contains the auth information.

+ + + + remoteNamespace
string diff --git a/pkg/provider/kubernetes/auth.go b/pkg/provider/kubernetes/auth.go index a4118de67..547283e2e 100644 --- a/pkg/provider/kubernetes/auth.go +++ b/pkg/provider/kubernetes/auth.go @@ -22,6 +22,8 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" esmeta "github.com/external-secrets/external-secrets/apis/meta/v1" @@ -36,35 +38,63 @@ const ( errUnableCreateToken = "cannot create service account token: %q" ) -func (c *Client) setAuth(ctx context.Context) error { - err := c.setCA(ctx) +func (c *Client) getAuth(ctx context.Context) (*rest.Config, error) { + if c.store.AuthRef != nil { + cfg, err := c.fetchSecretKey(ctx, *c.store.AuthRef) + if err != nil { + return nil, err + } + + return clientcmd.RESTConfigFromKubeConfig(cfg) + } + + ca, err := c.getCA(ctx) if err != nil { - return err + return nil, err } + + var token []byte if c.store.Auth.Token != nil { - c.BearerToken, err = c.fetchSecretKey(ctx, c.store.Auth.Token.BearerToken) + token, err = c.fetchSecretKey(ctx, c.store.Auth.Token.BearerToken) if err != nil { - return fmt.Errorf("could not fetch Auth.Token.BearerToken: %w", err) + return nil, fmt.Errorf("could not fetch Auth.Token.BearerToken: %w", err) } - return nil - } - if c.store.Auth.ServiceAccount != nil { - c.BearerToken, err = c.serviceAccountToken(ctx, c.store.Auth.ServiceAccount) + } else if c.store.Auth.ServiceAccount != nil { + token, err = c.serviceAccountToken(ctx, c.store.Auth.ServiceAccount) if err != nil { - return fmt.Errorf("could not fetch Auth.ServiceAccount: %w", err) + return nil, fmt.Errorf("could not fetch Auth.ServiceAccount: %w", err) } - return nil + } else { + return nil, fmt.Errorf("no auth provider given") } + + var key, cert []byte if c.store.Auth.Cert != nil { - return c.setClientCert(ctx) + key, cert, err = c.getClientKeyAndCert(ctx) + if err != nil { + return nil, fmt.Errorf("could not fetch client key and cert: %w", err) + } } - return fmt.Errorf("no credentials provided") + + if c.store.Server.URL == "" { + return nil, fmt.Errorf("no server URL provided") + } + + return &rest.Config{ + Host: c.store.Server.URL, + BearerToken: string(token), + TLSClientConfig: rest.TLSClientConfig{ + Insecure: false, + CertData: cert, + KeyData: key, + CAData: ca, + }, + }, nil } -func (c *Client) setCA(ctx context.Context) error { +func (c *Client) getCA(ctx context.Context) ([]byte, error) { if c.store.Server.CABundle != nil { - c.CA = c.store.Server.CABundle - return nil + return c.store.Server.CABundle, nil } if c.store.Server.CAProvider != nil { var ca []byte @@ -78,7 +108,7 @@ func (c *Client) setCA(ctx context.Context) error { } ca, err = c.fetchConfigMapKey(ctx, keySelector) if err != nil { - return fmt.Errorf("unable to fetch Server.CAProvider ConfigMap: %w", err) + return nil, fmt.Errorf("unable to fetch Server.CAProvider ConfigMap: %w", err) } case esv1beta1.CAProviderTypeSecret: keySelector := esmeta.SecretKeySelector{ @@ -88,26 +118,25 @@ func (c *Client) setCA(ctx context.Context) error { } ca, err = c.fetchSecretKey(ctx, keySelector) if err != nil { - return fmt.Errorf("unable to fetch Server.CAProvider Secret: %w", err) + return nil, fmt.Errorf("unable to fetch Server.CAProvider Secret: %w", err) } } - c.CA = ca - return nil + return ca, nil } - return fmt.Errorf("no Certificate Authority provided") + return nil, fmt.Errorf("no Certificate Authority provided") } -func (c *Client) setClientCert(ctx context.Context) error { +func (c *Client) getClientKeyAndCert(ctx context.Context) ([]byte, []byte, error) { var err error - c.Certificate, err = c.fetchSecretKey(ctx, c.store.Auth.Cert.ClientCert) + cert, err := c.fetchSecretKey(ctx, c.store.Auth.Cert.ClientCert) if err != nil { - return fmt.Errorf("unable to fetch client certificate: %w", err) + return nil, nil, fmt.Errorf("unable to fetch client certificate: %w", err) } - c.Key, err = c.fetchSecretKey(ctx, c.store.Auth.Cert.ClientKey) + key, err := c.fetchSecretKey(ctx, c.store.Auth.Cert.ClientKey) if err != nil { - return fmt.Errorf("unable to fetch client key: %w", err) + return nil, nil, fmt.Errorf("unable to fetch client key: %w", err) } - return nil + return key, cert, nil } func (c *Client) serviceAccountToken(ctx context.Context, serviceAccountRef *esmeta.ServiceAccountSelector) ([]byte, error) { diff --git a/pkg/provider/kubernetes/auth_test.go b/pkg/provider/kubernetes/auth_test.go index 9784c1372..5d23de3c7 100644 --- a/pkg/provider/kubernetes/auth_test.go +++ b/pkg/provider/kubernetes/auth_test.go @@ -22,6 +22,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/rest" pointer "k8s.io/utils/ptr" kclient "sigs.k8s.io/controller-runtime/pkg/client" fclient "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -31,6 +32,43 @@ import ( utilfake "github.com/external-secrets/external-secrets/pkg/provider/util/fake" ) +const ( + caCert = `-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- +` + authTestKubeConfig = `apiVersion: v1 +clusters: +- cluster: + server: https://api.my-domain.tld + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNHVENDQVorZ0F3SUJBZ0lRQ2VDVFphejMyY2k1UGh3TEJDb3U4ekFLQmdncWhrak9QUVFEQXpCT01Rc3cKQ1FZRFZRUUdFd0pWVXpFWE1CVUdBMVVFQ2hNT1JHbG5hVU5sY25Rc0lFbHVZeTR4SmpBa0JnTlZCQU1USFVScApaMmxEWlhKMElGUk1VeUJGUTBNZ1VETTROQ0JTYjI5MElFYzFNQjRYRFRJeE1ERXhOVEF3TURBd01Gb1hEVFEyCk1ERXhOREl6TlRrMU9Wb3dUakVMTUFrR0ExVUVCaE1DVlZNeEZ6QVZCZ05WQkFvVERrUnBaMmxEWlhKMExDQkoKYm1NdU1TWXdKQVlEVlFRREV4MUVhV2RwUTJWeWRDQlVURk1nUlVORElGQXpPRFFnVW05dmRDQkhOVEIyTUJBRwpCeXFHU000OUFnRUdCU3VCQkFBaUEySUFCTUZFb2M4UmwxQ2EzaU9DTlFmTjBNc1luZEx4ZjNjMVR6dmRsSEpTCjdjSTcrT3o2ZTJ0WUlPeVpyc244YUxOMXVkc0o3TWdUOVU3R0NoMW1NRXk3SDBjS1BHRVFRaWw4cFFnTzRDTHAKMHpWb3pwdGpuNFMxbVUxWW9JNzFWT2VWeWFOQ01FQXdIUVlEVlIwT0JCWUVGTUZSUlZCWnF6N25MRnI2SUNJUwpCNENJZkJGcU1BNEdBMVVkRHdFQi93UUVBd0lCaGpBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUFvR0NDcUdTTTQ5CkJBTURBMmdBTUdVQ01RQ0phbzFINSt6OGJsVUQyV2RzSms2RHh2M0oreXNUdkxkNmpMUmwwbWxwWXhOak95WlEKTGdHaGVRYVJuVWkvd3I0Q01FZkRGWHV4b0pHWlNaT29QSHpvUmdhTExQSXhBSlNkWXNpSnZSbUVGT21sK3dHNApEWFpEakM1VHkzemZEQmVXVUE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + name: mycluster +contexts: +- context: + cluster: mycluster + user: myuser + name: mycontext +current-context: mycontext +kind: Config +preferences: {} +users: +- name: myuser + user: + token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE3MTkzOTY4OTksImV4cCI6MTc1MDkzMjg4NywiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJFbWFpbCI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJSb2xlIjpbIk1hbmFnZXIiLCJQcm9qZWN0IEFkbWluaXN0cmF0b3IiXX0.xXrfIl0akhfjWU_BDl7Ad54SXje0YlJdnugzwh96VmM +` +) + func TestSetAuth(t *testing.T) { type fields struct { kube kclient.Client @@ -39,16 +77,11 @@ func TestSetAuth(t *testing.T) { namespace string storeKind string } - type want struct { - Certificate []byte - Key []byte - CA []byte - BearerToken []byte - } + type want = rest.Config tests := []struct { name string fields fields - want want + want *want wantErr bool }{ { @@ -58,7 +91,7 @@ func TestSetAuth(t *testing.T) { Server: esv1beta1.KubernetesServer{}, }, }, - want: want{}, + want: nil, wantErr: true, }, { @@ -70,9 +103,7 @@ func TestSetAuth(t *testing.T) { }, }, }, - want: want{ - CA: []byte("1234"), - }, + want: nil, wantErr: true, }, { @@ -85,29 +116,52 @@ func TestSetAuth(t *testing.T) { Namespace: "default", }, Data: map[string][]byte{ - "cert": []byte("1234"), + "cert": []byte("1234"), + "token": []byte("mytoken"), }, }).Build(), store: &esv1beta1.KubernetesProvider{ Server: esv1beta1.KubernetesServer{ + URL: "https://my.test.tld", CAProvider: &esv1beta1.CAProvider{ Type: esv1beta1.CAProviderTypeSecret, Name: "foobar", Key: "cert", }, }, + Auth: esv1beta1.KubernetesAuth{ + Token: &esv1beta1.TokenAuth{ + BearerToken: v1.SecretKeySelector{ + Name: "foobar", + Namespace: pointer.To("shouldnotberelevant"), + Key: "token", + }, + }, + }, }, }, - want: want{ - CA: []byte("1234"), + want: &want{ + Host: "https://my.test.tld", + BearerToken: "mytoken", + TLSClientConfig: rest.TLSClientConfig{ + CAData: []byte("1234"), + }, }, - wantErr: true, + wantErr: false, }, { name: "should fetch ca from ConfigMap", fields: fields{ namespace: "default", - kube: fclient.NewClientBuilder().WithObjects(&corev1.ConfigMap{ + kube: fclient.NewClientBuilder().WithObjects(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foobar", + Namespace: "default", + }, + Data: map[string][]byte{ + "token": []byte("mytoken"), + }, + }, &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "foobar", Namespace: "default", @@ -118,18 +172,32 @@ func TestSetAuth(t *testing.T) { }).Build(), store: &esv1beta1.KubernetesProvider{ Server: esv1beta1.KubernetesServer{ + URL: "https://my.test.tld", CAProvider: &esv1beta1.CAProvider{ Type: esv1beta1.CAProviderTypeConfigMap, Name: "foobar", Key: "cert", }, }, + Auth: esv1beta1.KubernetesAuth{ + Token: &esv1beta1.TokenAuth{ + BearerToken: v1.SecretKeySelector{ + Name: "foobar", + Namespace: pointer.To("shouldnotberelevant"), + Key: "token", + }, + }, + }, }, }, - want: want{ - CA: []byte("1234"), + want: &want{ + Host: "https://my.test.tld", + BearerToken: "mytoken", + TLSClientConfig: rest.TLSClientConfig{ + CAData: []byte("1234"), + }, }, - wantErr: true, + wantErr: false, }, { name: "should set token from secret", @@ -146,6 +214,7 @@ func TestSetAuth(t *testing.T) { }).Build(), store: &esv1beta1.KubernetesProvider{ Server: esv1beta1.KubernetesServer{ + URL: "https://my.test.tld", CABundle: []byte("1234"), }, Auth: esv1beta1.KubernetesAuth{ @@ -159,9 +228,12 @@ func TestSetAuth(t *testing.T) { }, }, }, - want: want{ - CA: []byte("1234"), - BearerToken: []byte("mytoken"), + want: &want{ + Host: "https://my.test.tld", + BearerToken: "mytoken", + TLSClientConfig: rest.TLSClientConfig{ + CAData: []byte("1234"), + }, }, wantErr: false, }, @@ -178,12 +250,28 @@ func TestSetAuth(t *testing.T) { "cert": []byte("my-cert"), "key": []byte("my-key"), }, + }, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foobar", + Namespace: "default", + }, + Data: map[string][]byte{ + "token": []byte("mytoken"), + }, }).Build(), store: &esv1beta1.KubernetesProvider{ Server: esv1beta1.KubernetesServer{ + URL: "https://my.test.tld", CABundle: []byte("1234"), }, Auth: esv1beta1.KubernetesAuth{ + Token: &esv1beta1.TokenAuth{ + BearerToken: v1.SecretKeySelector{ + Name: "foobar", + Namespace: pointer.To("shouldnotberelevant"), + Key: "token", + }, + }, Cert: &esv1beta1.CertAuth{ ClientCert: v1.SecretKeySelector{ Name: "mycert", @@ -197,15 +285,52 @@ func TestSetAuth(t *testing.T) { }, }, }, - want: want{ - CA: []byte("1234"), - Certificate: []byte("my-cert"), - Key: []byte("my-key"), + want: &want{ + Host: "https://my.test.tld", + BearerToken: "mytoken", + TLSClientConfig: rest.TLSClientConfig{ + CAData: []byte("1234"), + CertData: []byte("my-cert"), + KeyData: []byte("my-key"), + }, }, wantErr: false, }, { name: "should set token from service account", + fields: fields{ + namespace: "default", + kube: fclient.NewClientBuilder().WithObjects(&corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-sa", + Namespace: "default", + }, + }).Build(), + kubeclientset: utilfake.NewCreateTokenMock().WithToken("my-sa-token"), + store: &esv1beta1.KubernetesProvider{ + Server: esv1beta1.KubernetesServer{ + URL: "https://my.test.tld", + CABundle: []byte("1234"), + }, + Auth: esv1beta1.KubernetesAuth{ + ServiceAccount: &v1.ServiceAccountSelector{ + Name: "my-sa", + Namespace: pointer.To("shouldnotberelevant"), + }, + }, + }, + }, + want: &want{ + Host: "https://my.test.tld", + BearerToken: "my-sa-token", + TLSClientConfig: rest.TLSClientConfig{ + CAData: []byte("1234"), + }, + }, + wantErr: false, + }, + { + name: "should fail with missing URL", fields: fields{ namespace: "default", kube: fclient.NewClientBuilder().WithObjects(&corev1.ServiceAccount{ @@ -227,9 +352,36 @@ func TestSetAuth(t *testing.T) { }, }, }, - want: want{ - CA: []byte("1234"), - BearerToken: []byte("my-sa-token"), + want: nil, + wantErr: true, + }, + { + name: "should read config from secret", + fields: fields{ + namespace: "default", + kube: fclient.NewClientBuilder().WithObjects(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foobar", + Namespace: "default", + }, + Data: map[string][]byte{ + "config": []byte(authTestKubeConfig), + }, + }).Build(), + store: &esv1beta1.KubernetesProvider{ + AuthRef: &v1.SecretKeySelector{ + Name: "foobar", + Namespace: pointer.To("default"), + Key: "config", + }, + }, + }, + want: &want{ + Host: "https://api.my-domain.tld", + BearerToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE3MTkzOTY4OTksImV4cCI6MTc1MDkzMjg4NywiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJFbWFpbCI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJSb2xlIjpbIk1hbmFnZXIiLCJQcm9qZWN0IEFkbWluaXN0cmF0b3IiXX0.xXrfIl0akhfjWU_BDl7Ad54SXje0YlJdnugzwh96VmM", + TLSClientConfig: rest.TLSClientConfig{ + CAData: []byte(caCert), + }, }, wantErr: false, }, @@ -243,17 +395,12 @@ func TestSetAuth(t *testing.T) { namespace: tt.fields.namespace, storeKind: tt.fields.storeKind, } - if err := k.setAuth(context.Background()); (err != nil) != tt.wantErr { + cfg, err := k.getAuth(context.Background()) + if (err != nil) != tt.wantErr { t.Errorf("BaseClient.setAuth() error = %v, wantErr %v", err, tt.wantErr) } - w := want{ - Certificate: k.Certificate, - Key: k.Key, - CA: k.CA, - BearerToken: k.BearerToken, - } - if !cmp.Equal(w, tt.want) { - t.Errorf("unexpected value: expected %#v, got %#v", tt.want, w) + if !cmp.Equal(cfg, tt.want) { + t.Errorf("unexpected value: expected %#v, got %#v", tt.want, cfg) } }) } diff --git a/pkg/provider/kubernetes/provider.go b/pkg/provider/kubernetes/provider.go index c24df39ad..82fcecaac 100644 --- a/pkg/provider/kubernetes/provider.go +++ b/pkg/provider/kubernetes/provider.go @@ -23,7 +23,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" - "k8s.io/client-go/rest" kclient "sigs.k8s.io/controller-runtime/pkg/client" ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config" @@ -73,11 +72,7 @@ type Client struct { // namespace is the namespace of the // ExternalSecret referencing this provider. - namespace string - Certificate []byte - Key []byte - CA []byte - BearerToken []byte + namespace string } func init() { @@ -123,22 +118,12 @@ func (p *Provider) newClient(ctx context.Context, store esv1beta1.GenericStore, return client, nil } - if err := client.setAuth(ctx); err != nil { - return nil, err + cfg, err := client.getAuth(ctx) + if err != nil { + return nil, fmt.Errorf("failed to prepare auth: %w", err) } - config := &rest.Config{ - Host: client.store.Server.URL, - BearerToken: string(client.BearerToken), - TLSClientConfig: rest.TLSClientConfig{ - Insecure: false, - CertData: client.Certificate, - KeyData: client.Key, - CAData: client.CA, - }, - } - - userClientset, err := kubernetes.NewForConfig(config) + userClientset, err := kubernetes.NewForConfig(cfg) if err != nil { return nil, fmt.Errorf("error configuring clientset: %w", err) } diff --git a/pkg/provider/kubernetes/provider_test.go b/pkg/provider/kubernetes/provider_test.go index 26e3b0837..6a7e48c0f 100644 --- a/pkg/provider/kubernetes/provider_test.go +++ b/pkg/provider/kubernetes/provider_test.go @@ -51,6 +51,24 @@ mv+AggtK0aRFb9o47z/BypLdk5mhbf3Mmr88C8XBzEnfdYyf4JpTlZrYLBmDCu5d 9RLLsjXxhag8xqMtd1uLUM8XOTGzVWacw8iGY+CTtBKqyA+AE6/bDwZvEwVtsKtC QJ85ioEpy00NioqcF0WyMZH80uMsPycfpnl5uF7RkW8u -----END CERTIFICATE-----` + testKubeConfig = `apiVersion: v1 +clusters: +- cluster: + server: https://api.my-domain.tld + name: mycluster +contexts: +- context: + cluster: mycluster + user: myuser + name: mycontext +current-context: mycontext +kind: Config +preferences: {} +users: +- name: myuser + user: + token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE3MTkzOTY4OTksImV4cCI6MTc1MDkzMjg4NywiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJFbWFpbCI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJSb2xlIjpbIk1hbmFnZXIiLCJQcm9qZWN0IEFkbWluaXN0cmF0b3IiXX0.xXrfIl0akhfjWU_BDl7Ad54SXje0YlJdnugzwh96VmM +` ) func TestNewClient(t *testing.T) { @@ -88,6 +106,40 @@ func TestNewClient(t *testing.T) { }, wantErr: true, }, + { + name: "test auth ref", + fields: fields{}, + args: args{ + store: &esv1beta1.ClusterSecretStore{ + TypeMeta: metav1.TypeMeta{ + Kind: esv1beta1.ClusterSecretStoreKind, + }, + Spec: esv1beta1.SecretStoreSpec{ + Provider: &esv1beta1.SecretStoreProvider{ + Kubernetes: &esv1beta1.KubernetesProvider{ + AuthRef: &v1.SecretKeySelector{ + Name: "foo", + Namespace: pointer.To("default"), + Key: "config", + }, + }, + }, + }, + }, + namespace: "", + kube: fclient.NewClientBuilder().WithObjects(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "default", + }, + Data: map[string][]byte{ + "config": []byte(testKubeConfig), + }, + }).Build(), + clientset: clientgofake.NewSimpleClientset(), + }, + want: true, + }, { name: "test referent auth return", fields: fields{}, @@ -100,6 +152,7 @@ func TestNewClient(t *testing.T) { Provider: &esv1beta1.SecretStoreProvider{ Kubernetes: &esv1beta1.KubernetesProvider{ Server: esv1beta1.KubernetesServer{ + URL: "https://my.test.tld", CABundle: []byte(testCertificate), }, Auth: esv1beta1.KubernetesAuth{ @@ -132,6 +185,7 @@ func TestNewClient(t *testing.T) { Provider: &esv1beta1.SecretStoreProvider{ Kubernetes: &esv1beta1.KubernetesProvider{ Server: esv1beta1.KubernetesServer{ + URL: "https://my.test.tld", CABundle: []byte(testCertificate), }, RemoteNamespace: "remote", @@ -166,6 +220,7 @@ func TestNewClient(t *testing.T) { Provider: &esv1beta1.SecretStoreProvider{ Kubernetes: &esv1beta1.KubernetesProvider{ Server: esv1beta1.KubernetesServer{ + URL: "https://my.test.tld", CABundle: []byte(testCertificate), }, RemoteNamespace: "remote", diff --git a/pkg/provider/kubernetes/validate.go b/pkg/provider/kubernetes/validate.go index 8f264d0cc..11d1a6ef1 100644 --- a/pkg/provider/kubernetes/validate.go +++ b/pkg/provider/kubernetes/validate.go @@ -31,7 +31,7 @@ import ( func (p *Provider) ValidateStore(store esv1beta1.GenericStore) (admission.Warnings, error) { storeSpec := store.GetSpec() k8sSpec := storeSpec.Provider.Kubernetes - if k8sSpec.Server.CABundle == nil && k8sSpec.Server.CAProvider == nil { + if k8sSpec.AuthRef == nil && k8sSpec.Server.CABundle == nil && k8sSpec.Server.CAProvider == nil { return nil, fmt.Errorf("a CABundle or CAProvider is required") } if store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind &&