1
0
Fork 0
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:
Moritz Johner 2022-03-22 21:59:01 +01:00 committed by GitHub
parent d0a32b6f2d
commit cf7e3832ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 870 additions and 203 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View 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

View file

@ -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>&#34;ManagedIdentity&#34;</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>&#34;ServicePrincipal&#34;</p></td>
<td><p>Using service principal to authenticate, which needs a tenantId, a clientId and a clientSecret.</p>
</td>
</tr><tr><td><p>&#34;WorkloadIdentity&#34;</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
View file

@ -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
View file

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

View 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
}

View file

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

View file

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

View 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 ")
}

View file

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