1
0
Fork 0
mirror of https://github.com/external-secrets/external-secrets.git synced 2024-12-14 11:57:59 +00:00

Merge pull request #514 from vazul/azure_managed_identity

Supporting Managed Identity authentication for Azure Keyvault
This commit is contained in:
paul-the-alien[bot] 2021-12-07 08:45:26 +00:00 committed by GitHub
commit 1e9ba0ceb5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 211 additions and 58 deletions

View file

@ -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.

View file

@ -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.

View file

@ -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:

View file

@ -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:

View file

@ -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).

View file

@ -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"

View file

@ -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
}
anAzure.baseClient = azClient
anAzure.vaultURL = vaultURL
return anAzure, nil
clientSet, err = anAzure.setAzureClientWithServicePrincipal(ctx)
if clientSet {
return anAzure, err
}
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.AuthSecretRef == nil {
return nil, "", fmt.Errorf("missing clientID/clientSecret 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
}
csec, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *spec.AuthSecretRef.ClientSecret, clusterScoped)
if err != nil {
return nil, "", err
if *spec.AuthType != esv1alpha1.ManagedIdentity {
return false, nil
}
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"
authorizer, err := clientCredentialsConfig.Authorizer()
msiConfig := kvauth.NewMSIConfig()
msiConfig.Resource = vaultResource
if spec.IdentityID != nil {
msiConfig.ClientID = *spec.IdentityID
}
authorizer, err := msiConfig.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) 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 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
}
cid, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *spec.AuthSecretRef.ClientID, clusterScoped)
if err != nil {
return true, err
}
csec, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *spec.AuthSecretRef.ClientSecret, clusterScoped)
if err != nil {
return true, err
}
clientCredentialsConfig := kvauth.NewClientCredentialsConfig(cid, csec, *spec.TenantID)
clientCredentialsConfig.Resource = vaultResource
authorizer, err := clientCredentialsConfig.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) secretKeyRef(ctx context.Context, namespace string, secretRef smmeta.SecretKeySelector, clusterScoped bool) (string, error) {

View file

@ -39,15 +39,46 @@ func newAzure() (Azure, *fake.AzureMock) {
return testAzure, azureMock
}
func TestNewClientNoCreds(t *testing.T) {
func TestNewClientManagedIdentityNoNeedForCredentials(t *testing.T) {
namespace := "internal"
vaultURL := "https://local.vault.url"
tenantID := "1234"
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 (