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.
|
||||
// If none of the following auth type is specified, the default one
|
||||
// is ServicePrincipal.
|
||||
// +kubebuilder:validation:Enum=ServicePrincipal;ManagedIdentity
|
||||
type AuthType string
|
||||
// +kubebuilder:validation:Enum=ServicePrincipal;ManagedIdentity;WorkloadIdentity
|
||||
type AzureAuthType string
|
||||
|
||||
const (
|
||||
// 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.
|
||||
ManagedIdentity AuthType = "ManagedIdentity"
|
||||
// Using Managed Identity to authenticate. Used with aad-pod-identity installed in the clister.
|
||||
AzureManagedIdentity AzureAuthType = "ManagedIdentity"
|
||||
|
||||
// Using Workload Identity service accounts to authenticate.
|
||||
AzureWorkloadIdentity AzureAuthType = "WorkloadIdentity"
|
||||
)
|
||||
|
||||
// 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)
|
||||
// +optional
|
||||
// +kubebuilder:default=ServicePrincipal
|
||||
AuthType *AuthType `json:"authType,omitempty"`
|
||||
AuthType *AzureAuthType `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. 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"`
|
||||
|
||||
// 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
|
||||
// +optional
|
||||
IdentityID *string `json:"identityId,omitempty"`
|
||||
|
@ -56,7 +68,10 @@ type AzureKVProvider struct {
|
|||
// Configuration used to authenticate with Azure.
|
||||
type AzureKVAuth struct {
|
||||
// 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.
|
||||
ClientSecret *smmeta.SecretKeySelector `json:"clientSecret"`
|
||||
// +optional
|
||||
ClientSecret *smmeta.SecretKeySelector `json:"clientSecret,omitempty"`
|
||||
}
|
||||
|
|
|
@ -245,7 +245,7 @@ func (in *AzureKVProvider) DeepCopyInto(out *AzureKVProvider) {
|
|||
*out = *in
|
||||
if in.AuthType != nil {
|
||||
in, out := &in.AuthType, &out.AuthType
|
||||
*out = new(AuthType)
|
||||
*out = new(AzureAuthType)
|
||||
**out = **in
|
||||
}
|
||||
if in.VaultURL != nil {
|
||||
|
@ -263,6 +263,11 @@ func (in *AzureKVProvider) DeepCopyInto(out *AzureKVProvider) {
|
|||
*out = new(AzureKVAuth)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.ServiceAccountRef != nil {
|
||||
in, out := &in.ServiceAccountRef, &out.ServiceAccountRef
|
||||
*out = new(metav1.ServiceAccountSelector)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.IdentityID != nil {
|
||||
in, out := &in.IdentityID, &out.IdentityID
|
||||
*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.
|
||||
// If none of the following auth type is specified, the default one
|
||||
// is ServicePrincipal.
|
||||
// +kubebuilder:validation:Enum=ServicePrincipal;ManagedIdentity
|
||||
type AuthType string
|
||||
// +kubebuilder:validation:Enum=ServicePrincipal;ManagedIdentity;WorkloadIdentity
|
||||
type AzureAuthType string
|
||||
|
||||
const (
|
||||
// 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.
|
||||
ManagedIdentity AuthType = "ManagedIdentity"
|
||||
// Using Managed Identity to authenticate. Used with aad-pod-identity installed in the clister.
|
||||
AzureManagedIdentity AzureAuthType = "ManagedIdentity"
|
||||
|
||||
// Using Workload Identity service accounts to authenticate.
|
||||
AzureWorkloadIdentity AzureAuthType = "WorkloadIdentity"
|
||||
)
|
||||
|
||||
// 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)
|
||||
// +optional
|
||||
// +kubebuilder:default=ServicePrincipal
|
||||
AuthType *AuthType `json:"authType,omitempty"`
|
||||
AuthType *AzureAuthType `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. 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"`
|
||||
|
||||
// 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
|
||||
// +optional
|
||||
IdentityID *string `json:"identityId,omitempty"`
|
||||
|
@ -56,7 +68,10 @@ type AzureKVProvider struct {
|
|||
// Configuration used to authenticate with Azure.
|
||||
type AzureKVAuth struct {
|
||||
// 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.
|
||||
ClientSecret *smmeta.SecretKeySelector `json:"clientSecret"`
|
||||
// +optional
|
||||
ClientSecret *smmeta.SecretKeySelector `json:"clientSecret,omitempty"`
|
||||
}
|
||||
|
|
|
@ -245,7 +245,7 @@ func (in *AzureKVProvider) DeepCopyInto(out *AzureKVProvider) {
|
|||
*out = *in
|
||||
if in.AuthType != nil {
|
||||
in, out := &in.AuthType, &out.AuthType
|
||||
*out = new(AuthType)
|
||||
*out = new(AzureAuthType)
|
||||
**out = **in
|
||||
}
|
||||
if in.VaultURL != nil {
|
||||
|
@ -263,6 +263,11 @@ func (in *AzureKVProvider) DeepCopyInto(out *AzureKVProvider) {
|
|||
*out = new(AzureKVAuth)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.ServiceAccountRef != nil {
|
||||
in, out := &in.ServiceAccountRef, &out.ServiceAccountRef
|
||||
*out = new(metav1.ServiceAccountSelector)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.IdentityID != nil {
|
||||
in, out := &in.IdentityID, &out.IdentityID
|
||||
*out = new(string)
|
||||
|
|
|
@ -353,9 +353,6 @@ spec:
|
|||
defaults to the namespace of the referent.
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- clientId
|
||||
- clientSecret
|
||||
type: object
|
||||
authType:
|
||||
default: ServicePrincipal
|
||||
|
@ -367,11 +364,28 @@ spec:
|
|||
enum:
|
||||
- ServicePrincipal
|
||||
- ManagedIdentity
|
||||
- WorkloadIdentity
|
||||
type: string
|
||||
identityId:
|
||||
description: If multiple Managed Identity is assigned to the
|
||||
pod, you can select the one to be used
|
||||
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:
|
||||
description: TenantID configures the Azure Tenant to send
|
||||
requests to. Required for ServicePrincipal auth type.
|
||||
|
@ -1658,9 +1672,6 @@ spec:
|
|||
defaults to the namespace of the referent.
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- clientId
|
||||
- clientSecret
|
||||
type: object
|
||||
authType:
|
||||
default: ServicePrincipal
|
||||
|
@ -1672,11 +1683,28 @@ spec:
|
|||
enum:
|
||||
- ServicePrincipal
|
||||
- ManagedIdentity
|
||||
- WorkloadIdentity
|
||||
type: string
|
||||
identityId:
|
||||
description: If multiple Managed Identity is assigned to the
|
||||
pod, you can select the one to be used
|
||||
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:
|
||||
description: TenantID configures the Azure Tenant to send
|
||||
requests to. Required for ServicePrincipal auth type.
|
||||
|
|
|
@ -353,9 +353,6 @@ spec:
|
|||
defaults to the namespace of the referent.
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- clientId
|
||||
- clientSecret
|
||||
type: object
|
||||
authType:
|
||||
default: ServicePrincipal
|
||||
|
@ -367,11 +364,28 @@ spec:
|
|||
enum:
|
||||
- ServicePrincipal
|
||||
- ManagedIdentity
|
||||
- WorkloadIdentity
|
||||
type: string
|
||||
identityId:
|
||||
description: If multiple Managed Identity is assigned to the
|
||||
pod, you can select the one to be used
|
||||
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:
|
||||
description: TenantID configures the Azure Tenant to send
|
||||
requests to. Required for ServicePrincipal auth type.
|
||||
|
@ -1661,9 +1675,6 @@ spec:
|
|||
defaults to the namespace of the referent.
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- clientId
|
||||
- clientSecret
|
||||
type: object
|
||||
authType:
|
||||
default: ServicePrincipal
|
||||
|
@ -1675,11 +1686,28 @@ spec:
|
|||
enum:
|
||||
- ServicePrincipal
|
||||
- ManagedIdentity
|
||||
- WorkloadIdentity
|
||||
type: string
|
||||
identityId:
|
||||
description: If multiple Managed Identity is assigned to the
|
||||
pod, you can select the one to be used
|
||||
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:
|
||||
description: TenantID configures the Azure Tenant to send
|
||||
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.
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- clientId
|
||||
- clientSecret
|
||||
type: object
|
||||
authType:
|
||||
default: ServicePrincipal
|
||||
|
@ -595,10 +592,23 @@ spec:
|
|||
enum:
|
||||
- ServicePrincipal
|
||||
- ManagedIdentity
|
||||
- WorkloadIdentity
|
||||
type: string
|
||||
identityId:
|
||||
description: If multiple Managed Identity is assigned to the pod, you can select the one to be used
|
||||
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:
|
||||
description: TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
|
||||
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.
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- clientId
|
||||
- clientSecret
|
||||
type: object
|
||||
authType:
|
||||
default: ServicePrincipal
|
||||
|
@ -1558,10 +1565,23 @@ spec:
|
|||
enum:
|
||||
- ServicePrincipal
|
||||
- ManagedIdentity
|
||||
- WorkloadIdentity
|
||||
type: string
|
||||
identityId:
|
||||
description: If multiple Managed Identity is assigned to the pod, you can select the one to be used
|
||||
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:
|
||||
description: TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
|
||||
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.
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- clientId
|
||||
- clientSecret
|
||||
type: object
|
||||
authType:
|
||||
default: ServicePrincipal
|
||||
|
@ -3061,10 +3078,23 @@ spec:
|
|||
enum:
|
||||
- ServicePrincipal
|
||||
- ManagedIdentity
|
||||
- WorkloadIdentity
|
||||
type: string
|
||||
identityId:
|
||||
description: If multiple Managed Identity is assigned to the pod, you can select the one to be used
|
||||
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:
|
||||
description: TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
|
||||
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.
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- clientId
|
||||
- clientSecret
|
||||
type: object
|
||||
authType:
|
||||
default: ServicePrincipal
|
||||
|
@ -4027,10 +4054,23 @@ spec:
|
|||
enum:
|
||||
- ServicePrincipal
|
||||
- ManagedIdentity
|
||||
- WorkloadIdentity
|
||||
type: string
|
||||
identityId:
|
||||
description: If multiple Managed Identity is assigned to the pod, you can select the one to be used
|
||||
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:
|
||||
description: TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
|
||||
type: string
|
||||
|
|
|
@ -7,7 +7,7 @@ External Secrets Operator integrates with [Azure Key vault](https://azure.micros
|
|||
|
||||
### 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.
|
||||
|
||||
|
@ -25,6 +25,46 @@ If there are multiple Managed Identitites for different keyvaults, the operator
|
|||
{% 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
|
||||
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>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="external-secrets.io/v1alpha1.AuthType">AuthType
|
||||
<h3 id="external-secrets.io/v1alpha1.AzureAuthType">AzureAuthType
|
||||
(<code>string</code> alias)</p></h3>
|
||||
<p>
|
||||
(<em>Appears on:</em>
|
||||
|
@ -494,11 +494,14 @@ is ServicePrincipal.</p>
|
|||
</tr>
|
||||
</thead>
|
||||
<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>
|
||||
</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>
|
||||
</tr><tr><td><p>"WorkloadIdentity"</p></td>
|
||||
<td><p>Using Workload Identity service accounts to authenticate.</p>
|
||||
</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
<h3 id="external-secrets.io/v1alpha1.AzureKVAuth">AzureKVAuth
|
||||
|
@ -526,6 +529,7 @@ github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
|
|||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>The Azure clientId of the service principle used for authentication.</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -537,6 +541,7 @@ github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
|
|||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>The Azure ClientSecret of the service principle used for authentication.</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -563,8 +568,8 @@ github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
|
|||
<td>
|
||||
<code>authType</code></br>
|
||||
<em>
|
||||
<a href="#external-secrets.io/v1alpha1.AuthType">
|
||||
AuthType
|
||||
<a href="#external-secrets.io/v1alpha1.AzureAuthType">
|
||||
AzureAuthType
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
|
@ -615,6 +620,19 @@ AzureKVAuth
|
|||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<em>
|
||||
string
|
||||
|
|
7
go.mod
7
go.mod
|
@ -37,7 +37,10 @@ require (
|
|||
cloud.google.com/go/iam v0.3.0
|
||||
cloud.google.com/go/secretmanager v1.3.0
|
||||
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/AzureAD/microsoft-authentication-library-for-go v0.4.0
|
||||
github.com/IBM/go-sdk-core/v5 v5.9.3
|
||||
github.com/IBM/secrets-manager-go-sdk v1.0.37
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
|
@ -90,8 +93,6 @@ require (
|
|||
require (
|
||||
cloud.google.com/go/compute v1.5.0 // 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/date v0.3.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/goccy/go-json v0.9.4 // 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/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
|
@ -152,6 +154,7 @@ require (
|
|||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // 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/lestrrat-go/backoff/v2 v2.0.8 // 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/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
|
||||
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/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=
|
||||
|
@ -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/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
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.1.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.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
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.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
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/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
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-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
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.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/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.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
authv1 "k8s.io/api/authentication/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
k8sv1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
|
||||
"github.com/external-secrets/external-secrets/pkg/provider/aws/auth/fake"
|
||||
)
|
||||
|
||||
func TestTokenFetcher(t *testing.T) {
|
||||
tf := &authTokenFetcher{
|
||||
ServiceAccount: "foobar",
|
||||
Namespace: "example",
|
||||
k8sClient: &mockK8sV1{},
|
||||
k8sClient: fake.NewCreateTokenMock("FAKETOKEN"),
|
||||
}
|
||||
token, err := tf.FetchToken(context.Background())
|
||||
assert.Nil(t, err)
|
||||
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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
|
||||
"github.com/tidwall/gjson"
|
||||
authv1 "k8s.io/api/authentication/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"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"
|
||||
ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||
|
||||
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||
smmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
|
||||
|
@ -38,6 +47,9 @@ const (
|
|||
objectTypeCert = "cert"
|
||||
objectTypeKey = "key"
|
||||
vaultResource = "https://vault.azure.net"
|
||||
azureDefaultAudience = "api://AzureADTokenExchange"
|
||||
annotationClientID = "azure.workload.identity/client-id"
|
||||
annotationTenantID = "azure.workload.identity/tenant-id"
|
||||
|
||||
errUnexpectedStoreSpec = "unexpected store spec"
|
||||
errMissingAuthType = "cannot initialize Azure Client: no valid authType was specified"
|
||||
|
@ -58,6 +70,11 @@ const (
|
|||
errInvalidAzureProv = "invalid azure keyvault provider"
|
||||
errInvalidSecRefClientID = "invalid AuthSecretRef.ClientID: %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.
|
||||
|
@ -69,7 +86,8 @@ type SecretClient interface {
|
|||
}
|
||||
|
||||
type Azure struct {
|
||||
kube client.Client
|
||||
crClient client.Client
|
||||
kubeClient kcorev1.CoreV1Interface
|
||||
store esv1beta1.GenericStore
|
||||
provider *esv1beta1.AzureKVProvider
|
||||
baseClient SecretClient
|
||||
|
@ -92,24 +110,39 @@ func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Cl
|
|||
if err != nil {
|
||||
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{
|
||||
kube: kube,
|
||||
crClient: kube,
|
||||
kubeClient: kubeClient.CoreV1(),
|
||||
store: store,
|
||||
namespace: namespace,
|
||||
provider: provider,
|
||||
}
|
||||
|
||||
ok, err := az.setAzureClientWithManagedIdentity()
|
||||
if ok {
|
||||
return az, err
|
||||
var authorizer autorest.Authorizer
|
||||
switch *provider.AuthType {
|
||||
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)
|
||||
if ok {
|
||||
return az, err
|
||||
}
|
||||
cl := keyvault.New()
|
||||
cl.Authorizer = authorizer
|
||||
az.baseClient = &cl
|
||||
|
||||
return nil, fmt.Errorf(errMissingAuthType)
|
||||
return az, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -239,40 +277,126 @@ func (a *Azure) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDa
|
|||
return nil, fmt.Errorf(errUnknownObjectType, secretName)
|
||||
}
|
||||
|
||||
func (a *Azure) setAzureClientWithManagedIdentity() (bool, error) {
|
||||
if *a.provider.AuthType != esv1beta1.ManagedIdentity {
|
||||
return false, nil
|
||||
func (a *Azure) authorizerForWorkloadIdentity(ctx context.Context, tokenProvider tokenProviderFunc) (autorest.Authorizer, error) {
|
||||
// if no serviceAccountRef was provided
|
||||
// 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.Resource = vaultResource
|
||||
if a.provider.IdentityID != nil {
|
||||
msiConfig.ClientID = *a.provider.IdentityID
|
||||
}
|
||||
authorizer, err := 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) {
|
||||
if *a.provider.AuthType != esv1beta1.ServicePrincipal {
|
||||
return false, nil
|
||||
return msiConfig.Authorizer()
|
||||
}
|
||||
|
||||
func (a *Azure) authorizerForServicePrincipal(ctx context.Context) (autorest.Authorizer, error) {
|
||||
if a.provider.TenantID == nil {
|
||||
return true, fmt.Errorf(errMissingTenant)
|
||||
return nil, fmt.Errorf(errMissingTenant)
|
||||
}
|
||||
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 {
|
||||
return true, fmt.Errorf(errMissingClientIDSecret)
|
||||
return nil, fmt.Errorf(errMissingClientIDSecret)
|
||||
}
|
||||
clusterScoped := false
|
||||
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)
|
||||
if err != nil {
|
||||
return true, err
|
||||
return nil, err
|
||||
}
|
||||
csec, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *a.provider.AuthSecretRef.ClientSecret, clusterScoped)
|
||||
if err != nil {
|
||||
return true, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientCredentialsConfig := kvauth.NewClientCredentialsConfig(cid, csec, *a.provider.TenantID)
|
||||
clientCredentialsConfig.Resource = vaultResource
|
||||
authorizer, err := clientCredentialsConfig.Authorizer()
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
cl := keyvault.New()
|
||||
cl.Authorizer = authorizer
|
||||
a.baseClient = &cl
|
||||
return true, nil
|
||||
return clientCredentialsConfig.Authorizer()
|
||||
}
|
||||
|
||||
// secretKeyRef fetch a secret key.
|
||||
func (a *Azure) secretKeyRef(ctx context.Context, namespace string, secretRef smmeta.SecretKeySelector, clusterScoped bool) (string, error) {
|
||||
var secret corev1.Secret
|
||||
ref := types.NamespacedName{
|
||||
|
@ -309,7 +426,7 @@ func (a *Azure) secretKeyRef(ctx context.Context, namespace string, secretRef sm
|
|||
if clusterScoped && secretRef.Namespace != nil {
|
||||
ref.Namespace = *secretRef.Namespace
|
||||
}
|
||||
err := a.kube.Get(ctx, ref, &secret)
|
||||
err := a.crClient.Get(ctx, ref, &secret)
|
||||
if err != nil {
|
||||
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"
|
||||
|
||||
"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"
|
||||
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"
|
||||
|
@ -82,76 +79,6 @@ func makeValidSecretManagerTestCaseCustom(tweaks ...func(smtc *secretManagerTest
|
|||
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 (
|
||||
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"}`
|
||||
|
|
Loading…
Reference in a new issue