Managing Kubernetes secrets manually—especially across multiple namespaces and environments—quickly becomes error-prone. A better approach is to store secrets centrally in Azure Key Vault, and sync them into Kubernetes automatically. This post walks you through how to fetch Azure Key Vault secrets into an AKS namespace managed by Rancher using External Secrets Operator (ESO) with Workload Identity—so you don’t store client secrets anywhere and applications can keep using the same Rancher Secret name (often with no code changes when names match)
Architecture
High-level flow
The solution uses a User-Assigned Managed Identity (UAMI) federated to a Kubernetes Service Account via AKS OIDC—then ESO uses that identity to read secrets from Key Vault and write them into Kubernetes as Opaque secrets.
Flow:
Azure Key Vault → ESO → Kubernetes Secret (Opaque) → Rancher / App
Prerequisites
From AKS Rancher Secrets from Azure Key Vault using Workload Identity, you need:
- AKS cluster with OIDC issuer enabled
- External Secrets Operator (ESO) deployed and operational
- Azure Key Vault with required secrets present
- UAMI + Federated Identity Credential trusting an AKS namespace Service Account
- Appropriate Key Vault roles (e.g., Key Vault Secrets Officer/User) depending on what you need to do
- Rancher access to the target namespace
Note: The AKS Workload Identity setup requires enabling OIDC/workload identity, creating a managed identity, creating a Service Account annotated with the client-id, and creating a federated identity credential.
Step-by-step: End-to-end implementation
Step 1 — Enable AKS OIDC issuer + Workload Identity
If you’re creating or updating a cluster, Microsoft’s AKS guidance is to enable both flags. The example below is from Deploy and Configure an Azure Kubernetes Service (AKS) Cluster with Microsoft Entra Workload ID.
# Create a new cluster (example) az aks create \ --resource-group "${RESOURCE_GROUP}" \ --name "${CLUSTER_NAME}" \ --enable-oidc-issuer \ --enable-workload-identity \ --generate-ssh-keys
If you already have a cluster:
az aks update \ --resource-group "${RESOURCE_GROUP}" \ --name "${CLUSTER_NAME}" \ --enable-oidc-issuer \ --enable-workload-identity
Retrieve the cluster OIDC issuer URL:
export AKS_OIDC_ISSUER="$(az aks show \ --name "${CLUSTER_NAME}" \ --resource-group "${RESOURCE_GROUP}" \ --query "oidcIssuerProfile.issuerUrl" \ --output tsv)"export AKS_OIDC_ISSUER="$(az aks show \ --name "${CLUSTER_NAME}" \ --resource-group "${RESOURCE_GROUP}" \ --query "oidcIssuerProfile.issuerUrl" \ --output tsv)"
Step 2 — Create a User-Assigned Managed Identity (UAMI)
az identity create \ --name "${USER_ASSIGNED_IDENTITY_NAME}" \ --resource-group "${RESOURCE_GROUP}" \ --location "${LOCATION}" \ --subscription "${SUBSCRIPTION}"
Capture the identity clientId (used in Service Account annotation):
export USER_ASSIGNED_CLIENT_ID="$(az identity show \ --resource-group "${RESOURCE_GROUP}" \ --name "${USER_ASSIGNED_IDENTITY_NAME}" \ --query 'clientId' \ --output tsv)"
Step 3 — Create/annotate a Kubernetes Service Account (namespace-scoped)
Service Account (Optional if already exists)
apiVersion: v1 kind: ServiceAccount metadata: name: <NAME> namespace: <NAMESPACE> annotations: azure.workload.identity/client-id: "<UAMI_CLIENT_ID>"
Apply:
kubectl apply -f serviceaccount.yaml
Step 4 — Create the Federated Identity Credential (UAMI ↔ ServiceAccount)
This binds:
- issuer = AKS_OIDC_ISSUER
- subject = system:serviceaccount:<namespace>:<serviceaccount>
- audience = api://AzureADTokenExchange
az identity federated-credential create \ --name "${FEDERATED_IDENTITY_CREDENTIAL_NAME}" \ --identity-name "${USER_ASSIGNED_IDENTITY_NAME}" \ --resource-group "${RESOURCE_GROUP}" \ --issuer "${AKS_OIDC_ISSUER}" \ --subject system:serviceaccount:"<NAMESPACE>":"<SERVICEACCOUNT>" \ --audience api://AzureADTokenExchange
Step 5 — Grant Key Vault permissions to the UAMI
export KEYVAULT_RESOURCE_ID=$(az keyvault show \ --resource-group "${RESOURCE_GROUP}" \ --name "${KEYVAULT_NAME}" \ --query id --output tsv) export IDENTITY_PRINCIPAL_ID=$(az identity show \ --name "${USER_ASSIGNED_IDENTITY_NAME}" \ --resource-group "${RESOURCE_GROUP}" \ --query principalId --output tsv) az role assignment create \ --assignee-object-id "${IDENTITY_PRINCIPAL_ID}" \ --role "Key Vault Secrets User" \ --scope "${KEYVAULT_RESOURCE_ID}" \ --assignee-principal-type ServicePrincipal
Step 6 — Create the SecretStore (ESO → Azure Key Vault) in Rancher
Example SecretStore YAML from the document:
apiVersion: external-secrets.io/v1beta1 kind: SecretStore metadata: name: <NAME> namespace: <NAMESPACE> spec: provider: azurekv: tenantId: "<tenantID>" vaultUrl: "https://<keyvaultname>.vault.azure.net/" authType: WorkloadIdentity serviceAccountRef: name: <SERVICEACCOUNT>
This matches ESO’s Azure Key Vault provider model: ESO integrates with Azure Key Vault using SecretStore / ClusterSecretStore, and supports authentication methods including Workload Identity.
Apply (if you’re using kubectl instead of Rancher UI):
kubectl apply -f secretstore.yaml
Step 7 — Create the ExternalSecret (Pattern-based or “sync all”)
Option A: Sync secrets matching a name pattern (password/secret/key)
apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: <NAME> namespace: <NAMESPACE> spec: refreshInterval: 30s secretStoreRef: kind: SecretStore name: <SECRETSTORENAME> target: name: <ANYNAME> creationPolicy: Owner dataFrom: - find: name: regexp: ".*(password|secret|key).*"
Option B: Sync all secrets from the Key Vault
apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: <NAME> namespace: <NAMESPACE> spec: refreshInterval: 30s secretStoreRef: kind: SecretStore name: <SECRETSTORENAME> target: name: <ANYNAME> creationPolicy: Owner dataFrom: - find: name: {}
- ESO fetches secrets from Azure Key Vault
- ESO creates an Opaque Kubernetes Secret in the namespace
- Rancher exposes it to the app
- Changes propagate on next refresh interval
Apply:
kubectl apply -f externalsecret.yaml
Validation checklist (what devs should verify)
SecretStore + ExternalSecret CRDs exist and are healthy
kubectl get secretstore -n <NAMESPACE> kubectl get externalsecret -n <NAMESPACE>
The Kubernetes Secret is created
kubectl get secret <SECRETNAME> -n <NAMESPACE> # or kubectl get secret <SECRETNAME> -n <NAMESPACE>
Workload Identity plumbing is correct
- AKS cluster has OIDC issuer and workload identity enabled.
- ServiceAccount has annotation azure.workload.identity/client-id.
- Federated identity credential exists and matches issuer+subject
Operational notes & best practices (practical guidance)
1) “No code change” strategy
Key advantage: If Azure Key Vault secret names are created the same as Rancher Secrets, applications can keep using the same secret references with no code changes.
2) Prefer Azure RBAC model with least privilege
For ESO read-only sync, Key Vault Secrets User is typically sufficient (per AKS workload identity walkthrough).
3) Refresh intervals
Adjust this based on your rotation policy and Key Vault throttling considerations (org-dependent).
Troubleshooting quick hits
Symptom: Access denied from Key Vault
- Ensure the UAMI has Key Vault roles assigned at the vault scope (e.g., Key Vault Secrets User)
- Ensure Key Vault URL and tenant ID are correct in SecretStore
Symptom: Token exchange issues
- Ensure cluster OIDC issuer and workload identity are enabled.
- Ensure federated credential subject matches system:serviceaccount:<ns>:<sa>.
Key benefits
- No client secrets stored (uses Workload Identity).
- Automatic discovery via regex filters.
- No code changes when secret naming aligns.
- Future-proof: new Key Vault secrets matching patterns can auto-sync.