Skip to content

How to Use Reloader with Azure Key Vault and External Secrets Operator#

This guide explains how to sync secrets from Azure Key Vault into Kubernetes using External Secrets Operator (ESO), and use Stakater Reloader to automatically restart pods when those secrets change.

Authentication Options#

ESO supports two authentication methods for Azure Key Vault:

Method Description Use Case
Workload Identity Azure managed identity bound to a Kubernetes ServiceAccount Recommended for AKS — no static credentials
Client Secret Azure App Registration client ID and secret stored in a K8s Secret Any cluster — simpler but requires secret rotation

How It Works#

sequenceDiagram
    actor Ops as Operator / Azure Function
    participant AKV as Azure Key Vault
    participant AAD as Azure AD
    participant ESO as External Secrets Operator
    participant K8s as Kubernetes Secret
    participant RL as Reloader
    participant Pod as Application Pod

    Note over ESO: Authenticates via Workload Identity or Client Secret
    ESO->>AAD: Exchange federated token (Workload Identity)
    AAD-->>ESO: Access token

    Ops->>AKV: Update secret
    loop Every refreshInterval
        ESO->>AKV: Get secret (latest version)
        AKV-->>ESO: Updated secret value
    end
    ESO->>K8s: Update Secret data
    Note over K8s: annotation: reloader.stakater.com/match: "true"
    K8s-->>RL: Watch event (Secret changed)
    RL->>Pod: Trigger rolling restart
    Note over Pod: New pod starts with updated secret

Prerequisites#

  • Kubernetes cluster (v1.19+)
  • Helm v3+
  • Azure subscription with an existing Key Vault
  • Azure CLI configured locally
  • Stakater Reloader installed
  • External Secrets Operator installed

Create secrets in Azure Key Vault#

# Set your Key Vault name
VAULT_NAME=my-key-vault

# Create individual secrets
az keyvault secret set \
  --vault-name $VAULT_NAME \
  --name myapp-username \
  --value "admin-user"

az keyvault secret set \
  --vault-name $VAULT_NAME \
  --name myapp-password \
  --value "super-secret-password"

Install Stakater Reloader#

helm repo add stakater https://stakater.github.io/stakater-charts
helm repo update
helm install reloader stakater/reloader --namespace reloader --create-namespace

Install External Secrets Operator#

helm repo add external-secrets https://charts.external-secrets.io
helm repo update
helm install external-secrets external-secrets/external-secrets \
  --namespace external-secrets \
  --create-namespace \
  --wait

Verify:

kubectl get pods -n external-secrets

Option 1: Workload Identity#

Azure Workload Identity lets a Kubernetes ServiceAccount authenticate to Azure services without storing credentials. It is the recommended approach for AKS clusters.

Workload Identity: Step 1 — Enable Workload Identity on your AKS cluster#

az aks update \
  --name my-cluster \
  --resource-group my-rg \
  --enable-oidc-issuer \
  --enable-workload-identity

Get the OIDC issuer URL:

OIDC_ISSUER=$(az aks show \
  --name my-cluster \
  --resource-group my-rg \
  --query "oidcIssuerProfile.issuerUrl" \
  --output tsv)

Workload Identity: Step 2 — Create an Azure managed identity#

az identity create \
  --name eso-identity \
  --resource-group my-rg \
  --location eastus

IDENTITY_CLIENT_ID=$(az identity show \
  --name eso-identity \
  --resource-group my-rg \
  --query clientId \
  --output tsv)

IDENTITY_OBJECT_ID=$(az identity show \
  --name eso-identity \
  --resource-group my-rg \
  --query principalId \
  --output tsv)

Workload Identity: Step 3 — Grant Key Vault access to the managed identity#

VAULT_ID=$(az keyvault show \
  --name $VAULT_NAME \
  --query id \
  --output tsv)

# Assign the Key Vault Secrets User role (read-only access to secret values)
az role assignment create \
  --assignee-object-id $IDENTITY_OBJECT_ID \
  --assignee-principal-type ServicePrincipal \
  --role "Key Vault Secrets User" \
  --scope $VAULT_ID

If your Key Vault still uses the legacy access policy model instead of Azure RBAC, use:

az keyvault set-policy \
  --name $VAULT_NAME \
  --object-id $IDENTITY_OBJECT_ID \
  --secret-permissions get list

Workload Identity: Step 4 — Create namespace and Kubernetes ServiceAccount#

kubectl create namespace azure-eso-test

kubectl create serviceaccount eso-sa -n azure-eso-test \
  --dry-run=client -o yaml | \
  kubectl annotate --local -f - \
    "azure.workload.identity/client-id=${IDENTITY_CLIENT_ID}" \
    --dry-run=client -o yaml | kubectl apply -f -

Or create the manifest directly:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: eso-sa
  namespace: azure-eso-test
  annotations:
    azure.workload.identity/client-id: "<IDENTITY_CLIENT_ID>"

Workload Identity: Step 5 — Create the federated identity credential#

This binds the Kubernetes ServiceAccount to the Azure managed identity:

az identity federated-credential create \
  --name eso-federated-credential \
  --identity-name eso-identity \
  --resource-group my-rg \
  --issuer "$OIDC_ISSUER" \
  --subject "system:serviceaccount:azure-eso-test:eso-sa" \
  --audience api://AzureADTokenExchange

Workload Identity: Step 6 — Create SecretStore#

apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
  name: azure-secret-store
  namespace: azure-eso-test
spec:
  provider:
    azurekv:
      authType: WorkloadIdentity
      vaultUrl: "https://my-key-vault.vault.azure.net"
      serviceAccountRef:
        name: eso-sa

Apply and verify:

kubectl apply -f secret-store.yaml
kubectl get secretstore -n azure-eso-test

Should show STATUS: Valid and READY: True.

Now proceed to Create ExternalSecret.


Option 2: Client Secret#

Use an Azure App Registration client secret stored in a Kubernetes Secret. Works with any Kubernetes cluster, not just AKS.

Client Secret: Step 1 — Create an App Registration#

APP_ID=$(az ad app create \
  --display-name eso-app \
  --query appId \
  --output tsv)

# Create a service principal for the app
az ad sp create --id $APP_ID

TENANT_ID=$(az account show --query tenantId --output tsv)

# Create a client secret (note the value — it is shown only once)
CLIENT_SECRET=$(az ad app credential reset \
  --id $APP_ID \
  --query password \
  --output tsv)

Client Secret: Step 2 — Grant Key Vault access to the App Registration#

SP_OBJECT_ID=$(az ad sp show --id $APP_ID --query id --output tsv)

VAULT_ID=$(az keyvault show --name $VAULT_NAME --query id --output tsv)

az role assignment create \
  --assignee-object-id $SP_OBJECT_ID \
  --assignee-principal-type ServicePrincipal \
  --role "Key Vault Secrets User" \
  --scope $VAULT_ID

For Key Vaults using access policies:

az keyvault set-policy \
  --name $VAULT_NAME \
  --spn $APP_ID \
  --secret-permissions get list

Client Secret: Step 3 — Store credentials in a Kubernetes Secret#

kubectl create namespace azure-eso-test

kubectl create secret generic azure-credentials -n azure-eso-test \
  --from-literal=client-id="$APP_ID" \
  --from-literal=client-secret="$CLIENT_SECRET"

Client Secret: Step 4 — Create SecretStore#

apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
  name: azure-secret-store
  namespace: azure-eso-test
spec:
  provider:
    azurekv:
      authType: ServicePrincipal
      vaultUrl: "https://my-key-vault.vault.azure.net"
      tenantId: "<TENANT_ID>"
      authSecretRef:
        clientId:
          name: azure-credentials
          key: client-id
        clientSecret:
          name: azure-credentials
          key: client-secret

Apply and verify:

kubectl apply -f secret-store.yaml
kubectl get secretstore -n azure-eso-test

Should show STATUS: Valid and READY: True.

Now proceed to Create ExternalSecret.


Common Configuration#

The following steps apply to both authentication methods.

Create ExternalSecret#

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: app-secrets
  namespace: azure-eso-test
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: azure-secret-store
    kind: SecretStore
  target:
    name: app-secrets
    creationPolicy: Owner
    template:
      metadata:
        annotations:
          reloader.stakater.com/match: "true"
  data:
    - secretKey: username
      remoteRef:
        key: myapp-username
    - secretKey: password
      remoteRef:
        key: myapp-password

Apply and verify:

kubectl apply -f external-secret.yaml
kubectl get externalsecret -n azure-eso-test

Should show STATUS: SecretSynced and READY: True.

Deploy Application#

apiVersion: apps/v1
kind: Deployment
metadata:
  name: azure-eso-test-app
  namespace: azure-eso-test
  annotations:
    reloader.stakater.com/search: "true"
spec:
  replicas: 1
  selector:
    matchLabels:
      app: azure-eso-test-app
  template:
    metadata:
      labels:
        app: azure-eso-test-app
    spec:
      containers:
        - name: app
          image: busybox:latest
          command:
            - "sh"
            - "-c"
            - |
              while true; do
                echo "Username: $APP_USERNAME"
                echo "Password: $APP_PASSWORD"
                sleep 30
              done
          env:
            - name: APP_USERNAME
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key: username
            - name: APP_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key: password

Apply:

kubectl apply -f deployment.yaml

Verify the Setup#

# SecretStore
kubectl get secretstore -n azure-eso-test

# ExternalSecret
kubectl get externalsecret -n azure-eso-test

# Secret (created by ESO)
kubectl get secret app-secrets -n azure-eso-test

# Pod
kubectl get pods -n azure-eso-test

# Verify secret contents
kubectl get secret app-secrets -n azure-eso-test -o jsonpath='{.data.password}' | base64 -d

# Check app logs
kubectl logs -n azure-eso-test -l app=azure-eso-test-app

Test Secret Rotation#

Update the secret in Azure Key Vault#

az keyvault secret set \
  --vault-name $VAULT_NAME \
  --name myapp-password \
  --value "new-rotated-password"

Azure Key Vault retains all previous versions. ESO always reads the latest enabled version unless a specific version is pinned in the ExternalSecret.

Wait and verify#

To force an immediate ESO sync without waiting for refreshInterval:

kubectl annotate externalsecret app-secrets -n azure-eso-test \
  force-sync=$(date +%s) --overwrite

Then verify:

# Check secret was updated
kubectl get secret app-secrets -n azure-eso-test -o jsonpath='{.data.password}' | base64 -d

# Check pod was restarted
kubectl get pods -n azure-eso-test -l app=azure-eso-test-app

# Check app logs show new password
kubectl logs -n azure-eso-test -l app=azure-eso-test-app --tail=5

Configuration Reference#

SecretStore — Workload Identity#

Field Description
provider.azurekv.authType WorkloadIdentity
provider.azurekv.vaultUrl Full URL of the Azure Key Vault (e.g. https://my-vault.vault.azure.net)
provider.azurekv.serviceAccountRef.name Kubernetes ServiceAccount annotated with the managed identity client ID

SecretStore — Client Secret#

Field Description
provider.azurekv.authType ServicePrincipal
provider.azurekv.vaultUrl Full URL of the Azure Key Vault
provider.azurekv.tenantId Azure Active Directory tenant ID
provider.azurekv.authSecretRef.clientId K8s Secret and key containing the App Registration client ID
provider.azurekv.authSecretRef.clientSecret K8s Secret and key containing the client secret value

ExternalSecret#

Field Description
refreshInterval How often ESO polls Azure for changes (e.g. 1h, 5m)
secretStoreRef Reference to the SecretStore
target.name Name of the K8s Secret to create
target.template.metadata.annotations Annotations to add to the created Secret
data[].secretKey Key name in the K8s Secret
data[].remoteRef.key Azure Key Vault secret name
data[].remoteRef.version (Optional) Specific secret version. Omit to use the latest.

Reloader Annotations#

Resource Annotation
Deployment reloader.stakater.com/search: "true"
Secret (via ExternalSecret template) reloader.stakater.com/match: "true"

Comparison: Workload Identity vs Client Secret#

Aspect Workload Identity Client Secret
Credentials No static secret — short-lived federated tokens Long-lived client secret
Security No credentials to leak or rotate Client secret must be rotated before expiry
Cluster requirement AKS with OIDC issuer + Workload Identity enabled Any Kubernetes cluster
Setup complexity Requires managed identity + federated credential binding Just an App Registration + K8s Secret
Best for Production on AKS Development, non-AKS clusters