Pide tu presupuesto ya!
Ya han pasado tres semanas de su nueva implementación de AKS y acaba de encontrar un secreto de cliente en un objeto secreto de Kubernetes, codificado en base64, ubicado allí mismo en el clúster, rotando según un cronograma que nadie ha verificado en seis meses. Felicitaciones: ha descubierto lo que la mayoría de los equipos de AKS descubren eventualmente. Los secretos de Kubernetes no son lo suficientemente secretos.
Identidad de carga de trabajo de Microsoft Entra resuelve este problema eliminando la credencial por completo. En lugar de almacenar un secreto de cliente que sus pods pueden usar para autenticarse en los recursos de Azure, su propio clúster se convierte en el proveedor de identidad. Los pods reciben un token de Kubernetes ServiceAccount proyectado, lo intercambian por un token de acceso de Entra a través de un protocolo de enlace OIDC y acceden a Key Vault, Storage, SQL (lo que sea que necesiten) sin que un solo secreto toque el almacén etcd de su clúster (la base de datos de secretos interna de Kubernetes).
Esta guía le guiará para habilitar Workload Identity en AKS desde cero, conectar la credencial de identidad federada e implementar una carga de trabajo que se autentique de forma limpia. Se incluyen fragmentos de Bicep y Terraform para ambos campos.
Antes de comenzar, verifique que tenga instaladas las versiones correctas. La identidad de la carga de trabajo tiene requisitos mínimos de versión para AKS y Azure CLI—Consulta la sección de requisitos previos antes de comenzar. Ejecute estos para confirmar sus versiones actuales:
az --version az aks show --resource-group myRG --name myAKS --query kubernetesVersion -o tsv
También necesitarás kubectl configurado en su clúster y permiso para crear identidades administradas en su suscripción.
| Requisito | Qué comprobar | Dominio |
|---|---|---|
| Clúster de AKS | Versión mínima admitida | az aks show ... --query kubernetesVersion |
| CLI de Azure | Versión mínima admitida | az --version |
| kubectl | Configurado contra su clúster | kubectl cluster-info |
| permisos de IAM | Crear identidades administradas en suscripción | Portal de Azure → Suscripciones → IAM |
Consejo profesional: si todavía está ejecutando Azure AD Pod Identity, ese complemento perdió el soporte oficial en septiembre de 2025. No se trata de “debería migrar”, ya debería haberlo hecho. El guía de migración cubre tres enfoques según la versión del SDK de Azure Identity en la que se encuentren sus aplicaciones.
Workload Identity comienza cuando su clúster de AKS publica un Punto final del emisor OpenID Connect (OIDC)—una URL que Entra ID utiliza para recuperar las claves de firma públicas del clúster y verificar los tokens. Sin él, el intercambio de tokens no tiene adónde ir.
Para un nuevo clúster:
export RESOURCE_GROUP="myResourceGroup"
export CLUSTER_NAME="myAKSCluster"
export LOCATION="eastus"
az aks create \
--resource-group "${RESOURCE_GROUP}" \
--name "${CLUSTER_NAME}" \
--location "${LOCATION}" \
--enable-oidc-issuer \
--enable-workload-identity \
--generate-ssh-keys
Para un clúster existente, es un único comando de actualización:
az aks update \
--resource-group "${RESOURCE_GROUP}" \
--name "${CLUSTER_NAME}" \
--enable-oidc-issuer \
--enable-workload-identity
Una vez que se complete, capture la URL del emisor; la necesitará para cada credencial federada que cree:
export AKS_OIDC_ISSUER="$(az aks show \
--name "${CLUSTER_NAME}" \
--resource-group "${RESOURCE_GROUP}" \
--query "oidcIssuerProfile.issuerUrl" \
--output tsv)"
echo $AKS_OIDC_ISSUER
La URL del emisor sigue el patrón https://{region}.oic.prod-aks.azure.com/.... Verifique que esté completa antes de continuar; una variable vacía aquí provoca un error de discrepancia más adelante que es molestamente difícil de rastrear hasta este paso.
Su carga de trabajo necesita un identidad administrada asignada por el usuario en azur. Esta identidad es la que obtiene las asignaciones de RBAC a los recursos de Azure. La credencial federada es el vínculo de confianza entre esa identidad administrada y una cuenta de servicio de Kubernetes específica.
La credencial federada tiene cuatro campos que deben ser exactos. Esto es a lo que se asigna cada uno:
| Campo | ¿Qué es? | Valor de ejemplo |
|---|---|---|
issuer |
La URL del emisor OIDC de su clúster | https://eastus.oic.prod-aks.azure.com/... |
subject |
Cuenta de servicio de Kubernetes en system:serviceaccount:<namespace>:<name> formato |
system:serviceaccount:my-namespace:my-service-account |
audiences |
Siempre este valor fijo para Azure | api://AzureADTokenExchange |
name |
Etiqueta para esta credencial (tu elección) | my-app-federation |
export USER_ASSIGNED_IDENTITY_NAME="myWorkloadIdentity"
export SERVICE_ACCOUNT_NAMESPACE="my-namespace"
export SERVICE_ACCOUNT_NAME="my-service-account"
export FEDERATED_CREDENTIAL_NAME="my-app-federation"
az identity create \
--name "${USER_ASSIGNED_IDENTITY_NAME}" \
--resource-group "${RESOURCE_GROUP}"
export USER_ASSIGNED_CLIENT_ID="$(az identity show \
--name "${USER_ASSIGNED_IDENTITY_NAME}" \
--resource-group "${RESOURCE_GROUP}" \
--query 'clientId' \
--output tsv)"
az identity federated-credential create \
--name "${FEDERATED_CREDENTIAL_NAME}" \
--identity-name "${USER_ASSIGNED_IDENTITY_NAME}" \
--resource-group "${RESOURCE_GROUP}" \
--issuer "${AKS_OIDC_ISSUER}" \
--subject "system:serviceaccount:${SERVICE_ACCOUNT_NAMESPACE}:${SERVICE_ACCOUNT_NAME}" \
--audience api://AzureADTokenExchange
El subject El campo es la pieza en la que la mayoría de la gente se equivoca. Debe coincidir exactamente con el espacio de nombres de Kubernetes y el nombre de ServiceAccount: distingue entre mayúsculas y minúsculas y no tiene espacios al final. system:serviceaccount:My-Namespace:my-service-account es un tema diferente al system:serviceaccount:my-namespace:my-service-account. La búsqueda de credenciales federadas fallará silenciosamente en el momento del intercambio de tokens y obtendrá una genérica AADSTS70021 error sin indicación de qué campo no coincide. Anota lo que configuraste aquí.
Información clave: puede tener hasta 20 credenciales de identidad federadas por identidad administrada. Para equipos con muchos microservicios, planifique con anticipación su mapeo de identidad a servicio. Varias cuentas de servicio pueden hacer referencia a la misma identidad administrada (muchos a uno), lo que simplifica RBAC pero reduce el aislamiento del radio de explosión.
Con la credencial federada implementada, cree la cuenta de servicio en el lado del clúster. La anotación vincula la identidad de Kubernetes con la identidad administrada de Azure:
kubectl create namespace "${SERVICE_ACCOUNT_NAMESPACE}"
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
name: ${SERVICE_ACCOUNT_NAME}
namespace: ${SERVICE_ACCOUNT_NAMESPACE}
annotations:
azure.workload.identity/client-id: "${USER_ASSIGNED_CLIENT_ID}"
EOF
Cualquier pod que use esta ServiceAccount y tenga la etiqueta azure.workload.identity/use: "true" en su plantilla de pod obtiene la inyección de identidad de carga de trabajo automáticamente. El webhook de admisión mutante, instalado como parte de --enable-workload-identity: inyecta las variables de entorno requeridas y monta el volumen del token proyectado. El código de su aplicación no necesita saber que nada de esto está sucediendo. (Ese es el punto: la gestión de credenciales está completamente fuera de los límites de la aplicación).
Cuando el webhook se activa correctamente, su pod recibe tres variables de entorno inyectadas automáticamente:
| Variable | Qué contiene |
|---|---|
AZURE_CLIENT_ID |
El ID de cliente de la identidad administrada |
AZURE_TENANT_ID |
Su ID de inquilino de Azure |
AZURE_FEDERATED_TOKEN_FILE |
Ruta al token de Kubernetes proyectado en el disco |
AZURE_AUTHORITY_HOST |
La URL del punto final de la autoridad de Entra ID |
Aquí hay una especificación mínima de pod que se autentica mediante Workload Identity:
apiVersion: v1
kind: Pod
metadata:
name: workload-identity-demo
namespace: my-namespace
labels:
azure.workload.identity/use: "true"
spec:
serviceAccountName: my-service-account
containers:
- name: app
image: mcr.microsoft.com/azure-cli:latest
command: ["sleep", "infinity"]
La etiqueta de la plantilla del pod no es opcional. Sin azure.workload.identity/use: "true"el webhook no hace nada y su pod se autentica como… nada. Este es el segundo error de configuración más común después de la falta de coincidencia del tema.
Aplíquelo y verifique que la inyección haya funcionado:
kubectl apply -f pod.yaml kubectl describe pod workload-identity-demo -n my-namespace | grep -A 5 "Environment:"
deberías ver AZURE_CLIENT_ID, AZURE_TENANT_IDy AZURE_FEDERATED_TOKEN_FILE en el medio ambiente. Si están ausentes, el webhook no se activó; verifique que la etiqueta esté en la especificación de la plantilla del pod, no en los metadatos externos del pod.
Si su equipo aprovisiona AKS a través de Bicep, habilite OIDC y Workload Identity en el recurso del clúster y luego conecte la credencial federada como recurso secundario de su identidad administrada:
“`texto sin formato
recurso aks ‘Microsoft.ContainerService/managedClusters@2023-10-01’ = {
nombre: ‘mis-aks’
ubicación: grupo de recursos().ubicación
identidad: {
tipo: ‘SistemaAsignado’
}
propiedades: {
Perfil de emisor oidc: {
habilitado: verdadero
}
perfil de seguridad: {
identidad de carga de trabajo: {
habilitado: verdadero
}
}
//… agentPoolProfiles, dnsPrefix, etc.
}
}
recurso federatedCredential ‘Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials@2023-01-31’ = {
nombre: ‘mi-federación-de-aplicaciones’
padre: usuarioAssignedIdentity
propiedades: {
emisor: aks.properties.oidcIssuerProfile.issuerURL
asunto: ‘sistema:cuenta de servicio:mi-espacio de nombres:mi-cuenta-de-servicio’
audiencias: [‘api://AzureADTokenExchange’]
}
}
Note that `aks.properties.oidcIssuerProfile.issuerURL` pulls the URL directly from the cluster resource output—no manual copy-paste, no trailing slash mismatch. The trailing slash problem is more common than you'd expect; Entra ID treats `https://example.com/` and `https://example.com` as different issuers. ### Terraform The `azurerm` provider exposes `oidc_issuer_url` as an output from the `azurerm_kubernetes_cluster` resource:
recurso “azurerm_kubernetes_cluster” “aks” {
nombre = “mis-aks”
ubicación = azurerm_resource_group.rg.ubicación
nombre_grupo_recurso = azurerm_grupo_recursos.rg.nombre
dns_prefix = “myaks”
oidc_issuer_enabled = verdadero
workload_identity_enabled = verdadero
#… bloque de identidad, default_node_pool, etc.
}
recurso “azurerm_user_assigned_identity” “carga de trabajo” {
nombre = “mi-identidad-de-carga-de-trabajo”
ubicación = azurerm_resource_group.rg.ubicación
nombre_grupo_recurso = azurerm_grupo_recursos.rg.nombre
}
recurso “azurerm_federated_identity_credential” “aks_federation” {
nombre = “mi-federación-de-aplicaciones”
nombre_grupo_recurso = azurerm_grupo_recursos.rg.nombre
audiencia = [“api://AzureADTokenExchange”]
emisor = azurerm_kubernetes_cluster.aks.oidc_issuer_url
user_assigned_identity_id = azurerm_user_assigned_identity.workload.id
asunto = “sistema: cuenta de servicio: mi espacio de nombres: mi cuenta de servicio”
}
Both modules follow the same principle: let the IaC layer handle the issuer URL hand-off rather than hardcoding it. If you hardcode the issuer URL and then recreate the cluster, the URL changes and every federated credential silently breaks at token exchange time—with the same AADSTS70021 error that's impossible to trace from the application side. ## Common Errors and How to Fix Them You'll hit at least one of these. Everyone does. | Error | Most Likely Cause | Fix | | --- | --- | --- | | AADSTS70021 | Issuer or subject mismatch in federated credential | Compare issuer URL against `az aks show` output; verify subject is case-exact | | Webhook not injecting | Missing `azure.workload.identity/use: "true"` label | Label must be on `spec.template.metadata.labels`, not outer pod metadata | | Virtual node failure | Workload Identity not supported on virtual nodes | Pin Workload Identity workloads to regular node pools with nodeSelector | | AADSTS70021 after credential update | Propagation delay | Wait 10–15 seconds after updating the federated credential before testing | **AADSTS70021 — No matching federated identity record found** The `issuer` or `subject` in your federated credential doesn't match what the cluster sent. Start by comparing the issuer URL in the credential against what `az aks show` returns. Then verify the `subject` field matches your namespace and ServiceAccount name character-for-character. If you updated the federated credential recently, wait 10–15 seconds—[propagation can take a few seconds](https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation-considerations) and early token requests during that window will fail. **Webhook injection not happening** Missing the `azure.workload.identity/use: "true"` label on the pod template. This is distinct from the pod metadata—it needs to be in `spec.template.metadata.labels` for Deployments and StatefulSets, not on the outer object. **Virtual Node workloads fail** [Virtual Nodes (Virtual Kubelet)](https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview) do not support Workload Identity. If your pods schedule onto virtual nodes, they won't get the token injection. Keep Workload Identity workloads on regular node pools. --- ***Warning: The ******`subject`****** claim in your federated credential is strictly case-sensitive. ******`my-namespace`****** and ******`My-Namespace`****** are different subjects. Entra ID will reject the token exchange with no indication of which field caused the mismatch. Store the exact values in your IaC and never type them by hand downstream.*** --- ## How the Token Exchange Actually Works Understanding the flow helps when things go wrong. Here's what happens from the moment your pod starts: The Kubelet projects a signed Kubernetes ServiceAccount token into your pod's filesystem at `/var/run/secrets/azure/tokens/azure-identity-token`. This token is a JWT signed with the cluster's private OIDC key. When your application calls an Azure SDK method that requires a credential—say, [`DefaultAzureCredential`](https://learn.microsoft.com/en-us/dotnet/api/azure.identity.defaultazurecredential) in the [Azure Identity library](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme)—the SDK reads that token file (via the `AZURE_FEDERATED_TOKEN_FILE` environment variable), then sends it to Entra ID's token endpoint. Entra ID fetches the cluster's public OIDC signing keys from the issuer URL, verifies the Kubernetes token's signature, checks that the `subject` claim matches a federated identity credential on the target managed identity, and—if everything checks out—returns an Azure access token scoped to your requested resource. Your application never touches a client secret. It never stores credentials. The Kubernetes token it holds is short-lived and scoped specifically to the ServiceAccount. If the pod is compromised, the blast radius is limited to whatever RBAC the managed identity has—which you control entirely through Azure role assignments. ## Assigning Azure Roles to Your Identity The federated credential lets your workload authenticate. RBAC determines what it can actually do. Grant the managed identity the minimum permissions it needs:
exportar KEYVAULT_RESOURCE_ID=$(az keyvault show \
–nombre miKeyVault \
–grupo de recursos “${RESOURCE_GROUP}” \
–ID de consulta \
–salida tsv)
asignación de roles az crear \
–asignado “${USER_ASSIGNED_CLIENT_ID}” \
–rol “Usuario de secretos de Key Vault” \
–alcance “${KEYVAULT_RESOURCE_ID}”
Scope role assignments to the specific resource, not the subscription. The whole point of eliminating static secrets is reducing credential scope—don't undermine that with a Contributor assignment on the subscription. (Yes, we know subscription-scope is faster to configure. That's not a reason.) ## Cleaning Up If you're done with this workload, remove the pieces in order:
az identidad eliminación de credenciales federadas \
–nombre “${FEDERATED_CREDENTIAL_NAME}” \
–nombre-identidad “${USER_ASSIGNED_IDENTITY_NAME}” \
–grupo de recursos “${RESOURCE_GROUP}”
eliminación de asignación de roles az \
–asignado “${USER_ASSIGNED_CLIENT_ID}” \
–rol “Usuario de secretos de Key Vault” \
–alcance “${KEYVAULT_RESOURCE_ID}”
eliminación de identidad az \
–nombre “${USER_ASSIGNED_IDENTITY_NAME}” \
–grupo de recursos “${RESOURCE_GROUP}”
“`
Elimine las asignaciones de roles antes de eliminar la identidad. Si omite ese paso, las asignaciones huérfanas no desaparecen: pierden su nombre para mostrar y se convierten en un desorden sin atribuciones en la vista IAM de su suscripción. No pueden hacer nada, pero resultan confusos durante las auditorías y pasarás tiempo buscando cuáles eran.
La cuenta de servicio de Kubernetes y el espacio de nombres se pueden eliminar con kubectl delete namespace ${SERVICE_ACCOUNT_NAMESPACE}.
Su carga de trabajo ahora se autentica en los recursos de Azure sin tener ninguna credencial. El clúster emite tokens, Entra ID los valida y sus pods obtienen tokens de acceso que se ajustan exactamente a lo que usted ha otorgado. Sin horario de rotación. Sin expansión secreta. No hay certificados que caduquen en ConfigMaps.
La comparación con Azure AD Pod Identity (que ahora ha finalizado su soporte técnico) es cruda. Pod Identity funcionó interceptando el tráfico IMDS a través de un DaemonSet NMI (Node Managed Identity), un pod por nodo, que representaba las solicitudes en todo el clúster. Era solo para Linux, tenía una latencia de asignación de identidad medida en segundos y expandía su superficie de ataque al nivel de nodo. Workload Identity es un protocolo de enlace OIDC limpio que utiliza primitivas de Kubernetes. Funciona en nodos de Windows. La proyección del token es inmediata. Y el alcance de un pod comprometido es la cuenta de servicio, no el nodo.
Si todavía está ejecutando cargas de trabajo de Pod Identity, la guía de migración vale la tarde que le toma. Si estás empezando de nuevo, ya lo estás haciendo de la manera correcta.
Leave a comment