Browse Source

add AuthRef to kubernetes provider fixes #3627 (#3628)

* add AuthRef to kubernetes provider fixes #3627

Signed-off-by: kaedwen <kaedwen@heinrich.blue>

* run make reviewable

Signed-off-by: kaedwen <kaedwen@heinrich.blue>

* fix validation for given authRef

Signed-off-by: kaedwen <kaedwen@heinrich.blue>

* refactor kubernetes provider auth

Signed-off-by: kaedwen <kaedwen@heinrich.blue>

* satisfy linter

Signed-off-by: kaedwen <kaedwen@heinrich.blue>

* add URL for kubernetes provider tests

Signed-off-by: kaedwen <kaedwen@heinrich.blue>

---------

Signed-off-by: kaedwen <kaedwen@heinrich.blue>
kaedwen 1 year ago
parent
commit
48cccaeded

+ 6 - 0
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

+ 5 - 0
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.

+ 19 - 2
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

+ 19 - 2
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

+ 34 - 4
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

+ 16 - 0
docs/api/spec.md

@@ -4864,6 +4864,7 @@ KubernetesServer
 </em>
 </td>
 <td>
+<em>(Optional)</em>
 <p>configures the Kubernetes server Address.</p>
 </td>
 </tr>
@@ -4877,11 +4878,26 @@ KubernetesAuth
 </em>
 </td>
 <td>
+<em>(Optional)</em>
 <p>Auth configures how secret-manager authenticates with a Kubernetes instance.</p>
 </td>
 </tr>
 <tr>
 <td>
+<code>authRef</code></br>
+<em>
+<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
+External Secrets meta/v1.SecretKeySelector
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>A reference to a secret that contains the auth information.</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>remoteNamespace</code></br>
 <em>
 string

+ 56 - 27
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) {

File diff suppressed because it is too large
+ 185 - 38
pkg/provider/kubernetes/auth_test.go


+ 5 - 20
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
-	}
-
-	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,
-		},
+	cfg, err := client.getAuth(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("failed to prepare auth: %w", err)
 	}
 
-	userClientset, err := kubernetes.NewForConfig(config)
+	userClientset, err := kubernetes.NewForConfig(cfg)
 	if err != nil {
 		return nil, fmt.Errorf("error configuring clientset: %w", err)
 	}

+ 55 - 0
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) {
@@ -89,6 +107,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{},
 			args: args{
@@ -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",

+ 1 - 1
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 &&