Módulo 2: Federated Identity And Least Privilege Iam
5. Manos a la obra: una trust policy de mínimo privilegio
Descripción
La lección 4 dejó modules/oidc-provider/ con el "en quién confío" declarado. Esta lección agrega el "qué le permito hacer": un aws_iam_role cuya assume_role_policy no acepta cualquier token firmado por GitHub, sino únicamente uno cuyo sub sea, letra por letra, repo:andes-cargo/andes-cargo-infra:ref:refs/heads/main. terraform fmt, validate y plan vuelven a correr de verdad, y esta vez vas a ver algo nuevo en la salida: un data que Terraform no puede resolver todavía, por una razón real que vale la pena entender.
Conexión con el módulo
Con esta lección, modules/oidc-provider/ queda completo — las dos piezas del diagrama de la lección 3, el identity provider y la trust policy, viviendo en el mismo módulo. La lección 6 usa exactamente este rol, AndesCargoDeployRole, como destino de la llamada sts:AssumeRoleWithWebIdentity del experimento central de este módulo.
Paso 1 — Extendiendo modules/oidc-provider/variables.tf
Agrega estas dos variables al archivo que ya existe (no borres nada de la lección 4):
variable "role_name" {
description = "Name of the IAM role that GitHub Actions assumes via OIDC."
type = string
}
variable "github_repo_ref" {
description = "repo:owner/name:ref:refs/heads/branch pattern allowed to assume this role."
type = string
}
A diferencia de thumbprint_list y tags de la lección 4, estas dos no tienen default — el mismo criterio de diseño que ya viste en modules/iam-role/: un rol sin nombre, o una trust policy sin ningún patrón de repositorio, no tiene ningún sentido operativo. Obligar a que quien llame al módulo piense estos dos valores es parte de lo que hace que este módulo sea seguro de reutilizar para un segundo repositorio, el día que Andes Cargo lo necesite.
Paso 2 — Extendiendo modules/oidc-provider/main.tf
Agrega esto al final del archivo, después del recurso de la lección 4:
data "aws_iam_policy_document" "trust" {
statement {
sid = "GitHubActionsOIDC"
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.github_actions.arn]
}
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:aud"
values = ["sts.amazonaws.com"]
}
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:sub"
values = [var.github_repo_ref]
}
}
}
resource "aws_iam_role" "deploy" {
name = var.role_name
assume_role_policy = data.aws_iam_policy_document.trust.json
tags = var.tags
}
Léelo pieza por pieza, contra el vocabulario exacto de la lección 3:
actions = ["sts:AssumeRoleWithWebIdentity"]— nosts:AssumeRolea secas. Es la variante específica de STS para federación con un token externo, distinta de la que usanLambdaManifestProcessorRole/AppServerRole(que confían en unServicede AWS, no en un token de un tercero).principals { type = "Federated", identifiers = [aws_iam_openid_connect_provider.github_actions.arn] }— la referencia directa al identity provider de la lección 4. Esta línea es, literalmente, el HCL de "confío en tokens que ya pasaron por ese identity provider" — sin ella, no habría ninguna conexión entre los dos recursos del módulo.- Dos bloques
condition, uno por cada claim que importa:StringEqualssobreaud(comparación exacta, porquests.amazonaws.comnunca varía) yStringLikesobresub(permite un patrón, aunque en este caso el patrón sea, de hecho, un valor exacto sin comodines — se usaStringLikeen vez deStringEqualspor convención de la documentación oficial de AWS para esta condición específica, dejando espacio para un comodín futuro sin cambiar el tipo de condición). resource "aws_iam_role" "deploy"— nota el nombre localdeploy, distinto dethisque usamodules/iam-role/. Es una decisión deliberada: este módulo, a diferencia demodules/iam-role/, no es un molde genérico reutilizable para cualquier rol — es un módulo específico para el rol de despliegue federado, así que su nombre interno puede ser descriptivo sin perder generalidad real.
Paso 3 — Extendiendo modules/oidc-provider/outputs.tf
output "role_name" {
description = "Name of the created deploy role."
value = aws_iam_role.deploy.name
}
output "role_arn" {
description = "ARN of the created deploy role."
value = aws_iam_role.deploy.arn
}
Paso 4 — Actualizando la llamada al módulo en oidc.tf
module "github_oidc" {
source = "./modules/oidc-provider"
role_name = "AndesCargoDeployRole"
github_repo_ref = "repo:andes-cargo/andes-cargo-infra:ref:refs/heads/main"
tags = local.common_tags
}
Dos inputs nuevos, con los valores exactos de Andes Cargo: el nombre del rol que un pipeline real usaría, y el patrón de repositorio/rama acotado a main — no repo:andes-cargo/andes-cargo-infra:*, el error de configuración que cicd-and-gitops-on-aws-guide M4.5 ya nombró como el más común en implementaciones reales.
Paso 5 — fmt, validate: sin sorpresas
terraform fmt -recursive
terraform validate
Qué esperar (literal, ejecutado para escribir esta lección):
Success! The configuration is valid.
Paso 6 — El plan, y algo nuevo que vale la pena entender
terraform plan
Qué esperar (literal, ejecutado para escribir esta lección):
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
<= read (data resources)
Terraform will perform the following actions:
# module.github_oidc.data.aws_iam_policy_document.trust will be read during apply
# (config refers to values not yet known)
<= data "aws_iam_policy_document" "trust" {
+ id = (known after apply)
+ json = (known after apply)
+ minified_json = (known after apply)
+ statement {
+ actions = [
+ "sts:AssumeRoleWithWebIdentity",
]
+ effect = "Allow"
+ sid = "GitHubActionsOIDC"
+ condition {
+ test = "StringEquals"
+ values = [
+ "sts.amazonaws.com",
]
+ variable = "token.actions.githubusercontent.com:aud"
}
+ condition {
+ test = "StringLike"
+ values = [
+ "repo:andes-cargo/andes-cargo-infra:ref:refs/heads/main",
]
+ variable = "token.actions.githubusercontent.com:sub"
}
+ principals {
+ identifiers = [
+ (known after apply),
]
+ type = "Federated"
}
}
}
# module.github_oidc.aws_iam_openid_connect_provider.github_actions will be created
+ resource "aws_iam_openid_connect_provider" "github_actions" {
+ arn = (known after apply)
+ client_id_list = [
+ "sts.amazonaws.com",
]
+ id = (known after apply)
+ tags = {
+ "Environment" = "dev"
+ "ManagedBy" = "terraform"
+ "Project" = "andes-cargo"
}
+ tags_all = {
+ "Environment" = "dev"
+ "ManagedBy" = "terraform"
+ "Project" = "andes-cargo"
}
+ thumbprint_list = [
+ "6938fd4d98bab03faadb97b34396831e3780aea1",
]
+ url = "https://token.actions.githubusercontent.com"
}
# module.github_oidc.aws_iam_role.deploy will be created
+ resource "aws_iam_role" "deploy" {
+ arn = (known after apply)
+ assume_role_policy = (known after apply)
+ create_date = (known after apply)
+ force_detach_policies = false
+ id = (known after apply)
+ managed_policy_arns = (known after apply)
+ max_session_duration = 3600
+ name = "AndesCargoDeployRole"
+ name_prefix = (known after apply)
+ path = "/"
+ tags = {
+ "Environment" = "dev"
+ "ManagedBy" = "terraform"
+ "Project" = "andes-cargo"
}
+ tags_all = {
+ "Environment" = "dev"
+ "ManagedBy" = "terraform"
+ "Project" = "andes-cargo"
}
+ unique_id = (known after apply)
+ inline_policy (known after apply)
}
Plan: 2 to add, 0 to change, 0 to destroy.
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
El detalle nuevo que vale la pena detenerse a entender: data.aws_iam_policy_document.trust aparece marcado <= ("will be read during apply"), no resuelto directamente en el plan, con la nota explícita # (config refers to values not yet known). Compara esto con data.aws_iam_policy_document.lambda_trust de terraform-and-iac-guide, que sí se resolvía completo en el plan (Read complete after 0s) — la diferencia es que aquel data solo dependía de un literal ("lambda.amazonaws.com"), mientras que este depende de aws_iam_openid_connect_provider.github_actions.arn, un valor que todavía no existe porque el recurso que lo produce se va a crear en el mismo apply. Terraform no puede leer el data hasta que ese ARN exista de verdad — así que difiere esa lectura hasta el momento del apply, y por eso assume_role_policy en aws_iam_role.deploy también aparece (known after apply), no como el JSON completo que sí viste en la lección 4 del Módulo 6 de terraform-and-iac-guide. No es un error ni una advertencia — es Terraform siendo honesto sobre una dependencia real entre dos recursos que se crean en el mismo apply.
Plan: 2 to add — el identity provider y el rol; el data nunca cuenta en ese número, la misma regla que ya confirmaste con Plan: 2 to add en terraform-and-iac-guide, Módulo 6, lección 4.
Paso 7 — Aplicando y verificando (representativo)
Qué esperar (representativo), mismo motivo que toda esta guía — sin LOCALSTACK_AUTH_TOKEN, el contenedor no arranca en este entorno:
tflocal apply -auto-approve
module.github_oidc.aws_iam_openid_connect_provider.github_actions: Creating...
module.github_oidc.aws_iam_openid_connect_provider.github_actions: Creation complete after 1s [id=arn:aws:iam::000000000000:oidc-provider/token.actions.githubusercontent.com]
module.github_oidc.data.aws_iam_policy_document.trust: Reading...
module.github_oidc.data.aws_iam_policy_document.trust: Read complete after 0s [id=3924781605]
module.github_oidc.aws_iam_role.deploy: Creating...
module.github_oidc.aws_iam_role.deploy: Creation complete after 1s [id=AndesCargoDeployRole]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Fíjate en el orden: el identity provider se crea primero, después Terraform recién puede leer el data.aws_iam_policy_document.trust (ahora que el ARN existe de verdad), y solo entonces crea el rol — exactamente la secuencia que la dependencia implícita del Paso 6 predijo.
awslocal iam get-role --role-name AndesCargoDeployRole
Qué esperar (representativo — RoleId y CreateDate son variables; el resto es fijo para este HCL exacto):
{
"Role": {
"Path": "/",
"RoleName": "AndesCargoDeployRole",
"RoleId": "AROAQZ3EXAMPLEDEPLOYROL",
"Arn": "arn:aws:iam::000000000000:role/AndesCargoDeployRole",
"CreateDate": "2026-08-13T10:14:22+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "GitHubActionsOIDC",
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::000000000000:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:andes-cargo/andes-cargo-infra:ref:refs/heads/main"
}
}
}
]
},
"MaxSessionDuration": 3600
}
}
Ahí está, completo: el Principal.Federated apuntando al identity provider de la lección 4 por su ARN real (no un literal — Terraform lo resolvió en el apply), y las dos condiciones exactas que escribiste en HCL, ahora como JSON real dentro de la cuenta. Cualquiera que revise este rol en la consola de IAM, o con este mismo comando, puede confirmar sin ambigüedad qué repositorio y qué rama tienen permitido asumirlo — la misma auditabilidad que la lección 3 prometió como ventaja central de OIDC frente a una clave compartida.
Errores comunes
Escribir repo:andes-cargo/andes-cargo-infra:* en vez del patrón acotado a main (el error ya nombrado en cicd-and-gitops-on-aws-guide M4.5, ahora con consecuencia real en HCL). Qué pasa: alguien, queriendo "ser flexible" para que cualquier rama pueda desplegar, quita la parte ref:refs/heads/main del patrón. Cómo detectarlo: si tu github_repo_ref termina en :* inmediatamente después del nombre del repositorio, sin ninguna referencia a una rama específica. Cómo corregirlo: ese patrón amplio permitiría que cualquier rama de ese repositorio —incluida una rama de feature abierta por cualquier colaborador, o una Pull Request— asumiera un rol pensado para despliegues de producción. La lección 3 de este módulo ya predijo, en su Ejercicio 3, que un intento desde una PR debería fallar con este rol — un patrón :* rompe esa garantía.
Usar StringEquals en vez de StringLike para la condición sobre sub, y sorprenderse de que "funciona igual" (de confusión conceptual). Qué pasa: alguien cambia StringLike por StringEquals en la condición de sub, y el plan/apply funcionan sin ningún error. Cómo detectarlo: en este caso específico, no hay ningún síntoma visible — un valor sin comodines se comporta igual con ambos operadores. Cómo corregirlo: no es un error funcional hoy, pero sí una decisión que te ata las manos más adelante: si algún día necesitas que cualquier rama de un repositorio pueda asumir un rol distinto (por ejemplo, uno de solo lectura para ramas de feature), StringLike te permite usar repo:andes-cargo/andes-cargo-infra:ref:refs/heads/* sin cambiar el tipo de condición; con StringEquals tendrías que reescribir esa condición desde cero. La documentación oficial de AWS usa StringLike para sub precisamente por esta flexibilidad futura.
Olvidar que aws_iam_openid_connect_provider.github_actions.arn solo existe dentro del mismo módulo (de alcance de nombres). Qué pasa: alguien, escribiendo la referencia del Paso 2, intenta usar module.github_oidc.aws_iam_openid_connect_provider.github_actions.arn (con el prefijo module.github_oidc.) desde dentro del propio main.tf del módulo. Cómo detectarlo: el error Error: Reference to undeclared resource — dentro de un módulo, sus propios recursos se referencian por su nombre local, sin ningún prefijo de módulo; el prefijo module.github_oidc. solo aplica fuera del módulo, para acceder a sus outputs. Cómo corregirlo: dentro de modules/oidc-provider/main.tf, la referencia correcta es exactamente la que usa esta lección: aws_iam_openid_connect_provider.github_actions.arn, sin prefijo.
Ejercicios
Ejercicio 1 — Explica, con tus propias palabras, por qué data.aws_iam_policy_document.trust aparece <= en este plan pero no en los de terraform-and-iac-guide. Sin volver a mirar esta lección, explica la diferencia exacta entre este data y data.aws_iam_policy_document.lambda_trust de terraform-and-iac-guide, que sí se resolvía completo en el plan.
Ver solución
data.aws_iam_policy_document.lambda_trust, en terraform-and-iac-guide, solo depende de literales ("lambda.amazonaws.com" como principal) — nada de lo que ese data necesita depende de ningún recurso que se vaya a crear en el mismo apply, así que Terraform puede resolverlo completo en el momento del plan, sin ninguna llamada real a AWS. El data.aws_iam_policy_document.trust de esta lección, en cambio, referencia aws_iam_openid_connect_provider.github_actions.arn dentro de su bloque principals — un valor que solo existe después de que ese recurso se cree de verdad. Terraform no puede adivinar ese ARN, así que difiere la lectura completa del data hasta el momento del apply, cuando el ARN ya es real.
Ejercicio 2 — Predice el orden exacto de creación en el apply, y explica por qué ese orden no es opcional. Basándote en las referencias dentro del HCL de esta lección, ¿en qué orden tiene que crear Terraform el identity provider, leer el data, y crear el rol? ¿Podría Terraform, en teoría, crear el rol primero?
Ver solución
El orden tiene que ser: (1) crear aws_iam_openid_connect_provider.github_actions, (2) leer data.aws_iam_policy_document.trust (que ahora sí puede resolver el ARN real), (3) crear aws_iam_role.deploy con assume_role_policy = data.aws_iam_policy_document.trust.json. No, Terraform no podría crear el rol primero — su assume_role_policy depende directamente del resultado del data, que a su vez depende del ARN del identity provider. Es una cadena de dependencias implícitas de tres eslabones, calculada automáticamente por el grafo de dependencias de Terraform, la misma mecánica que ya viste con terraform graph en terraform-and-iac-guide, Módulo 8, lección 2 — nadie escribió depends_on en ningún lugar de este módulo; las referencias directas ya le dicen a Terraform todo lo que necesita.
Ejercicio 3 — Reescribe la condición de sub para permitir despliegues desde main o desde una rama release/*. Usando StringLike (no StringEquals), escribe el valor que tendría values en la condición sobre sub si Andes Cargo quisiera permitir que este mismo rol se asuma tanto desde main como desde cualquier rama que empiece con release/.
Ver solución
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:sub"
values = [
"repo:andes-cargo/andes-cargo-infra:ref:refs/heads/main",
"repo:andes-cargo/andes-cargo-infra:ref:refs/heads/release/*",
]
}
values acepta una lista, no solo un string — StringLike evalúa cada patrón de la lista de forma independiente, y la condición completa pasa si el sub del token coincide con al menos uno de ellos (comportamiento OR dentro del mismo operador de condición). El comodín * en release/* es exactamente el mecanismo que justificó, en el Errores comunes de esta lección, preferir StringLike sobre StringEquals desde el principio.
Resumen y siguiente paso
En esta lección completaste modules/oidc-provider/: el rol AndesCargoDeployRole, con una trust policy condicionada, con precisión de repositorio y rama, sobre los claims aud y sub del JWT. Viste, en un plan real, por qué un data que depende de un recurso creado en el mismo apply se difiere hasta ese momento (<=, "will be read during apply") — un comportamiento honesto de Terraform, no un error. Confirmaste (representativo) que el apply crea ambos recursos en el orden correcto, y que get-role devuelve exactamente la trust policy que escribiste, ahora auditable por cualquiera con acceso a IAM.
Antes de avanzar deberías poder: escribir de memoria el data "aws_iam_policy_document" "trust" completo, con sus dos condiciones; explicar por qué este data específico no se resuelve en el plan; y modificar el patrón de sub para permitir una rama adicional, usando StringLike.
La lección 6 usa este mismo rol —AndesCargoDeployRole, con la trust policy que acabas de construir— como destino de un experimento real: construir un JWT de prueba con Python y PyJWT, y observar en vivo qué de todo esto LocalStack Hobby sí verifica, y qué no.
Recursos
- AWS Docs — Creating a role for web identity or OpenID Connect Federation — documentación oficial completa de la trust policy declarada en esta lección, incluida la sintaxis de
Condition. - GitHub Docs — Configuring OpenID Connect in Amazon Web Services — la misma fuente que
cicd-and-gitops-on-aws-guideM4.5 ya citó para el YAML; esta lección construye, del lado de AWS, exactamente la trust policy que esa página documenta. - Terraform Registry —
aws_iam_role— referencia completa del recurso, ya usado desdeterraform-and-iac-guide. - Terraform Registry —
data.aws_iam_policy_document— referencia completa deldata source, incluidosprincipalsycondition. - AWS CLI —
iam get-role— referencia completa del comando de verificación del Paso 7. cicd-and-gitops-on-aws-guide, Módulo 4, lección 5 — la fuente original del patrónrepo:andes-cargo/andes-cargo-infra:ref:refs/heads/mainy el error de configuración (:*sin rama) que esta lección evita.