Quick Start

In this tutorial, we will cover the basics of how to use the webhook to acquire an Azure AD token to access a secret in an Azure Key Vault.

Before we get started, ensure the following:

  • You are logged in with the Azure CLI as a user.
    • If you are logged in with a Service Principal, ensure that it has the correct API permissions enabled.
  • Your logged in account must have sufficient permissions to create applications and service principals in Azure AD.

1. Complete the installation guide

Installation guide. At this point, you should have already:

  • installed the mutating admission webhook
  • obtained your cluster’s OIDC issuer URL
  • [optional] installed the Azure AD Workload Identity CLI

2. Export environment variables

# environment variables for the Azure Key Vault resource
export KEYVAULT_NAME="azwi-kv-$(openssl rand -hex 2)"
export KEYVAULT_SECRET_NAME="my-secret"
export RESOURCE_GROUP="azwi-quickstart-$(openssl rand -hex 2)"
export LOCATION="westus2"

# environment variables for the AAD application
export APPLICATION_NAME="<your application name>"

# environment variables for the Kubernetes service account & federated identity credential
export SERVICE_ACCOUNT_NAMESPACE="default"
export SERVICE_ACCOUNT_NAME="workload-identity-sa"
export SERVICE_ACCOUNT_ISSUER="<your service account issuer url>" # see section 1.1 on how to get the service account issuer url

3. Create an Azure Key Vault and secret

Create an Azure resource group:

az group create --name "${RESOURCE_GROUP}" --location "${LOCATION}"

Create an Azure Key Vault:

az keyvault create --resource-group "${RESOURCE_GROUP}" \
   --location "${LOCATION}" \
   --name "${KEYVAULT_NAME}"

Create a secret:

az keyvault secret set --vault-name "${KEYVAULT_NAME}" \
   --name "${KEYVAULT_SECRET_NAME}" \
   --value "Hello\!"

4. Create an AAD application and grant permissions to access the secret

Azure Workload Identity CLI
azwi serviceaccount create phase app --aad-application-name "${APPLICATION_NAME}"
Output
INFO[0000] No subscription provided, using selected subscription from Azure CLI: REDACTED
INFO[0005] [aad-application] created an AAD application  clientID=REDACTED name=azwi-test objectID=REDACTED
WARN[0005] --service-principal-name not specified, falling back to AAD application name
INFO[0005] [aad-application] created service principal   clientID=REDACTED name=azwi-test objectID=REDACTED

Azure CLI
az ad sp create-for-rbac --name "${APPLICATION_NAME}"

Set access policy for the AAD application to access the keyvault secret:

export APPLICATION_CLIENT_ID="$(az ad sp list --display-name "${APPLICATION_NAME}" --query '[0].appId' -otsv)"
az keyvault set-policy --name "${KEYVAULT_NAME}" \
  --secret-permissions get \
  --spn "${APPLICATION_CLIENT_ID}"

5. Create a Kubernetes service account

Create a Kubernetes service account and annotate it with the client ID of the AAD application we created in step 4:

Azure Workload Identity CLI
azwi serviceaccount create phase sa \
  --aad-application-name "${APPLICATION_NAME}" \
  --service-account-namespace "${SERVICE_ACCOUNT_NAMESPACE}" \
  --service-account-name "${SERVICE_ACCOUNT_NAME}"
Output
INFO[0000] No subscription provided, using selected subscription from Azure CLI: REDACTED
INFO[0002] [service-account] created Kubernetes service account  name=workload-identity-sa namespace=default

kubectl
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    azure.workload.identity/client-id: ${APPLICATION_CLIENT_ID}
  labels:
    azure.workload.identity/use: "true"
  name: ${SERVICE_ACCOUNT_NAME}
  namespace: ${SERVICE_ACCOUNT_NAMESPACE}
EOF
Output
serviceaccount/workload-identity-sa created

If the AAD application is not in the same tenant as the default tenant defined during installation, then annotate the service account with the application tenant ID:

kubectl annotate sa ${SERVICE_ACCOUNT_NAME} -n ${SERVICE_ACCOUNT_NAMESPACE} azure.workload.identity/tenant-id="${APPLICATION_TENANT_ID}" --overwrite

6. Establish federated identity credential between the AAD application and the service account issuer & subject

Azure Workload Identity CLI
azwi serviceaccount create phase federated-identity \
  --aad-application-name "${APPLICATION_NAME}" \
  --service-account-namespace "${SERVICE_ACCOUNT_NAMESPACE}" \
  --service-account-name "${SERVICE_ACCOUNT_NAME}" \
  --service-account-issuer-url "${SERVICE_ACCOUNT_ISSUER}"
Output
INFO[0000] No subscription provided, using selected subscription from Azure CLI: REDACTED
INFO[0032] [federated-identity] added federated credential  objectID=REDACTED subject="system:serviceaccount:default:workload-identity-sa"

Azure CLI

Login to Azure Cloud Shell and run the following commands:

# Get the object ID of the AAD application
export APPLICATION_OBJECT_ID="$(az ad app show --id ${APPLICATION_CLIENT_ID} --query id -otsv)"

Add the federated identity credential:

cat <<EOF > body.json
{
  "name": "kubernetes-federated-credential",
  "issuer": "${SERVICE_ACCOUNT_ISSUER}",
  "subject": "system:serviceaccount:${SERVICE_ACCOUNT_NAMESPACE}:${SERVICE_ACCOUNT_NAME}",
  "description": "Kubernetes service account federated credential",
  "audiences": [
    "api://AzureADTokenExchange"
  ]
}
EOF

az rest --method POST --uri "https://graph.microsoft.com/beta/applications/${APPLICATION_OBJECT_ID}/federatedIdentityCredentials" --body @body.json

7. Deploy workload

Deploy a pod that references the service account created in the last step:

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: quick-start
  namespace: ${SERVICE_ACCOUNT_NAMESPACE}
spec:
  serviceAccountName: ${SERVICE_ACCOUNT_NAME}
  containers:
    - image: ghcr.io/azure/azure-workload-identity/msal-go
      name: oidc
      env:
      - name: KEYVAULT_NAME
        value: ${KEYVAULT_NAME}
      - name: SECRET_NAME
        value: ${KEYVAULT_SECRET_NAME}
  nodeSelector:
    kubernetes.io/os: linux
EOF

Feel free to swap the msal-go example image above with a list of language-specific examples we provide.

To check whether all properties are injected properly by the webhook:

kubectl describe pod quick-start
Output

You can verify the following injected properties in the output:

Environment variableDescription
AZURE_AUTHORITY_HOSTThe Azure Active Directory (AAD) endpoint.
AZURE_CLIENT_IDThe client ID of the AAD application.
AZURE_TENANT_IDThe tenant ID of the registered AAD application.
AZURE_FEDERATED_TOKEN_FILEThe path of the projected service account token file.

Volume mountDescription
/var/run/secrets/azure/tokens/azure-identity-tokenThe path of the projected service account token file.

VolumeDescription
azure-identity-tokenThe projected service account volume.
Name:         quick-start
Namespace:    default
Priority:     0
Node:         k8s-agentpool1-38097163-vmss000002/10.240.0.34
Start Time:   Wed, 13 Oct 2021 15:49:25 -0700
Labels:       <none>
Annotations:  <none>
Status:       Running
IP:           10.240.0.55
IPs:
  IP:  10.240.0.55
Containers:
  oidc:
    Container ID:   containerd://f425e89eef9aa3a62eb51a3daa5af8c06d8a59baa79c4e4dbb1887aea2647048
    Image:          ghcr.io/azure/azure-workload-identity/msal-go:latest
    Image ID:       ghcr.io/azure/azure-workload-identity/msal-go@sha256:84421aeea707ce66ade0891d9fcd3bb3f7bbd5dd3f810caced0acd315dcf8751
    Port:           <none>
    Host Port:      <none>
    State:          Running
      Started:      Wed, 13 Oct 2021 15:49:29 -0700
    Ready:          True
    Restart Count:  0
    Environment:
      KEYVAULT_NAME:              ${KEYVAULT_NAME}
      SECRET_NAME:                ${KEYVAULT_SECRET_NAME}
      AZURE_AUTHORITY_HOST:       (Injected by the webhook)
      AZURE_CLIENT_ID:            (Injected by the webhook)
      AZURE_TENANT_ID:            (Injected by the webhook)
      AZURE_FEDERATED_TOKEN_FILE: (Injected by the webhook)
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-844ns (ro)
      /var/run/secrets/azure/tokens from azure-identity-token (ro) (Injected by the webhook)
Conditions:
  Type              Status
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True
Volumes:
  kube-api-access-844ns:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  3607
    ConfigMapName:           kube-root-ca.crt
    ConfigMapOptional:       <nil>
    DownwardAPI:             true
  azure-identity-token: (Injected by the webhook)
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  86400
QoS Class:                   BestEffort
Node-Selectors:              kubernetes.io/os=linux
Tolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                             node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  19s   default-scheduler  Successfully assigned oidc/quick-start to k8s-agentpool1-38097163-vmss000002
  Normal  Pulling    18s   kubelet            Pulling image "ghcr.io/azure/azure-workload-identity/msal-go:latest"
  Normal  Pulled     16s   kubelet            Successfully pulled image "ghcr.io/azure/azure-workload-identity/msal-go:latest" in 1.987165801s
  Normal  Created    15s   kubelet            Created container oidc
  Normal  Started    15s   kubelet            Started container oidc

To verify that pod is able to get a token and access the secret from the Key Vault:

kubectl logs quick-start
Output

If successful, the log output would be similar to the following output:

I1013 22:49:29.872708       1 main.go:30] "successfully got secret" secret="Hello!"

8. Cleanup

kubectl delete pod quick-start
kubectl delete sa "${SERVICE_ACCOUNT_NAME}" --namespace "${SERVICE_ACCOUNT_NAMESPACE}"

az group delete --name "${RESOURCE_GROUP}"
az ad sp delete --id "${APPLICATION_CLIENT_ID}"