Browse Source

:sparkles: implement azure referent auth (#1886)

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Moritz Johner 3 years ago
parent
commit
736b287b6d

+ 2 - 2
docs/introduction/stability-support.md

@@ -48,9 +48,9 @@ The following table show the support for features across different providers.
 |---------------------------|:------------:|:------------:| :------------------: | :---------------------: | :--------------: | :---------: |
 | AWS Secrets Manager       |      x       |      x       |                      |            x            |        x         |             |
 | AWS Parameter Store       |      x       |      x       |                      |            x            |        x         |             |
-| Hashicorp Vault           |      x       |      x       |                      |                         |        x         |             |
+| Hashicorp Vault           |      x       |      x       |                      |            x            |        x         |             |
 | GCP Secret Manager        |      x       |      x       |                      |            x            |        x         |             |
-| Azure Keyvault            |      x       |      x       |          x           |            x            |        x         |     x        |
+| Azure Keyvault            |      x       |      x       |          x           |            x            |        x         |      x      |
 | Kubernetes                |      x       |      x       |                      |            x            |        x         |             |
 | IBM Cloud Secrets Manager |              |              |                      |                         |        x         |             |
 | Yandex Lockbox            |              |              |                      |                         |        x         |             |

+ 33 - 13
e2e/suites/provider/cases/azure/azure_secret.go

@@ -19,6 +19,12 @@ import (
 
 	"github.com/external-secrets/external-secrets-e2e/framework"
 	"github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common"
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+)
+
+const (
+	withStaticCredentials = "with static credentials"
+	withReferentAuth      = "with referent auth"
 )
 
 // keyvault type=secret should behave just like any other secret store.
@@ -27,18 +33,32 @@ var _ = Describe("[azure]", Label("azure", "keyvault", "secret"), func() {
 	prov := newFromEnv(f)
 
 	DescribeTable("sync secrets", framework.TableFunc(f, prov),
-		Entry(common.SimpleDataSync(f)),
-		Entry(common.NestedJSONWithGJSON(f)),
-		Entry(common.JSONDataFromSync(f)),
-		Entry(common.JSONDataFromRewrite(f)),
-		Entry(common.JSONDataWithProperty(f)),
-		Entry(common.JSONDataWithTemplate(f)),
-		Entry(common.DockerJSONConfig(f)),
-		Entry(common.DataPropertyDockerconfigJSON(f)),
-		Entry(common.SSHKeySync(f)),
-		Entry(common.SSHKeySyncDataProperty(f)),
-		Entry(common.SyncWithoutTargetName(f)),
-		Entry(common.JSONDataWithoutTargetName(f)),
-		Entry(common.SyncV1Alpha1(f)),
+		framework.Compose(withStaticCredentials, f, common.SimpleDataSync, useStaticCredentials),
+		framework.Compose(withStaticCredentials, f, common.NestedJSONWithGJSON, useStaticCredentials),
+		framework.Compose(withStaticCredentials, f, common.JSONDataFromSync, useStaticCredentials),
+		framework.Compose(withStaticCredentials, f, common.JSONDataFromRewrite, useStaticCredentials),
+		framework.Compose(withStaticCredentials, f, common.JSONDataWithProperty, useStaticCredentials),
+		framework.Compose(withStaticCredentials, f, common.JSONDataWithTemplate, useStaticCredentials),
+		framework.Compose(withStaticCredentials, f, common.DockerJSONConfig, useStaticCredentials),
+		framework.Compose(withStaticCredentials, f, common.DataPropertyDockerconfigJSON, useStaticCredentials),
+		framework.Compose(withStaticCredentials, f, common.SSHKeySync, useStaticCredentials),
+		framework.Compose(withStaticCredentials, f, common.SSHKeySyncDataProperty, useStaticCredentials),
+		framework.Compose(withStaticCredentials, f, common.SyncWithoutTargetName, useStaticCredentials),
+		framework.Compose(withStaticCredentials, f, common.JSONDataWithoutTargetName, useStaticCredentials),
+		framework.Compose(withStaticCredentials, f, common.SyncV1Alpha1, useStaticCredentials),
+
+		framework.Compose(withStaticCredentials, f, common.SimpleDataSync, useReferentAuth),
 	)
 })
+
+func useStaticCredentials(tc *framework.TestCase) {
+	tc.ExternalSecret.Spec.SecretStoreRef.Name = tc.Framework.Namespace.Name
+	if tc.ExternalSecretV1Alpha1 != nil {
+		tc.ExternalSecretV1Alpha1.Spec.SecretStoreRef.Name = tc.Framework.Namespace.Name
+	}
+}
+
+func useReferentAuth(tc *framework.TestCase) {
+	tc.ExternalSecret.Spec.SecretStoreRef.Name = referentAuthName(tc.Framework)
+	tc.ExternalSecret.Spec.SecretStoreRef.Kind = esapi.ClusterSecretStoreKind
+}

+ 95 - 32
e2e/suites/provider/cases/azure/provider.go

@@ -34,8 +34,6 @@ import (
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
 )
 
-const providerSecretName = "provider-secret"
-
 type azureProvider struct {
 	clientID     string
 	clientSecret string
@@ -70,6 +68,7 @@ func newazureProvider(f *framework.Framework, clientID, clientSecret, tenantID,
 		})
 		prov.CreateSecretStoreWithWI()
 		prov.CreateSecretStore()
+		prov.CreateReferentSecretStore()
 	})
 
 	return prov
@@ -92,7 +91,7 @@ func (s *azureProvider) CreateSecret(key string, val framework.SecretEntry) {
 			Value: &val.Value,
 			SecretAttributes: &keyvault.SecretAttributes{
 				RecoveryLevel: keyvault.Purgeable,
-				Enabled:       utilpointer.BoolPtr(true),
+				Enabled:       utilpointer.Bool(true),
 			},
 		})
 	Expect(err).ToNot(HaveOccurred())
@@ -115,7 +114,7 @@ func (s *azureProvider) CreateKey(key string) *keyvault.JSONWebKey {
 			Kty: keyvault.RSA,
 			KeyAttributes: &keyvault.KeyAttributes{
 				RecoveryLevel: keyvault.Purgeable,
-				Enabled:       utilpointer.BoolPtr(true),
+				Enabled:       utilpointer.Bool(true),
 			},
 		},
 	)
@@ -144,12 +143,12 @@ func (s *azureProvider) CreateCertificate(key string) {
 				},
 				Attributes: &keyvault.CertificateAttributes{
 					RecoveryLevel: keyvault.Purgeable,
-					Enabled:       utilpointer.BoolPtr(true),
+					Enabled:       utilpointer.Bool(true),
 				},
 			},
 			CertificateAttributes: &keyvault.CertificateAttributes{
 				RecoveryLevel: keyvault.Purgeable,
-				Enabled:       utilpointer.BoolPtr(true),
+				Enabled:       utilpointer.Bool(true),
 			},
 		},
 	)
@@ -183,15 +182,52 @@ func (s *azureProvider) DeleteCertificate(key string) {
 	Expect(err).ToNot(HaveOccurred())
 }
 
+const (
+	staticSecretName                  = "provider-secret"
+	referentSecretName                = "referent-secret"
+	workloadIdentityServiceAccountNme = "external-secrets-operator"
+	credentialKeyClientID             = "client-id"
+	credentialKeyClientSecret         = "client-secret"
+)
+
+func newProviderWithStaticCredentials(tenantID, vaultURL, secretName string) *esv1beta1.AzureKVProvider {
+	return &esv1beta1.AzureKVProvider{
+		TenantID: &tenantID,
+		VaultURL: &vaultURL,
+		AuthSecretRef: &esv1beta1.AzureKVAuth{
+			ClientID: &esmeta.SecretKeySelector{
+				Name: staticSecretName,
+				Key:  credentialKeyClientID,
+			},
+			ClientSecret: &esmeta.SecretKeySelector{
+				Name: staticSecretName,
+				Key:  credentialKeyClientSecret,
+			},
+		},
+	}
+}
+
+func newProviderWithServiceAccount(tenantID, vaultURL string, authType esv1beta1.AzureAuthType, serviceAccountName string, serviceAccountNamespace *string) *esv1beta1.AzureKVProvider {
+	return &esv1beta1.AzureKVProvider{
+		TenantID: &tenantID,
+		VaultURL: &vaultURL,
+		AuthType: &authType,
+		ServiceAccountRef: &esmeta.ServiceAccountSelector{
+			Name:      serviceAccountName,
+			Namespace: serviceAccountNamespace,
+		},
+	}
+}
+
 func (s *azureProvider) CreateSecretStore() {
 	azureCreds := &v1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:      providerSecretName,
+			Name:      staticSecretName,
 			Namespace: s.framework.Namespace.Name,
 		},
 		StringData: map[string]string{
-			"client-id":     s.clientID,
-			"client-secret": s.clientSecret,
+			credentialKeyClientID:     s.clientID,
+			credentialKeyClientSecret: s.clientSecret,
 		},
 	}
 	err := s.framework.CRClient.Create(context.Background(), azureCreds)
@@ -203,20 +239,7 @@ func (s *azureProvider) CreateSecretStore() {
 		},
 		Spec: esv1beta1.SecretStoreSpec{
 			Provider: &esv1beta1.SecretStoreProvider{
-				AzureKV: &esv1beta1.AzureKVProvider{
-					TenantID: &s.tenantID,
-					VaultURL: &s.vaultURL,
-					AuthSecretRef: &esv1beta1.AzureKVAuth{
-						ClientID: &esmeta.SecretKeySelector{
-							Name: providerSecretName,
-							Key:  "client-id",
-						},
-						ClientSecret: &esmeta.SecretKeySelector{
-							Name: providerSecretName,
-							Key:  "client-secret",
-						},
-					},
-				},
+				AzureKV: newProviderWithStaticCredentials(s.tenantID, s.vaultURL, staticSecretName),
 			},
 		},
 	}
@@ -224,6 +247,38 @@ func (s *azureProvider) CreateSecretStore() {
 	Expect(err).ToNot(HaveOccurred())
 }
 
+func (s *azureProvider) CreateReferentSecretStore() {
+	azureCreds := &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      referentSecretName,
+			Namespace: s.framework.Namespace.Name,
+		},
+		StringData: map[string]string{
+			credentialKeyClientID:     s.clientID,
+			credentialKeyClientSecret: s.clientSecret,
+		},
+	}
+	err := s.framework.CRClient.Create(context.Background(), azureCreds)
+	Expect(err).ToNot(HaveOccurred())
+	secretStore := &esv1beta1.ClusterSecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      referentAuthName(s.framework),
+			Namespace: s.framework.Namespace.Name,
+		},
+		Spec: esv1beta1.SecretStoreSpec{
+			Provider: &esv1beta1.SecretStoreProvider{
+				AzureKV: newProviderWithStaticCredentials(s.tenantID, s.vaultURL, referentSecretName),
+			},
+		},
+	}
+	err = s.framework.CRClient.Create(context.Background(), secretStore)
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func referentAuthName(f *framework.Framework) string {
+	return "referent-auth-" + f.Namespace.Name
+}
+
 func (s *azureProvider) CreateSecretStoreWithWI() {
 	authType := esv1beta1.AzureWorkloadIdentity
 	namespace := "external-secrets-operator"
@@ -233,15 +288,23 @@ func (s *azureProvider) CreateSecretStoreWithWI() {
 		},
 		Spec: esv1beta1.SecretStoreSpec{
 			Provider: &esv1beta1.SecretStoreProvider{
-				AzureKV: &esv1beta1.AzureKVProvider{
-					TenantID: &s.tenantID,
-					VaultURL: &s.vaultURL,
-					AuthType: &authType,
-					ServiceAccountRef: &esmeta.ServiceAccountSelector{
-						Name:      "external-secrets-operator",
-						Namespace: &namespace,
-					},
-				},
+				AzureKV: newProviderWithServiceAccount(s.tenantID, s.vaultURL, authType, workloadIdentityServiceAccountNme, &namespace),
+			},
+		},
+	}
+	err := s.framework.CRClient.Create(context.Background(), ClusterSecretStore)
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func (s *azureProvider) CreateReferentSecretStoreWithWI() {
+	authType := esv1beta1.AzureWorkloadIdentity
+	ClusterSecretStore := &esv1beta1.ClusterSecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: referentAuthName(s.framework),
+		},
+		Spec: esv1beta1.SecretStoreSpec{
+			Provider: &esv1beta1.SecretStoreProvider{
+				AzureKV: newProviderWithServiceAccount(s.tenantID, s.vaultURL, authType, workloadIdentityServiceAccountNme, nil),
 			},
 		},
 	}

+ 34 - 8
pkg/provider/azure/keyvault/keyvault.go

@@ -152,6 +152,14 @@ func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Cl
 		provider:   provider,
 	}
 
+	// allow SecretStore controller validation to pass
+	// when using referent namespace.
+	if store.GetKind() == esv1beta1.ClusterSecretStoreKind &&
+		namespace == "" &&
+		isReferentSpec(provider) {
+		return az, nil
+	}
+
 	var authorizer autorest.Authorizer
 	switch *provider.AuthType {
 	case esv1beta1.AzureManagedIdentity:
@@ -197,18 +205,18 @@ func (a *Azure) ValidateStore(store esv1beta1.GenericStore) error {
 	}
 	if p.AuthSecretRef != nil {
 		if p.AuthSecretRef.ClientID != nil {
-			if err := utils.ValidateSecretSelector(store, *p.AuthSecretRef.ClientID); err != nil {
+			if err := utils.ValidateReferentSecretSelector(store, *p.AuthSecretRef.ClientID); err != nil {
 				return fmt.Errorf(errInvalidSecRefClientID, err)
 			}
 		}
 		if p.AuthSecretRef.ClientSecret != nil {
-			if err := utils.ValidateSecretSelector(store, *p.AuthSecretRef.ClientSecret); err != nil {
+			if err := utils.ValidateReferentSecretSelector(store, *p.AuthSecretRef.ClientSecret); err != nil {
 				return fmt.Errorf(errInvalidSecRefClientSecret, err)
 			}
 		}
 	}
 	if p.ServiceAccountRef != nil {
-		if err := utils.ValidateServiceAccountSelector(store, *p.ServiceAccountRef); err != nil {
+		if err := utils.ValidateReferentServiceAccountSelector(store, *p.ServiceAccountRef); err != nil {
 			return fmt.Errorf(errInvalidSARef, err)
 		}
 	}
@@ -722,7 +730,7 @@ func (a *Azure) authorizerForWorkloadIdentity(ctx context.Context, tokenProvider
 		return autorest.NewBearerAuthorizer(tp), nil
 	}
 	ns := a.namespace
-	if a.store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
+	if a.store.GetKind() == esv1beta1.ClusterSecretStoreKind && a.provider.ServiceAccountRef.Namespace != nil {
 		ns = *a.provider.ServiceAccountRef.Namespace
 	}
 	var sa corev1.ServiceAccount
@@ -826,14 +834,14 @@ func (a *Azure) authorizerForServicePrincipal(ctx context.Context) (autorest.Aut
 		return nil, fmt.Errorf(errMissingClientIDSecret)
 	}
 	clusterScoped := false
-	if a.store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
+	if a.store.GetKind() == esv1beta1.ClusterSecretStoreKind {
 		clusterScoped = true
 	}
-	cid, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *a.provider.AuthSecretRef.ClientID, clusterScoped)
+	cid, err := a.secretKeyRef(ctx, a.namespace, *a.provider.AuthSecretRef.ClientID, clusterScoped)
 	if err != nil {
 		return nil, err
 	}
-	csec, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *a.provider.AuthSecretRef.ClientSecret, clusterScoped)
+	csec, err := a.secretKeyRef(ctx, a.namespace, *a.provider.AuthSecretRef.ClientSecret, clusterScoped)
 	if err != nil {
 		return nil, err
 	}
@@ -847,8 +855,8 @@ func (a *Azure) authorizerForServicePrincipal(ctx context.Context) (autorest.Aut
 func (a *Azure) secretKeyRef(ctx context.Context, namespace string, secretRef smmeta.SecretKeySelector, clusterScoped bool) (string, error) {
 	var secret corev1.Secret
 	ref := types.NamespacedName{
-		Namespace: namespace,
 		Name:      secretRef.Name,
+		Namespace: namespace,
 	}
 	if clusterScoped && secretRef.Namespace != nil {
 		ref.Namespace = *secretRef.Namespace
@@ -870,9 +878,27 @@ func (a *Azure) Close(ctx context.Context) error {
 }
 
 func (a *Azure) Validate() (esv1beta1.ValidationResult, error) {
+	if a.store.GetKind() == esv1beta1.ClusterSecretStoreKind && isReferentSpec(a.provider) {
+		return esv1beta1.ValidationResultUnknown, nil
+	}
 	return esv1beta1.ValidationResultReady, nil
 }
 
+func isReferentSpec(prov *esv1beta1.AzureKVProvider) bool {
+	if prov.AuthSecretRef != nil &&
+		((prov.AuthSecretRef.ClientID != nil &&
+			prov.AuthSecretRef.ClientID.Namespace == nil) ||
+			(prov.AuthSecretRef.ClientSecret != nil &&
+				prov.AuthSecretRef.ClientSecret.Namespace == nil)) {
+		return true
+	}
+	if prov.ServiceAccountRef != nil &&
+		prov.ServiceAccountRef.Namespace == nil {
+		return true
+	}
+	return false
+}
+
 func AadEndpointForType(t esv1beta1.AzureEnvironmentType) string {
 	switch t {
 	case esv1beta1.AzureEnvironmentPublicCloud: