Pods de Kubernetes como identidades en la nube: EKS IRSA vs. GKE Workload Identity Federation
Cómo una ServiceAccount de Kubernetes se convierte en una identidad real en la nube en AWS y Google Cloud - la cadena de confianza de federación OIDC detrás de EKS IRSA y GKE Workload Identity Federation, mapeada uno a uno, con las particularidades que rompen tu memoria muscular.
Tanto AWS como Google Cloud resuelven el mismo problema de la misma manera: un pod se autentica en las API de la nube como sí mismo, con un token de corta duración y sin secreto almacenado, al convertir su ServiceAccount de Kubernetes en una identidad federada en la nube. En AWS, la característica es IRSA (IAM Roles for Service Accounts); en Google Cloud es Workload Identity Federation for GKE (renombrada en 2024 de simplemente "Workload Identity"). En el fondo, ambos son el mismo truco de federación OIDC, pero GKE oculta gran parte de la infraestructura, y eso cambia la forma en que lo configuras, depuras y razonas sobre ello.
La versión corta, si solo lees un párrafo: el clúster firma un token de corta duración para la ServiceAccount de cada pod, y la nube intercambia ese token por credenciales reales con alcance a una identidad. En AWS, registras un proveedor IAM OIDC por clúster, creas un rol IAM y lo proteges con una política de confianza; el SDK dentro del pod llama a AssumeRoleWithWebIdentity. En GKE no registras nada: cada proyecto tiene un pool de identidades de carga de trabajo permanente y gestionado por Google llamado PROJECT_ID.svc.id.goog, cada ServiceAccount es automáticamente un principal en él, y un servidor de metadatos local del nodo realiza el intercambio de tokens para que tu aplicación simplemente use Application Default Credentials como si estuviera en una VM simple. Tres cosas difieren lo suficiente como para causarte problemas: GKE necesita dos interruptores de habilitación (clúster y pool de nodos), GKE puede otorgar permisos en la nube a una ServiceAccount de Kubernetes sin ningún objeto de identidad en la nube, y, como siempre, extraer tu imagen de contenedor es una identidad diferente de tu identidad de carga de trabajo.
El problema que ambos resuelven
Antes de la federación, otorgar permisos en la nube a un pod significaba malas opciones: integrar una clave estática en la imagen o en un Secret (filtraciones, dificultad de rotación), o conceder el permiso a todo el nodo para que cada pod en él lo heredara (sin aislamiento, alcance excesivamente amplio). Las soluciones provisionales del lado de AWS eran interceptores de IMDS de nodo como kube2iam y kiam; la del lado de GCP era montar una clave de cuenta de servicio de nodo en los pods. Todas eran complicadas, demasiado amplias y propensas a condiciones de carrera.
La federación reemplaza eso con confianza criptográfica. El clúster proyecta un token OIDC firmado y de corta duración con alcance a la ServiceAccount del pod. La nube verifica ese token contra las claves publicadas del clúster y, si la identidad del token coincide con una regla que configuraste, devuelve credenciales para exactamente una identidad en la nube. No se almacena ningún secreto en ningún lugar; el token rota automáticamente (vida útil predeterminada de aproximadamente una hora) y es inútil fuera del clúster.
El mecanismo compartido: federación OIDC
Cada configuración de identidad de carga de trabajo, en cualquiera de las nubes, es el mismo triángulo de confianza:
- El clúster es un emisor OIDC. Expone un documento de descubrimiento y un endpoint JWKS con las claves públicas con las que firma los tokens de ServiceAccount. Ese emisor es el ancla de confianza.
- La nube confía en ese emisor para una identidad de ServiceAccount específica. AWS expresa la confianza por rol; GCP la expresa una vez por proyecto, de forma permanente, y Google la gestiona por ti.
- La carga de trabajo intercambia el token proyectado por credenciales de la nube y recibe credenciales de corta duración para una única identidad en la nube.
Todo lo demás es nomenclatura, y cuánto del paso 2 tienes que construir tú mismo. Ahí es donde las dos nubes divergen más.
El mapeo uno a uno
- El lado de Kubernetes. En AWS, anotas la ServiceAccount con
eks.amazonaws.com/role-arn: <role arn>. En GKE, en el modelo directo moderno, no anotas nada en la ServiceAccount; simplemente la referencias como un principal IAM. (El modelo de suplantación más antiguo sí usa una anotación; más detalles a continuación). - El "proveedor OIDC". En AWS, este es el emisor OIDC del clúster EKS, que registras una vez en IAM como un proveedor de identidad OIDC, por clúster. En GKE, es el pool de identidades de carga de trabajo del proyecto
PROJECT_ID.svc.id.goog, creado automáticamente y gestionado por Google; nunca lo registras, y cada ServiceAccount en cada clúster del proyecto ya es un principal dentro de él. - La identidad. AWS: un rol IAM. Modelo directo de GKE: ningún objeto de identidad dedicado: la propia ServiceAccount de Kubernetes es el principal. Modelo de suplantación de GKE: una cuenta de servicio de Google que la KSA suplanta.
- La regla de confianza. AWS: la política de confianza del rol IAM fija
sub = system:serviceaccount:<ns>:<sa>yaud = sts.amazonaws.com. GKE directo: no hay un documento de confianza separado; otorgas un rol directamente a un identificador principal que codifica el namespace y la ServiceAccount. GKE impersonación: otorgas a la KSA el rolroles/iam.workloadIdentityUseren la cuenta de servicio de Google. - Los permisos. AWS: una política IAM adjunta al rol. GKE: vinculaciones de rol de política de permiso IAM en el recurso de destino (o proyecto) -
roles/storage.objectViewer,roles/secretmanager.secretAccessor, y así sucesivamente. Esta es la división más profunda: AWS adjunta una política a la identidad; GCP otorga un rol en el ámbito del recurso. - El intercambio. AWS: el SDK dentro del pod llama a
sts:AssumeRoleWithWebIdentitycon el token proyectado. GKE: el servidor de metadatos del nodo realiza el intercambio de forma transparente y tu aplicación utiliza Application Default Credentials sin ningún código de intercambio.
El resto de esta publicación recorre cada nube de principio a fin, y luego se detiene en las diferencias que realmente confunden a la gente.
Recorrido: AWS EKS IRSA, de principio a fin
Las piezas, en el orden en que fluye la confianza:
1. El emisor OIDC del clúster, en una URL como https://oidc.eks.<region>.amazonaws.com/id/<hash>. Lo registras una vez en IAM como un proveedor de identidad OIDC para que IAM confíe en los tokens que firma.
2. El rol IAM y su política de confianza. La política de confianza hace que el rol sea asumible por una ServiceAccount y nada más:
{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::<account>:oidc-provider/oidc.eks.<region>.amazonaws.com/id/<hash>" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.<region>.amazonaws.com/id/<hash>:sub": "system:serviceaccount:apps:checkout",
"oidc.eks.<region>.amazonaws.com/id/<hash>:aud": "sts.amazonaws.com"
}
}
}
Fija tanto :sub como :aud; fijar solo el emisor permitiría que cualquier ServiceAccount en el clúster asumiera el rol.
3. La política IAM adjunta al rol otorga lo que la aplicación necesita (s3:GetObject en un bucket, y así sucesivamente).
4. La ServiceAccount lleva una anotación:
apiVersion: v1
kind: ServiceAccount
metadata:
name: checkout
namespace: apps
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::<account>:role/checkout
5. La proyección. EKS incluye el Pod Identity Webhook integrado en el clúster. Cuando un pod utiliza la ServiceAccount anotada, el webhook inyecta AWS_ROLE_ARN y AWS_WEB_IDENTITY_TOKEN_FILE y monta un token proyectado (audiencia sts.amazonaws.com, auto-rotado).
6. El intercambio. El SDK de AWS detecta AWS_WEB_IDENTITY_TOKEN_FILE, llama a sts:AssumeRoleWithWebIdentity y almacena en caché las credenciales de corta duración devueltas. Tu código es simplemente boto3.client("s3").
Alternativa más reciente: EKS Pod Identity (2023) realiza el mismo trabajo a través de un agente en el clúster y una API de asociación en lugar de un proveedor IAM OIDC por clúster y una política de confianza por rol, lo que, notablemente, lo hace sentir mucho más parecido al modelo gestionado de GKE. Tanto IRSA como Pod Identity coexisten en producción; IRSA es el que se mapea limpiamente al marco de federación OIDC, por lo que es el que debes tener en cuenta aquí.
Recorrido: GKE Workload Identity Federation, de principio a fin
Mismo flujo de confianza, mucho menos que construir:
1. Dos interruptores de habilitación. Habilita el pool de identidades de carga de trabajo en el clúster y el servidor de metadatos en el pool de nodos:
gcloud container clusters update <cluster> --workload-pool=<project-id>.svc.id.goog
gcloud container node-pools update <pool> --cluster=<cluster> --workload-metadata=GKE_METADATA
Ambos son obligatorios. El flag del clúster opta el clúster en el pool del proyecto; el flag del pool de nodos despliega el servidor de metadatos de GKE (un DaemonSet en cada nodo) que interceptará las solicitudes de credenciales. Un pod en un pool de nodos sin GKE_METADATA obtiene la identidad del nodo, no la suya propia, un fallback silencioso y demasiado amplio.
2. El pool ya existe. <project-id>.svc.id.goog se crea automáticamente para el proyecto; nunca registras un proveedor OIDC, y no hay un límite de proveedores por cuenta que alcanzar. Cada ServiceAccount en los clústeres del proyecto ya es un principal.
3a. Acceso directo (el valor predeterminado moderno). Otorga un rol directamente a la ServiceAccount, dirigida como un principal IAM. Sin cuenta de servicio de Google, sin anotación:
gcloud storage buckets add-iam-policy-binding gs://<bucket> \
--role=roles/storage.objectViewer \
--member="principal://iam.googleapis.com/projects/<project-number>/locations/global/workloadIdentityPools/<project-id>.svc.id.goog/subject/ns/apps/sa/checkout"
La ruta principal codifica ns/apps/sa/checkout: el namespace y la ServiceAccount son la regla de confianza; no hay un documento de confianza separado para escribir.
3b. Suplantación (el modelo más antiguo, todavía necesario para algunos servicios). Crea una cuenta de servicio de Google, permite que la KSA la suplante y anota la KSA:
gcloud iam service-accounts add-iam-policy-binding checkout@<project-id>.iam.gserviceaccount.com \
--role=roles/iam.workloadIdentityUser \
--member="serviceAccount:<project-id>.svc.id.goog[apps/checkout]"
apiVersion: v1
kind: ServiceAccount
metadata:
name: checkout
namespace: apps
annotations:
iam.gke.io/gcp-service-account: checkout@<project-id>.iam.gserviceaccount.com
Aquí la cuenta de servicio de Google posee los permisos (a través de vinculaciones de rol normales), y la KSA los toma prestados.
4. El intercambio. Cuando tu aplicación solicita credenciales, el servidor de metadatos de GKE intercepta la solicitud a http://metadata.google.internal, obtiene un JWT de ServiceAccount de la API de Kubernetes para ese pod, lo intercambia a través del Security Token Service por un token de acceso federado de corta duración (predeterminado de 1 hora, actualizado proactivamente antes de su vencimiento) y lo devuelve. Tu aplicación utiliza Application Default Credentials y las bibliotecas cliente estándar de Google Cloud; se comporta exactamente como si estuviera ejecutándose en una VM de GCE. No hay un webhook que inyecte variables de entorno, no hay variables al estilo AZURE_*, y no hay una llamada de intercambio en tu código: una lectura de Cloud Storage es simplemente storage.Client() en Python.
El sujeto es el punto clave (en ambas nubes)
El valor que debe coincidir exactamente, en ambas nubes, es la identidad de la ServiceAccount:
- AWS fija el sujeto del token
system:serviceaccount:<namespace>:<serviceaccount>en la política de confianza del rol. - GKE directo codifica el mismo par en la ruta principal
.../subject/ns/<namespace>/sa/<serviceaccount>. - GKE impersonación lo codifica en el miembro
serviceAccount:<project-id>.svc.id.goog[<namespace>/<serviceaccount>].
El fallo más común de "no se autentica como nadie" en cualquiera de las nubes es una falta de coincidencia aquí: el chart se desplegó en un namespace diferente, la ServiceAccount obtuvo el nombre de la versión en lugar del nombre de la aplicación, o el pod recurrió a la ServiceAccount default. Cuando la federación falla silenciosamente, verifica primero el namespace y el nombre de la ServiceAccount.
Diferencias que realmente te confundirán
1. GKE registra el "proveedor OIDC" por ti - una vez, por proyecto, para siempre. En EKS, registras un proveedor IAM OIDC por clúster, y a escala de flota puedes alcanzar el límite flexible de 100 proveedores OIDC por cuenta de AWS y tener que buscar una solución. En GKE hay exactamente un pool por proyecto (PROJECT_ID.svc.id.goog), creado y gestionado por Google, compartido por cada clúster en el proyecto. Nada que registrar, nada que limitar. La otra cara de la moneda: la confianza es a nivel de proyecto por defecto, por lo que el alcance se define con vinculaciones IAM (a qué namespace y ServiceAccount otorgas), no dividiendo proveedores.
2. GKE intercambia el token en un servidor de metadatos local del nodo, no con una llamada al SDK dentro del pod. IRSA muta el pod (un webhook inyecta variables de entorno y un token proyectado) y el SDK llama a STS. GKE intercepta metadata.google.internal en el nodo y devuelve las credenciales de forma transparente, por lo que la aplicación utiliza Application Default Credentials como si estuviera en una VM simple. Dos consecuencias: tu aplicación no necesita ningún código de credenciales específico de la nube, y el pool de nodos debe tener el servidor de metadatos habilitado (GKE_METADATA) o el pod obtiene silenciosamente la identidad del nodo en lugar de la suya propia.
3. GKE puede otorgar permisos en la nube a una ServiceAccount sin ningún objeto de identidad en la nube. En IRSA siempre hay un rol IAM. En el modelo directo de GKE no hay nada que crear en el lado de la nube excepto la propia vinculación de rol: la ServiceAccount de Kubernetes es el principal (principal://.../subject/ns/NS/sa/SA). El modelo de suplantación más antiguo sí introduce una cuenta de servicio de Google y la anotación iam.gke.io/gcp-service-account, y todavía la necesitas para el puñado de servicios que no aceptan un principal federado directamente, pero opta primero por la vinculación directa.
4. GKE necesita dos interruptores, como Azure. Clúster (--workload-pool) más pool de nodos (--workload-metadata=GKE_METADATA). Si falta la mitad del pool de nodos, no hay fallo, solo la identidad incorrecta (del nodo). IRSA integra la proyección en el propio EKS, por lo que no hay un segundo interruptor equivalente.
5. Extraer la imagen es una identidad diferente de la identidad de la carga de trabajo, en ambas nubes. La extracción de la imagen es tarea de la identidad del nodo: en GKE, la cuenta de servicio de Google del pool de nodos necesita roles/artifactregistry.reader para extraer de Artifact Registry; en EKS, el rol de instancia del grupo de nodos necesita AmazonEC2ContainerRegistryReadOnly (o usas secretos de extracción). La identidad de carga de trabajo es solo para cuando el código de la aplicación llama a una API de la nube. Un servicio HTTP puro que nunca se comunica con un SDK de la nube no necesita ninguna identidad de carga de trabajo, y otorgar una identidad de carga de trabajo artifactregistry.reader no hace nada para la extracción de imágenes, porque la extracción ocurre antes de que la aplicación (y su token federado) se ejecute.
6. Los permisos se adjuntan de manera diferente. AWS adjunta una política IAM al rol; el permiso viaja con la identidad. GCP otorga una vinculación de rol IAM en el ámbito del recurso de destino (o del proyecto); la concesión reside en el elemento al que se accede, no en la identidad. Mismo estado final, diferente lugar para auditarlo: en AWS, lee las políticas adjuntas al rol; en GCP, lista la política IAM del recurso (o usa Policy Analyzer para ver a qué puede acceder un principal).
Qué hace el código de la aplicación
Ambas nubes terminan en "sin secreto, sin código de credenciales", pero llegan allí de manera diferente:
- AWS: el webhook establece
AWS_ROLE_ARNyAWS_WEB_IDENTITY_TOKEN_FILE; la cadena predeterminada del SDK llama aAssumeRoleWithWebIdentity. Tu código:boto3.client("s3"). - GKE: no se inyecta nada en el pod; la cadena de Application Default Credentials del SDK accede al servidor de metadatos del nodo, que realiza el intercambio. Tu código:
storage.Client().
En ambos casos, las credenciales son de corta duración y se actualizan de forma transparente. No hay nada que rotar, nada que almacenar y nada que filtrar.
Una lista de verificación de depuración que funciona en ambas nubes
Cuando un pod "no puede autenticarse", recorre la cadena de confianza en orden:
- ¿Federación de clúster/proyecto activada? EKS: el proveedor IAM OIDC está registrado. GKE: el clúster tiene
--workload-pooly el pool de nodos tieneGKE_METADATA. En GKE, un interruptor del pool de nodos faltante es el clásico fallo silencioso: el pod obtiene la identidad del nodo, no la suya propia. - ¿ServiceAccount correcta? El pod realmente usa la ServiceAccount que crees que usa (no
default). Ejecuta exec y verifica. - ¿Coincidencia de identidad? Compara la regla de confianza con el namespace y la ServiceAccount reales:
system:serviceaccount:<ns>:<sa>en AWS,.../subject/ns/<ns>/sa/<sa>(o[ns/sa]) en GKE. Este es el fallo más frecuente en ambos. - ¿Intercambio funcionando? EKS: el pod tiene
AWS_WEB_IDENTITY_TOKEN_FILE. GKE: curl -H "Metadata-Flavor: Google" metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email desde el pod devuelve la identidad esperada. - ¿Permisos? Solo después de que la identidad se resuelva: la política IAM en el rol (AWS) o la vinculación de rol en el recurso de destino (GCP). Un resultado vacío aquí es un fallo de autorización, no de autenticación, lo que indica que la federación en sí está funcionando.
Qué certificaciones abordan esto
La identidad de carga de trabajo se encuentra justo donde convergen Kubernetes, IAM en la nube y la federación OIDC, por lo que aparece en tres vías de examen. Si ya posees una credencial de una nube, el mismo concepto en la otra nube es un salto corto.
Por el lado de AWS:
- AWS Certified Solutions Architect - Associate (SAA-C03) - Roles IAM, EKS y cómo las identidades obtienen permisos en la nube.
- AWS Certified Security - Specialty (SCS-C03) - Políticas de confianza IAM, federación OIDC y alcance de mínimo privilegio (exactamente la cadena de confianza IRSA).
Por el lado de Google Cloud:
- Google Cloud Associate Cloud Engineer - GKE, IAM, cuentas de servicio y vinculaciones de rol.
- Google Cloud Professional Cloud Security Engineer - Identidad de carga de trabajo, profundidad de IAM, seguridad de cuentas de servicio y mínimo privilegio.
Por el lado de Kubernetes:
- CNCF Certified Kubernetes Administrator (CKA) - ServiceAccounts, tokens proyectados y especificaciones de pod.
- CNCF Certified Kubernetes Security Specialist (CKS) - Seguridad de tokens de ServiceAccount, mínimo privilegio y reducción de la exposición de secretos estáticos.
Conclusión
IRSA y GKE Workload Identity Federation son la misma idea con diferentes niveles de infraestructura: el clúster firma un token OIDC de corta duración para una ServiceAccount, y la nube lo intercambia por credenciales con alcance a una única identidad. AWS te hace ensamblar la confianza tú mismo: un proveedor OIDC por clúster, un rol IAM, una política de confianza, un token proyectado que el SDK intercambia. GKE te entrega un pool gestionado a nivel de proyecto, te permite vincular un rol directamente a la ServiceAccount de Kubernetes sin ninguna identidad en la nube, y realiza el intercambio en un servidor de metadatos del nodo para que tu aplicación nunca vea una credencial. Aprende el triángulo de confianza una vez y la traducción es mecánica: el rol se convierte en una vinculación principal (o una cuenta de servicio que suplantas), la política de confianza se convierte en una cadena de miembro IAM, la política adjunta se convierte en una vinculación de rol en el recurso, y AssumeRoleWithWebIdentity se convierte en un intercambio invisible del servidor de metadatos. Ten en cuenta tres cosas: los dos interruptores de GKE, su modelo directo sin objeto de identidad y el hecho de que la extracción de imágenes es una identidad completamente diferente, y las denegaciones que antes parecían aleatorias comenzarán a leerse como un diseño coherente y deliberado.