Browse Source

Merge pull request #1176 from external-secrets/feat-vault-referant-auth

fix: loosen validation to enable referent auth
paul-the-alien[bot] 3 years ago
parent
commit
6cf51a046a

+ 7 - 0
apis/externalsecrets/v1beta1/provider.go

@@ -21,8 +21,15 @@ import (
 )
 
 const (
+	// Ready indicates that the client is confgured correctly
+	// and can be used.
 	ValidationResultReady ValidationResult = iota
+
+	// Unknown indicates that the client can be used
+	// but information is missing and it can not be validated.
 	ValidationResultUnknown
+
+	// Error indicates that there is a misconfiguration.
 	ValidationResultError
 )
 

+ 46 - 6
docs/spec.md

@@ -1552,14 +1552,16 @@ string
 </tr>
 <tr>
 <td>
-<code>version</code></br>
+<code>metadataPolicy</code></br>
 <em>
-string
+<a href="#external-secrets.io/v1beta1.ExternalSecretMetadataPolicy">
+ExternalSecretMetadataPolicy
+</a>
 </em>
 </td>
 <td>
 <em>(Optional)</em>
-<p>Used to select a specific version of the Provider value, if supported</p>
+<p>Policy for fetching tags/labels from provider secrets, possible options are Fetch, None. Defaults to None</p>
 </td>
 </tr>
 <tr>
@@ -1576,6 +1578,18 @@ string
 </tr>
 <tr>
 <td>
+<code>version</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Used to select a specific version of the Provider value, if supported</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>conversionStrategy</code></br>
 <em>
 <a href="#external-secrets.io/v1beta1.ExternalSecretConversionStrategy">
@@ -1695,6 +1709,27 @@ ExternalSecretConversionStrategy
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1beta1.ExternalSecretMetadataPolicy">ExternalSecretMetadataPolicy
+(<code>string</code> alias)</p></h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.ExternalSecretDataRemoteRef">ExternalSecretDataRemoteRef</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Value</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody><tr><td><p>&#34;Fetch&#34;</p></td>
+<td></td>
+</tr><tr><td><p>&#34;None&#34;</p></td>
+<td></td>
+</tr></tbody>
+</table>
 <h3 id="external-secrets.io/v1beta1.ExternalSecretSpec">ExternalSecretSpec
 </h3>
 <p>
@@ -4075,11 +4110,16 @@ github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
 </tr>
 </thead>
 <tbody><tr><td><p>2</p></td>
-<td></td>
+<td><p>Error indicates that there is a misconfiguration.</p>
+</td>
 </tr><tr><td><p>0</p></td>
-<td></td>
+<td><p>Ready indicates that the client is confgured correctly
+and can be used.</p>
+</td>
 </tr><tr><td><p>1</p></td>
-<td></td>
+<td><p>Unknown indicates that the client can be used
+but information is missing and it can not be validated.</p>
+</td>
 </tr></tbody>
 </table>
 <h3 id="external-secrets.io/v1beta1.VaultAppRole">VaultAppRole

+ 42 - 0
e2e/suites/provider/cases/vault/provider.go

@@ -48,6 +48,8 @@ const (
 	jwtProviderSecretName   = "jwt-provider-credentials"
 	jwtK8sProviderName      = "jwt-k8s-provider"
 	kubernetesProviderName  = "kubernetes-provider"
+	referentSecretName      = "referent-secret"
+	referentKey             = "referent-secret-key"
 )
 
 var (
@@ -99,6 +101,7 @@ func (s *vaultProvider) BeforeEach() {
 	s.CreateJWTStore(v, ns)
 	s.CreateJWTK8sStore(v, ns)
 	s.CreateKubernetesAuthStore(v, ns)
+	s.CreateReferentTokenStore(v, ns)
 }
 
 func makeStore(name, ns string, v *addon.Vault) *esv1beta1.SecretStore {
@@ -120,6 +123,14 @@ func makeStore(name, ns string, v *addon.Vault) *esv1beta1.SecretStore {
 	}
 }
 
+func makeClusterStore(name, ns string, v *addon.Vault) *esv1beta1.ClusterSecretStore {
+	store := makeStore(name, ns, v)
+	return &esv1beta1.ClusterSecretStore{
+		ObjectMeta: store.ObjectMeta,
+		Spec:       store.Spec,
+	}
+}
+
 func (s *vaultProvider) CreateCertStore(v *addon.Vault, ns string) {
 	By("creating a vault secret")
 	clientCert := v.ClientCert
@@ -179,6 +190,33 @@ func (s vaultProvider) CreateTokenStore(v *addon.Vault, ns string) {
 	Expect(err).ToNot(HaveOccurred())
 }
 
+// CreateReferentTokenStore creates a secret in the ExternalSecrets
+// namespace and creates a ClusterSecretStore with an empty namespace
+// that can be used to test the referent namespace feature.
+func (s vaultProvider) CreateReferentTokenStore(v *addon.Vault, ns string) {
+	referentSecret := &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      referentSecretName,
+			Namespace: s.framework.Namespace.Name,
+		},
+		Data: map[string][]byte{
+			referentKey: []byte(v.RootToken),
+		},
+	}
+	_, err := s.framework.KubeClientSet.CoreV1().Secrets(s.framework.Namespace.Name).Create(context.Background(), referentSecret, metav1.CreateOptions{})
+	Expect(err).ToNot(HaveOccurred())
+
+	secretStore := makeClusterStore(referentSecretStoreName(s.framework), ns, v)
+	secretStore.Spec.Provider.Vault.Auth = esv1beta1.VaultAuth{
+		TokenSecretRef: &esmeta.SecretKeySelector{
+			Name: referentSecretName,
+			Key:  referentKey,
+		},
+	}
+	err = s.framework.CRClient.Create(context.Background(), secretStore)
+	Expect(err).ToNot(HaveOccurred())
+}
+
 func (s vaultProvider) CreateAppRoleStore(v *addon.Vault, ns string) {
 	By("creating a vault secret")
 	vaultCreds := &v1.Secret{
@@ -296,3 +334,7 @@ func (s vaultProvider) CreateKubernetesAuthStore(v *addon.Vault, ns string) {
 	err := s.framework.CRClient.Create(context.Background(), secretStore)
 	Expect(err).ToNot(HaveOccurred())
 }
+
+func referentSecretStoreName(f *framework.Framework) string {
+	return "referent-provider-" + f.Namespace.Name
+}

+ 15 - 7
e2e/suites/provider/cases/vault/vault.go

@@ -25,13 +25,14 @@ import (
 )
 
 const (
-	withTokenAuth = "with token auth"
-	withCertAuth  = "with cert auth"
-	withApprole   = "with approle auth"
-	withV1        = "with v1 provider"
-	withJWT       = "with jwt provider"
-	withJWTK8s    = "with jwt k8s provider"
-	withK8s       = "with kubernetes provider"
+	withTokenAuth    = "with token auth"
+	withCertAuth     = "with cert auth"
+	withApprole      = "with approle auth"
+	withV1           = "with v1 provider"
+	withJWT          = "with jwt provider"
+	withJWTK8s       = "with jwt k8s provider"
+	withK8s          = "with kubernetes provider"
+	withReferentAuth = "with referent provider"
 )
 
 var _ = Describe("[vault]", Label("vault"), func() {
@@ -88,6 +89,8 @@ var _ = Describe("[vault]", Label("vault"), func() {
 		framework.Compose(withK8s, f, common.JSONDataWithTemplate, useKubernetesProvider),
 		framework.Compose(withK8s, f, common.DataPropertyDockerconfigJSON, useKubernetesProvider),
 		framework.Compose(withK8s, f, common.JSONDataWithoutTargetName, useKubernetesProvider),
+		// use referent auth
+		framework.Compose(withReferentAuth, f, common.JSONDataFromSync, useReferentAuth),
 		// vault-specific test cases
 		Entry("secret value via data without property should return json-encoded string", Label("json"), testJSONWithoutProperty),
 		Entry("secret value via data with property should return json-encoded string", Label("json"), testJSONWithProperty),
@@ -124,6 +127,11 @@ func useKubernetesProvider(tc *framework.TestCase) {
 	tc.ExternalSecret.Spec.SecretStoreRef.Name = kubernetesProviderName
 }
 
+func useReferentAuth(tc *framework.TestCase) {
+	tc.ExternalSecret.Spec.SecretStoreRef.Name = referentSecretStoreName(tc.Framework)
+	tc.ExternalSecret.Spec.SecretStoreRef.Kind = esapi.ClusterSecretStoreKind
+}
+
 const jsonVal = `{"foo":{"nested":{"bar":"mysecret","baz":"bang"}}}`
 
 // when no property is set it should return the json-encoded at path.

+ 1 - 0
pkg/controllers/secretstore/common.go

@@ -64,6 +64,7 @@ func reconcile(ctx context.Context, req ctrl.Request, ss esapi.GenericStore, cl
 	log.V(1).Info("validating")
 	err := validateStore(ctx, req.Namespace, ss, cl, recorder)
 	if err != nil {
+		log.Error(err, "unable to validate store")
 		return ctrl.Result{}, err
 	}
 

+ 40 - 9
pkg/provider/vault/vault.go

@@ -277,6 +277,11 @@ func (c *connector) newClient(ctx context.Context, store esv1beta1.GenericStore,
 	vStore.logical = client.Logical()
 	vStore.token = client.AuthToken()
 
+	// allow SecretStore controller validation to pass
+	// when using referent namespace.
+	if vStore.storeKind == esv1beta1.ClusterSecretStoreKind && vStore.namespace == "" {
+		return vStore, nil
+	}
 	if err := vStore.setAuth(ctx, cfg); err != nil {
 		return nil, err
 	}
@@ -300,25 +305,25 @@ func (c *connector) ValidateStore(store esv1beta1.GenericStore) error {
 		return fmt.Errorf(errInvalidVaultProv)
 	}
 	if p.Auth.AppRole != nil {
-		if err := utils.ValidateSecretSelector(store, p.Auth.AppRole.SecretRef); err != nil {
+		if err := utils.ValidateReferentSecretSelector(store, p.Auth.AppRole.SecretRef); err != nil {
 			return fmt.Errorf(errInvalidAppRoleSec, err)
 		}
 	}
 	if p.Auth.Cert != nil {
-		if err := utils.ValidateSecretSelector(store, p.Auth.Cert.ClientCert); err != nil {
+		if err := utils.ValidateReferentSecretSelector(store, p.Auth.Cert.ClientCert); err != nil {
 			return fmt.Errorf(errInvalidClientCert, err)
 		}
-		if err := utils.ValidateSecretSelector(store, p.Auth.Cert.SecretRef); err != nil {
+		if err := utils.ValidateReferentSecretSelector(store, p.Auth.Cert.SecretRef); err != nil {
 			return fmt.Errorf(errInvalidCertSec, err)
 		}
 	}
 	if p.Auth.Jwt != nil {
 		if p.Auth.Jwt.SecretRef != nil {
-			if err := utils.ValidateSecretSelector(store, *p.Auth.Jwt.SecretRef); err != nil {
+			if err := utils.ValidateReferentSecretSelector(store, *p.Auth.Jwt.SecretRef); err != nil {
 				return fmt.Errorf(errInvalidJwtSec, err)
 			}
 		} else if p.Auth.Jwt.KubernetesServiceAccountToken != nil {
-			if err := utils.ValidateServiceAccountSelector(store, p.Auth.Jwt.KubernetesServiceAccountToken.ServiceAccountRef); err != nil {
+			if err := utils.ValidateReferentServiceAccountSelector(store, p.Auth.Jwt.KubernetesServiceAccountToken.ServiceAccountRef); err != nil {
 				return fmt.Errorf(errInvalidJwtK8sSA, err)
 			}
 		} else {
@@ -327,23 +332,23 @@ func (c *connector) ValidateStore(store esv1beta1.GenericStore) error {
 	}
 	if p.Auth.Kubernetes != nil {
 		if p.Auth.Kubernetes.ServiceAccountRef != nil {
-			if err := utils.ValidateServiceAccountSelector(store, *p.Auth.Kubernetes.ServiceAccountRef); err != nil {
+			if err := utils.ValidateReferentServiceAccountSelector(store, *p.Auth.Kubernetes.ServiceAccountRef); err != nil {
 				return fmt.Errorf(errInvalidKubeSA, err)
 			}
 		}
 		if p.Auth.Kubernetes.SecretRef != nil {
-			if err := utils.ValidateSecretSelector(store, *p.Auth.Kubernetes.SecretRef); err != nil {
+			if err := utils.ValidateReferentSecretSelector(store, *p.Auth.Kubernetes.SecretRef); err != nil {
 				return fmt.Errorf(errInvalidKubeSec, err)
 			}
 		}
 	}
 	if p.Auth.Ldap != nil {
-		if err := utils.ValidateSecretSelector(store, p.Auth.Ldap.SecretRef); err != nil {
+		if err := utils.ValidateReferentSecretSelector(store, p.Auth.Ldap.SecretRef); err != nil {
 			return fmt.Errorf(errInvalidLdapSec, err)
 		}
 	}
 	if p.Auth.TokenSecretRef != nil {
-		if err := utils.ValidateSecretSelector(store, *p.Auth.TokenSecretRef); err != nil {
+		if err := utils.ValidateReferentSecretSelector(store, *p.Auth.TokenSecretRef); err != nil {
 			return fmt.Errorf(errInvalidTokenRef, err)
 		}
 	}
@@ -581,6 +586,32 @@ func (v *client) Close(ctx context.Context) error {
 }
 
 func (v *client) Validate() (esv1beta1.ValidationResult, error) {
+	// when using referent namespace we can not validate the token
+	// because the namespace is not known yet when Validate() is called
+	// from the SecretStore controller.
+	if v.storeKind == esv1beta1.ClusterSecretStoreKind {
+		if v.store.Auth.TokenSecretRef != nil && v.store.Auth.TokenSecretRef.Namespace == nil {
+			return esv1beta1.ValidationResultUnknown, nil
+		}
+		if v.store.Auth.AppRole != nil && v.store.Auth.AppRole.SecretRef.Namespace == nil {
+			return esv1beta1.ValidationResultUnknown, nil
+		}
+		if v.store.Auth.Kubernetes != nil && v.store.Auth.Kubernetes.SecretRef != nil && v.store.Auth.Kubernetes.SecretRef.Namespace == nil {
+			return esv1beta1.ValidationResultUnknown, nil
+		}
+		if v.store.Auth.Kubernetes != nil && v.store.Auth.Kubernetes.ServiceAccountRef != nil && v.store.Auth.Kubernetes.ServiceAccountRef.Namespace == nil {
+			return esv1beta1.ValidationResultUnknown, nil
+		}
+		if v.store.Auth.Ldap != nil && v.store.Auth.Ldap.SecretRef.Namespace == nil {
+			return esv1beta1.ValidationResultUnknown, nil
+		}
+		if v.store.Auth.Jwt != nil && v.store.Auth.Jwt.SecretRef.Namespace == nil {
+			return esv1beta1.ValidationResultUnknown, nil
+		}
+		if v.store.Auth.Cert != nil && v.store.Auth.Cert.SecretRef.Namespace == nil {
+			return esv1beta1.ValidationResultUnknown, nil
+		}
+	}
 	_, err := checkToken(context.Background(), v)
 	if err != nil {
 		return esv1beta1.ValidationResultError, fmt.Errorf(errInvalidCredentials, err)

+ 34 - 4
pkg/utils/utils.go

@@ -18,6 +18,7 @@ import (
 
 	// nolint:gosec
 	"crypto/md5"
+	"errors"
 	"fmt"
 	"net"
 	"net/url"
@@ -110,16 +111,33 @@ func ErrorContains(out error, want string) bool {
 	return strings.Contains(out.Error(), want)
 }
 
+var (
+	errNamespaceNotAllowed = errors.New("namespace not allowed with namespaced SecretStore")
+	errRequireNamespace    = errors.New("cluster scope requires namespace")
+)
+
 // ValidateSecretSelector just checks if the namespace field is present/absent
 // depending on the secret store type.
 // We MUST NOT check the name or key property here. It MAY be defaulted by the provider.
 func ValidateSecretSelector(store esv1beta1.GenericStore, ref esmeta.SecretKeySelector) error {
 	clusterScope := store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind
 	if clusterScope && ref.Namespace == nil {
-		return fmt.Errorf("cluster scope requires namespace")
+		return errRequireNamespace
+	}
+	if !clusterScope && ref.Namespace != nil {
+		return errNamespaceNotAllowed
 	}
+	return nil
+}
+
+// ValidateReferentSecretSelector allows
+// cluster scoped store without namespace
+// this should replace above ValidateServiceAccountSelector once all providers
+// support referent auth.
+func ValidateReferentSecretSelector(store esv1beta1.GenericStore, ref esmeta.SecretKeySelector) error {
+	clusterScope := store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind
 	if !clusterScope && ref.Namespace != nil {
-		return fmt.Errorf("namespace not allowed with namespaced SecretStore")
+		return errNamespaceNotAllowed
 	}
 	return nil
 }
@@ -130,10 +148,22 @@ func ValidateSecretSelector(store esv1beta1.GenericStore, ref esmeta.SecretKeySe
 func ValidateServiceAccountSelector(store esv1beta1.GenericStore, ref esmeta.ServiceAccountSelector) error {
 	clusterScope := store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind
 	if clusterScope && ref.Namespace == nil {
-		return fmt.Errorf("cluster scope requires namespace")
+		return errRequireNamespace
+	}
+	if !clusterScope && ref.Namespace != nil {
+		return errNamespaceNotAllowed
 	}
+	return nil
+}
+
+// ValidateReferentServiceAccountSelector allows
+// cluster scoped store without namespace
+// this should replace above ValidateServiceAccountSelector once all providers
+// support referent auth.
+func ValidateReferentServiceAccountSelector(store esv1beta1.GenericStore, ref esmeta.ServiceAccountSelector) error {
+	clusterScope := store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind
 	if !clusterScope && ref.Namespace != nil {
-		return fmt.Errorf("namespace not allowed with namespaced SecretStore")
+		return errNamespaceNotAllowed
 	}
 	return nil
 }