From f2d77e03240cd967ad862de246a3d69d1f46a7ef Mon Sep 17 00:00:00 2001 From: Cameron McAvoy Date: Thu, 8 Apr 2021 12:11:56 -0500 Subject: [PATCH] Add service account selector to vault provider to look up the sa token --- .../v1alpha1/secretstore_vault_types.go | 7 ++++ .../v1alpha1/zz_generated.deepcopy.go | 5 +++ apis/meta/v1/types.go | 10 +++++ apis/meta/v1/zz_generated.deepcopy.go | 20 ++++++++++ .../external-secrets/templates/rbac.yaml | 8 ++++ ...ternal-secrets.io_clustersecretstores.yaml | 21 ++++++++++ .../external-secrets.io_secretstores.yaml | 21 ++++++++++ pkg/provider/vault/vault.go | 37 +++++++++++++++++- pkg/provider/vault/vault_test.go | 38 ++++++++++++++++--- 9 files changed, 160 insertions(+), 7 deletions(-) diff --git a/apis/externalsecrets/v1alpha1/secretstore_vault_types.go b/apis/externalsecrets/v1alpha1/secretstore_vault_types.go index c2223077f..ccfa173e2 100644 --- a/apis/externalsecrets/v1alpha1/secretstore_vault_types.go +++ b/apis/externalsecrets/v1alpha1/secretstore_vault_types.go @@ -105,6 +105,13 @@ type VaultKubernetesAuth struct { // +kubebuilder:default=kubernetes Path string `json:"mountPath"` + // Optional service account field containing the name of a kubernetes ServiceAccount. + // If the service account is specified, the service account secret token JWT will be used + // for authenticating with Vault. If the service account selector is not supplied, + // the secretRef will be used instead. + // +optional + ServiceAccountRef *esmeta.ServiceAccountSelector `json:"serviceAccountRef,omitempty"` + // Optional secret field containing a Kubernetes ServiceAccount JWT used // for authenticating with Vault. If a name is specified without a key, // `token` is the default. If one is not specified, the one bound to diff --git a/apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go b/apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go index c9584e6a0..8427679c4 100644 --- a/apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go +++ b/apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go @@ -562,6 +562,11 @@ func (in *VaultAuth) DeepCopy() *VaultAuth { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VaultKubernetesAuth) DeepCopyInto(out *VaultKubernetesAuth) { *out = *in + if in.ServiceAccountRef != nil { + in, out := &in.ServiceAccountRef, &out.ServiceAccountRef + *out = new(metav1.ServiceAccountSelector) + (*in).DeepCopyInto(*out) + } if in.SecretRef != nil { in, out := &in.SecretRef, &out.SecretRef *out = new(metav1.SecretKeySelector) diff --git a/apis/meta/v1/types.go b/apis/meta/v1/types.go index cc087b656..0bb3a2514 100644 --- a/apis/meta/v1/types.go +++ b/apis/meta/v1/types.go @@ -28,3 +28,13 @@ type SecretKeySelector struct { // +optional Key string `json:"key,omitempty"` } + +// A reference to a ServiceAccount resource. +type ServiceAccountSelector struct { + // The name of the ServiceAccount resource being referred to. + Name string `json:"name"` + // Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults + // to the namespace of the referent. + // +optional + Namespace *string `json:"namespace,omitempty"` +} diff --git a/apis/meta/v1/zz_generated.deepcopy.go b/apis/meta/v1/zz_generated.deepcopy.go index cc2c5684a..92d808bb6 100644 --- a/apis/meta/v1/zz_generated.deepcopy.go +++ b/apis/meta/v1/zz_generated.deepcopy.go @@ -39,3 +39,23 @@ func (in *SecretKeySelector) DeepCopy() *SecretKeySelector { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceAccountSelector) DeepCopyInto(out *ServiceAccountSelector) { + *out = *in + if in.Namespace != nil { + in, out := &in.Namespace, &out.Namespace + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountSelector. +func (in *ServiceAccountSelector) DeepCopy() *ServiceAccountSelector { + if in == nil { + return nil + } + out := new(ServiceAccountSelector) + in.DeepCopyInto(out) + return out +} diff --git a/deploy/charts/external-secrets/templates/rbac.yaml b/deploy/charts/external-secrets/templates/rbac.yaml index bea2f5925..c596ff24f 100644 --- a/deploy/charts/external-secrets/templates/rbac.yaml +++ b/deploy/charts/external-secrets/templates/rbac.yaml @@ -24,6 +24,14 @@ rules: verbs: - "update" - "patch" + - apiGroups: + - "" + resources: + - "serviceaccounts" + verbs: + - "get" + - "list" + - "watch" - apiGroups: - "" resources: diff --git a/deploy/crds/external-secrets.io_clustersecretstores.yaml b/deploy/crds/external-secrets.io_clustersecretstores.yaml index dcb9fc99e..90b8265bc 100644 --- a/deploy/crds/external-secrets.io_clustersecretstores.yaml +++ b/deploy/crds/external-secrets.io_clustersecretstores.yaml @@ -230,6 +230,27 @@ spec: required: - name type: object + serviceAccountRef: + description: Optional service account field containing + the name of a kubernetes ServiceAccount. If the + service account is specified, the service account + secret token JWT will be used for authenticating + with Vault. If the service account selector is not + supplied, the secretRef will be used instead. + properties: + name: + description: The name of the ServiceAccount 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 + required: + - name + type: object required: - mountPath - role diff --git a/deploy/crds/external-secrets.io_secretstores.yaml b/deploy/crds/external-secrets.io_secretstores.yaml index 67ece4dd2..3085d6775 100644 --- a/deploy/crds/external-secrets.io_secretstores.yaml +++ b/deploy/crds/external-secrets.io_secretstores.yaml @@ -230,6 +230,27 @@ spec: required: - name type: object + serviceAccountRef: + description: Optional service account field containing + the name of a kubernetes ServiceAccount. If the + service account is specified, the service account + secret token JWT will be used for authenticating + with Vault. If the service account selector is not + supplied, the secretRef will be used instead. + properties: + name: + description: The name of the ServiceAccount 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 + required: + - name + type: object required: - mountPath - role diff --git a/pkg/provider/vault/vault.go b/pkg/provider/vault/vault.go index 7ef0cc365..32be0c1e5 100644 --- a/pkg/provider/vault/vault.go +++ b/pkg/provider/vault/vault.go @@ -57,6 +57,9 @@ const ( errVaultResponse = "cannot parse Vault response: %w" errServiceAccount = "cannot read Kubernetes service account token from file system: %w" + errGetKubeSA = "cannot get Kubernetes service account %q: %w" + errGetKubeSASecrets = "cannot find secrets bound to service account: %q" + errGetKubeSecret = "cannot get Kubernetes secret %q: %w" errSecretKeyFmt = "cannot find secret data for key: %q" ) @@ -257,6 +260,32 @@ func (v *client) setAuth(ctx context.Context, client Client) error { return errors.New(errAuthFormat) } +func (v *client) secretKeyRefForServiceAccount(ctx context.Context, serviceAccountRef *esmeta.ServiceAccountSelector) (string, error) { + serviceAccount := &corev1.ServiceAccount{} + ref := types.NamespacedName{ + Namespace: v.namespace, + Name: serviceAccountRef.Name, + } + if (v.storeKind == esv1alpha1.ClusterSecretStoreKind) && + (serviceAccountRef.Namespace != nil) { + ref.Namespace = *serviceAccountRef.Namespace + } + err := v.kube.Get(ctx, ref, serviceAccount) + if err != nil { + return "", fmt.Errorf(errGetKubeSA, ref.Name, err) + } + if len(serviceAccount.Secrets) == 0 { + return "", fmt.Errorf(errGetKubeSASecrets, ref.Name) + } + tokenRef := serviceAccount.Secrets[0] + + return v.secretKeyRef(ctx, &esmeta.SecretKeySelector{ + Name: tokenRef.Name, + Namespace: &ref.Namespace, + Key: "token", + }) +} + func (v *client) secretKeyRef(ctx context.Context, secretRef *esmeta.SecretKeySelector) (string, error) { secret := &corev1.Secret{} ref := types.NamespacedName{ @@ -339,7 +368,13 @@ func kubeParameters(role, jwt string) map[string]string { func (v *client) requestTokenWithKubernetesAuth(ctx context.Context, client Client, kubernetesAuth *esv1alpha1.VaultKubernetesAuth) (string, error) { jwtString := "" - if kubernetesAuth.SecretRef != nil { + if kubernetesAuth.ServiceAccountRef != nil { + jwt, err := v.secretKeyRefForServiceAccount(ctx, kubernetesAuth.ServiceAccountRef) + if err != nil { + return "", err + } + jwtString = jwt + } else if kubernetesAuth.SecretRef != nil { tokenRef := kubernetesAuth.SecretRef if tokenRef.Key == "" { tokenRef = kubernetesAuth.SecretRef.DeepCopy() diff --git a/pkg/provider/vault/vault_test.go b/pkg/provider/vault/vault_test.go index 39d50b77e..4de0c8fe5 100644 --- a/pkg/provider/vault/vault_test.go +++ b/pkg/provider/vault/vault_test.go @@ -52,9 +52,8 @@ func makeValidSecretStore() *esv1alpha1.SecretStore { Kubernetes: &esv1alpha1.VaultKubernetesAuth{ Path: "kubernetes", Role: "kubernetes-auth-role", - SecretRef: &esmeta.SecretKeySelector{ - Name: "vault-secret", - Key: "key", + ServiceAccountRef: &esmeta.ServiceAccountSelector{ + Name: "example-sa", }, }, }, @@ -144,7 +143,7 @@ func TestNewVault(t *testing.T) { err: errors.New(errAuthFormat), }, }, - "GetKubeSecretError": { + "GetKubeServiceAccountError": { reason: "Should return error if fetching kubernetes secret fails.", args: args{ store: makeSecretStore(), @@ -153,7 +152,25 @@ func TestNewVault(t *testing.T) { }, }, want: want{ - err: fmt.Errorf(errGetKubeSecret, makeSecretStore().Spec.Provider.Vault.Auth.Kubernetes.SecretRef.Name, errBoom), + err: fmt.Errorf(errGetKubeSA, "example-sa", errBoom), + }, + }, + "GetKubeSecretError": { + reason: "Should return error if fetching kubernetes secret fails.", + args: args{ + store: makeSecretStore(func(s *esv1alpha1.SecretStore) { + s.Spec.Provider.Vault.Auth.Kubernetes.ServiceAccountRef = nil + s.Spec.Provider.Vault.Auth.Kubernetes.SecretRef = &esmeta.SecretKeySelector{ + Name: "vault-secret", + Key: "key", + } + }), + kube: &test.MockClient{ + MockGet: test.NewMockGetFn(errBoom), + }, + }, + want: want{ + err: fmt.Errorf(errGetKubeSecret, "vault-secret", errBoom), }, }, "SuccessfulVaultStore": { @@ -162,10 +179,19 @@ func TestNewVault(t *testing.T) { store: makeSecretStore(), kube: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj kclient.Object) error { + if o, ok := obj.(*corev1.ServiceAccount); ok { + o.Secrets = []corev1.ObjectReference{ + { + Name: "example-secret-token", + }, + } + return nil + } if o, ok := obj.(*corev1.Secret); ok { o.Data = map[string][]byte{ - "key": secretData, + "token": secretData, } + return nil } return nil }),