Skip to main content

Kubernetes to Azure - Secretless Authentication

Connect Kubernetes workloads to Azure using AKS Workload Identity for secretless authentication - no service principal secrets required.

Overview

This guide shows how to configure Kubernetes pods running on AKS to authenticate with Azure services using Workload Identity instead of storing service principal credentials.

Time Required: 25-35 minutes Difficulty: Intermediate

What You'll Accomplish

  • ✓ Enable AKS Workload Identity on your cluster
  • ✓ Create an Azure Managed Identity with appropriate permissions
  • ✓ Configure federated credentials linking K8s ServiceAccount to Azure identity
  • ✓ Deploy a pod that can access Azure services without credentials
  • ✓ Test and verify the end-to-end authentication flow

Benefits

  • No Stored Secrets: Zero service principal passwords/certificates stored in Kubernetes
  • Automatic Token Refresh: Identity tokens are generated on-demand and expire automatically
  • Fine-Grained Access: Azure RBAC provides precise control over resource access
  • Audit Trail: Complete authentication history in Azure Activity Log
  • Pod-Level Security: Different pods can have different Azure permissions

Prerequisites

Azure Requirements

  • Azure subscription with appropriate permissions
  • AKS cluster (version 1.22+ recommended)
  • Azure CLI (az) installed and configured
  • Permissions to create managed identities and assign RBAC roles

Kubernetes Requirements

  • AKS cluster with OIDC issuer and workload identity enabled
  • kubectl configured to access your cluster
  • Cluster admin permissions

Knowledge Requirements

  • Understanding of Kubernetes ServiceAccounts
  • Familiarity with Azure Managed Identities and RBAC
  • Basic understanding of OIDC authentication flows

Architecture

Implementation

Step 1: Enable Workload Identity on AKS Cluster

For New Clusters

az aks create \
--resource-group myResourceGroup \
--name myAKSCluster \
--location eastus \
--node-count 1 \
--enable-oidc-issuer \
--enable-workload-identity \
--generate-ssh-keys

For Existing Clusters

# Enable OIDC issuer
az aks update \
--resource-group myResourceGroup \
--name myAKSCluster \
--enable-oidc-issuer \
--enable-workload-identity

Get the OIDC issuer URL (save for later):

az aks show \
--resource-group myResourceGroup \
--name myAKSCluster \
--query "oidcIssuerProfile.issuerUrl" \
--output tsv

# Example output:
# https://eastus.oic.prod-aks.azure.com/00000000-0000-0000-0000-000000000000/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/

Step 2: Install Workload Identity Webhook (if not auto-installed)

Check if webhook is installed:

kubectl get pods -n kube-system | grep workload-identity

If not present, install:

# The webhook is typically installed automatically with --enable-workload-identity
# For manual installation:
helm repo add azure-workload-identity https://azure.github.io/azure-workload-identity/charts
helm repo update
helm install workload-identity-webhook azure-workload-identity/workload-identity-webhook \
--namespace azure-workload-identity-system \
--create-namespace

Step 3: Create Azure Managed Identity

# Set variables
export RESOURCE_GROUP="myResourceGroup"
export LOCATION="eastus"
export IDENTITY_NAME="my-app-identity"
export SUBSCRIPTION="$(az account show --query id --output tsv)"

# Create managed identity
az identity create \
--resource-group $RESOURCE_GROUP \
--name $IDENTITY_NAME \
--location $LOCATION

# Get identity client ID (save for later)
export IDENTITY_CLIENT_ID=$(az identity show \
--resource-group $RESOURCE_GROUP \
--name $IDENTITY_NAME \
--query clientId \
--output tsv)

echo "Identity Client ID: $IDENTITY_CLIENT_ID"

Step 4: Assign Azure RBAC Permissions

Grant the managed identity permissions to access Azure resources:

# Example: Grant Storage Blob Data Reader role
az role assignment create \
--assignee $IDENTITY_CLIENT_ID \
--role "Storage Blob Data Reader" \
--scope "/subscriptions/$SUBSCRIPTION/resourceGroups/$RESOURCE_GROUP"

# Example: Grant Key Vault Secrets User role
az role assignment create \
--assignee $IDENTITY_CLIENT_ID \
--role "Key Vault Secrets User" \
--scope "/subscriptions/$SUBSCRIPTION/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.KeyVault/vaults/my-keyvault"

Common Azure roles:

Use CaseRoleDescription
Read blob storageStorage Blob Data ReaderRead blobs and containers
Write blob storageStorage Blob Data ContributorRead/write blobs
Access Key Vault secretsKey Vault Secrets UserRead secret values
Query Cosmos DBCosmos DB Account Reader RoleRead Cosmos DB account
Read SQL DatabaseSQL DB ContributorManage SQL databases

Step 5: Create Federated Credential

Link the Kubernetes ServiceAccount to the Azure Managed Identity:

# Set K8s ServiceAccount details
export SERVICE_ACCOUNT_NAME="my-app-ksa"
export SERVICE_ACCOUNT_NAMESPACE="default"

# Get AKS OIDC issuer URL
export AKS_OIDC_ISSUER="$(az aks show \
--resource-group $RESOURCE_GROUP \
--name myAKSCluster \
--query "oidcIssuerProfile.issuerUrl" \
--output tsv)"

# Create federated credential
az identity federated-credential create \
--name my-app-federated-credential \
--identity-name $IDENTITY_NAME \
--resource-group $RESOURCE_GROUP \
--issuer $AKS_OIDC_ISSUER \
--subject system:serviceaccount:${SERVICE_ACCOUNT_NAMESPACE}:${SERVICE_ACCOUNT_NAME} \
--audience api://AzureADTokenExchange

Verify:

az identity federated-credential list \
--identity-name $IDENTITY_NAME \
--resource-group $RESOURCE_GROUP \
--output table

Step 6: Create Kubernetes ServiceAccount

# serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app-ksa
namespace: default
annotations:
azure.workload.identity/client-id: YOUR_IDENTITY_CLIENT_ID
labels:
azure.workload.identity/use: "true"

Apply:

# Replace with actual client ID
sed "s/YOUR_IDENTITY_CLIENT_ID/$IDENTITY_CLIENT_ID/g" serviceaccount.yaml | kubectl apply -f -

Step 7: Deploy Pod Using ServiceAccount

# pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: my-app-pod
namespace: default
labels:
azure.workload.identity/use: "true" # Required for webhook injection
spec:
serviceAccountName: my-app-ksa
containers:
- name: azure-cli
image: mcr.microsoft.com/azure-cli:latest
command: ["sleep", "3600"]
# Azure SDK automatically uses Workload Identity via injected env vars

Deploy:

kubectl apply -f pod.yaml

Testing and Verification

Verify ServiceAccount Configuration

# Check annotations and labels
kubectl get serviceaccount my-app-ksa -n default -o yaml

# Expected output includes:
# annotations:
# azure.workload.identity/client-id: YOUR_CLIENT_ID
# labels:
# azure.workload.identity/use: "true"

Verify Pod Environment Variables

The workload identity webhook should inject these:

kubectl exec my-app-pod -- env | grep AZURE

# Expected output:
# AZURE_CLIENT_ID=YOUR_CLIENT_ID
# AZURE_TENANT_ID=YOUR_TENANT_ID
# AZURE_FEDERATED_TOKEN_FILE=/var/run/secrets/azure/tokens/azure-identity-token
# AZURE_AUTHORITY_HOST=https://login.microsoftonline.com/

Verify Token File

kubectl exec my-app-pod -- ls -la /var/run/secrets/azure/tokens/

# Should show azure-identity-token file

Test Azure Authentication

# Login using managed identity
kubectl exec my-app-pod -- az login --identity

# Get current identity
kubectl exec my-app-pod -- az account show

# Expected output shows managed identity details

Test Resource Access

# List storage accounts
kubectl exec my-app-pod -- az storage account list

# Access Key Vault secret
kubectl exec my-app-pod -- az keyvault secret show \
--vault-name my-keyvault \
--name my-secret

# List resource groups (if permissions granted)
kubectl exec my-app-pod -- az group list

Expected Success Output

✓ ServiceAccount has azure.workload.identity annotations
✓ Pod has azure.workload.identity/use label
✓ Pod has AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_FEDERATED_TOKEN_FILE env vars
✓ Token file exists at /var/run/secrets/azure/tokens/azure-identity-token
✓ az login --identity succeeds
✓ Pod can access Azure resources per RBAC assignments

Production Hardening

Security Best Practices

1. Use Namespace Isolation

apiVersion: v1
kind: Namespace
metadata:
name: production
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app-ksa
namespace: production # Isolated namespace
annotations:
azure.workload.identity/client-id: PROD_CLIENT_ID
labels:
azure.workload.identity/use: "true"

2. Implement Least Privilege RBAC

Don't do this:

# Too broad - grants Contributor at subscription level
az role assignment create \
--assignee $IDENTITY_CLIENT_ID \
--role "Contributor" \
--scope "/subscriptions/$SUBSCRIPTION"

Do this:

# Specific resource-level permissions
az role assignment create \
--assignee $IDENTITY_CLIENT_ID \
--role "Storage Blob Data Reader" \
--scope "/subscriptions/$SUBSCRIPTION/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Storage/storageAccounts/mystorageaccount/blobServices/default/containers/mycontainer"

3. Use Custom Azure Roles

# Create custom role JSON
cat > custom-role.json <<EOF
{
"Name": "My App Custom Role",
"Description": "Custom role for my-app with minimal permissions",
"Actions": [
"Microsoft.Storage/storageAccounts/blobServices/containers/read",
"Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read"
],
"NotActions": [],
"AssignableScopes": [
"/subscriptions/$SUBSCRIPTION/resourceGroups/$RESOURCE_GROUP"
]
}
EOF

# Create role
az role definition create --role-definition custom-role.json

# Assign role
az role assignment create \
--assignee $IDENTITY_CLIENT_ID \
--role "My App Custom Role" \
--scope "/subscriptions/$SUBSCRIPTION/resourceGroups/$RESOURCE_GROUP"

4. Restrict Federated Credential Subject

# Specific namespace and ServiceAccount
az identity federated-credential create \
--name production-only-credential \
--identity-name $IDENTITY_NAME \
--resource-group $RESOURCE_GROUP \
--issuer $AKS_OIDC_ISSUER \
--subject system:serviceaccount:production:my-app-ksa \
--audience api://AzureADTokenExchange

# ✅ SECURE: Specific namespace and SA name
# ❌ INSECURE: system:serviceaccount:*:* (never use wildcards)

5. Enable Azure Activity Log

# Create diagnostic setting for activity logs
az monitor diagnostic-settings create \
--name workload-identity-audit \
--resource "/subscriptions/$SUBSCRIPTION" \
--logs '[{"category": "Administrative", "enabled": true}]' \
--workspace "/subscriptions/$SUBSCRIPTION/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.OperationalInsights/workspaces/my-workspace"

Troubleshooting

Issue: Pod Cannot Authenticate to Azure

Symptoms:

  • Error: "DefaultAzureCredential failed to retrieve a token"
  • Missing environment variables

Solutions:

  1. Verify Workload Identity is Enabled:

    az aks show \
    --resource-group $RESOURCE_GROUP \
    --name myAKSCluster \
    --query "oidcIssuerProfile.enabled" \
    --output tsv
    # Should return: true
  2. Check Webhook is Running:

    kubectl get pods -n azure-workload-identity-system
    # Should show workload-identity-webhook pods running
  3. Verify Pod Labels:

    kubectl get pod my-app-pod -o yaml | grep -A 1 "labels:"
    # Must have: azure.workload.identity/use: "true"
  4. Check Environment Variable Injection:

    kubectl exec my-app-pod -- env | grep AZURE_
    # Should show AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_FEDERATED_TOKEN_FILE

Issue: Federated Credential Validation Failed

Symptoms:

  • Error: "AADSTS70021: No matching federated identity record found"
  • Authentication fails despite correct setup

Solutions:

  1. Verify Federated Credential Configuration:

    az identity federated-credential show \
    --name my-app-federated-credential \
    --identity-name $IDENTITY_NAME \
    --resource-group $RESOURCE_GROUP
  2. Check Subject Claim Match:

    # Subject must exactly match: system:serviceaccount:NAMESPACE:SA_NAME
    kubectl get sa my-app-ksa -n default -o jsonpath='{.metadata.namespace}/{.metadata.name}'
    # Compare with federated credential subject
  3. Verify OIDC Issuer URL:

    az aks show \
    --resource-group $RESOURCE_GROUP \
    --name myAKSCluster \
    --query "oidcIssuerProfile.issuerUrl"
    # Must match issuer in federated credential
  4. Check Audience:

    # Audience must be: api://AzureADTokenExchange
    az identity federated-credential show \
    --name my-app-federated-credential \
    --identity-name $IDENTITY_NAME \
    --resource-group $RESOURCE_GROUP \
    --query audiences

Issue: Access Denied to Azure Resources

Symptoms:

  • Authentication succeeds but operations fail
  • Error: "does not have authorization to perform action"

Solutions:

  1. Verify RBAC Assignments:

    az role assignment list \
    --assignee $IDENTITY_CLIENT_ID \
    --output table
  2. Check Role Scope:

    # Ensure role is assigned at appropriate scope
    az role assignment list \
    --assignee $IDENTITY_CLIENT_ID \
    --all \
    --query "[].{Role:roleDefinitionName, Scope:scope}"
  3. Test Specific Permission:

    # Check if identity has specific permission
    az role assignment list \
    --assignee $IDENTITY_CLIENT_ID \
    --query "[?roleDefinitionName=='Storage Blob Data Reader']"
  4. Grant Missing Role:

    az role assignment create \
    --assignee $IDENTITY_CLIENT_ID \
    --role "Storage Blob Data Reader" \
    --scope "/subscriptions/$SUBSCRIPTION/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Storage/storageAccounts/mystorageaccount"

Complete Example

Complete Terraform Configuration

# complete-workload-identity-setup.tf

terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.0"
}
}
}

provider "azurerm" {
features {}
}

variable "resource_group_name" {
default = "myResourceGroup"
}

variable "location" {
default = "eastus"
}

variable "cluster_name" {
default = "myAKSCluster"
}

# Get AKS cluster data
data "azurerm_kubernetes_cluster" "cluster" {
name = var.cluster_name
resource_group_name = var.resource_group_name
}

provider "kubernetes" {
host = data.azurerm_kubernetes_cluster.cluster.kube_config.0.host
client_certificate = base64decode(data.azurerm_kubernetes_cluster.cluster.kube_config.0.client_certificate)
client_key = base64decode(data.azurerm_kubernetes_cluster.cluster.kube_config.0.client_key)
cluster_ca_certificate = base64decode(data.azurerm_kubernetes_cluster.cluster.kube_config.0.cluster_ca_certificate)
}

# Get subscription data
data "azurerm_subscription" "current" {}
data "azurerm_resource_group" "rg" {
name = var.resource_group_name
}

# Create managed identity
resource "azurerm_user_assigned_identity" "my_app" {
name = "my-app-identity"
resource_group_name = var.resource_group_name
location = var.location
}

# Assign RBAC role
resource "azurerm_role_assignment" "my_app_storage" {
scope = data.azurerm_resource_group.rg.id
role_definition_name = "Storage Blob Data Reader"
principal_id = azurerm_user_assigned_identity.my_app.principal_id
}

# Create federated credential
resource "azurerm_federated_identity_credential" "my_app" {
name = "my-app-federated-credential"
resource_group_name = var.resource_group_name
parent_id = azurerm_user_assigned_identity.my_app.id
audience = ["api://AzureADTokenExchange"]
issuer = data.azurerm_kubernetes_cluster.cluster.oidc_issuer_url
subject = "system:serviceaccount:default:my-app-ksa"
}

# Create Kubernetes ServiceAccount
resource "kubernetes_service_account" "my_app" {
metadata {
name = "my-app-ksa"
namespace = "default"
annotations = {
"azure.workload.identity/client-id" = azurerm_user_assigned_identity.my_app.client_id
}
labels = {
"azure.workload.identity/use" = "true"
}
}
}

# Deploy test pod
resource "kubernetes_pod" "my_app_test" {
metadata {
name = "my-app-test-pod"
namespace = "default"
labels = {
"azure.workload.identity/use" = "true"
}
}

spec {
service_account_name = kubernetes_service_account.my_app.metadata[0].name

container {
name = "azure-cli"
image = "mcr.microsoft.com/azure-cli:latest"
command = ["sleep", "3600"]
}
}
}

# Outputs
output "identity_client_id" {
value = azurerm_user_assigned_identity.my_app.client_id
}

output "serviceaccount_name" {
value = kubernetes_service_account.my_app.metadata[0].name
}

Next Steps

Expand Your Implementation

  • Multiple environments: Create separate managed identities for dev/staging/production
  • Different Azure services: Assign roles for Key Vault, Cosmos DB, SQL Database, etc.
  • Cross-subscription access: Configure RBAC for accessing resources in other subscriptions
  • Monitoring: Set up Azure Monitor workbooks for Workload Identity usage

Learn More

Additional Resources

Official Documentation

Tools

  • Azure CLI - Azure command-line interface
  • kubectl - Kubernetes command-line tool

Blog Posts