Browse Source

feat(akeyless): support azure_ad Workload Identity via serviceAccountRef (#6530)

* feat(akeyless): support azure_ad Workload Identity via serviceAccountRef

Add authSecretRef.serviceAccountRef on AkeylessAuth so SecretStore and
ClusterSecretStore can authenticate with azure_ad using a namespace-scoped
ServiceAccount token (AKS Workload Identity) instead of the ESO controller
pod identity.

Signed-off-by: baraka-akeyless <barak.a@akeyless.io>
Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(akeyless): clarify azure_ad cloud ID base64 format

Document that Workload Identity path returns the same base64-encoded
access token format as akeyless-go-cloud-id GetCloudId.

Signed-off-by: baraka-akeyless <barak.a@akeyless.io>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(akeyless): address PR review for azure_ad Workload Identity

- Skip optional accessTypeParam resolution when secret ref name is unset
- Document ClusterSecretStore serviceAccountRef.namespace requirement
- Add ValidateStore tests for serviceAccountRef namespace boundaries

Signed-off-by: baraka-akeyless <barak.a@akeyless.io>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(akeyless): fix gci import order and regenerate API spec

Fix golangci-lint gci failure in azure_cloud_id.go and commit
generated docs/api/spec.md for serviceAccountRef so check-diff passes.

Signed-off-by: baraka-akeyless <barak.a@akeyless.io>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(akeyless): address maintainer review on azure_ad Workload Identity

Document azure_ad-only serviceAccountRef scope, clarify ClusterSecretStore
namespace default behavior, validate access key auth requires AccessTypeParam,
and select sovereign Azure cloud endpoints from AZURE_ENVIRONMENT/AZURE_CLOUD
for the Workload Identity token path.

Signed-off-by: baraka-akeyless <barak.a@akeyless.io>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(akeyless): fix gci formatting in azure_cloud_id.go

Align annotationTenantID const spacing to satisfy check-diff gci lint.

Signed-off-by: baraka-akeyless <barak.a@akeyless.io>
Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(akeyless): regenerate CRDs and API spec for serviceAccountRef docs

Commit generated CRD and docs/api/spec.md changes from the updated
AkeylessAuth ServiceAccountRef godoc so check-diff passes in CI.

Signed-off-by: baraka-akeyless <barak.a@akeyless.io>
Co-authored-by: Cursor <cursoragent@cursor.com>

* test(akeyless): cover AZURE_CLOUD fallback in cloud settings test

Add a subtest for azureCloudSettingsFromEnv when only AZURE_CLOUD is set,
matching the documented sovereign-cloud configuration path.

Signed-off-by: baraka-akeyless <barak.a@akeyless.io>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(akeyless): address @alekc review nits on WI PR

Use a named constant for service account token expiration, inline-if
ValidateSecretSelector for accessTypeParam, and simplify aws/gcp cloud ID
returns in getCloudID.

Signed-off-by: baraka-akeyless <barak.a@akeyless.io>
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Signed-off-by: baraka-akeyless <barak.a@akeyless.io>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Jean-Philippe Evrard <jean-philippe.evrard+rochepub@external.roche.com>
baraka-akeyless 6 days ago
parent
commit
fcc5c43751

+ 7 - 0
apis/externalsecrets/v1/secretstore_akeyless_types.go

@@ -52,6 +52,13 @@ type AkeylessAuth struct {
 	// token stored in the named Secret resource.
 	// +optional
 	KubernetesAuth *AkeylessKubernetesAuth `json:"kubernetesAuth,omitempty"`
+
+	// ServiceAccountRef specifies a Kubernetes ServiceAccount used for azure_ad
+	// authentication on AKS Workload Identity. The operator obtains a federated
+	// identity token from this ServiceAccount via the TokenRequest API instead
+	// of using the ESO controller pod identity. Ignored for other access types.
+	// +optional
+	ServiceAccountRef *esmeta.ServiceAccountSelector `json:"serviceAccountRef,omitempty"`
 }
 
 // AkeylessAuthSecretRef references a Secret that contains the details

+ 5 - 0
apis/externalsecrets/v1/zz_generated.deepcopy.go

@@ -151,6 +151,11 @@ func (in *AkeylessAuth) DeepCopyInto(out *AkeylessAuth) {
 		*out = new(AkeylessKubernetesAuth)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.ServiceAccountRef != nil {
+		in, out := &in.ServiceAccountRef, &out.ServiceAccountRef
+		*out = new(apismetav1.ServiceAccountSelector)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AkeylessAuth.

+ 33 - 0
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -321,6 +321,39 @@ spec:
                                     type: string
                                 type: object
                             type: object
+                          serviceAccountRef:
+                            description: |-
+                              ServiceAccountRef specifies a Kubernetes ServiceAccount used for azure_ad
+                              authentication on AKS Workload Identity. The operator obtains a federated
+                              identity token from this ServiceAccount via the TokenRequest API instead
+                              of using the ESO controller pod identity. Ignored for other access types.
+                            properties:
+                              audiences:
+                                description: |-
+                                  Audience specifies the `aud` claim for the service account token
+                                  If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                                  then this audiences will be appended to the list
+                                items:
+                                  type: string
+                                type: array
+                              name:
+                                description: The name of the ServiceAccount resource
+                                  being referred to.
+                                maxLength: 253
+                                minLength: 1
+                                pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                type: string
+                              namespace:
+                                description: |-
+                                  Namespace of the resource being referred to.
+                                  Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                maxLength: 63
+                                minLength: 1
+                                pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                type: string
+                            required:
+                            - name
+                            type: object
                         type: object
                       caBundle:
                         description: |-

+ 33 - 0
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -321,6 +321,39 @@ spec:
                                     type: string
                                 type: object
                             type: object
+                          serviceAccountRef:
+                            description: |-
+                              ServiceAccountRef specifies a Kubernetes ServiceAccount used for azure_ad
+                              authentication on AKS Workload Identity. The operator obtains a federated
+                              identity token from this ServiceAccount via the TokenRequest API instead
+                              of using the ESO controller pod identity. Ignored for other access types.
+                            properties:
+                              audiences:
+                                description: |-
+                                  Audience specifies the `aud` claim for the service account token
+                                  If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                                  then this audiences will be appended to the list
+                                items:
+                                  type: string
+                                type: array
+                              name:
+                                description: The name of the ServiceAccount resource
+                                  being referred to.
+                                maxLength: 253
+                                minLength: 1
+                                pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                type: string
+                              namespace:
+                                description: |-
+                                  Namespace of the resource being referred to.
+                                  Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                maxLength: 63
+                                minLength: 1
+                                pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                type: string
+                            required:
+                            - name
+                            type: object
                         type: object
                       caBundle:
                         description: |-

+ 64 - 0
deploy/crds/bundle.yaml

@@ -2636,6 +2636,38 @@ spec:
                                       type: string
                                   type: object
                               type: object
+                            serviceAccountRef:
+                              description: |-
+                                ServiceAccountRef specifies a Kubernetes ServiceAccount used for azure_ad
+                                authentication on AKS Workload Identity. The operator obtains a federated
+                                identity token from this ServiceAccount via the TokenRequest API instead
+                                of using the ESO controller pod identity. Ignored for other access types.
+                              properties:
+                                audiences:
+                                  description: |-
+                                    Audience specifies the `aud` claim for the service account token
+                                    If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                                    then this audiences will be appended to the list
+                                  items:
+                                    type: string
+                                  type: array
+                                name:
+                                  description: The name of the ServiceAccount resource being referred to.
+                                  maxLength: 253
+                                  minLength: 1
+                                  pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                  type: string
+                                namespace:
+                                  description: |-
+                                    Namespace of the resource being referred to.
+                                    Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                  maxLength: 63
+                                  minLength: 1
+                                  pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                  type: string
+                              required:
+                                - name
+                              type: object
                           type: object
                         caBundle:
                           description: |-
@@ -15375,6 +15407,38 @@ spec:
                                       type: string
                                   type: object
                               type: object
+                            serviceAccountRef:
+                              description: |-
+                                ServiceAccountRef specifies a Kubernetes ServiceAccount used for azure_ad
+                                authentication on AKS Workload Identity. The operator obtains a federated
+                                identity token from this ServiceAccount via the TokenRequest API instead
+                                of using the ESO controller pod identity. Ignored for other access types.
+                              properties:
+                                audiences:
+                                  description: |-
+                                    Audience specifies the `aud` claim for the service account token
+                                    If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                                    then this audiences will be appended to the list
+                                  items:
+                                    type: string
+                                  type: array
+                                name:
+                                  description: The name of the ServiceAccount resource being referred to.
+                                  maxLength: 253
+                                  minLength: 1
+                                  pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                  type: string
+                                namespace:
+                                  description: |-
+                                    Namespace of the resource being referred to.
+                                    Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                  maxLength: 63
+                                  minLength: 1
+                                  pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                  type: string
+                              required:
+                                - name
+                              type: object
                           type: object
                         caBundle:
                           description: |-

+ 17 - 0
docs/api/spec.md

@@ -408,6 +408,23 @@ AkeylessKubernetesAuth
 token stored in the named Secret resource.</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>serviceAccountRef</code></br>
+<em>
+<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#ServiceAccountSelector">
+External Secrets meta/v1.ServiceAccountSelector
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>ServiceAccountRef specifies a Kubernetes ServiceAccount used for azure_ad
+authentication on AKS Workload Identity. The operator obtains a federated
+identity token from this ServiceAccount via the TokenRequest API instead
+of using the ESO controller pod identity. Ignored for other access types.</p>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1.AkeylessAuthSecretRef">AkeylessAuthSecretRef

+ 8 - 0
docs/provider/akeyless.md

@@ -43,6 +43,14 @@ The supported auth-methods and their parameters are:
 | `access_key`   | The access key (alias for api_key) |
 | `k8s`          | The k8s configuration name         |
 
+For `azure_ad` on AKS Workload Identity, set `authSecretRef.serviceAccountRef` to a ServiceAccount annotated with `azure.workload.identity/client-id` and `azure.workload.identity/tenant-id`. This field is only used when `accessType` is `azure_ad`; other access types ignore it.
+
+```yaml
+{% include 'akeyless-secret-store-azure-ad-wi.yaml' %}
+```
+
+For `ClusterSecretStore`, set `serviceAccountRef.namespace` when the ServiceAccount is not in the same namespace as the consuming `ExternalSecret`; otherwise the ServiceAccount is resolved from that namespace. Sovereign Azure clouds (US Government, China) are supported when `AZURE_ENVIRONMENT` or `AZURE_CLOUD` is set accordingly, matching the non-WI `GetCloudId` path.
+
 For more information see [Akeyless Authentication Methods](https://docs.akeyless.io/docs/access-and-authentication-methods)
 
 #### Creating an Akeyless Credentials Secret

+ 22 - 0
docs/snippets/akeyless-secret-store-azure-ad-wi.yaml

@@ -0,0 +1,22 @@
+apiVersion: external-secrets.io/v1
+kind: SecretStore
+metadata:
+  name: akeyless-azure-ad-wi
+  namespace: app-test
+spec:
+  provider:
+    akeyless:
+      akeylessGWApiURL: "https://api.akeyless.io"
+      authSecretRef:
+        secretRef:
+          accessID:
+            name: akeyless-secret-creds
+            key: accessId
+          accessType:
+            name: akeyless-secret-creds
+            key: accessType
+          accessTypeParam:
+            name: akeyless-secret-creds
+            key: accessTypeParam
+        serviceAccountRef:
+          name: akeyless-wi-sa

+ 10 - 3
providers/v1/akeyless/akeyless.go

@@ -180,9 +180,16 @@ func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, e
 	}
 
 	accessTypeParam := akeylessSpec.Auth.SecretRef.AccessTypeParam
-	err = esutils.ValidateSecretSelector(store, accessTypeParam)
-	if err != nil {
-		return nil, err
+	if accessTypeParam.Name != "" {
+		if err := esutils.ValidateSecretSelector(store, accessTypeParam); err != nil {
+			return nil, err
+		}
+	}
+
+	if akeylessSpec.Auth.ServiceAccountRef != nil {
+		if err := esutils.ValidateReferentServiceAccountSelector(store, *akeylessSpec.Auth.ServiceAccountRef); err != nil {
+			return nil, fmt.Errorf("invalid Auth.ServiceAccountRef: %w", err)
+		}
 	}
 
 	return nil, nil

+ 10 - 12
providers/v1/akeyless/akeyless_api.go

@@ -27,7 +27,6 @@ import (
 	"strings"
 
 	aws_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/aws"
-	azure_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/azure"
 	gcp_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/gcp"
 	"github.com/akeylesslabs/akeyless-go/v4"
 	authenticationv1 "k8s.io/api/authentication/v1"
@@ -62,13 +61,16 @@ type Tokener interface {
 // GetToken retrieves an authentication token from Akeyless Gateway.
 // It supports various authentication methods including API key, access key,
 // Kubernetes service account token, and cloud provider-specific methods.
-func (a *akeylessBase) GetToken(ctx context.Context, accessID, accType, accTypeParam string, k8sAuth *esv1.AkeylessKubernetesAuth) (string, error) {
+func (a *akeylessBase) GetToken(ctx context.Context, accessID, accType, accTypeParam string, auth *esv1.AkeylessAuth) (string, error) {
 	authBody := akeyless.NewAuthWithDefaults()
 	authBody.AccessId = new(accessID)
 	if accType == "api_key" || accType == "access_key" {
 		authBody.AccessKey = new(accTypeParam)
 	} else if accType == "k8s" {
-		jwtString, err := a.getK8SServiceAccountJWT(ctx, k8sAuth)
+		if auth == nil || auth.KubernetesAuth == nil {
+			return "", errors.New("kubernetes auth configuration is required")
+		}
+		jwtString, err := a.getK8SServiceAccountJWT(ctx, auth.KubernetesAuth)
 		if err != nil {
 			return "", fmt.Errorf("failed to read JWT with Kubernetes Auth from %v. error: %w", DefServiceAccountFile, err)
 		}
@@ -78,7 +80,7 @@ func (a *akeylessBase) GetToken(ctx context.Context, accessID, accType, accTypeP
 		authBody.K8sServiceAccountToken = new(jwtStringBase64)
 		authBody.K8sAuthConfigName = new(K8SAuthConfigName)
 	} else {
-		cloudID, err := a.getCloudID(accType, accTypeParam)
+		cloudID, err := a.getCloudID(ctx, accType, accTypeParam, auth)
 		if err != nil {
 			return "", errors.New("Require Cloud ID " + err.Error())
 		}
@@ -290,21 +292,17 @@ func (a *akeylessBase) GetStaticSecret(ctx context.Context, secretName string, v
 	return valStr, nil
 }
 
-func (a *akeylessBase) getCloudID(provider, accTypeParam string) (string, error) {
-	var cloudID string
-	var err error
-
+func (a *akeylessBase) getCloudID(ctx context.Context, provider, accTypeParam string, auth *esv1.AkeylessAuth) (string, error) {
 	switch provider {
 	case "azure_ad":
-		cloudID, err = azure_cloud_id.GetCloudId(accTypeParam)
+		return a.getAzureCloudID(ctx, accTypeParam, auth)
 	case "aws_iam":
-		cloudID, err = aws_cloud_id.GetCloudId()
+		return aws_cloud_id.GetCloudId()
 	case "gcp":
-		cloudID, err = gcp_cloud_id.GetCloudID(accTypeParam)
+		return gcp_cloud_id.GetCloudID(accTypeParam)
 	default:
 		return "", fmt.Errorf("unable to determine provider: %s", provider)
 	}
-	return cloudID, err
 }
 
 func (a *akeylessBase) ListSecrets(ctx context.Context, path, tag string) ([]string, error) {

+ 106 - 0
providers/v1/akeyless/akeyless_test.go

@@ -26,6 +26,7 @@ import (
 	"github.com/akeylesslabs/akeyless-go/v4"
 	"github.com/stretchr/testify/require"
 	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
@@ -201,6 +202,111 @@ func TestValidateStore(t *testing.T) {
 		require.NoError(t, err)
 	})
 
+	t.Run("secret auth with serviceAccountRef", func(t *testing.T) {
+		store := &esv1.SecretStore{
+			Spec: esv1.SecretStoreSpec{
+				Provider: &esv1.SecretStoreProvider{
+					Akeyless: &esv1.AkeylessProvider{
+						AkeylessGWApiURL: &akeylessGWApiURL,
+						Auth: &esv1.AkeylessAuth{
+							SecretRef: esv1.AkeylessAuthSecretRef{
+								AccessID: esmeta.SecretKeySelector{
+									Name: "accessId",
+									Key:  "key-1",
+								},
+								AccessType: esmeta.SecretKeySelector{
+									Name: "accessId",
+									Key:  "key-1",
+								},
+							},
+							ServiceAccountRef: &esmeta.ServiceAccountSelector{
+								Name: "akeyless-wi-sa",
+							},
+						},
+					},
+				},
+			},
+		}
+
+		_, err := provider.ValidateStore(store)
+		require.NoError(t, err)
+	})
+
+	t.Run("secret auth with serviceAccountRef in different namespace", func(t *testing.T) {
+		ns := "other-ns"
+		store := &esv1.SecretStore{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: "app-test",
+			},
+			TypeMeta: metav1.TypeMeta{
+				Kind: esv1.SecretStoreKind,
+			},
+			Spec: esv1.SecretStoreSpec{
+				Provider: &esv1.SecretStoreProvider{
+					Akeyless: &esv1.AkeylessProvider{
+						AkeylessGWApiURL: &akeylessGWApiURL,
+						Auth: &esv1.AkeylessAuth{
+							SecretRef: esv1.AkeylessAuthSecretRef{
+								AccessID: esmeta.SecretKeySelector{
+									Name: "accessId",
+									Key:  "key-1",
+								},
+								AccessType: esmeta.SecretKeySelector{
+									Name: "accessId",
+									Key:  "key-1",
+								},
+							},
+							ServiceAccountRef: &esmeta.ServiceAccountSelector{
+								Name:      "akeyless-wi-sa",
+								Namespace: &ns,
+							},
+						},
+					},
+				},
+			},
+		}
+
+		_, err := provider.ValidateStore(store)
+		require.Error(t, err)
+	})
+
+	t.Run("cluster secret auth with serviceAccountRef namespace", func(t *testing.T) {
+		ns := "app-test"
+		store := &esv1.ClusterSecretStore{
+			TypeMeta: metav1.TypeMeta{
+				Kind: esv1.ClusterSecretStoreKind,
+			},
+			Spec: esv1.SecretStoreSpec{
+				Provider: &esv1.SecretStoreProvider{
+					Akeyless: &esv1.AkeylessProvider{
+						AkeylessGWApiURL: &akeylessGWApiURL,
+						Auth: &esv1.AkeylessAuth{
+							SecretRef: esv1.AkeylessAuthSecretRef{
+								AccessID: esmeta.SecretKeySelector{
+									Name:      "accessId",
+									Key:       "key-1",
+									Namespace: &ns,
+								},
+								AccessType: esmeta.SecretKeySelector{
+									Name:      "accessId",
+									Key:       "key-1",
+									Namespace: &ns,
+								},
+							},
+							ServiceAccountRef: &esmeta.ServiceAccountSelector{
+								Name:      "akeyless-wi-sa",
+								Namespace: &ns,
+							},
+						},
+					},
+				},
+			},
+		}
+
+		_, err := provider.ValidateStore(store)
+		require.NoError(t, err)
+	})
+
 	t.Run("k8s auth", func(t *testing.T) {
 		store := &esv1.SecretStore{
 			Spec: esv1.SecretStoreSpec{

+ 19 - 11
providers/v1/akeyless/auth.go

@@ -30,6 +30,7 @@ const (
 	errFetchAccessTypeParamSecret = "could not fetch AccessTypeParam secret: %w"
 	errMissingSAK                 = "missing SecretAccessKey"
 	errMissingAKID                = "missing AccessKeyID"
+	errMissingAccessTypeParam     = "missing AccessTypeParam for Akeyless access key auth"
 )
 
 func (a *akeylessBase) TokenFromSecretRef(ctx context.Context) (string, error) {
@@ -40,7 +41,7 @@ func (a *akeylessBase) TokenFromSecretRef(ctx context.Context) (string, error) {
 
 	if prov.Auth.KubernetesAuth != nil {
 		auth := prov.Auth.KubernetesAuth
-		return a.GetToken(ctx, auth.AccessID, "k8s", auth.K8sConfName, auth)
+		return a.GetToken(ctx, auth.AccessID, "k8s", auth.K8sConfName, prov.Auth)
 	}
 
 	accessID, err := resolvers.SecretKeyRef(
@@ -63,15 +64,19 @@ func (a *akeylessBase) TokenFromSecretRef(ctx context.Context) (string, error) {
 	if err != nil {
 		return "", fmt.Errorf(errFetchAccessTypeSecret, err)
 	}
-	accessTypeParam, err := resolvers.SecretKeyRef(
-		ctx,
-		a.kube,
-		a.storeKind,
-		a.namespace,
-		&prov.Auth.SecretRef.AccessTypeParam,
-	)
-	if err != nil {
-		return "", fmt.Errorf(errFetchAccessTypeParamSecret, err)
+
+	var accessTypeParam string
+	if prov.Auth.SecretRef.AccessTypeParam.Name != "" {
+		accessTypeParam, err = resolvers.SecretKeyRef(
+			ctx,
+			a.kube,
+			a.storeKind,
+			a.namespace,
+			&prov.Auth.SecretRef.AccessTypeParam,
+		)
+		if err != nil {
+			return "", fmt.Errorf(errFetchAccessTypeParamSecret, err)
+		}
 	}
 
 	if accessID == "" {
@@ -80,6 +85,9 @@ func (a *akeylessBase) TokenFromSecretRef(ctx context.Context) (string, error) {
 	if accessType == "" {
 		return "", errors.New(errMissingAKID)
 	}
+	if (accessType == "api_key" || accessType == "access_key") && accessTypeParam == "" {
+		return "", errors.New(errMissingAccessTypeParam)
+	}
 
-	return a.GetToken(ctx, accessID, accessType, accessTypeParam, prov.Auth.KubernetesAuth)
+	return a.GetToken(ctx, accessID, accessType, accessTypeParam, prov.Auth)
 }

+ 173 - 0
providers/v1/akeyless/azure_cloud_id.go

@@ -0,0 +1,173 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 akeyless
+
+import (
+	"context"
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
+	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
+	azure_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/azure"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+const (
+	azureDefaultAudience = "api://AzureADTokenExchange"
+	annotationClientID   = "azure.workload.identity/client-id"
+	annotationTenantID   = "azure.workload.identity/tenant-id"
+
+	serviceAccountTokenExpirationSeconds int64 = 600
+
+	errMissingAzureClientID = "missing Azure client ID: set accessTypeParam or annotate the service account with %s"
+	errMissingAzureTenantID = "missing Azure tenant ID: annotate the service account with %s or set AZURE_TENANT_ID"
+)
+
+func (a *akeylessBase) getAzureCloudID(ctx context.Context, accTypeParam string, auth *esv1.AkeylessAuth) (string, error) {
+	if auth == nil || auth.ServiceAccountRef == nil {
+		cloudID, err := azure_cloud_id.GetCloudId(accTypeParam)
+		if err != nil {
+			return "", err
+		}
+		return cloudID, nil
+	}
+	return a.getAzureCloudIDFromServiceAccount(ctx, auth.ServiceAccountRef, accTypeParam)
+}
+
+func (a *akeylessBase) getAzureCloudIDFromServiceAccount(ctx context.Context, ref *esmeta.ServiceAccountSelector, accTypeParam string) (string, error) {
+	if ref == nil {
+		return "", errors.New("serviceAccountRef is required")
+	}
+
+	ns := a.namespace
+	if a.storeKind == esv1.ClusterSecretStoreKind && ref.Namespace != nil {
+		ns = *ref.Namespace
+	}
+
+	sa := &corev1.ServiceAccount{}
+	if err := a.kube.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: ns}, sa); err != nil {
+		return "", fmt.Errorf("failed to get service account %q: %w", ref.Name, err)
+	}
+
+	clientID, err := azureClientID(sa, accTypeParam)
+	if err != nil {
+		return "", err
+	}
+	tenantID, err := azureTenantID(sa)
+	if err != nil {
+		return "", err
+	}
+
+	getAssertion := func(ctx context.Context) (string, error) {
+		return a.getJWTfromServiceAccountToken(ctx, *ref, []string{azureDefaultAudience}, serviceAccountTokenExpirationSeconds)
+	}
+
+	azureCloud := azureCloudSettingsFromEnv()
+	cred, err := azidentity.NewClientAssertionCredential(tenantID, clientID, getAssertion, &azidentity.ClientAssertionCredentialOptions{
+		ClientOptions: azcore.ClientOptions{Cloud: azureCloud.cloud},
+	})
+	if err != nil {
+		return "", fmt.Errorf("failed to create Azure client assertion credential: %w", err)
+	}
+
+	accessToken, err := cred.GetToken(ctx, policy.TokenRequestOptions{Scopes: []string{azureCloud.scope}})
+	if err != nil {
+		return "", fmt.Errorf("failed to get Azure access token: %w", err)
+	}
+
+	// akeyless-go-cloud-id GetCloudId returns a base64-encoded access token (see
+	// cloudprovider/azure/cloud_id.go); keep the same format for Workload Identity.
+	return base64.StdEncoding.EncodeToString([]byte(accessToken.Token)), nil
+}
+
+func azureClientID(sa *corev1.ServiceAccount, accTypeParam string) (string, error) {
+	if sa != nil {
+		if val, ok := sa.Annotations[annotationClientID]; ok && val != "" {
+			return val, nil
+		}
+	}
+	if accTypeParam != "" {
+		return accTypeParam, nil
+	}
+	return "", fmt.Errorf(errMissingAzureClientID, annotationClientID)
+}
+
+type azureCloudSettings struct {
+	scope string
+	cloud cloud.Configuration
+}
+
+var (
+	publicAzureCloudSettings = azureCloudSettings{
+		scope: azure_cloud_id.AzureADManagementScope,
+		cloud: cloud.AzurePublic,
+	}
+	usGovAzureCloudSettings = azureCloudSettings{
+		scope: "https://management.usgovcloudapi.net/.default",
+		cloud: cloud.AzureGovernment,
+	}
+	chinaAzureCloudSettings = azureCloudSettings{
+		scope: "https://management.chinacloudapi.cn/.default",
+		cloud: cloud.AzureChina,
+	}
+)
+
+func azureCloudSettingsFromEnv() azureCloudSettings {
+	for _, key := range []string{"AZURE_ENVIRONMENT", "AZURE_CLOUD"} {
+		if v, ok := os.LookupEnv(key); ok {
+			if cfg, ok := azureCloudSettingsFromName(v); ok {
+				return cfg
+			}
+		}
+	}
+	return publicAzureCloudSettings
+}
+
+func azureCloudSettingsFromName(raw string) (azureCloudSettings, bool) {
+	switch strings.ToLower(strings.TrimSpace(raw)) {
+	case "azurecloud", "azurepubliccloud":
+		return publicAzureCloudSettings, true
+	case "azureusgovernment", "azureusgovernmentcloud":
+		return usGovAzureCloudSettings, true
+	case "azurechinacloud", "azurechinacloud21vianet":
+		return chinaAzureCloudSettings, true
+	default:
+		return azureCloudSettings{}, false
+	}
+}
+
+func azureTenantID(sa *corev1.ServiceAccount) (string, error) {
+	if sa != nil {
+		if val, ok := sa.Annotations[annotationTenantID]; ok && val != "" {
+			return val, nil
+		}
+	}
+	if tenantID := os.Getenv("AZURE_TENANT_ID"); tenantID != "" {
+		return tenantID, nil
+	}
+	return "", fmt.Errorf(errMissingAzureTenantID, annotationTenantID)
+}

+ 132 - 0
providers/v1/akeyless/azure_cloud_id_test.go

@@ -0,0 +1,132 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 akeyless
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestAzureClientID(t *testing.T) {
+	t.Parallel()
+
+	sa := &corev1.ServiceAccount{
+		ObjectMeta: metav1.ObjectMeta{
+			Annotations: map[string]string{
+				annotationClientID: "from-annotation",
+			},
+		},
+	}
+
+	id, err := azureClientID(sa, "from-param")
+	require.NoError(t, err)
+	require.Equal(t, "from-annotation", id)
+
+	id, err = azureClientID(&corev1.ServiceAccount{}, "from-param")
+	require.NoError(t, err)
+	require.Equal(t, "from-param", id)
+
+	_, err = azureClientID(&corev1.ServiceAccount{}, "")
+	require.Error(t, err)
+}
+
+func TestAzureCloudSettingsFromName(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name     string
+		env      string
+		expected azureCloudSettings
+		ok       bool
+	}{
+		{
+			name:     "public cloud",
+			env:      "AzurePublicCloud",
+			expected: publicAzureCloudSettings,
+			ok:       true,
+		},
+		{
+			name:     "us gov",
+			env:      "AzureUSGovernment",
+			expected: usGovAzureCloudSettings,
+			ok:       true,
+		},
+		{
+			name:     "china",
+			env:      "AzureChinaCloud",
+			expected: chinaAzureCloudSettings,
+			ok:       true,
+		},
+		{
+			name: "unknown",
+			env:  "UnknownCloud",
+			ok:   false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			got, ok := azureCloudSettingsFromName(tt.env)
+			require.Equal(t, tt.ok, ok)
+			if ok {
+				require.Equal(t, tt.expected, got)
+			}
+		})
+	}
+}
+
+func TestAzureCloudSettingsFromEnv(t *testing.T) {
+	t.Run("AZURE_ENVIRONMENT", func(t *testing.T) {
+		t.Setenv("AZURE_ENVIRONMENT", "AzureUSGovernment")
+		t.Setenv("AZURE_CLOUD", "")
+		require.Equal(t, usGovAzureCloudSettings, azureCloudSettingsFromEnv())
+	})
+
+	t.Run("AZURE_CLOUD fallback", func(t *testing.T) {
+		t.Setenv("AZURE_ENVIRONMENT", "")
+		t.Setenv("AZURE_CLOUD", "AzureChinaCloud")
+		require.Equal(t, chinaAzureCloudSettings, azureCloudSettingsFromEnv())
+	})
+}
+
+func TestAzureTenantID(t *testing.T) {
+	t.Setenv("AZURE_TENANT_ID", "from-env")
+
+	sa := &corev1.ServiceAccount{
+		ObjectMeta: metav1.ObjectMeta{
+			Annotations: map[string]string{
+				annotationTenantID: "from-annotation",
+			},
+		},
+	}
+
+	id, err := azureTenantID(sa)
+	require.NoError(t, err)
+	require.Equal(t, "from-annotation", id)
+
+	id, err = azureTenantID(&corev1.ServiceAccount{})
+	require.NoError(t, err)
+	require.Equal(t, "from-env", id)
+
+	t.Setenv("AZURE_TENANT_ID", "")
+	_, err = azureTenantID(&corev1.ServiceAccount{})
+	require.Error(t, err)
+}

+ 2 - 2
providers/v1/akeyless/go.mod

@@ -3,6 +3,8 @@ module github.com/external-secrets/external-secrets/providers/v1/akeyless
 go 1.26.4
 
 require (
+	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1
+	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0
 	github.com/akeylesslabs/akeyless-go-cloud-id v0.3.7
 	github.com/akeylesslabs/akeyless-go/v4 v4.3.0
 	github.com/external-secrets/external-secrets/apis v0.0.0
@@ -20,8 +22,6 @@ require (
 	cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
 	cloud.google.com/go/compute/metadata v0.9.0 // indirect
 	dario.cat/mergo v1.0.2 // indirect
-	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect
-	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
 	github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect

+ 4 - 0
tests/__snapshot__/clustersecretstore-v1.yaml

@@ -40,6 +40,10 @@ spec:
             key: string
             name: string
             namespace: string
+        serviceAccountRef:
+          audiences: [] # minItems 0 of type string
+          name: string
+          namespace: string
       caBundle: c3RyaW5n
       caProvider:
         key: string

+ 4 - 0
tests/__snapshot__/secretstore-v1.yaml

@@ -40,6 +40,10 @@ spec:
             key: string
             name: string
             namespace: string
+        serviceAccountRef:
+          audiences: [] # minItems 0 of type string
+          name: string
+          namespace: string
       caBundle: c3RyaW5n
       caProvider:
         key: string