Azure Kubernetes Service KeyVault access via Workload Identity

When accessing Azure Key Vault from Azure Kubernetes Service, one of the ways available to do so is through the use of a Workload Identity. A Workload Identity in Azure works by allowing the pods in your Azure Kubernetes cluster assume an existing Managed Identity so that they can gain access to other resources such Azure KeyVault. More so, it allows your application in AKS to authenticate with Azure EntraId (AAD) successfully for resources that require it.

Here is what we will do with our app on Azure Kubernetes Service to read and write values to KeyVault:

  • Enable OpenID Connect and Workload Identity on the AKS cluster. These 2 are essentially addons available on the AKS cluster (Your AKS cluster MUST have Kubernetes version 1.25 or higher to use Workload Identity)
  • Create a new Managed Identity and Federate this identity
  • Assign Get and Set permissions on the Azure KeyVault for the Managed Identity by using an Access Policy. Note that using an RBAC Role of 'KeyVault Secrets Officer' for the Managed Identity can also be correct although this gives further permissions beyond the scope of this blog post (expand higher for your needs where needed)
  • Associate the Managed Identity to the Pod(s) via the Service Account YAML
  • Deploy the Service Account and Deployment YAML in our deployment process
  • Access KeyVault via application code (.NET 6 Web App) using the DefaultAzureCredential class

Enable OpenID Connect and Workload Identity on AKS cluster

Using the Azure CLI, run the following commands:

az login

az aks get-credentials --resource-group [Your_Resource_Group] --name [Your_Cluster_Name] --overwrite-existing

az aks update --resource-group [Your_Resource_Group] --name [Your_Cluster_Name] --enable-oidc-issuer --enable-workload-identity

Update AKS cluster to have required add-ons

What you should get after running the update to the cluster is a large json response confirming the update. Crucially this json will show us that Workload Identity has been enabled:

Workload Identity Enabled

And that OpenIdConnect is enabled too:

OIDC enabled on AKS Cluster

Create a new Managed Identity in Azure

Now we can create a managed identity via the Azure CLI as follows:

az identity create --name [Your_ManagedIdentityName] --resource-group [Your_ResourceGroupName]

Create a new Managed Identity

After creating the Managed Identity, keep a note of the ClientId seen in the response json here as we will need it in the next sections. (If not, you can always view it in you Azure EntraId (AAD) by searching the name you made for it in Azure EntraId where this ClientId is shown as the ApplicationId there)

Next, assign the rights of being able to read and write secrets of a known KeyVault to this Managed Identity by assigning an Access Policy for Get and Set permissions on Secrets:

//You can find the managed Identity object id by searching its name in EntraId (AAD)
az keyvault set-policy --name [Your_KeyVault_Name] --object-id [Your_ManagedIdentity_ObjectId] --secret-permissions get set

Assign Access Policy on Managed Identity for KeyVault

After you get a json response back, you can confirm in KeyVault itself in the portal under Access Policies:

Access Policy assigned to the Managed Identity

Now you need to set a Federated Credential for your Managed Identity. The Federated Identity will make sure that a Federated Token is provided by the time it is required by the Workload Identity.

💡
Generally, we require a ClientId, TenantId, and Federated Token by the time we use the Workload Identity in our app (for example when using DefaultAzureCredential() or WorkloadIdentityCredential()). The following sections detail how these get provided automatically

Let's create a Federated Credential for the Managed Identity in the Portal:

From the Portal Home, Search Managed Identity, [Your_ManagedIdentityFor_AKS]>Settings>Federated credentials> Add Credential. Set the scenario to 'Kubernetes accessing Azure Resources':

Set Federated credential scenario to Kubernetes scenario

Here, you will need your Cluster Issuer URL. Use the following command to see this Issuer URL:

az aks show --resource-group [your_resource_group] --name [your_cluster_name] --query "oidcIssuerProfile.issuerUrl" -o tsv

Get your Cluster Issuer URL

Then add this Issuer URL (including the "/" at the end!!) as the Cluster Issuer URL:

Adding a Federated Identity for a Kubernetes scenario for accessing Azure Resources

The Service Account will be a Name of our AKS Service Account which we will set in the following section

Then choose a Name for your Federated Credential Name, leave the Audience unchanged, then click Add.

We will now associate this Managed Identity to our Service Account YAML next so our pods know which Identity to use when application code requiring KeyVault runs.

AKS Service Account YAML Configuration for Workload Identity

We can have a YAML of the kind 'ServiceAccount', that contains an annotation of our Managed Identity:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: podserviceaccount ## The name we used in Federated identity setup earlier
  namespace: default
  annotations:
    azure.workload.identity/client-id: [Your_ManagedIdentity_ClientId]
  labels:
    azure.workload.identity/use: "true" 

Service Account YAML example (serviceaccount.yaml)

Then, we can write our deployment YAML and Service YAML bundled together as follows:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: demoappdeployment
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demoapp
  template:
    metadata:
      labels:
        app: demoapp
        azure.workload.identity/use: "true" ## this line MUST exist here so that Azure injects the ClientId, TenantId, and the Federated Token automatically ready for the DefaultAzureCredential class to pick up in code 
    spec:
      serviceAccountName: podserviceaccount ##Link the named Service Account we created earlier
      containers:
      - name: demoapp
        image: <your_full_registry_image_location>
      ports:
      - containerPort: 80

---

apiVersion: v1
kind: Service
metadata:
  name: demoservice
spec:
  selector:
    app: demoapp
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: LoadBalancer

Deployment and Service YAML (deployment.yaml)

You can deploy to Azure Kubernetes Service via the following:

az aks get-credentials --resource-group [Your_Resource_Group] --name [Your_Cluster_Name] --overwrite-existing

kubectl apply -f [your_serviceaccount.yaml]

kubectl apply -f [your_deployment.yaml]

Deploy to AKS with kubectl commands

You can also run your YAML scripts through your CI/CD pipeline, but regardless, you should end up seeing the External IP address of the LoadBalancer under Services in your AKS instance.

Your application running in Azure Kubernetes should be able to access KeyVault using its application code as normal where your app code uses it.

The next section is optional and provides sample reference code for demonstration purposes.

Application Code Reference

Here is some quick example C# code that can be containerised in the context of an ASP.NET web app we would then use to write and read from our KeyVault using the DefaultAzureCredential class:

//assuming we are using .NET6 (.NET7 and beyond adds complexieties around certificates when containerised, beyond scope!!)
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;

//....

public class ArbitraryData { 

    public string SecretData { get; set; }
}

public async Task<IActionResult> Index()
{

    var keyVaultUrl = "https://[Your_KeyVault_Name].vault.azure.net/";

    var client = new SecretClient(new Uri(keyVaultUrl), new DefaultAzureCredential());

    DateTime currentTime = DateTime.UtcNow;
    await client.SetSecretAsync("AGISecret", $"The weights of a state of the art AI model as of {currentTime.ToString()}") , training in progress;

    KeyVaultSecret secret = await client.GetSecretAsync("AGISecret");
    Console.WriteLine($"Secret value: {secret.Value}");

    var result = new ArbitraryData()
    {
        SecretData = secret.Value
    };

    return View(result);
}

Accessing KeyVault from within AKS pod under DefautAzureCredential

Where the View code is simple as:

@{
    ViewData["Title"] = "Home Page";
}


@model AKSVaultConnect.Controllers.HomeController.ArbitraryData;

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Discovered @Model.SecretData </p>
</div>

Razor View code displaying back the data from KeyVault

Example Docker File:

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443


FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["AKSVaultConnect/AKSVaultConnect.csproj", "AKSVaultConnect/"]
RUN dotnet restore "./AKSVaultConnect/AKSVaultConnect.csproj"
COPY . .
WORKDIR "/src/AKSVaultConnect"
RUN dotnet build "./AKSVaultConnect.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./AKSVaultConnect.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "AKSVaultConnect.dll"]

The Expected result would be as follows after deploying the service as a LoadBalancer on an Azure Kubernetes Service instance, and behold, the application is setting and getting a secret in Azure KeyVault:

Final Output using a LoadBalancer type in Azure Kubernetes Service.

REMEMBER TO DELETE THE AKS RESOURCE AFTER USING!!