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:
commit
1e9ba0ceb5
8 changed files with 211 additions and 58 deletions
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
docs/snippets/azkv-secret-store-mi.yaml
Normal file
13
docs/snippets/azkv-secret-store-mi.yaml
Normal 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"
|
|
@ -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) {
|
||||
|
|
|
@ -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 (
|
||||
|
|
Loading…
Reference in a new issue