Browse Source

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>
Moritz Johner 4 years ago
parent
commit
cf7e3832ae

+ 23 - 8
apis/externalsecrets/v1alpha1/secretstore_azurekv_types.go

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

+ 6 - 1
apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go

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

+ 23 - 8
apis/externalsecrets/v1beta1/secretstore_azurekv_types.go

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

+ 6 - 1
apis/externalsecrets/v1beta1/zz_generated.deepcopy.go

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

+ 34 - 6
config/crds/bases/external-secrets.io_clustersecretstores.yaml

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

+ 34 - 6
config/crds/bases/external-secrets.io_secretstores.yaml

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

+ 52 - 12
deploy/crds/bundle.yaml

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

File diff suppressed because it is too large
+ 41 - 1
docs/provider-azure-key-vault.md


+ 19 - 0
docs/snippets/azkv-workload-identity-mounted.yaml

@@ -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 - 0
docs/snippets/azkv-workload-identity.yaml

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

+ 22 - 4
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>&#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

+ 5 - 2
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 - 0
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 - 0
pkg/provider/aws/auth/fake/token_fetcher.go

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

+ 3 - 32
pkg/provider/aws/auth/token_fetcher_test.go

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

+ 165 - 48
pkg/provider/azure/keyvault/keyvault.go

@@ -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"
@@ -34,10 +43,13 @@ import (
 )
 
 const (
-	defaultObjType = "secret"
-	objectTypeCert = "cert"
-	objectTypeKey  = "key"
-	vaultResource  = "https://vault.azure.net"
+	defaultObjType       = "secret"
+	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,
-		store:     store,
-		namespace: namespace,
-		provider:  provider,
+		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
+}
 
-	msiConfig := kvauth.NewMSIConfig()
-	msiConfig.Resource = vaultResource
-	if a.provider.IdentityID != nil {
-		msiConfig.ClientID = *a.provider.IdentityID
+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
 	}
-	authorizer, err := msiConfig.Authorizer()
+
+	// 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 true, err
+		return nil, err
 	}
 
-	cl := keyvault.New()
-	cl.Authorizer = authorizer
-	a.baseClient = &cl
-	return true, nil
+	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) setAzureClientWithServicePrincipal(ctx context.Context) (bool, error) {
-	if *a.provider.AuthType != esv1beta1.ServicePrincipal {
-		return false, nil
+func (a *Azure) authorizerForManagedIdentity() (autorest.Authorizer, error) {
+	msiConfig := kvauth.NewMSIConfig()
+	msiConfig.Resource = vaultResource
+	if a.provider.IdentityID != nil {
+		msiConfig.ClientID = *a.provider.IdentityID
 	}
+	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 - 0
pkg/provider/azure/keyvault/keyvault_auth_test.go

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

+ 0 - 73
pkg/provider/azure/keyvault/keyvault_test.go

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