Módulo 2: Federated Identity And Least Privilege Iam
3. Cómo funciona la federación OIDC, paso a paso
Descripción
Si viniste de cicd-and-gitops-on-aws-guide, ya conoces el mecanismo completo de OIDC: el JWT, el identity provider, la trust policy, las credenciales temporales. Esta lección no lo vuelve a enseñar desde cero — lo repasa con precisión quirúrgica sobre las dos piezas exactas que vas a escribir en HCL en las próximas dos lecciones: qué claim exacto del JWT lee cada condición de la trust policy, y qué recurso de AWS representa cada mitad del flujo. Si esta es tu primera guía del ecosistema, esta lección construye el mecanismo completo, con el mismo rigor.
Conexión con el módulo
La lección 2 estableció el problema con evidencia real: un secreto de larga vida, una vez filtrado, es permanente. Esta lección presenta la solución completa, del lado de AWS, antes de que la lección 4 declare el primer recurso de Terraform. Todo lo que sigue en este módulo —el identity provider (4), la trust policy (5), el experimento del M2.6— se apoya en el vocabulario exacto que fijas aquí.
Analogía: el pase de visitante que un guardia puede verificar sin llamar a nadie
Retoma al guardia de un edificio de oficinas. Con una credencial física —una tarjeta, una llave— el guardia solo puede confirmar una cosa: que quien la presenta tiene el objeto físico. No sabe si esa tarjeta fue robada, prestada, o copiada. Ahora imagina un sistema distinto: el visitante presenta un documento firmado por una autoridad en la que el edificio ya decidió confiar de antemano —digamos, la oficina central de la empresa que lo invitó—, con una firma que el guardia puede verificar en el momento, sin llamar a nadie, y con una fecha de vencimiento impresa en el propio documento. El guardia no necesita conocer a la persona. Necesita dos cosas, y solo dos: saber en qué autoridad confía (para verificar la firma), y tener una lista de qué le permite hacer a cada tipo de documento firmado por esa autoridad (a qué pisos puede entrar, con qué documento exacto).
Esas dos cosas —"en quién confío" y "qué le permito hacer"— son, literalmente, los dos recursos de AWS que vas a construir en las lecciones 4 y 5: el identity provider (en quién confía la cuenta) y la trust policy de un rol (qué le permite hacer a un token firmado por ese emisor específico).
El JWT, con sus tres claims exactos
Un JWT (JSON Web Token) tiene tres partes, separadas por puntos: header.payload.signature. Las dos primeras son JSON codificado en Base64URL —cualquiera puede decodificarlas y leerlas, no están cifradas, solo codificadas—; la tercera es la firma criptográfica que prueba que el payload no fue alterado desde que el emisor lo firmó. Vas a construir uno de verdad, con Python, en la lección 6 — por ahora, tres claims del payload son los que importan para todo lo que sigue en este módulo:
| Claim | Qué significa | Valor real, para un push a main de andes-cargo-infra |
|---|---|---|
iss (issuer) | Quién firmó este token | https://token.actions.githubusercontent.com |
aud (audience) | Para quién es este token | sts.amazonaws.com |
sub (subject) | Quién es, específicamente, el sujeto de este token | repo:andes-cargo/andes-cargo-infra:ref:refs/heads/main |
iss responde "¿de qué autoridad viene este documento?" — es lo que el identity provider de la lección 4 declara que AWS reconoce. aud responde "¿para quién fue emitido este documento?" — GitHub firma JWTs para muchas audiencias distintas (no solo AWS); sts.amazonaws.com es, específicamente, el valor que le dice a este token "eres válido para autenticarte contra AWS STS, no contra ningún otro servicio". sub responde "¿quién, específicamente, es el portador de este documento?" — y es, con precisión de repositorio y rama, la pieza que la trust policy de la lección 5 va a condicionar.
El flujo completo, con los recursos de AWS superpuestos
OIDC DE PUNTA A PUNTA — GITHUB ACTIONS → AWS STS → CREDENCIALES TEMPORALES
(con los recursos de AWS que este módulo construye, superpuestos)
┌──────────────────────────────┐
│ 1. Job de GitHub Actions │ permissions: id-token: write
│ pide un JWT │ (ya viste esto en cicd-and-gitops-on-aws-guide)
└──────────────┬────────────────┘
│
▼
┌──────────────────────────────┐
│ 2. GitHub firma el JWT │ iss: token.actions.githubusercontent.com
│ (identity provider) │ aud: sts.amazonaws.com
│ │ sub: repo:andes-cargo/andes-cargo-infra:
│ │ ref:refs/heads/main
└──────────────┬────────────────┘
│
▼ ═══════════════ LADO DE AWS ═══════════════
┌──────────────────────────────┐ ┌─────────────────────────────────────┐
│ 3. El job presenta el JWT │ │ aws_iam_openid_connect_provider │
│ a AWS STS, pide asumir │─▶│ (Lección 4 — "en quién confío") │
│ AndesCargoDeployRole │ │ url = token.actions.githubuser... │
└──────────────────────────────┘ └──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ aws_iam_role.deploy │
│ assume_role_policy (Lección 5 — │
│ "qué le permito hacer") │
│ │
│ Principal: Federated = <arn del │
│ provider de arriba> │
│ Condition: │
│ aud == sts.amazonaws.com │
│ sub LIKE repo:andes-cargo/ │
│ andes-cargo-infra:ref: │
│ refs/heads/main │
└──────────────┬──────────────────────┘
│ ¿coinciden aud y sub?
┌──────────────────────┴──────────────────────┐
│ SÍ │ NO
▼ ▼
┌────────────────────────┐ ┌────────────────────────┐
│ 4. STS devuelve │ │ AccessDenied │
│ credenciales │ │ (la trust policy no │
│ TEMPORALES │ │ coincide — nunca │
│ (minutos, no siempre) │ │ llega a existir │
└────────────────────────┘ │ ninguna credencial) │
└────────────────────────┘
Fíjate en algo que la lección 4 de cicd-and-gitops-on-aws-guide ya mencionó pero que vale la pena remarcar aquí, ahora que ves los dos recursos de AWS superpuestos: son dos objetos distintos, con dos responsabilidades distintas, que trabajan juntos. El identity provider (arriba) no sabe nada de repositorios ni de ramas — solo sabe "confío en tokens firmados por este emisor, con este certificado". La trust policy del rol (abajo) no sabe nada de criptografía — solo sabe "de los tokens que ya pasaron la verificación de firma, acepto únicamente los que tengan este sub exacto". Uno sin el otro no sirve de nada: un identity provider sin ningún rol que lo referencie no le da acceso a nadie a nada; un rol con una trust policy perfecta pero sin ningún identity provider configurado no tiene ninguna fuente de confianza real que validar.
Las dos piezas que vas a construir, nombradas con precisión
aws_iam_openid_connect_provider (lección 4) — el registro, dentro de IAM, de que esta cuenta de AWS confía en tokens firmados por token.actions.githubusercontent.com. Se declara una sola vez por cuenta, sin importar cuántos roles distintos terminen usándolo — exactamente la misma relación de un identity provider a muchos roles que ya viste con modules/iam-role/ en terraform-and-iac-guide, ahora aplicada a un recurso distinto.
aws_iam_role con assume_role_policy condicionado (lección 5) — el rol específico que un pipeline de GitHub Actions puede asumir, con una trust policy que no dice "confío en cualquier token de GitHub" sino, con precisión de repositorio y rama, "confío únicamente en tokens cuyo sub sea exactamente repo:andes-cargo/andes-cargo-infra:ref:refs/heads/main". Esta precisión —no cualquier repositorio, no cualquier rama— es, verificado contra la documentación oficial de AWS y ya nombrado en cicd-and-gitops-on-aws-guide M4.5, el error de configuración de OIDC más común en implementaciones reales: una trust policy demasiado amplia (por ejemplo, sin la parte de la rama) permitiría que cualquier rama de ese repositorio —incluida una rama de feature abierta por cualquier colaborador— asumiera un rol pensado únicamente para despliegues desde main.
Por qué esto es, matemáticamente, menos superficie de ataque que una clave estática
Vale la pena cuantificar la diferencia, no solo describirla. Una clave de acceso de AWS estática, una vez creada, es válida indefinidamente, hasta que alguien la revoque activamente — su ventana de exposición, si se filtra, es "desde que se filtró hasta que alguien lo note y actúe", que puede ser minutos o puede ser años. Una credencial temporal obtenida vía OIDC tiene, por diseño, una ventana de exposición acotada desde su creación: expira en minutos u horas, sin que nadie tenga que hacer nada. Si un atacante lograra interceptar una credencial temporal en tránsito —un escenario ya mucho más difícil que encontrar una clave en un commit de hace meses—, esa credencial deja de servir para nada apenas expira, sin ninguna acción de revocación humana de por medio.
Errores comunes
Confundir aud con sub, y escribir la condición equivocada en la trust policy (adelanto del error que la lección 5 corrige en detalle). Qué pasa: alguien, apurado, pone el patrón de repositorio (repo:andes-cargo/...) en la condición de aud, y sts.amazonaws.com en la condición de sub. Cómo detectarlo: si tu trust policy tiene token.actions.githubusercontent.com:aud comparado contra un patrón de repositorio, o token.actions.githubusercontent.com:sub comparado contra sts.amazonaws.com. Cómo corregirlo: aud siempre compara contra la audiencia fija sts.amazonaws.com (el servicio que va a consumir el token) — casi nunca cambia entre proyectos. sub siempre compara contra el patrón específico de repositorio/rama — es la pieza que sí cambia por cada rol que declares. Confundirlos no produce un error de sintaxis (ambos son strings válidos), produce una trust policy que nunca coincide con ningún token real, con un AccessDenied sin ninguna pista obvia de por qué.
Pensar que el identity provider, por sí solo, ya le da acceso a algo a GitHub Actions (de secuencia). Qué pasa: alguien declara aws_iam_openid_connect_provider (lección 4) y espera que eso, solo, habilite que un workflow asuma algún rol. Cómo detectarlo: si tu mental model salta directo de "declaré el identity provider" a "ya funciona la federación", sin ningún rol de por medio. Cómo corregirlo: el identity provider, solo, no le da permiso a nadie de hacer nada — únicamente le dice a AWS "estos tokens, si alguien los presenta, vienen de una fuente que reconozco". Hace falta, además, al menos un rol cuya trust policy referencie ese identity provider como Principal.Federated — exactamente lo que construye la lección 5.
Asumir que "cualquiera puede leer un JWT" significa "cualquiera puede falsificar uno" (conceptual, retomado de cicd-and-gitops-on-aws-guide). Qué pasa: alguien, al enterarse de que el payload de un JWT es solo Base64 sin cifrar, concluye que cualquiera podría escribir un JWT con el sub que quisiera y hacerse pasar por el repositorio real. Cómo detectarlo: si tu preocupación de seguridad sobre OIDC es "¿pero alguien no podría inventar un token con el sub correcto?". Cómo corregirlo: leer el payload (que sí es público, por diseño — cualquier sistema necesita poder inspeccionar las claims sin descifrar nada) es completamente distinto de firmar un payload de forma que la tercera parte del JWT (la signature) sea válida — eso requiere la clave privada del emisor, que en un JWT real de GitHub Actions solo existe dentro de la infraestructura de github.com. La lección 6 de este módulo construye un JWT de prueba firmado con una clave simétrica propia, precisamente para demostrar, en vivo, la diferencia entre "un token con las claims correctas" y "un token que una cuenta AWS real aceptaría como legítimo".
Ejercicios
Ejercicio 1 — Traza el flujo completo señalando qué recurso de AWS corresponde a cada paso. Sin mirar el diagrama de esta lección, dibuja los cuatro pasos del flujo (pedir JWT, firmarlo, presentarlo a STS, recibir credenciales) y anota, en los pasos 3 y 4, qué recurso de AWS (aws_iam_openid_connect_provider o aws_iam_role) es responsable de cada verificación.
Ver solución
Pasos 1 y 2 ocurren enteramente del lado de GitHub, sin ningún recurso de AWS involucrado todavía. El paso 3 (el job presenta el JWT a STS) es donde AWS empieza a actuar: primero, el aws_iam_openid_connect_provider verifica que el emisor (iss) y la firma sean confiables; después, la assume_role_policy del aws_iam_role específico evalúa las condiciones sobre aud y sub. El paso 4 (credenciales temporales) solo ocurre si ambas verificaciones —identity provider y trust policy— pasan; si cualquiera de las dos falla, el flujo termina en AccessDenied sin llegar nunca a emitir ninguna credencial.
Ejercicio 2 — Explica, con el vocabulario de esta lección, qué le falta a una trust policy que solo verifica aud. Un colega escribe una trust policy con la condición token.actions.githubusercontent.com:aud == sts.amazonaws.com, pero sin ninguna condición sobre sub. ¿Qué problema real tiene esa configuración?
Ver solución
Verificar solo aud confirma que el token fue emitido para AWS STS — pero aud es el mismo valor (sts.amazonaws.com) para cualquier repositorio de GitHub que use este mismo patrón de OIDC hacia AWS, no solo para andes-cargo/andes-cargo-infra. Sin una condición sobre sub, esa trust policy aceptaría un JWT válido emitido para cualquier repositorio de GitHub Actions configurado con OIDC hacia AWS, siempre que ese repositorio también apunte a este mismo identity provider — un radio de confianza mucho más amplio del que Andes Cargo necesita. La condición sobre sub, acotada a repo:andes-cargo/andes-cargo-infra:ref:refs/heads/main, es la que reduce ese radio a exactamente un repositorio y una rama.
Ejercicio 3 — Predice el resultado de intentar asumir el rol desde una Pull Request, no desde main. Con la trust policy de esta lección (sub == repo:andes-cargo/andes-cargo-infra:ref:refs/heads/main), ¿qué pasaría si un workflow disparado por una Pull Request —no por un push a main— intentara asumir AndesCargoDeployRole?
Ver solución
Fallaría con AccessDenied, y es el comportamiento correcto, no un bug. El sub de un JWT emitido para un evento de Pull Request tiene un formato distinto —típicamente repo:andes-cargo/andes-cargo-infra:pull_request, sin la parte ref:refs/heads/main—, así que no coincide con el patrón exacto que la trust policy exige. Es, precisamente, el comportamiento que hace que esta trust policy sea de mínimo privilegio: un rol pensado para aplicar cambios en producción (apply.yml, disparado por push a main) no debería poder asumirse desde una rama de feature ni desde una Pull Request todavía sin revisar — la misma disciplina de plan-en-PR/apply-en-merge que ya conoces de cicd-and-gitops-on-aws-guide.
Resumen y siguiente paso
En esta lección repasaste el mecanismo completo de OIDC con precisión quirúrgica sobre lo que vas a construir: los tres claims exactos del JWT (iss, aud, sub) y qué pregunta responde cada uno; el flujo completo de cuatro pasos, con los dos recursos de AWS —identity provider y trust policy— superpuestos en el punto exacto donde actúan; y por qué esos dos recursos son responsabilidades separadas que trabajan juntas, ninguna suficiente por sí sola.
Antes de avanzar deberías poder: explicar qué pregunta responde cada uno de los tres claims (iss/aud/sub); dibujar el flujo completo señalando en qué punto actúa cada recurso de AWS; y explicar por qué el payload de un JWT siendo público no significa que sea falsificable.
La lección 4 declara el primer recurso real de este módulo: aws_iam_openid_connect_provider, dentro de modules/oidc-provider/, con terraform fmt/init/validate/plan corridos de verdad.
Recursos
- AWS Docs — Creating OpenID Connect (OIDC) identity providers — documentación oficial de AWS sobre el identity provider que la lección 4 declara.
- AWS Docs — Creating a role for web identity or OpenID Connect Federation — documentación oficial de AWS sobre la trust policy que la lección 5 declara, incluida la sintaxis completa de
Condition. - GitHub Docs — About security hardening with OpenID Connect — la explicación oficial de GitHub sobre las claims del JWT, incluida la lista completa disponible más allá de
iss/aud/sub. cicd-and-gitops-on-aws-guide, Módulo 4, lecciones 4 y 5 — el mecanismo conceptual completo y el YAML representativo que esta lección asume como base.