Browse Source

Merge pull request #106 from cnmcavoy/cnmcavoy/vault-sa-token-lookup

Add service account selector to vault provider to look up the sa token
paul-the-alien[bot] 5 years ago
parent
commit
ebad566f49

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

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

+ 10 - 0
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"`
+}

+ 20 - 0
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
+}

+ 8 - 0
deploy/charts/external-secrets/templates/rbac.yaml

@@ -27,6 +27,14 @@ rules:
   - apiGroups:
     - ""
     resources:
+    - "serviceaccounts"
+    verbs:
+    - "get"
+    - "list"
+    - "watch"
+  - apiGroups:
+    - ""
+    resources:
     - "secrets"
     verbs:
     - "get"

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

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

+ 36 - 1
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()

+ 32 - 6
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
 					}),