Browse Source

Merge pull request #514 from vazul/azure_managed_identity

Supporting Managed Identity authentication for Azure Keyvault
paul-the-alien[bot] 4 years ago
parent
commit
1e9ba0ceb5

+ 31 - 4
apis/externalsecrets/v1alpha1/secretstore_azurekv_types.go

@@ -16,14 +16,41 @@ package v1alpha1
 
 import smmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
 
+// AuthType describes how to authenticate to the Azure Keyvault
+// Only one of the following auth types may be specified.
+// If none of the following auth type is specified, the default one
+// is ServicePrincipal.
+// +kubebuilder:validation:Enum=ServicePrincipal;ManagedIdentity
+type AuthType string
+
+const (
+	// Using service principal to authenticate, which needs a tenantId, a clientId and a clientSecret.
+	ServicePrincipal AuthType = "ServicePrincipal"
+
+	// Using Managed Identity to authenticate. Used with aad-pod-identity instelled in the clister.
+	ManagedIdentity AuthType = "ManagedIdentity"
+)
+
 // Configures an store to sync secrets using Azure KV.
 type AzureKVProvider struct {
+	// Auth type defines how to authenticate to the keyvault service.
+	// Valid values are:
+	// - "ServicePrincipal" (default): Using a service principal (tenantId, clientId, clientSecret)
+	// - "ManagedIdentity": Using Managed Identity assigned to the pod (see aad-pod-identity)
+	// +optional
+	// +kubebuilder:default=ServicePrincipal
+	AuthType *AuthType `json:"authType,omitempty"`
 	// Vault Url from which the secrets to be fetched from.
 	VaultURL *string `json:"vaultUrl"`
-	// TenantID configures the Azure Tenant to send requests to.
-	TenantID *string `json:"tenantId"`
-	// Auth configures how the operator authenticates with Azure.
-	AuthSecretRef *AzureKVAuth `json:"authSecretRef"`
+	// TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
+	// +optional
+	TenantID *string `json:"tenantId,omitempty"`
+	// Auth configures how the operator authenticates with Azure. Required for ServicePrincipal auth type.
+	// +optional
+	AuthSecretRef *AzureKVAuth `json:"authSecretRef,omitempty"`
+	// If multiple Managed Identity is assigned to the pod, you can select the one to be used
+	// +optional
+	IdentityID *string `json:"identityId,omitempty"`
 }
 
 // Configuration used to authenticate with Azure.

+ 10 - 0
apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go

@@ -242,6 +242,11 @@ func (in *AzureKVAuth) DeepCopy() *AzureKVAuth {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *AzureKVProvider) DeepCopyInto(out *AzureKVProvider) {
 	*out = *in
+	if in.AuthType != nil {
+		in, out := &in.AuthType, &out.AuthType
+		*out = new(AuthType)
+		**out = **in
+	}
 	if in.VaultURL != nil {
 		in, out := &in.VaultURL, &out.VaultURL
 		*out = new(string)
@@ -257,6 +262,11 @@ func (in *AzureKVProvider) DeepCopyInto(out *AzureKVProvider) {
 		*out = new(AzureKVAuth)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.IdentityID != nil {
+		in, out := &in.IdentityID, &out.IdentityID
+		*out = new(string)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureKVProvider.

+ 17 - 4
deploy/crds/external-secrets.io_clustersecretstores.yaml

@@ -310,7 +310,7 @@ spec:
                     properties:
                       authSecretRef:
                         description: Auth configures how the operator authenticates
-                          with Azure.
+                          with Azure. Required for ServicePrincipal auth type.
                         properties:
                           clientId:
                             description: The Azure clientId of the service principle
@@ -354,17 +354,30 @@ spec:
                         - clientId
                         - clientSecret
                         type: object
+                      authType:
+                        default: ServicePrincipal
+                        description: 'Auth type defines how to authenticate to the
+                          keyvault service. Valid values are: - "ServicePrincipal"
+                          (default): Using a service principal (tenantId, clientId,
+                          clientSecret) - "ManagedIdentity": Using Managed Identity
+                          assigned to the pod (see aad-pod-identity)'
+                        enum:
+                        - ServicePrincipal
+                        - ManagedIdentity
+                        type: string
+                      identityId:
+                        description: If multiple Managed Identity is assigned to the
+                          pod, you can select the one to be used
+                        type: string
                       tenantId:
                         description: TenantID configures the Azure Tenant to send
-                          requests to.
+                          requests to. Required for ServicePrincipal auth type.
                         type: string
                       vaultUrl:
                         description: Vault Url from which the secrets to be fetched
                           from.
                         type: string
                     required:
-                    - authSecretRef
-                    - tenantId
                     - vaultUrl
                     type: object
                   gcpsm:

+ 17 - 4
deploy/crds/external-secrets.io_secretstores.yaml

@@ -310,7 +310,7 @@ spec:
                     properties:
                       authSecretRef:
                         description: Auth configures how the operator authenticates
-                          with Azure.
+                          with Azure. Required for ServicePrincipal auth type.
                         properties:
                           clientId:
                             description: The Azure clientId of the service principle
@@ -354,17 +354,30 @@ spec:
                         - clientId
                         - clientSecret
                         type: object
+                      authType:
+                        default: ServicePrincipal
+                        description: 'Auth type defines how to authenticate to the
+                          keyvault service. Valid values are: - "ServicePrincipal"
+                          (default): Using a service principal (tenantId, clientId,
+                          clientSecret) - "ManagedIdentity": Using Managed Identity
+                          assigned to the pod (see aad-pod-identity)'
+                        enum:
+                        - ServicePrincipal
+                        - ManagedIdentity
+                        type: string
+                      identityId:
+                        description: If multiple Managed Identity is assigned to the
+                          pod, you can select the one to be used
+                        type: string
                       tenantId:
                         description: TenantID configures the Azure Tenant to send
-                          requests to.
+                          requests to. Required for ServicePrincipal auth type.
                         type: string
                       vaultUrl:
                         description: Vault Url from which the secrets to be fetched
                           from.
                         type: string
                     required:
-                    - authSecretRef
-                    - tenantId
                     - vaultUrl
                     type: object
                   gcpsm:

+ 16 - 2
docs/provider-azure-key-vault.md

@@ -7,23 +7,37 @@ External Secrets Operator integrates with [Azure Key vault](https://azure.micros
 
 ### Authentication
 
-At the moment, we only support [service principals](https://docs.microsoft.com/en-us/azure/key-vault/general/authentication) authentication.
+We support Service Principals and Managed Identity [authentication](https://docs.microsoft.com/en-us/azure/key-vault/general/authentication).
+
+To use Managed Identity authentication, you should use [aad-pod-identity](https://azure.github.io/aad-pod-identity/docs/) to assign the identity to external-secrets operator. To add the selector to external-secrets operator, use `podLabels` in your values.yaml in case of Helm installation of external-secrets.
 
 #### Service Principal key authentication
 
 A service Principal client and Secret is created and the JSON keyfile is stored in a `Kind=Secret`. The `ClientID` and `ClientSecret` should be configured for the secret. This service principal should have proper access rights to the keyvault to be managed by the operator
 
+#### Managed Identity authentication
+
+A Managed Identity should be created in Azure, and that Identity should have proper rights to the keyvault to be managed by the operator.
+
+If there are multiple Managed Identitites for different keyvaults, the operator should have been assigned all identities via [aad-pod-identity](https://azure.github.io/aad-pod-identity/docs/), then the SecretStore configuration should include the Id of the idenetity to be used via the `identityId` field.
+
 ```yaml
 {% include 'azkv-credentials-secret.yaml' %}
 ```
 
 ### Update secret store
-Be sure the `azkv` provider is listed in the `Kind=SecretStore`
+Be sure the `azurekv` provider is listed in the `Kind=SecretStore`
 
 ```yaml
 {% include 'azkv-secret-store.yaml' %}
 ```
 
+Or in case of Managed Idenetity authentication:
+
+```yaml
+{% include 'azkv-secret-store-mi.yaml' %}
+```
+
 ### Object Types
 
 Azure KeyVault manages different [object types](https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#object-types), we support `keys`, `secrets` and `certificates`. Simply prefix the key with `key`, `secret` or `cert` to retrieve the desired type (defaults to secret).

+ 13 - 0
docs/snippets/azkv-secret-store-mi.yaml

@@ -0,0 +1,13 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: SecretStore
+metadata:
+  name: example-secret-store
+spec:
+  provider:
+    # provider type: azure keyvault
+    azurekv:
+      authType: ManagedIdentity
+      # Optionally set the Id of the Managed Identity, if multiple identities is assignet to external-secrets operator
+      identityId: "<MI_clientId>"
+      # URL of your vault instance, see: https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates
+      vaultUrl: "https://my-keyvault-name.vault.azure.net"

+ 57 - 20
pkg/provider/azure/keyvault/keyvault.go

@@ -35,6 +35,7 @@ import (
 
 const (
 	defaultObjType = "secret"
+	vaultResource  = "https://vault.azure.net"
 )
 
 // Provider satisfies the provider interface.
@@ -74,15 +75,18 @@ func newClient(ctx context.Context, store esv1alpha1.GenericStore, kube client.C
 		store:     store,
 		namespace: namespace,
 	}
-	azClient, vaultURL, err := anAzure.newAzureClient(ctx)
 
-	if err != nil {
-		return nil, err
+	clientSet, err := anAzure.setAzureClientWithManagedIdentity()
+	if clientSet {
+		return anAzure, err
+	}
+
+	clientSet, err = anAzure.setAzureClientWithServicePrincipal(ctx)
+	if clientSet {
+		return anAzure, err
 	}
 
-	anAzure.baseClient = azClient
-	anAzure.vaultURL = vaultURL
-	return anAzure, nil
+	return nil, fmt.Errorf("cannot initialize Azure Client: no valid authType was specified")
 }
 
 // Implements store.Client.GetSecret Interface.
@@ -168,42 +172,75 @@ func (a *Azure) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretD
 	return nil, fmt.Errorf("unknown Azure Keyvault object Type for %s", secretName)
 }
 
-func (a *Azure) newAzureClient(ctx context.Context) (*keyvault.BaseClient, string, error) {
+func (a *Azure) setAzureClientWithManagedIdentity() (bool, error) {
 	spec := *a.store.GetSpec().Provider.AzureKV
-	tenantID := *spec.TenantID
-	vaultURL := *spec.VaultURL
 
+	if *spec.AuthType != esv1alpha1.ManagedIdentity {
+		return false, nil
+	}
+
+	msiConfig := kvauth.NewMSIConfig()
+	msiConfig.Resource = vaultResource
+	if spec.IdentityID != nil {
+		msiConfig.ClientID = *spec.IdentityID
+	}
+	authorizer, err := msiConfig.Authorizer()
+	if err != nil {
+		return true, err
+	}
+
+	basicClient := keyvault.New()
+	basicClient.Authorizer = authorizer
+
+	a.baseClient = basicClient
+	a.vaultURL = *spec.VaultURL
+
+	return true, nil
+}
+
+func (a *Azure) setAzureClientWithServicePrincipal(ctx context.Context) (bool, error) {
+	spec := *a.store.GetSpec().Provider.AzureKV
+
+	if *spec.AuthType != esv1alpha1.ServicePrincipal {
+		return false, nil
+	}
+
+	if spec.TenantID == nil {
+		return true, fmt.Errorf("missing tenantID in store config")
+	}
 	if spec.AuthSecretRef == nil {
-		return nil, "", fmt.Errorf("missing clientID/clientSecret in store config")
+		return true, fmt.Errorf("missing clientID/clientSecret in store config")
+	}
+	if spec.AuthSecretRef.ClientID == nil || spec.AuthSecretRef.ClientSecret == nil {
+		return true, fmt.Errorf("missing accessKeyID/secretAccessKey in store config")
 	}
 	clusterScoped := false
 	if a.store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
 		clusterScoped = true
 	}
-	if spec.AuthSecretRef.ClientID == nil || spec.AuthSecretRef.ClientSecret == nil {
-		return nil, "", fmt.Errorf("missing accessKeyID/secretAccessKey in store config")
-	}
 	cid, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *spec.AuthSecretRef.ClientID, clusterScoped)
 	if err != nil {
-		return nil, "", err
+		return true, err
 	}
 	csec, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *spec.AuthSecretRef.ClientSecret, clusterScoped)
 	if err != nil {
-		return nil, "", err
+		return true, err
 	}
 
-	clientCredentialsConfig := kvauth.NewClientCredentialsConfig(cid, csec, tenantID)
-	// the default resource api is the management URL and not the vault URL which we need for keyvault operations
-	clientCredentialsConfig.Resource = "https://vault.azure.net"
+	clientCredentialsConfig := kvauth.NewClientCredentialsConfig(cid, csec, *spec.TenantID)
+	clientCredentialsConfig.Resource = vaultResource
 	authorizer, err := clientCredentialsConfig.Authorizer()
 	if err != nil {
-		return nil, "", err
+		return true, err
 	}
 
 	basicClient := keyvault.New()
 	basicClient.Authorizer = authorizer
 
-	return &basicClient, vaultURL, nil
+	a.baseClient = &basicClient
+	a.vaultURL = *spec.VaultURL
+
+	return true, nil
 }
 
 func (a *Azure) secretKeyRef(ctx context.Context, namespace string, secretRef smmeta.SecretKeySelector, clusterScoped bool) (string, error) {

+ 36 - 10
pkg/provider/azure/keyvault/keyvault_test.go

@@ -39,15 +39,46 @@ func newAzure() (Azure, *fake.AzureMock) {
 	return testAzure, azureMock
 }
 
+func TestNewClientManagedIdentityNoNeedForCredentials(t *testing.T) {
+	namespace := "internal"
+	vaultURL := "https://local.vault.url"
+	identityID := "1234"
+	authType := esv1alpha1.ManagedIdentity
+	store := esv1alpha1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+		},
+		Spec: esv1alpha1.SecretStoreSpec{Provider: &esv1alpha1.SecretStoreProvider{AzureKV: &esv1alpha1.AzureKVProvider{
+			AuthType:   &authType,
+			IdentityID: &identityID,
+			VaultURL:   &vaultURL,
+		}}},
+	}
+
+	provider, err := schema.GetProvider(&store)
+	tassert.Nil(t, err, "the return err should be nil")
+	k8sClient := clientfake.NewClientBuilder().Build()
+	secretClient, err := provider.NewClient(context.Background(), &store, k8sClient, namespace)
+	if err != nil {
+		// On non Azure environment, MSI auth not available, so this error should be returned
+		tassert.EqualError(t, err, "failed to get oauth token from MSI: MSI not available")
+	} else {
+		// On Azure (where GitHub Actions are running) a secretClient is returned, as only an Authorizer is configured, but no token is requested for MI
+		tassert.NotNil(t, secretClient)
+	}
+}
+
 func TestNewClientNoCreds(t *testing.T) {
 	namespace := "internal"
 	vaultURL := "https://local.vault.url"
 	tenantID := "1234"
+	authType := esv1alpha1.ServicePrincipal
 	store := esv1alpha1.SecretStore{
 		ObjectMeta: metav1.ObjectMeta{
 			Namespace: namespace,
 		},
 		Spec: esv1alpha1.SecretStoreSpec{Provider: &esv1alpha1.SecretStoreProvider{AzureKV: &esv1alpha1.AzureKVProvider{
+			AuthType: &authType,
 			VaultURL: &vaultURL,
 			TenantID: &tenantID,
 		}}},
@@ -55,32 +86,27 @@ func TestNewClientNoCreds(t *testing.T) {
 	provider, err := schema.GetProvider(&store)
 	tassert.Nil(t, err, "the return err should be nil")
 	k8sClient := clientfake.NewClientBuilder().Build()
-	secretClient, err := provider.NewClient(context.Background(), &store, k8sClient, namespace)
+	_, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
 	tassert.EqualError(t, err, "missing clientID/clientSecret in store config")
-	tassert.Nil(t, secretClient)
 
 	store.Spec.Provider.AzureKV.AuthSecretRef = &esv1alpha1.AzureKVAuth{}
-	secretClient, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
+	_, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
 	tassert.EqualError(t, err, "missing accessKeyID/secretAccessKey in store config")
-	tassert.Nil(t, secretClient)
 
 	store.Spec.Provider.AzureKV.AuthSecretRef.ClientID = &v1.SecretKeySelector{Name: "user"}
-	secretClient, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
+	_, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
 	tassert.EqualError(t, err, "missing accessKeyID/secretAccessKey in store config")
-	tassert.Nil(t, secretClient)
 
 	store.Spec.Provider.AzureKV.AuthSecretRef.ClientSecret = &v1.SecretKeySelector{Name: "password"}
-	secretClient, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
+	_, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
 	tassert.EqualError(t, err, "could not find secret internal/user: secrets \"user\" not found")
-	tassert.Nil(t, secretClient)
 	store.TypeMeta.Kind = esv1alpha1.ClusterSecretStoreKind
 	store.TypeMeta.APIVersion = esv1alpha1.ClusterSecretStoreKindAPIVersion
 	ns := "default"
 	store.Spec.Provider.AzureKV.AuthSecretRef.ClientID.Namespace = &ns
 	store.Spec.Provider.AzureKV.AuthSecretRef.ClientSecret.Namespace = &ns
-	secretClient, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
+	_, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
 	tassert.EqualError(t, err, "could not find secret default/user: secrets \"user\" not found")
-	tassert.Nil(t, secretClient)
 }
 
 const (