mirror of
https://github.com/external-secrets/external-secrets.git
synced 2024-12-14 11:57:59 +00:00
feat(azure): implement workload identity (#738)
* feat(azure): implement workload identity Signed-off-by: Moritz Johner <beller.moritz@googlemail.com> Co-authored-by: Henning Eggers <henning.eggers@inovex.de>
This commit is contained in:
parent
d0a32b6f2d
commit
cf7e3832ae
18 changed files with 870 additions and 203 deletions
|
@ -20,15 +20,18 @@ import smmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
|
||||||
// Only one of the following auth types may be specified.
|
// Only one of the following auth types may be specified.
|
||||||
// If none of the following auth type is specified, the default one
|
// If none of the following auth type is specified, the default one
|
||||||
// is ServicePrincipal.
|
// is ServicePrincipal.
|
||||||
// +kubebuilder:validation:Enum=ServicePrincipal;ManagedIdentity
|
// +kubebuilder:validation:Enum=ServicePrincipal;ManagedIdentity;WorkloadIdentity
|
||||||
type AuthType string
|
type AzureAuthType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Using service principal to authenticate, which needs a tenantId, a clientId and a clientSecret.
|
// Using service principal to authenticate, which needs a tenantId, a clientId and a clientSecret.
|
||||||
ServicePrincipal AuthType = "ServicePrincipal"
|
AzureServicePrincipal AzureAuthType = "ServicePrincipal"
|
||||||
|
|
||||||
// Using Managed Identity to authenticate. Used with aad-pod-identity instelled in the clister.
|
// Using Managed Identity to authenticate. Used with aad-pod-identity installed in the clister.
|
||||||
ManagedIdentity AuthType = "ManagedIdentity"
|
AzureManagedIdentity AzureAuthType = "ManagedIdentity"
|
||||||
|
|
||||||
|
// Using Workload Identity service accounts to authenticate.
|
||||||
|
AzureWorkloadIdentity AzureAuthType = "WorkloadIdentity"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Configures an store to sync secrets using Azure KV.
|
// Configures an store to sync secrets using Azure KV.
|
||||||
|
@ -39,15 +42,24 @@ type AzureKVProvider struct {
|
||||||
// - "ManagedIdentity": Using Managed Identity assigned to the pod (see aad-pod-identity)
|
// - "ManagedIdentity": Using Managed Identity assigned to the pod (see aad-pod-identity)
|
||||||
// +optional
|
// +optional
|
||||||
// +kubebuilder:default=ServicePrincipal
|
// +kubebuilder:default=ServicePrincipal
|
||||||
AuthType *AuthType `json:"authType,omitempty"`
|
AuthType *AzureAuthType `json:"authType,omitempty"`
|
||||||
|
|
||||||
// Vault Url from which the secrets to be fetched from.
|
// Vault Url from which the secrets to be fetched from.
|
||||||
VaultURL *string `json:"vaultUrl"`
|
VaultURL *string `json:"vaultUrl"`
|
||||||
|
|
||||||
// TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
|
// TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
|
||||||
// +optional
|
// +optional
|
||||||
TenantID *string `json:"tenantId,omitempty"`
|
TenantID *string `json:"tenantId,omitempty"`
|
||||||
|
|
||||||
// Auth configures how the operator authenticates with Azure. Required for ServicePrincipal auth type.
|
// Auth configures how the operator authenticates with Azure. Required for ServicePrincipal auth type.
|
||||||
// +optional
|
// +optional
|
||||||
AuthSecretRef *AzureKVAuth `json:"authSecretRef,omitempty"`
|
AuthSecretRef *AzureKVAuth `json:"authSecretRef,omitempty"`
|
||||||
|
|
||||||
|
// ServiceAccountRef specified the service account
|
||||||
|
// that should be used when authenticating with WorkloadIdentity.
|
||||||
|
// +optional
|
||||||
|
ServiceAccountRef *smmeta.ServiceAccountSelector `json:"serviceAccountRef,omitempty"`
|
||||||
|
|
||||||
// If multiple Managed Identity is assigned to the pod, you can select the one to be used
|
// If multiple Managed Identity is assigned to the pod, you can select the one to be used
|
||||||
// +optional
|
// +optional
|
||||||
IdentityID *string `json:"identityId,omitempty"`
|
IdentityID *string `json:"identityId,omitempty"`
|
||||||
|
@ -56,7 +68,10 @@ type AzureKVProvider struct {
|
||||||
// Configuration used to authenticate with Azure.
|
// Configuration used to authenticate with Azure.
|
||||||
type AzureKVAuth struct {
|
type AzureKVAuth struct {
|
||||||
// The Azure clientId of the service principle used for authentication.
|
// The Azure clientId of the service principle used for authentication.
|
||||||
ClientID *smmeta.SecretKeySelector `json:"clientId"`
|
// +optional
|
||||||
|
ClientID *smmeta.SecretKeySelector `json:"clientId,omitempty"`
|
||||||
|
|
||||||
// The Azure ClientSecret of the service principle used for authentication.
|
// The Azure ClientSecret of the service principle used for authentication.
|
||||||
ClientSecret *smmeta.SecretKeySelector `json:"clientSecret"`
|
// +optional
|
||||||
|
ClientSecret *smmeta.SecretKeySelector `json:"clientSecret,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -245,7 +245,7 @@ func (in *AzureKVProvider) DeepCopyInto(out *AzureKVProvider) {
|
||||||
*out = *in
|
*out = *in
|
||||||
if in.AuthType != nil {
|
if in.AuthType != nil {
|
||||||
in, out := &in.AuthType, &out.AuthType
|
in, out := &in.AuthType, &out.AuthType
|
||||||
*out = new(AuthType)
|
*out = new(AzureAuthType)
|
||||||
**out = **in
|
**out = **in
|
||||||
}
|
}
|
||||||
if in.VaultURL != nil {
|
if in.VaultURL != nil {
|
||||||
|
@ -263,6 +263,11 @@ func (in *AzureKVProvider) DeepCopyInto(out *AzureKVProvider) {
|
||||||
*out = new(AzureKVAuth)
|
*out = new(AzureKVAuth)
|
||||||
(*in).DeepCopyInto(*out)
|
(*in).DeepCopyInto(*out)
|
||||||
}
|
}
|
||||||
|
if in.ServiceAccountRef != nil {
|
||||||
|
in, out := &in.ServiceAccountRef, &out.ServiceAccountRef
|
||||||
|
*out = new(metav1.ServiceAccountSelector)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
if in.IdentityID != nil {
|
if in.IdentityID != nil {
|
||||||
in, out := &in.IdentityID, &out.IdentityID
|
in, out := &in.IdentityID, &out.IdentityID
|
||||||
*out = new(string)
|
*out = new(string)
|
||||||
|
|
|
@ -20,15 +20,18 @@ import smmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
|
||||||
// Only one of the following auth types may be specified.
|
// Only one of the following auth types may be specified.
|
||||||
// If none of the following auth type is specified, the default one
|
// If none of the following auth type is specified, the default one
|
||||||
// is ServicePrincipal.
|
// is ServicePrincipal.
|
||||||
// +kubebuilder:validation:Enum=ServicePrincipal;ManagedIdentity
|
// +kubebuilder:validation:Enum=ServicePrincipal;ManagedIdentity;WorkloadIdentity
|
||||||
type AuthType string
|
type AzureAuthType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Using service principal to authenticate, which needs a tenantId, a clientId and a clientSecret.
|
// Using service principal to authenticate, which needs a tenantId, a clientId and a clientSecret.
|
||||||
ServicePrincipal AuthType = "ServicePrincipal"
|
AzureServicePrincipal AzureAuthType = "ServicePrincipal"
|
||||||
|
|
||||||
// Using Managed Identity to authenticate. Used with aad-pod-identity instelled in the clister.
|
// Using Managed Identity to authenticate. Used with aad-pod-identity installed in the clister.
|
||||||
ManagedIdentity AuthType = "ManagedIdentity"
|
AzureManagedIdentity AzureAuthType = "ManagedIdentity"
|
||||||
|
|
||||||
|
// Using Workload Identity service accounts to authenticate.
|
||||||
|
AzureWorkloadIdentity AzureAuthType = "WorkloadIdentity"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Configures an store to sync secrets using Azure KV.
|
// Configures an store to sync secrets using Azure KV.
|
||||||
|
@ -39,15 +42,24 @@ type AzureKVProvider struct {
|
||||||
// - "ManagedIdentity": Using Managed Identity assigned to the pod (see aad-pod-identity)
|
// - "ManagedIdentity": Using Managed Identity assigned to the pod (see aad-pod-identity)
|
||||||
// +optional
|
// +optional
|
||||||
// +kubebuilder:default=ServicePrincipal
|
// +kubebuilder:default=ServicePrincipal
|
||||||
AuthType *AuthType `json:"authType,omitempty"`
|
AuthType *AzureAuthType `json:"authType,omitempty"`
|
||||||
|
|
||||||
// Vault Url from which the secrets to be fetched from.
|
// Vault Url from which the secrets to be fetched from.
|
||||||
VaultURL *string `json:"vaultUrl"`
|
VaultURL *string `json:"vaultUrl"`
|
||||||
|
|
||||||
// TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
|
// TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
|
||||||
// +optional
|
// +optional
|
||||||
TenantID *string `json:"tenantId,omitempty"`
|
TenantID *string `json:"tenantId,omitempty"`
|
||||||
|
|
||||||
// Auth configures how the operator authenticates with Azure. Required for ServicePrincipal auth type.
|
// Auth configures how the operator authenticates with Azure. Required for ServicePrincipal auth type.
|
||||||
// +optional
|
// +optional
|
||||||
AuthSecretRef *AzureKVAuth `json:"authSecretRef,omitempty"`
|
AuthSecretRef *AzureKVAuth `json:"authSecretRef,omitempty"`
|
||||||
|
|
||||||
|
// ServiceAccountRef specified the service account
|
||||||
|
// that should be used when authenticating with WorkloadIdentity.
|
||||||
|
// +optional
|
||||||
|
ServiceAccountRef *smmeta.ServiceAccountSelector `json:"serviceAccountRef,omitempty"`
|
||||||
|
|
||||||
// If multiple Managed Identity is assigned to the pod, you can select the one to be used
|
// If multiple Managed Identity is assigned to the pod, you can select the one to be used
|
||||||
// +optional
|
// +optional
|
||||||
IdentityID *string `json:"identityId,omitempty"`
|
IdentityID *string `json:"identityId,omitempty"`
|
||||||
|
@ -56,7 +68,10 @@ type AzureKVProvider struct {
|
||||||
// Configuration used to authenticate with Azure.
|
// Configuration used to authenticate with Azure.
|
||||||
type AzureKVAuth struct {
|
type AzureKVAuth struct {
|
||||||
// The Azure clientId of the service principle used for authentication.
|
// The Azure clientId of the service principle used for authentication.
|
||||||
ClientID *smmeta.SecretKeySelector `json:"clientId"`
|
// +optional
|
||||||
|
ClientID *smmeta.SecretKeySelector `json:"clientId,omitempty"`
|
||||||
|
|
||||||
// The Azure ClientSecret of the service principle used for authentication.
|
// The Azure ClientSecret of the service principle used for authentication.
|
||||||
ClientSecret *smmeta.SecretKeySelector `json:"clientSecret"`
|
// +optional
|
||||||
|
ClientSecret *smmeta.SecretKeySelector `json:"clientSecret,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -245,7 +245,7 @@ func (in *AzureKVProvider) DeepCopyInto(out *AzureKVProvider) {
|
||||||
*out = *in
|
*out = *in
|
||||||
if in.AuthType != nil {
|
if in.AuthType != nil {
|
||||||
in, out := &in.AuthType, &out.AuthType
|
in, out := &in.AuthType, &out.AuthType
|
||||||
*out = new(AuthType)
|
*out = new(AzureAuthType)
|
||||||
**out = **in
|
**out = **in
|
||||||
}
|
}
|
||||||
if in.VaultURL != nil {
|
if in.VaultURL != nil {
|
||||||
|
@ -263,6 +263,11 @@ func (in *AzureKVProvider) DeepCopyInto(out *AzureKVProvider) {
|
||||||
*out = new(AzureKVAuth)
|
*out = new(AzureKVAuth)
|
||||||
(*in).DeepCopyInto(*out)
|
(*in).DeepCopyInto(*out)
|
||||||
}
|
}
|
||||||
|
if in.ServiceAccountRef != nil {
|
||||||
|
in, out := &in.ServiceAccountRef, &out.ServiceAccountRef
|
||||||
|
*out = new(metav1.ServiceAccountSelector)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
if in.IdentityID != nil {
|
if in.IdentityID != nil {
|
||||||
in, out := &in.IdentityID, &out.IdentityID
|
in, out := &in.IdentityID, &out.IdentityID
|
||||||
*out = new(string)
|
*out = new(string)
|
||||||
|
|
|
@ -353,9 +353,6 @@ spec:
|
||||||
defaults to the namespace of the referent.
|
defaults to the namespace of the referent.
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
required:
|
|
||||||
- clientId
|
|
||||||
- clientSecret
|
|
||||||
type: object
|
type: object
|
||||||
authType:
|
authType:
|
||||||
default: ServicePrincipal
|
default: ServicePrincipal
|
||||||
|
@ -367,11 +364,28 @@ spec:
|
||||||
enum:
|
enum:
|
||||||
- ServicePrincipal
|
- ServicePrincipal
|
||||||
- ManagedIdentity
|
- ManagedIdentity
|
||||||
|
- WorkloadIdentity
|
||||||
type: string
|
type: string
|
||||||
identityId:
|
identityId:
|
||||||
description: If multiple Managed Identity is assigned to the
|
description: If multiple Managed Identity is assigned to the
|
||||||
pod, you can select the one to be used
|
pod, you can select the one to be used
|
||||||
type: string
|
type: string
|
||||||
|
serviceAccountRef:
|
||||||
|
description: ServiceAccountRef specified the service account
|
||||||
|
that should be used when authenticating with WorkloadIdentity.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: The name of the ServiceAccount resource being
|
||||||
|
referred to.
|
||||||
|
type: string
|
||||||
|
namespace:
|
||||||
|
description: Namespace of the resource being referred
|
||||||
|
to. Ignored if referent is not cluster-scoped. cluster-scoped
|
||||||
|
defaults to the namespace of the referent.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
tenantId:
|
tenantId:
|
||||||
description: TenantID configures the Azure Tenant to send
|
description: TenantID configures the Azure Tenant to send
|
||||||
requests to. Required for ServicePrincipal auth type.
|
requests to. Required for ServicePrincipal auth type.
|
||||||
|
@ -1658,9 +1672,6 @@ spec:
|
||||||
defaults to the namespace of the referent.
|
defaults to the namespace of the referent.
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
required:
|
|
||||||
- clientId
|
|
||||||
- clientSecret
|
|
||||||
type: object
|
type: object
|
||||||
authType:
|
authType:
|
||||||
default: ServicePrincipal
|
default: ServicePrincipal
|
||||||
|
@ -1672,11 +1683,28 @@ spec:
|
||||||
enum:
|
enum:
|
||||||
- ServicePrincipal
|
- ServicePrincipal
|
||||||
- ManagedIdentity
|
- ManagedIdentity
|
||||||
|
- WorkloadIdentity
|
||||||
type: string
|
type: string
|
||||||
identityId:
|
identityId:
|
||||||
description: If multiple Managed Identity is assigned to the
|
description: If multiple Managed Identity is assigned to the
|
||||||
pod, you can select the one to be used
|
pod, you can select the one to be used
|
||||||
type: string
|
type: string
|
||||||
|
serviceAccountRef:
|
||||||
|
description: ServiceAccountRef specified the service account
|
||||||
|
that should be used when authenticating with WorkloadIdentity.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: The name of the ServiceAccount resource being
|
||||||
|
referred to.
|
||||||
|
type: string
|
||||||
|
namespace:
|
||||||
|
description: Namespace of the resource being referred
|
||||||
|
to. Ignored if referent is not cluster-scoped. cluster-scoped
|
||||||
|
defaults to the namespace of the referent.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
tenantId:
|
tenantId:
|
||||||
description: TenantID configures the Azure Tenant to send
|
description: TenantID configures the Azure Tenant to send
|
||||||
requests to. Required for ServicePrincipal auth type.
|
requests to. Required for ServicePrincipal auth type.
|
||||||
|
|
|
@ -353,9 +353,6 @@ spec:
|
||||||
defaults to the namespace of the referent.
|
defaults to the namespace of the referent.
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
required:
|
|
||||||
- clientId
|
|
||||||
- clientSecret
|
|
||||||
type: object
|
type: object
|
||||||
authType:
|
authType:
|
||||||
default: ServicePrincipal
|
default: ServicePrincipal
|
||||||
|
@ -367,11 +364,28 @@ spec:
|
||||||
enum:
|
enum:
|
||||||
- ServicePrincipal
|
- ServicePrincipal
|
||||||
- ManagedIdentity
|
- ManagedIdentity
|
||||||
|
- WorkloadIdentity
|
||||||
type: string
|
type: string
|
||||||
identityId:
|
identityId:
|
||||||
description: If multiple Managed Identity is assigned to the
|
description: If multiple Managed Identity is assigned to the
|
||||||
pod, you can select the one to be used
|
pod, you can select the one to be used
|
||||||
type: string
|
type: string
|
||||||
|
serviceAccountRef:
|
||||||
|
description: ServiceAccountRef specified the service account
|
||||||
|
that should be used when authenticating with WorkloadIdentity.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: The name of the ServiceAccount resource being
|
||||||
|
referred to.
|
||||||
|
type: string
|
||||||
|
namespace:
|
||||||
|
description: Namespace of the resource being referred
|
||||||
|
to. Ignored if referent is not cluster-scoped. cluster-scoped
|
||||||
|
defaults to the namespace of the referent.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
tenantId:
|
tenantId:
|
||||||
description: TenantID configures the Azure Tenant to send
|
description: TenantID configures the Azure Tenant to send
|
||||||
requests to. Required for ServicePrincipal auth type.
|
requests to. Required for ServicePrincipal auth type.
|
||||||
|
@ -1661,9 +1675,6 @@ spec:
|
||||||
defaults to the namespace of the referent.
|
defaults to the namespace of the referent.
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
required:
|
|
||||||
- clientId
|
|
||||||
- clientSecret
|
|
||||||
type: object
|
type: object
|
||||||
authType:
|
authType:
|
||||||
default: ServicePrincipal
|
default: ServicePrincipal
|
||||||
|
@ -1675,11 +1686,28 @@ spec:
|
||||||
enum:
|
enum:
|
||||||
- ServicePrincipal
|
- ServicePrincipal
|
||||||
- ManagedIdentity
|
- ManagedIdentity
|
||||||
|
- WorkloadIdentity
|
||||||
type: string
|
type: string
|
||||||
identityId:
|
identityId:
|
||||||
description: If multiple Managed Identity is assigned to the
|
description: If multiple Managed Identity is assigned to the
|
||||||
pod, you can select the one to be used
|
pod, you can select the one to be used
|
||||||
type: string
|
type: string
|
||||||
|
serviceAccountRef:
|
||||||
|
description: ServiceAccountRef specified the service account
|
||||||
|
that should be used when authenticating with WorkloadIdentity.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: The name of the ServiceAccount resource being
|
||||||
|
referred to.
|
||||||
|
type: string
|
||||||
|
namespace:
|
||||||
|
description: Namespace of the resource being referred
|
||||||
|
to. Ignored if referent is not cluster-scoped. cluster-scoped
|
||||||
|
defaults to the namespace of the referent.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
tenantId:
|
tenantId:
|
||||||
description: TenantID configures the Azure Tenant to send
|
description: TenantID configures the Azure Tenant to send
|
||||||
requests to. Required for ServicePrincipal auth type.
|
requests to. Required for ServicePrincipal auth type.
|
||||||
|
|
|
@ -585,9 +585,6 @@ spec:
|
||||||
description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
|
description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
required:
|
|
||||||
- clientId
|
|
||||||
- clientSecret
|
|
||||||
type: object
|
type: object
|
||||||
authType:
|
authType:
|
||||||
default: ServicePrincipal
|
default: ServicePrincipal
|
||||||
|
@ -595,10 +592,23 @@ spec:
|
||||||
enum:
|
enum:
|
||||||
- ServicePrincipal
|
- ServicePrincipal
|
||||||
- ManagedIdentity
|
- ManagedIdentity
|
||||||
|
- WorkloadIdentity
|
||||||
type: string
|
type: string
|
||||||
identityId:
|
identityId:
|
||||||
description: If multiple Managed Identity is assigned to the pod, you can select the one to be used
|
description: If multiple Managed Identity is assigned to the pod, you can select the one to be used
|
||||||
type: string
|
type: string
|
||||||
|
serviceAccountRef:
|
||||||
|
description: ServiceAccountRef specified the service account that should be used when authenticating with WorkloadIdentity.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: The name of the ServiceAccount resource being referred to.
|
||||||
|
type: string
|
||||||
|
namespace:
|
||||||
|
description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
tenantId:
|
tenantId:
|
||||||
description: TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
|
description: TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
|
||||||
type: string
|
type: string
|
||||||
|
@ -1548,9 +1558,6 @@ spec:
|
||||||
description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
|
description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
required:
|
|
||||||
- clientId
|
|
||||||
- clientSecret
|
|
||||||
type: object
|
type: object
|
||||||
authType:
|
authType:
|
||||||
default: ServicePrincipal
|
default: ServicePrincipal
|
||||||
|
@ -1558,10 +1565,23 @@ spec:
|
||||||
enum:
|
enum:
|
||||||
- ServicePrincipal
|
- ServicePrincipal
|
||||||
- ManagedIdentity
|
- ManagedIdentity
|
||||||
|
- WorkloadIdentity
|
||||||
type: string
|
type: string
|
||||||
identityId:
|
identityId:
|
||||||
description: If multiple Managed Identity is assigned to the pod, you can select the one to be used
|
description: If multiple Managed Identity is assigned to the pod, you can select the one to be used
|
||||||
type: string
|
type: string
|
||||||
|
serviceAccountRef:
|
||||||
|
description: ServiceAccountRef specified the service account that should be used when authenticating with WorkloadIdentity.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: The name of the ServiceAccount resource being referred to.
|
||||||
|
type: string
|
||||||
|
namespace:
|
||||||
|
description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
tenantId:
|
tenantId:
|
||||||
description: TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
|
description: TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
|
||||||
type: string
|
type: string
|
||||||
|
@ -3051,9 +3071,6 @@ spec:
|
||||||
description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
|
description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
required:
|
|
||||||
- clientId
|
|
||||||
- clientSecret
|
|
||||||
type: object
|
type: object
|
||||||
authType:
|
authType:
|
||||||
default: ServicePrincipal
|
default: ServicePrincipal
|
||||||
|
@ -3061,10 +3078,23 @@ spec:
|
||||||
enum:
|
enum:
|
||||||
- ServicePrincipal
|
- ServicePrincipal
|
||||||
- ManagedIdentity
|
- ManagedIdentity
|
||||||
|
- WorkloadIdentity
|
||||||
type: string
|
type: string
|
||||||
identityId:
|
identityId:
|
||||||
description: If multiple Managed Identity is assigned to the pod, you can select the one to be used
|
description: If multiple Managed Identity is assigned to the pod, you can select the one to be used
|
||||||
type: string
|
type: string
|
||||||
|
serviceAccountRef:
|
||||||
|
description: ServiceAccountRef specified the service account that should be used when authenticating with WorkloadIdentity.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: The name of the ServiceAccount resource being referred to.
|
||||||
|
type: string
|
||||||
|
namespace:
|
||||||
|
description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
tenantId:
|
tenantId:
|
||||||
description: TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
|
description: TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
|
||||||
type: string
|
type: string
|
||||||
|
@ -4017,9 +4047,6 @@ spec:
|
||||||
description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
|
description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
required:
|
|
||||||
- clientId
|
|
||||||
- clientSecret
|
|
||||||
type: object
|
type: object
|
||||||
authType:
|
authType:
|
||||||
default: ServicePrincipal
|
default: ServicePrincipal
|
||||||
|
@ -4027,10 +4054,23 @@ spec:
|
||||||
enum:
|
enum:
|
||||||
- ServicePrincipal
|
- ServicePrincipal
|
||||||
- ManagedIdentity
|
- ManagedIdentity
|
||||||
|
- WorkloadIdentity
|
||||||
type: string
|
type: string
|
||||||
identityId:
|
identityId:
|
||||||
description: If multiple Managed Identity is assigned to the pod, you can select the one to be used
|
description: If multiple Managed Identity is assigned to the pod, you can select the one to be used
|
||||||
type: string
|
type: string
|
||||||
|
serviceAccountRef:
|
||||||
|
description: ServiceAccountRef specified the service account that should be used when authenticating with WorkloadIdentity.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: The name of the ServiceAccount resource being referred to.
|
||||||
|
type: string
|
||||||
|
namespace:
|
||||||
|
description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
tenantId:
|
tenantId:
|
||||||
description: TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
|
description: TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
|
||||||
type: string
|
type: string
|
||||||
|
|
|
@ -7,7 +7,7 @@ External Secrets Operator integrates with [Azure Key vault](https://azure.micros
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|
||||||
We support Service Principals and Managed Identity [authentication](https://docs.microsoft.com/en-us/azure/key-vault/general/authentication).
|
We support Service Principals, Managed Identity and Workload Identity 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.
|
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.
|
||||||
|
|
||||||
|
@ -25,6 +25,46 @@ If there are multiple Managed Identitites for different keyvaults, the operator
|
||||||
{% include 'azkv-credentials-secret.yaml' %}
|
{% include 'azkv-credentials-secret.yaml' %}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Workload Identity
|
||||||
|
|
||||||
|
You can use [Azure AD Workload Identity Federation](https://docs.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation) to access Azure managed services like Key Vault **without needing to manage secrets**. You need to configure a trust relationship between your Kubernetes Cluster and Azure AD. This can be done in various ways, for instance using `terraform`, the Azure Portal or the `az` cli. We found the [azwi](https://azure.github.io/azure-workload-identity/docs/installation/azwi.html) cli very helpful. The Azure [Workload Identity Quick Start Guide](https://azure.github.io/azure-workload-identity/docs/quick-start.html) is also good place to get started.
|
||||||
|
|
||||||
|
This is basically a two step process:
|
||||||
|
|
||||||
|
1. Create a Kubernetes Service Account ([guide](https://azure.github.io/azure-workload-identity/docs/quick-start.html#5-create-a-kubernetes-service-account))
|
||||||
|
|
||||||
|
```sh
|
||||||
|
azwi serviceaccount create phase sa \
|
||||||
|
--aad-application-name "${APPLICATION_NAME}" \
|
||||||
|
--service-account-namespace "${SERVICE_ACCOUNT_NAMESPACE}" \
|
||||||
|
--service-account-name "${SERVICE_ACCOUNT_NAME}"
|
||||||
|
```
|
||||||
|
2. Configure the trust relationship between Azure AD and Kubernetes ([guide](https://azure.github.io/azure-workload-identity/docs/quick-start.html#6-establish-federated-identity-credential-between-the-aad-application-and-the-service-account-issuer--subject))
|
||||||
|
|
||||||
|
```sh
|
||||||
|
azwi serviceaccount create phase federated-identity \
|
||||||
|
--aad-application-name "${APPLICATION_NAME}" \
|
||||||
|
--service-account-namespace "${SERVICE_ACCOUNT_NAMESPACE}" \
|
||||||
|
--service-account-name "${SERVICE_ACCOUNT_NAME}" \
|
||||||
|
--service-account-issuer-url "${SERVICE_ACCOUNT_ISSUER}"
|
||||||
|
```
|
||||||
|
|
||||||
|
With these prerequisites met you can configure `ESO` to use that Service Account. You have two options:
|
||||||
|
|
||||||
|
##### Mounted Service Account
|
||||||
|
You run the controller and mount that particular service account into the pod. That grants _everyone_ who is able to create a secret store or reference a correctly configured one the ability to read secrets. **This approach is usually not recommended**. But may make sense when you want to share an identity with multiple namespaces. Also see our [Multi-Tenancy Guide](guides-multi-tenancy.md) for design considerations.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
{% include 'azkv-workload-identity-mounted.yaml' %}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Referenced Service Account
|
||||||
|
You run the controller without service account (effectively without azure permissions). Now you have to configure the SecretStore and set the `serviceAccountRef` and point to the service account you have just created. **This is usually the recommended approach**. It makes sense for everyone who wants to run the controller withour Azure permissions and delegate authentication via service accounts in particular namespaces. Also see our [Multi-Tenancy Guide] for design considerations.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
{% include 'azkv-workload-identity.yaml' %}
|
||||||
|
```
|
||||||
|
|
||||||
### Update secret store
|
### Update secret store
|
||||||
Be sure the `azurekv` provider is listed in the `Kind=SecretStore`
|
Be sure the `azurekv` provider is listed in the `Kind=SecretStore`
|
||||||
|
|
||||||
|
|
19
docs/snippets/azkv-workload-identity-mounted.yaml
Normal file
19
docs/snippets/azkv-workload-identity-mounted.yaml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
# this service account was created by azwi
|
||||||
|
name: workload-identity-sa
|
||||||
|
annotations:
|
||||||
|
azure.workload.identity/client-id: 7d8cdf74-xxxx-xxxx-xxxx-274d963d358b
|
||||||
|
azure.workload.identity/tenant-id: 5a02a20e-xxxx-xxxx-xxxx-0ad5b634c5d8
|
||||||
|
---
|
||||||
|
apiVersion: external-secrets.io/v1beta1
|
||||||
|
kind: SecretStore
|
||||||
|
metadata:
|
||||||
|
name: example-secret-store
|
||||||
|
spec:
|
||||||
|
provider:
|
||||||
|
azurekv:
|
||||||
|
authType: WorkloadIdentity
|
||||||
|
vaultUrl: "https://xx-xxxx-xx.vault.azure.net"
|
||||||
|
# note: no serviceAccountRef was provided
|
20
docs/snippets/azkv-workload-identity.yaml
Normal file
20
docs/snippets/azkv-workload-identity.yaml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
# this service account was created by azwi
|
||||||
|
name: workload-identity-sa
|
||||||
|
annotations:
|
||||||
|
azure.workload.identity/client-id: 7d8cdf74-xxxx-xxxx-xxxx-274d963d358b
|
||||||
|
azure.workload.identity/tenant-id: 5a02a20e-xxxx-xxxx-xxxx-0ad5b634c5d8
|
||||||
|
---
|
||||||
|
apiVersion: external-secrets.io/v1beta1
|
||||||
|
kind: SecretStore
|
||||||
|
metadata:
|
||||||
|
name: example-secret-store
|
||||||
|
spec:
|
||||||
|
provider:
|
||||||
|
azurekv:
|
||||||
|
authType: WorkloadIdentity
|
||||||
|
vaultUrl: "https://xx-xxxx-xx.vault.azure.net"
|
||||||
|
serviceAccountRef:
|
||||||
|
name: workload-identity-sa
|
26
docs/spec.md
26
docs/spec.md
|
@ -474,7 +474,7 @@ string
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<h3 id="external-secrets.io/v1alpha1.AuthType">AuthType
|
<h3 id="external-secrets.io/v1alpha1.AzureAuthType">AzureAuthType
|
||||||
(<code>string</code> alias)</p></h3>
|
(<code>string</code> alias)</p></h3>
|
||||||
<p>
|
<p>
|
||||||
(<em>Appears on:</em>
|
(<em>Appears on:</em>
|
||||||
|
@ -494,11 +494,14 @@ is ServicePrincipal.</p>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody><tr><td><p>"ManagedIdentity"</p></td>
|
<tbody><tr><td><p>"ManagedIdentity"</p></td>
|
||||||
<td><p>Using Managed Identity to authenticate. Used with aad-pod-identity instelled in the clister.</p>
|
<td><p>Using Managed Identity to authenticate. Used with aad-pod-identity installed in the clister.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr><tr><td><p>"ServicePrincipal"</p></td>
|
</tr><tr><td><p>"ServicePrincipal"</p></td>
|
||||||
<td><p>Using service principal to authenticate, which needs a tenantId, a clientId and a clientSecret.</p>
|
<td><p>Using service principal to authenticate, which needs a tenantId, a clientId and a clientSecret.</p>
|
||||||
</td>
|
</td>
|
||||||
|
</tr><tr><td><p>"WorkloadIdentity"</p></td>
|
||||||
|
<td><p>Using Workload Identity service accounts to authenticate.</p>
|
||||||
|
</td>
|
||||||
</tr></tbody>
|
</tr></tbody>
|
||||||
</table>
|
</table>
|
||||||
<h3 id="external-secrets.io/v1alpha1.AzureKVAuth">AzureKVAuth
|
<h3 id="external-secrets.io/v1alpha1.AzureKVAuth">AzureKVAuth
|
||||||
|
@ -526,6 +529,7 @@ github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
|
||||||
</em>
|
</em>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
<em>(Optional)</em>
|
||||||
<p>The Azure clientId of the service principle used for authentication.</p>
|
<p>The Azure clientId of the service principle used for authentication.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -537,6 +541,7 @@ github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
|
||||||
</em>
|
</em>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
<em>(Optional)</em>
|
||||||
<p>The Azure ClientSecret of the service principle used for authentication.</p>
|
<p>The Azure ClientSecret of the service principle used for authentication.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -563,8 +568,8 @@ github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
|
||||||
<td>
|
<td>
|
||||||
<code>authType</code></br>
|
<code>authType</code></br>
|
||||||
<em>
|
<em>
|
||||||
<a href="#external-secrets.io/v1alpha1.AuthType">
|
<a href="#external-secrets.io/v1alpha1.AzureAuthType">
|
||||||
AuthType
|
AzureAuthType
|
||||||
</a>
|
</a>
|
||||||
</em>
|
</em>
|
||||||
</td>
|
</td>
|
||||||
|
@ -615,6 +620,19 @@ AzureKVAuth
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
<code>serviceAccountRef</code></br>
|
||||||
|
<em>
|
||||||
|
github.com/external-secrets/external-secrets/apis/meta/v1.ServiceAccountSelector
|
||||||
|
</em>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<em>(Optional)</em>
|
||||||
|
<p>ServiceAccountRef specified the service account
|
||||||
|
that should be used when authenticating with WorkloadIdentity.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
<code>identityId</code></br>
|
<code>identityId</code></br>
|
||||||
<em>
|
<em>
|
||||||
string
|
string
|
||||||
|
|
7
go.mod
7
go.mod
|
@ -37,7 +37,10 @@ require (
|
||||||
cloud.google.com/go/iam v0.3.0
|
cloud.google.com/go/iam v0.3.0
|
||||||
cloud.google.com/go/secretmanager v1.3.0
|
cloud.google.com/go/secretmanager v1.3.0
|
||||||
github.com/Azure/azure-sdk-for-go v62.2.0+incompatible
|
github.com/Azure/azure-sdk-for-go v62.2.0+incompatible
|
||||||
|
github.com/Azure/go-autorest/autorest v0.11.24
|
||||||
|
github.com/Azure/go-autorest/autorest/adal v0.9.18
|
||||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11
|
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0
|
||||||
github.com/IBM/go-sdk-core/v5 v5.9.3
|
github.com/IBM/go-sdk-core/v5 v5.9.3
|
||||||
github.com/IBM/secrets-manager-go-sdk v1.0.37
|
github.com/IBM/secrets-manager-go-sdk v1.0.37
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
|
@ -90,8 +93,6 @@ require (
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/compute v1.5.0 // indirect
|
cloud.google.com/go/compute v1.5.0 // indirect
|
||||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||||
github.com/Azure/go-autorest/autorest v0.11.24 // indirect
|
|
||||||
github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect
|
|
||||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
|
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
|
||||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||||
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
||||||
|
@ -123,6 +124,7 @@ require (
|
||||||
github.com/gobuffalo/flect v0.2.3 // indirect
|
github.com/gobuffalo/flect v0.2.3 // indirect
|
||||||
github.com/goccy/go-json v0.9.4 // indirect
|
github.com/goccy/go-json v0.9.4 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
|
@ -152,6 +154,7 @@ require (
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
github.com/leodido/go-urn v1.2.1 // indirect
|
github.com/leodido/go-urn v1.2.1 // indirect
|
||||||
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
||||||
github.com/lestrrat-go/blackmagic v1.0.0 // indirect
|
github.com/lestrrat-go/blackmagic v1.0.0 // indirect
|
||||||
|
|
8
go.sum
8
go.sum
|
@ -85,6 +85,8 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z
|
||||||
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
||||||
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
|
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
|
||||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 h1:WVsrXCnHlDDX8ls+tootqRE87/hL9S/g4ewig9RsD/c=
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||||
|
@ -302,6 +304,8 @@ github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||||
|
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
|
||||||
|
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||||
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||||
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||||
|
@ -545,6 +549,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||||
|
@ -621,6 +627,7 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||||
|
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
@ -671,6 +678,7 @@ github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR
|
||||||
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
|
github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
|
||||||
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
|
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
|
58
pkg/provider/aws/auth/fake/token_fetcher.go
Normal file
58
pkg/provider/aws/auth/fake/token_fetcher.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
package fake
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
authv1 "k8s.io/api/authentication/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
k8sv1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewCreateTokenMock(token string) *MockK8sV1 {
|
||||||
|
return &MockK8sV1{
|
||||||
|
token: token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock K8s client for creating tokens.
|
||||||
|
type MockK8sV1 struct {
|
||||||
|
k8sv1.CoreV1Interface
|
||||||
|
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockK8sV1) ServiceAccounts(namespace string) k8sv1.ServiceAccountInterface {
|
||||||
|
return &MockK8sV1SA{v1mock: m}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock the K8s service account client.
|
||||||
|
type MockK8sV1SA struct {
|
||||||
|
k8sv1.ServiceAccountInterface
|
||||||
|
v1mock *MockK8sV1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ma *MockK8sV1SA) CreateToken(
|
||||||
|
ctx context.Context,
|
||||||
|
serviceAccountName string,
|
||||||
|
tokenRequest *authv1.TokenRequest,
|
||||||
|
opts metav1.CreateOptions,
|
||||||
|
) (*authv1.TokenRequest, error) {
|
||||||
|
return &authv1.TokenRequest{
|
||||||
|
Status: authv1.TokenRequestStatus{
|
||||||
|
Token: ma.v1mock.token,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -18,46 +18,17 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
authv1 "k8s.io/api/authentication/v1"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
"github.com/external-secrets/external-secrets/pkg/provider/aws/auth/fake"
|
||||||
k8sv1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTokenFetcher(t *testing.T) {
|
func TestTokenFetcher(t *testing.T) {
|
||||||
tf := &authTokenFetcher{
|
tf := &authTokenFetcher{
|
||||||
ServiceAccount: "foobar",
|
ServiceAccount: "foobar",
|
||||||
Namespace: "example",
|
Namespace: "example",
|
||||||
k8sClient: &mockK8sV1{},
|
k8sClient: fake.NewCreateTokenMock("FAKETOKEN"),
|
||||||
}
|
}
|
||||||
token, err := tf.FetchToken(context.Background())
|
token, err := tf.FetchToken(context.Background())
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, []byte("FAKETOKEN"), token)
|
assert.Equal(t, []byte("FAKETOKEN"), token)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock K8s client for creating tokens.
|
|
||||||
type mockK8sV1 struct {
|
|
||||||
k8sv1.CoreV1Interface
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockK8sV1) ServiceAccounts(namespace string) k8sv1.ServiceAccountInterface {
|
|
||||||
return &mockK8sV1SA{v1mock: m}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock the K8s service account client.
|
|
||||||
type mockK8sV1SA struct {
|
|
||||||
k8sv1.ServiceAccountInterface
|
|
||||||
v1mock *mockK8sV1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ma *mockK8sV1SA) CreateToken(
|
|
||||||
ctx context.Context,
|
|
||||||
serviceAccountName string,
|
|
||||||
tokenRequest *authv1.TokenRequest,
|
|
||||||
opts metav1.CreateOptions,
|
|
||||||
) (*authv1.TokenRequest, error) {
|
|
||||||
return &authv1.TokenRequest{
|
|
||||||
Status: authv1.TokenRequestStatus{
|
|
||||||
Token: "FAKETOKEN",
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -19,14 +19,23 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Azure/azure-sdk-for-go/profiles/latest/keyvault/keyvault"
|
"github.com/Azure/azure-sdk-for-go/profiles/latest/keyvault/keyvault"
|
||||||
|
"github.com/Azure/go-autorest/autorest"
|
||||||
|
"github.com/Azure/go-autorest/autorest/adal"
|
||||||
kvauth "github.com/Azure/go-autorest/autorest/azure/auth"
|
kvauth "github.com/Azure/go-autorest/autorest/azure/auth"
|
||||||
|
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
|
authv1 "k8s.io/api/authentication/v1"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
kcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||||
|
|
||||||
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||||
smmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
|
smmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
|
||||||
|
@ -34,10 +43,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultObjType = "secret"
|
defaultObjType = "secret"
|
||||||
objectTypeCert = "cert"
|
objectTypeCert = "cert"
|
||||||
objectTypeKey = "key"
|
objectTypeKey = "key"
|
||||||
vaultResource = "https://vault.azure.net"
|
vaultResource = "https://vault.azure.net"
|
||||||
|
azureDefaultAudience = "api://AzureADTokenExchange"
|
||||||
|
annotationClientID = "azure.workload.identity/client-id"
|
||||||
|
annotationTenantID = "azure.workload.identity/tenant-id"
|
||||||
|
|
||||||
errUnexpectedStoreSpec = "unexpected store spec"
|
errUnexpectedStoreSpec = "unexpected store spec"
|
||||||
errMissingAuthType = "cannot initialize Azure Client: no valid authType was specified"
|
errMissingAuthType = "cannot initialize Azure Client: no valid authType was specified"
|
||||||
|
@ -58,6 +70,11 @@ const (
|
||||||
errInvalidAzureProv = "invalid azure keyvault provider"
|
errInvalidAzureProv = "invalid azure keyvault provider"
|
||||||
errInvalidSecRefClientID = "invalid AuthSecretRef.ClientID: %w"
|
errInvalidSecRefClientID = "invalid AuthSecretRef.ClientID: %w"
|
||||||
errInvalidSecRefClientSecret = "invalid AuthSecretRef.ClientSecret: %w"
|
errInvalidSecRefClientSecret = "invalid AuthSecretRef.ClientSecret: %w"
|
||||||
|
errInvalidSARef = "invalid ServiceAccountRef: %w"
|
||||||
|
|
||||||
|
errMissingWorkloadEnvVars = "missing environment variables. AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_FEDERATED_TOKEN_FILE must be set"
|
||||||
|
errReadTokenFile = "unable to read token file %s: %w"
|
||||||
|
errMissingSAAnnotation = "missing service account annotation: %s"
|
||||||
)
|
)
|
||||||
|
|
||||||
// interface to keyvault.BaseClient.
|
// interface to keyvault.BaseClient.
|
||||||
|
@ -69,7 +86,8 @@ type SecretClient interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Azure struct {
|
type Azure struct {
|
||||||
kube client.Client
|
crClient client.Client
|
||||||
|
kubeClient kcorev1.CoreV1Interface
|
||||||
store esv1beta1.GenericStore
|
store esv1beta1.GenericStore
|
||||||
provider *esv1beta1.AzureKVProvider
|
provider *esv1beta1.AzureKVProvider
|
||||||
baseClient SecretClient
|
baseClient SecretClient
|
||||||
|
@ -92,24 +110,39 @@ func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Cl
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
cfg, err := ctrlcfg.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
kubeClient, err := kubernetes.NewForConfig(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
az := &Azure{
|
az := &Azure{
|
||||||
kube: kube,
|
crClient: kube,
|
||||||
store: store,
|
kubeClient: kubeClient.CoreV1(),
|
||||||
namespace: namespace,
|
store: store,
|
||||||
provider: provider,
|
namespace: namespace,
|
||||||
|
provider: provider,
|
||||||
}
|
}
|
||||||
|
|
||||||
ok, err := az.setAzureClientWithManagedIdentity()
|
var authorizer autorest.Authorizer
|
||||||
if ok {
|
switch *provider.AuthType {
|
||||||
return az, err
|
case esv1beta1.AzureManagedIdentity:
|
||||||
|
authorizer, err = az.authorizerForManagedIdentity()
|
||||||
|
case esv1beta1.AzureServicePrincipal:
|
||||||
|
authorizer, err = az.authorizerForServicePrincipal(ctx)
|
||||||
|
case esv1beta1.AzureWorkloadIdentity:
|
||||||
|
authorizer, err = az.authorizerForWorkloadIdentity(ctx, newTokenProvider)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf(errMissingAuthType)
|
||||||
}
|
}
|
||||||
|
|
||||||
ok, err = az.setAzureClientWithServicePrincipal(ctx)
|
cl := keyvault.New()
|
||||||
if ok {
|
cl.Authorizer = authorizer
|
||||||
return az, err
|
az.baseClient = &cl
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf(errMissingAuthType)
|
return az, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func getProvider(store esv1beta1.GenericStore) (*esv1beta1.AzureKVProvider, error) {
|
func getProvider(store esv1beta1.GenericStore) (*esv1beta1.AzureKVProvider, error) {
|
||||||
|
@ -148,6 +181,11 @@ func (a *Azure) ValidateStore(store esv1beta1.GenericStore) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if p.ServiceAccountRef != nil {
|
||||||
|
if err := utils.ValidateServiceAccountSelector(store, *p.ServiceAccountRef); err != nil {
|
||||||
|
return fmt.Errorf(errInvalidSARef, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,40 +277,126 @@ func (a *Azure) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDa
|
||||||
return nil, fmt.Errorf(errUnknownObjectType, secretName)
|
return nil, fmt.Errorf(errUnknownObjectType, secretName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Azure) setAzureClientWithManagedIdentity() (bool, error) {
|
func (a *Azure) authorizerForWorkloadIdentity(ctx context.Context, tokenProvider tokenProviderFunc) (autorest.Authorizer, error) {
|
||||||
if *a.provider.AuthType != esv1beta1.ManagedIdentity {
|
// if no serviceAccountRef was provided
|
||||||
return false, nil
|
// we expect certain env vars to be present.
|
||||||
|
// They are set by the azure workload identity webhook.
|
||||||
|
if a.provider.ServiceAccountRef == nil {
|
||||||
|
clientID := os.Getenv("AZURE_CLIENT_ID")
|
||||||
|
tenantID := os.Getenv("AZURE_TENANT_ID")
|
||||||
|
tokenFilePath := os.Getenv("AZURE_FEDERATED_TOKEN_FILE")
|
||||||
|
if clientID == "" || tenantID == "" || tokenFilePath == "" {
|
||||||
|
return nil, errors.New(errMissingWorkloadEnvVars)
|
||||||
|
}
|
||||||
|
token, err := os.ReadFile(tokenFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(errReadTokenFile, tokenFilePath, err)
|
||||||
|
}
|
||||||
|
tp, err := tokenProvider(ctx, string(token), clientID, tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return autorest.NewBearerAuthorizer(tp), nil
|
||||||
|
}
|
||||||
|
ns := a.namespace
|
||||||
|
if a.store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
|
||||||
|
ns = *a.provider.ServiceAccountRef.Namespace
|
||||||
|
}
|
||||||
|
var sa corev1.ServiceAccount
|
||||||
|
err := a.crClient.Get(ctx, types.NamespacedName{
|
||||||
|
Name: a.provider.ServiceAccountRef.Name,
|
||||||
|
Namespace: ns,
|
||||||
|
}, &sa)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
clientID, ok := sa.ObjectMeta.Annotations[annotationClientID]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf(errMissingSAAnnotation, annotationClientID)
|
||||||
|
}
|
||||||
|
tenantID, ok := sa.ObjectMeta.Annotations[annotationTenantID]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf(errMissingSAAnnotation, annotationTenantID)
|
||||||
|
}
|
||||||
|
token, err := fetchSAToken(ctx, ns, a.provider.ServiceAccountRef.Name, a.kubeClient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tp, err := tokenProvider(ctx, token, clientID, tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return autorest.NewBearerAuthorizer(tp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchSAToken(ctx context.Context, ns, name string, kubeClient kcorev1.CoreV1Interface) (string, error) {
|
||||||
|
token, err := kubeClient.ServiceAccounts(ns).CreateToken(ctx, name, &authv1.TokenRequest{
|
||||||
|
Spec: authv1.TokenRequestSpec{
|
||||||
|
Audiences: []string{azureDefaultAudience},
|
||||||
|
},
|
||||||
|
}, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return token.Status.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokenProvider satisfies the adal.OAuthTokenProvider interface.
|
||||||
|
type tokenProvider struct {
|
||||||
|
accessToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
type tokenProviderFunc func(ctx context.Context, token, clientID, tenantID string) (adal.OAuthTokenProvider, error)
|
||||||
|
|
||||||
|
func newTokenProvider(ctx context.Context, token, clientID, tenantID string) (adal.OAuthTokenProvider, error) {
|
||||||
|
// exchange token with Azure AccessToken
|
||||||
|
cred, err := confidential.NewCredFromAssertion(token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AZURE_AUTHORITY_HOST
|
||||||
|
|
||||||
|
cClient, err := confidential.New(clientID, cred, confidential.WithAuthority(
|
||||||
|
fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/token", tenantID),
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
authRes, err := cClient.AcquireTokenByCredential(ctx, []string{
|
||||||
|
"https://vault.azure.net/.default",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &tokenProvider{
|
||||||
|
accessToken: authRes.AccessToken,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tokenProvider) OAuthToken() string {
|
||||||
|
return t.accessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Azure) authorizerForManagedIdentity() (autorest.Authorizer, error) {
|
||||||
msiConfig := kvauth.NewMSIConfig()
|
msiConfig := kvauth.NewMSIConfig()
|
||||||
msiConfig.Resource = vaultResource
|
msiConfig.Resource = vaultResource
|
||||||
if a.provider.IdentityID != nil {
|
if a.provider.IdentityID != nil {
|
||||||
msiConfig.ClientID = *a.provider.IdentityID
|
msiConfig.ClientID = *a.provider.IdentityID
|
||||||
}
|
}
|
||||||
authorizer, err := msiConfig.Authorizer()
|
return msiConfig.Authorizer()
|
||||||
if err != nil {
|
|
||||||
return true, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cl := keyvault.New()
|
|
||||||
cl.Authorizer = authorizer
|
|
||||||
a.baseClient = &cl
|
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Azure) setAzureClientWithServicePrincipal(ctx context.Context) (bool, error) {
|
func (a *Azure) authorizerForServicePrincipal(ctx context.Context) (autorest.Authorizer, error) {
|
||||||
if *a.provider.AuthType != esv1beta1.ServicePrincipal {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.provider.TenantID == nil {
|
if a.provider.TenantID == nil {
|
||||||
return true, fmt.Errorf(errMissingTenant)
|
return nil, fmt.Errorf(errMissingTenant)
|
||||||
}
|
}
|
||||||
if a.provider.AuthSecretRef == nil {
|
if a.provider.AuthSecretRef == nil {
|
||||||
return true, fmt.Errorf(errMissingSecretRef)
|
return nil, fmt.Errorf(errMissingSecretRef)
|
||||||
}
|
}
|
||||||
if a.provider.AuthSecretRef.ClientID == nil || a.provider.AuthSecretRef.ClientSecret == nil {
|
if a.provider.AuthSecretRef.ClientID == nil || a.provider.AuthSecretRef.ClientSecret == nil {
|
||||||
return true, fmt.Errorf(errMissingClientIDSecret)
|
return nil, fmt.Errorf(errMissingClientIDSecret)
|
||||||
}
|
}
|
||||||
clusterScoped := false
|
clusterScoped := false
|
||||||
if a.store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
|
if a.store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
|
||||||
|
@ -280,26 +404,19 @@ func (a *Azure) setAzureClientWithServicePrincipal(ctx context.Context) (bool, e
|
||||||
}
|
}
|
||||||
cid, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *a.provider.AuthSecretRef.ClientID, clusterScoped)
|
cid, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *a.provider.AuthSecretRef.ClientID, clusterScoped)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return true, err
|
return nil, err
|
||||||
}
|
}
|
||||||
csec, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *a.provider.AuthSecretRef.ClientSecret, clusterScoped)
|
csec, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *a.provider.AuthSecretRef.ClientSecret, clusterScoped)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return true, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
clientCredentialsConfig := kvauth.NewClientCredentialsConfig(cid, csec, *a.provider.TenantID)
|
clientCredentialsConfig := kvauth.NewClientCredentialsConfig(cid, csec, *a.provider.TenantID)
|
||||||
clientCredentialsConfig.Resource = vaultResource
|
clientCredentialsConfig.Resource = vaultResource
|
||||||
authorizer, err := clientCredentialsConfig.Authorizer()
|
return clientCredentialsConfig.Authorizer()
|
||||||
if err != nil {
|
|
||||||
return true, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cl := keyvault.New()
|
|
||||||
cl.Authorizer = authorizer
|
|
||||||
a.baseClient = &cl
|
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// secretKeyRef fetch a secret key.
|
||||||
func (a *Azure) secretKeyRef(ctx context.Context, namespace string, secretRef smmeta.SecretKeySelector, clusterScoped bool) (string, error) {
|
func (a *Azure) secretKeyRef(ctx context.Context, namespace string, secretRef smmeta.SecretKeySelector, clusterScoped bool) (string, error) {
|
||||||
var secret corev1.Secret
|
var secret corev1.Secret
|
||||||
ref := types.NamespacedName{
|
ref := types.NamespacedName{
|
||||||
|
@ -309,7 +426,7 @@ func (a *Azure) secretKeyRef(ctx context.Context, namespace string, secretRef sm
|
||||||
if clusterScoped && secretRef.Namespace != nil {
|
if clusterScoped && secretRef.Namespace != nil {
|
||||||
ref.Namespace = *secretRef.Namespace
|
ref.Namespace = *secretRef.Namespace
|
||||||
}
|
}
|
||||||
err := a.kube.Get(ctx, ref, &secret)
|
err := a.crClient.Get(ctx, ref, &secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf(errFindSecret, ref.Namespace, ref.Name, err)
|
return "", fmt.Errorf(errFindSecret, ref.Namespace, ref.Name, err)
|
||||||
}
|
}
|
||||||
|
|
350
pkg/provider/azure/keyvault/keyvault_auth_test.go
Normal file
350
pkg/provider/azure/keyvault/keyvault_auth_test.go
Normal file
|
@ -0,0 +1,350 @@
|
||||||
|
/*
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package keyvault
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Azure/go-autorest/autorest"
|
||||||
|
"github.com/Azure/go-autorest/autorest/adal"
|
||||||
|
tassert "github.com/stretchr/testify/assert"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/utils/pointer"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||||
|
|
||||||
|
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||||
|
v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
|
||||||
|
awsauthfake "github.com/external-secrets/external-secrets/pkg/provider/aws/auth/fake"
|
||||||
|
)
|
||||||
|
|
||||||
|
var vaultURL = "https://local.vault.url"
|
||||||
|
|
||||||
|
func TestNewClientManagedIdentityNoNeedForCredentials(t *testing.T) {
|
||||||
|
namespace := "internal"
|
||||||
|
identityID := "1234"
|
||||||
|
authType := esv1beta1.AzureManagedIdentity
|
||||||
|
store := esv1beta1.SecretStore{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: namespace,
|
||||||
|
},
|
||||||
|
Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{AzureKV: &esv1beta1.AzureKVProvider{
|
||||||
|
AuthType: &authType,
|
||||||
|
IdentityID: &identityID,
|
||||||
|
VaultURL: &vaultURL,
|
||||||
|
}}},
|
||||||
|
}
|
||||||
|
k8sClient := clientfake.NewClientBuilder().Build()
|
||||||
|
az := &Azure{
|
||||||
|
crClient: k8sClient,
|
||||||
|
namespace: namespace,
|
||||||
|
provider: store.Spec.Provider.AzureKV,
|
||||||
|
store: &store,
|
||||||
|
}
|
||||||
|
authorizer, err := az.authorizerForManagedIdentity()
|
||||||
|
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, authorizer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAuthorizorForWorkloadIdentity(t *testing.T) {
|
||||||
|
const (
|
||||||
|
tenantID = "my-tenant-id"
|
||||||
|
clientID = "my-client-id"
|
||||||
|
azAccessToken = "my-access-token"
|
||||||
|
saToken = "FAKETOKEN"
|
||||||
|
saName = "az-wi"
|
||||||
|
namespace = "default"
|
||||||
|
)
|
||||||
|
|
||||||
|
// create a temporary file to imitate
|
||||||
|
// azure workload identity webhook
|
||||||
|
// see AZURE_FEDERATED_TOKEN_FILE
|
||||||
|
tf, err := os.CreateTemp("", "")
|
||||||
|
tassert.Nil(t, err)
|
||||||
|
defer os.RemoveAll(tf.Name())
|
||||||
|
_, err = tf.WriteString(saToken)
|
||||||
|
tassert.Nil(t, err)
|
||||||
|
tokenFile := tf.Name()
|
||||||
|
|
||||||
|
authType := esv1beta1.AzureWorkloadIdentity
|
||||||
|
defaultProvider := &esv1beta1.AzureKVProvider{
|
||||||
|
VaultURL: &vaultURL,
|
||||||
|
AuthType: &authType,
|
||||||
|
ServiceAccountRef: &v1.ServiceAccountSelector{
|
||||||
|
Name: saName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
provider *esv1beta1.AzureKVProvider
|
||||||
|
k8sObjects []client.Object
|
||||||
|
prep func()
|
||||||
|
cleanup func()
|
||||||
|
expErr string
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range []testCase{
|
||||||
|
{
|
||||||
|
name: "missing service account",
|
||||||
|
provider: defaultProvider,
|
||||||
|
expErr: "serviceaccounts \"" + saName + "\" not found",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing webhook env vars",
|
||||||
|
provider: &esv1beta1.AzureKVProvider{},
|
||||||
|
expErr: "missing environment variables. AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_FEDERATED_TOKEN_FILE must be set",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing workload identity token file",
|
||||||
|
provider: &esv1beta1.AzureKVProvider{},
|
||||||
|
prep: func() {
|
||||||
|
os.Setenv("AZURE_CLIENT_ID", clientID)
|
||||||
|
os.Setenv("AZURE_TENANT_ID", tenantID)
|
||||||
|
os.Setenv("AZURE_FEDERATED_TOKEN_FILE", "invalid file")
|
||||||
|
},
|
||||||
|
cleanup: func() {
|
||||||
|
os.Unsetenv("AZURE_CLIENT_ID")
|
||||||
|
os.Unsetenv("AZURE_TENANT_ID")
|
||||||
|
os.Unsetenv("AZURE_FEDERATED_TOKEN_FILE")
|
||||||
|
},
|
||||||
|
expErr: "unable to read token file invalid file: open invalid file: no such file or directory",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "correct workload identity",
|
||||||
|
provider: &esv1beta1.AzureKVProvider{},
|
||||||
|
prep: func() {
|
||||||
|
os.Setenv("AZURE_CLIENT_ID", clientID)
|
||||||
|
os.Setenv("AZURE_TENANT_ID", tenantID)
|
||||||
|
os.Setenv("AZURE_FEDERATED_TOKEN_FILE", tokenFile)
|
||||||
|
},
|
||||||
|
cleanup: func() {
|
||||||
|
os.Unsetenv("AZURE_CLIENT_ID")
|
||||||
|
os.Unsetenv("AZURE_TENANT_ID")
|
||||||
|
os.Unsetenv("AZURE_FEDERATED_TOKEN_FILE")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing sa annotations",
|
||||||
|
provider: defaultProvider,
|
||||||
|
k8sObjects: []client.Object{
|
||||||
|
&corev1.ServiceAccount{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: saName,
|
||||||
|
Namespace: namespace,
|
||||||
|
Annotations: map[string]string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expErr: "missing service account annotation: azure.workload.identity/client-id",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "successful case",
|
||||||
|
provider: defaultProvider,
|
||||||
|
k8sObjects: []client.Object{
|
||||||
|
&corev1.ServiceAccount{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: saName,
|
||||||
|
Namespace: namespace,
|
||||||
|
Annotations: map[string]string{
|
||||||
|
annotationClientID: clientID,
|
||||||
|
annotationTenantID: tenantID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(row.name, func(t *testing.T) {
|
||||||
|
store := esv1beta1.SecretStore{
|
||||||
|
Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{
|
||||||
|
AzureKV: row.provider,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
k8sClient := clientfake.NewClientBuilder().
|
||||||
|
WithObjects(row.k8sObjects...).
|
||||||
|
Build()
|
||||||
|
az := &Azure{
|
||||||
|
store: &store,
|
||||||
|
namespace: namespace,
|
||||||
|
crClient: k8sClient,
|
||||||
|
kubeClient: awsauthfake.NewCreateTokenMock(saToken),
|
||||||
|
provider: store.Spec.Provider.AzureKV,
|
||||||
|
}
|
||||||
|
tokenProvider := func(ctx context.Context, token, clientID, tenantID string) (adal.OAuthTokenProvider, error) {
|
||||||
|
tassert.Equal(t, token, saToken)
|
||||||
|
tassert.Equal(t, clientID, clientID)
|
||||||
|
tassert.Equal(t, tenantID, tenantID)
|
||||||
|
return &tokenProvider{accessToken: azAccessToken}, nil
|
||||||
|
}
|
||||||
|
if row.prep != nil {
|
||||||
|
row.prep()
|
||||||
|
}
|
||||||
|
if row.cleanup != nil {
|
||||||
|
defer row.cleanup()
|
||||||
|
}
|
||||||
|
authorizer, err := az.authorizerForWorkloadIdentity(context.Background(), tokenProvider)
|
||||||
|
if row.expErr == "" {
|
||||||
|
tassert.NotNil(t, authorizer)
|
||||||
|
tassert.Equal(t, getTokenFromAuthorizer(t, authorizer), azAccessToken)
|
||||||
|
} else {
|
||||||
|
tassert.EqualError(t, err, row.expErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuth(t *testing.T) {
|
||||||
|
defaultStore := esv1beta1.SecretStore{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: esv1beta1.SecretStoreSpec{
|
||||||
|
Provider: &esv1beta1.SecretStoreProvider{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
authType := esv1beta1.AzureServicePrincipal
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
provider *esv1beta1.AzureKVProvider
|
||||||
|
store esv1beta1.GenericStore
|
||||||
|
objects []client.Object
|
||||||
|
expErr string
|
||||||
|
}
|
||||||
|
for _, row := range []testCase{
|
||||||
|
{
|
||||||
|
name: "bad config",
|
||||||
|
expErr: "missing secretRef in provider config",
|
||||||
|
store: &defaultStore,
|
||||||
|
provider: &esv1beta1.AzureKVProvider{
|
||||||
|
AuthType: &authType,
|
||||||
|
VaultURL: &vaultURL,
|
||||||
|
TenantID: pointer.StringPtr("mytenant"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad config",
|
||||||
|
expErr: "missing accessKeyID/secretAccessKey in store config",
|
||||||
|
store: &defaultStore,
|
||||||
|
provider: &esv1beta1.AzureKVProvider{
|
||||||
|
AuthType: &authType,
|
||||||
|
VaultURL: &vaultURL,
|
||||||
|
TenantID: pointer.StringPtr("mytenant"),
|
||||||
|
AuthSecretRef: &esv1beta1.AzureKVAuth{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad config: missing secret",
|
||||||
|
expErr: "could not find secret default/password: secrets \"password\" not found",
|
||||||
|
store: &defaultStore,
|
||||||
|
provider: &esv1beta1.AzureKVProvider{
|
||||||
|
AuthType: &authType,
|
||||||
|
VaultURL: &vaultURL,
|
||||||
|
TenantID: pointer.StringPtr("mytenant"),
|
||||||
|
AuthSecretRef: &esv1beta1.AzureKVAuth{
|
||||||
|
ClientSecret: &v1.SecretKeySelector{Name: "password"},
|
||||||
|
ClientID: &v1.SecretKeySelector{Name: "password"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cluster secret store",
|
||||||
|
expErr: "could not find secret foo/password: secrets \"password\" not found",
|
||||||
|
store: &esv1beta1.ClusterSecretStore{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: esv1beta1.ClusterSecretStoreKind,
|
||||||
|
},
|
||||||
|
Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{}},
|
||||||
|
},
|
||||||
|
provider: &esv1beta1.AzureKVProvider{
|
||||||
|
AuthType: &authType,
|
||||||
|
VaultURL: &vaultURL,
|
||||||
|
TenantID: pointer.StringPtr("mytenant"),
|
||||||
|
AuthSecretRef: &esv1beta1.AzureKVAuth{
|
||||||
|
ClientSecret: &v1.SecretKeySelector{Name: "password", Namespace: pointer.StringPtr("foo")},
|
||||||
|
ClientID: &v1.SecretKeySelector{Name: "password", Namespace: pointer.StringPtr("foo")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "correct cluster secret store",
|
||||||
|
objects: []client.Object{&corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "password",
|
||||||
|
Namespace: "foo",
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"id": []byte("foo"),
|
||||||
|
"secret": []byte("bar"),
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
store: &esv1beta1.ClusterSecretStore{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: esv1beta1.ClusterSecretStoreKind,
|
||||||
|
},
|
||||||
|
Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{}},
|
||||||
|
},
|
||||||
|
provider: &esv1beta1.AzureKVProvider{
|
||||||
|
AuthType: &authType,
|
||||||
|
VaultURL: &vaultURL,
|
||||||
|
TenantID: pointer.StringPtr("mytenant"),
|
||||||
|
AuthSecretRef: &esv1beta1.AzureKVAuth{
|
||||||
|
ClientSecret: &v1.SecretKeySelector{Name: "password", Namespace: pointer.StringPtr("foo"), Key: "secret"},
|
||||||
|
ClientID: &v1.SecretKeySelector{Name: "password", Namespace: pointer.StringPtr("foo"), Key: "id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(row.name, func(t *testing.T) {
|
||||||
|
k8sClient := clientfake.NewClientBuilder().WithObjects(row.objects...).Build()
|
||||||
|
spec := row.store.GetSpec()
|
||||||
|
spec.Provider.AzureKV = row.provider
|
||||||
|
az := &Azure{
|
||||||
|
crClient: k8sClient,
|
||||||
|
namespace: "default",
|
||||||
|
provider: spec.Provider.AzureKV,
|
||||||
|
store: row.store,
|
||||||
|
}
|
||||||
|
authorizer, err := az.authorizerForServicePrincipal(context.Background())
|
||||||
|
if row.expErr == "" {
|
||||||
|
tassert.Nil(t, err)
|
||||||
|
tassert.NotNil(t, authorizer)
|
||||||
|
} else {
|
||||||
|
tassert.EqualError(t, err, row.expErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTokenFromAuthorizer(t *testing.T, authorizer autorest.Authorizer) string {
|
||||||
|
rq, _ := http.NewRequest("POST", "http://example.com", nil)
|
||||||
|
_, err := authorizer.WithAuthorization()(
|
||||||
|
autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||||
|
return rq, nil
|
||||||
|
})).Prepare(rq)
|
||||||
|
tassert.Nil(t, err)
|
||||||
|
return strings.TrimPrefix(rq.Header.Get("Authorization"), "Bearer ")
|
||||||
|
}
|
|
@ -22,10 +22,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/Azure/azure-sdk-for-go/services/keyvault/2016-10-01/keyvault"
|
"github.com/Azure/azure-sdk-for-go/services/keyvault/2016-10-01/keyvault"
|
||||||
tassert "github.com/stretchr/testify/assert"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/utils/pointer"
|
"k8s.io/utils/pointer"
|
||||||
clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
||||||
|
|
||||||
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||||
v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
|
v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
|
||||||
|
@ -82,76 +79,6 @@ func makeValidSecretManagerTestCaseCustom(tweaks ...func(smtc *secretManagerTest
|
||||||
return smtc
|
return smtc
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewClientManagedIdentityNoNeedForCredentials(t *testing.T) {
|
|
||||||
namespace := "internal"
|
|
||||||
vaultURL := "https://local.vault.url"
|
|
||||||
identityID := "1234"
|
|
||||||
authType := esv1beta1.ManagedIdentity
|
|
||||||
store := esv1beta1.SecretStore{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Namespace: namespace,
|
|
||||||
},
|
|
||||||
Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{AzureKV: &esv1beta1.AzureKVProvider{
|
|
||||||
AuthType: &authType,
|
|
||||||
IdentityID: &identityID,
|
|
||||||
VaultURL: &vaultURL,
|
|
||||||
}}},
|
|
||||||
}
|
|
||||||
|
|
||||||
provider, err := esv1beta1.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 := esv1beta1.ServicePrincipal
|
|
||||||
store := esv1beta1.SecretStore{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Namespace: namespace,
|
|
||||||
},
|
|
||||||
Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{AzureKV: &esv1beta1.AzureKVProvider{
|
|
||||||
AuthType: &authType,
|
|
||||||
VaultURL: &vaultURL,
|
|
||||||
TenantID: &tenantID,
|
|
||||||
}}},
|
|
||||||
}
|
|
||||||
provider, err := esv1beta1.GetProvider(&store)
|
|
||||||
tassert.Nil(t, err, "the return err should be nil")
|
|
||||||
k8sClient := clientfake.NewClientBuilder().Build()
|
|
||||||
_, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
|
|
||||||
tassert.EqualError(t, err, "missing secretRef in provider config")
|
|
||||||
|
|
||||||
store.Spec.Provider.AzureKV.AuthSecretRef = &esv1beta1.AzureKVAuth{}
|
|
||||||
_, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
|
|
||||||
tassert.EqualError(t, err, "missing accessKeyID/secretAccessKey in store config")
|
|
||||||
|
|
||||||
store.Spec.Provider.AzureKV.AuthSecretRef.ClientID = &v1.SecretKeySelector{Name: "user"}
|
|
||||||
_, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
|
|
||||||
tassert.EqualError(t, err, "missing accessKeyID/secretAccessKey in store config")
|
|
||||||
|
|
||||||
store.Spec.Provider.AzureKV.AuthSecretRef.ClientSecret = &v1.SecretKeySelector{Name: "password"}
|
|
||||||
_, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
|
|
||||||
tassert.EqualError(t, err, "could not find secret internal/user: secrets \"user\" not found")
|
|
||||||
store.TypeMeta.Kind = esv1beta1.ClusterSecretStoreKind
|
|
||||||
store.TypeMeta.APIVersion = esv1beta1.ClusterSecretStoreKindAPIVersion
|
|
||||||
ns := "default"
|
|
||||||
store.Spec.Provider.AzureKV.AuthSecretRef.ClientID.Namespace = &ns
|
|
||||||
store.Spec.Provider.AzureKV.AuthSecretRef.ClientSecret.Namespace = &ns
|
|
||||||
_, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
|
|
||||||
tassert.EqualError(t, err, "could not find secret default/user: secrets \"user\" not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
jwkPubRSA = `{"kid":"ex","kty":"RSA","key_ops":["sign","verify","wrapKey","unwrapKey","encrypt","decrypt"],"n":"p2VQo8qCfWAZmdWBVaYuYb-a-tWWm78K6Sr9poCvNcmv8rUPSLACxitQWR8gZaSH1DklVkqz-Ed8Cdlf8lkDg4Ex5tkB64jRdC1Uvn4CDpOH6cp-N2s8hTFLqy9_YaDmyQS7HiqthOi9oVjil1VMeWfaAbClGtFt6UnKD0Vb_DvLoWYQSqlhgBArFJi966b4E1pOq5Ad02K8pHBDThlIIx7unibLehhDU6q3DCwNH_OOLx6bgNtmvGYJDd1cywpkLQ3YzNCUPWnfMBJRP3iQP_WI21uP6cvo0DqBPBM4wvVzHbCT0vnIflwkbgEWkq1FprqAitZlop9KjLqzjp9vyQ","e":"AQAB"}`
|
jwkPubRSA = `{"kid":"ex","kty":"RSA","key_ops":["sign","verify","wrapKey","unwrapKey","encrypt","decrypt"],"n":"p2VQo8qCfWAZmdWBVaYuYb-a-tWWm78K6Sr9poCvNcmv8rUPSLACxitQWR8gZaSH1DklVkqz-Ed8Cdlf8lkDg4Ex5tkB64jRdC1Uvn4CDpOH6cp-N2s8hTFLqy9_YaDmyQS7HiqthOi9oVjil1VMeWfaAbClGtFt6UnKD0Vb_DvLoWYQSqlhgBArFJi966b4E1pOq5Ad02K8pHBDThlIIx7unibLehhDU6q3DCwNH_OOLx6bgNtmvGYJDd1cywpkLQ3YzNCUPWnfMBJRP3iQP_WI21uP6cvo0DqBPBM4wvVzHbCT0vnIflwkbgEWkq1FprqAitZlop9KjLqzjp9vyQ","e":"AQAB"}`
|
||||||
jwkPubEC = `{"kid":"https://example.vault.azure.net/keys/ec-p-521/e3d0e9c179b54988860c69c6ae172c65","kty":"EC","key_ops":["sign","verify"],"crv":"P-521","x":"AedOAtb7H7Oz1C_cPKI_R4CN_eai5nteY6KFW07FOoaqgQfVCSkQDK22fCOiMT_28c8LZYJRsiIFz_IIbQUW7bXj","y":"AOnchHnmBphIWXvanmMAmcCDkaED6ycW8GsAl9fQ43BMVZTqcTkJYn6vGnhn7MObizmkNSmgZYTwG-vZkIg03HHs"}`
|
jwkPubEC = `{"kid":"https://example.vault.azure.net/keys/ec-p-521/e3d0e9c179b54988860c69c6ae172c65","kty":"EC","key_ops":["sign","verify"],"crv":"P-521","x":"AedOAtb7H7Oz1C_cPKI_R4CN_eai5nteY6KFW07FOoaqgQfVCSkQDK22fCOiMT_28c8LZYJRsiIFz_IIbQUW7bXj","y":"AOnchHnmBphIWXvanmMAmcCDkaED6ycW8GsAl9fQ43BMVZTqcTkJYn6vGnhn7MObizmkNSmgZYTwG-vZkIg03HHs"}`
|
||||||
|
|
Loading…
Reference in a new issue