Moving from AAD Pod Identity to Workload Identity in AKS

Managed identities are a good way to avoid secrets and enhance the security of your application. Workload identity provides a means to connect your AKS cluster to managed identities. The previous method of connecting these identities to AKS involved installing AAD Pod Identity in the cluster and using CRDs (Custom Resource Definitions) like AzureIdentityBinding and AzureIdentity. However, this method is currently in preview and has been deprecated! As a result, we need to switch to Microsoft's recommended approach for securing workloads.

Creating the cluster

We use Bicep to provision all Azure resources. While the following example does not cover the entire cluster creation process, it includes the necessary settings to activate the Workload Identity functionality, namely oidcIssuerProfile and securityProfile.workloadIdentity.

Here's an excerpt of what that looks like in Bicep:

resource aks 'Microsoft.ContainerService/managedClusters@2023-02-01' = {
  name: k8sName
  location: location
  properties: {
    ...
    securityProfile: {
      workloadIdentity:{
        enabled: true
      }
    }
    oidcIssuerProfile: {
      enabled: true
    }
  }
}

This configuration will create an OIDC cluster issuer URL. Every managed identity needs to connect to this URL using a federated identity credential. You can retrieve this URL with Bicep by accessing the information from the newly created cluster. It is available under aks.properties.oidcIssuerProfile.issuerURL when using the aforementioned Bicep script.

Configuring federated credentials for managed identites

This URL is passed to the Bicep code that creates and sets up a service managed identity. We use a managed identity for each service, which is written as a module.

param env string = 'dev'
param namespace string = 'default'
param serviceName string
param issuerUrl string = ''
param location string = 'westeurope'

resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
  name: '${serviceName}-${env}-identity'
  location: location
}

resource federatedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials@2023-01-31' = if (issuerUrl != '') {
  name: '${serviceName}-${env}-workload-identity'
  properties: {
    audiences: [
      'api://AzureADTokenExchange'
    ]
    issuer: issuerUrl
    subject: 'system:serviceaccount:${namespace}:${service}-workload-identity'
  }
  parent: managedIdentity
}

The federated credential contains a reference to the workload service account, which is the way Workload Identity associates a managed identity with the application. It includes the name of the service account and the namespace where it resides. This module is used in the process of provisioning a new cluster and deploying services to an existing cluster. Here's an example of how a configured federated credential looks in the Azure portal.

Kubernetes configuration

Workload identity relies solely on native Kubernetes resources to configure the binding of a managed identity. A service account references the client ID of the managed identity that you want to connect it to. The deployment/pod has a reference to the service account and a label indicating that the workload is enabled for authentication against Azure. The previous method would look like this:

apiVersion: "aadpodidentity.k8s.io/v1"
kind: AzureIdentity
metadata:
  name: my-service-identity
spec:
  type: 0
  resourceID: /path/to/resourceId/in/azure
  clientID: 00000000-0000-0000-0000-000000000000
  
---
apiVersion: "aadpodidentity.k8s.io/v1"
kind: AzureIdentityBinding
metadata:
  name: my-service-identity-binding
spec:
  azureIdentity: "my-service-identity"
  selector: "my-service"

The client ID from this file will be moved to the service account spec:

apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    azure.workload.identity/client-id: 00000000-0000-0000-0000-000000000000
  name: my-service-workload-identity

The deployment/pod resource also requires some changes. Remove the aadpodidbinding label and add an azure.workload.identity/use label. The spec section also needs to refer to the service account:

Before:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-service-web
  labels:
    component: web
spec:
  replicas: 1
  selector:
    matchLabels:
      component: web
      aadpodidbinding: my-service
  template:
    metadata:
      labels:
        component: web
        aadpodidbinding: my-service
    spec:
      containers:
      ...

After:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-service-web
  labels:
    component: web
spec:
  replicas: 1
  selector:
    matchLabels:
      component: web
      azure.workload.identity/use: 'true'
  template:
    metadata:
      labels:
        component: web
        azure.workload.identity/use: 'true'
    spec:
      serviceAccountName: my-service-workload-identity
      containers:
      ...

Apply the new configuration, and you're all set for the secure, secretless approach!