As per official documentation “Azure AD Workload Identity for Kubernetes integrates with the capabilities native to Kubernetes to federate with external identity providers”.

In other words, workload identity for Kubernetes is about establishing a trust relationship between some service accounts, whose tokens are issued by the Kubernetes cluster identity provider, and service principals in Microsoft Entra ID (a.k.a. Azure AD).

After a one-time setup of the cluster, all you need to do is to establish some “federated credentials” on the Entra ID side by configuring an app to trust an external identity provider.

This is instructing Entra ID on how to verify that a specific Kubernetes service account token presented as a client assertion is actually trustworthy and can be exchanged for a Entra ID token for the Service Principal behind that App Registration.

Mutating Admission Webhook

When following official documentation, you are asked to install a mutating admission webhook. In fact, the webhook is there to simplify the developers' life by letting them inject proper environment variables and mount a “projected” volume in the pod. What the developer is required to do is just write a bunch of annotations on the service account and a label on the Pod that needs to use workload identity. Regardless of its simplicity, using the webhook might limit the flexibility you can get.

In example, the webhook does not provide flexibility in case the “federated” service principal needs to be dynamically configured at runtime (instead of deployment time), or in case this service principal needs acquire tokens for different Service Principals (in potentially different tenants)

Avoiding the Webhook

Let’s consider a scenario where an application running in our Kubernetes cluster needs to use a Service Principal “R” with rights for reading a file from a storage account, and also needs to use an Service Principal “W” with rights for writing a blob in storage in a completely different tenant. This might be a common scenario when there is the need to interact with 3rd party systems for integration.

If using the webhook, as of today, this is not easy to achieve.

Can you avoid the webhook? Absolutely yes.

If you don’t want to use it, there’s just one simple step to worry about: mount a “projected” volume that exposes a Kubernetes service account token, as shown in the example below.

kind: ServiceAccount
metadata:
  name: "some-wi-sa"
  namespace: some-ns
---
apiVersion: v1
kind: Pod
metadata:
  name: "some-wi-pod"
  namespace: some-ns
  labels:
    app: "some-wi-pod"
spec:
  serviceAccountName: "some-wi-sa"
  containers:
    - image: omitted
      name: some-wi-container
      env:
      - name: AZURE_FEDERATED_TOKEN_FILE
        value: /var/run/secrets/azure/tokens/azure-identity-token
      volumeMounts:
      - mountPath: /var/run/secrets/azure/tokens
        name: azure-identity-token
        readOnly: true
  volumes:
    - name: azure-identity-token
      projected:
        defaultMode: 420
        sources:
        - serviceAccountToken:
            audience: api://AzureADTokenExchange
            expirationSeconds: 3600
            path: azure-identity-token

What gets mounted in the pod is just a generic JWT that is not connected in any way to Azure. This is presented as client assertion in the client credentials flow when requesting Entra ID a token for a specific client ID.

This means that if you want to get a token for different Entra ID entities, you can dynamically acquire a token for one client ID or another one, always presenting the same identical assertion.

The following Go snippet is showing how to acquire a token by manually specifying the credential options for the Azure Identity SDK

    //omitted imports
    wio := azidentity.WorkloadIdentityCredentialOptions{
        ClientID:                 config.ClientID,  //the clientID can be dynamically fetched from external configuration
        TenantID:                 config.TenantID,  //the tenantID can be dynamically fetched from external configuration
        DisableInstanceDiscovery: true,
        ClientOptions: azcore.ClientOptions{
            Cloud: cloud.Configuration{
                ActiveDirectoryAuthorityHost: config.AuthorityHost, //the AuthorityHost can be dynamically fetched from external configuration
            },
        },
    }
    cred, err := azidentity.NewWorkloadIdentityCredential(&wio)
    if err != nil {
        log.Fatalf("failed to obtain a credential: %v", err)
    }
    token, err := cred.GetToken(context.Background(), policy.TokenRequestOptions{Scopes: []string{"https://management.azure.com/"}})
    if err != nil {
        log.Fatalf("failed to obtain a credential: %v", err)
    }
    //omitted code