Al cerrar el módulo 3 dejamos una lista incómoda encima de la mesa. MercadoFresco está en internet, sirve desde el borde, escala solo y sobrevive a la caída de una zona de disponibilidad; pero el usuario mercadofresco-admin puede hacer literalmente cualquier cosa en la cuenta 111122223333, los roles rol-mercadofresco-tienda y rol-lambda-miniaturas se crearon sobre la marcha para que las cosas funcionaran, y nadie ha escrito todavía una sola política pensando en qué es lo mínimo que cada pieza necesita.

Eso no es un detalle pendiente. Es la capa de la que depende todo lo demás: si las identidades están mal, el cifrado no sirve de nada, porque quien puede llamar a la API puede leer los datos ya descifrados. Por eso IAM es la primera lección del módulo y la más larga.

AWS Identity and Access Management (IAM) es el servicio que responde, en cada una de los miles de millones de llamadas que recibe la API de AWS cada segundo, a dos preguntas: ¿quién eres? y ¿te dejo hacer esto?. En esta lección Marta convierte esas dos preguntas en políticas JSON concretas para MercadoFresco.

Advertencia. Los ejemplos de esta lección son didácticos y están simplificados para que se entiendan. Cualquier configuración de identidades, permisos o cumplimiento que vaya a aplicarse sobre datos reales de clientes —y más si entra en el ámbito del RGPD o de PCI DSS— debe ser revisada por un profesional de seguridad o de cumplimiento antes de llegar a producción. Todos los identificadores, cuentas y datos de este curso son ficticios.

Contenido

  1. Autenticación y autorización: dos preguntas distintas
  2. El vocabulario de IAM
  3. Anatomía de un ARN
  4. Usuarios, grupos y el problema de las claves de acceso
  5. Roles: identidades que nadie posee
  6. sts:AssumeRole y la relación de confianza
  7. Los seis tipos de política
  8. Anatomía de una política JSON
  9. Comodines, variables de política y condiciones
  10. La lógica de evaluación de políticas
  11. Formalizar rol-mercadofresco-tienda
  12. Formalizar rol-lambda-miniaturas
  13. Perfiles de instancia
  14. Mínimo privilegio en la práctica: leer los AccessDenied
  15. IAM Access Analyzer y la generación de políticas
  16. Las personas de MercadoFresco: usuarios y grupos
  17. MFA obligatorio por condición
  18. Rotación de claves e informe de credenciales
  19. IAM Identity Center y federación
  20. Herramientas de diagnóstico
  21. Coste y limpieza

Autenticación y autorización: dos preguntas distintas

Se confunden constantemente y son mecanismos separados:

Autenticación Autorización
Pregunta ¿Quién eres? ¿Puedes hacer esto?
Mecanismo Firma criptográfica de la petición (SigV4), contraseña + MFA en la consola Evaluación de políticas
Resultado Un principal identificado Allow o Deny
Dónde falla InvalidClientTokenId, SignatureDoesNotMatch AccessDenied

Cuando Luis ejecuta aws s3 ls con el perfil mercadofresco-dev, el CLI firma la petición con la clave secreta. AWS recalcula la firma; si coincide, sabe quién es Luis (autenticación). Solo entonces busca todas las políticas aplicables y decide si le deja listar buckets (autorización).

Distinguirlas ahorra horas de depuración: si el error es AccessDenied, las credenciales son correctas y el problema está en una política. Si el error es de firma, ni siquiera se ha llegado a mirar los permisos.

El vocabulario de IAM

Estos siete términos aparecen en toda la documentación de AWS y conviene fijarlos ahora.

Término Qué es Ejemplo en MercadoFresco
Principal Quien hace la petición Luis, rol-mercadofresco-tienda, el servicio s3.amazonaws.com
Identidad Objeto de IAM al que se pueden adjuntar políticas Usuario luis, grupo mercadofresco-desarrollo, rol rol-lambda-miniaturas
Entidad Identidad que AWS puede autenticar Usuario o rol (un grupo no es entidad: nadie inicia sesión como grupo)
Recurso El objeto sobre el que se actúa El bucket mercadofresco-catalogo-fotos, la instancia mercadofresco-tienda-01
Acción Operación de la API s3:GetObject, ec2:TerminateInstances, kms:Decrypt
Política Documento JSON que concede o niega La que escribimos dentro de tres apartados
Sesión Credenciales temporales resultado de asumir un rol La sesión que la instancia obtiene del servicio de metadatos

La distinción entre identidad y entidad parece pedantería y no lo es: explica por qué no puedes «iniciar sesión como el grupo de analítica» ni por qué un grupo no puede aparecer como Principal en una política de bucket. Los grupos son solo un contenedor de permisos para usuarios.

Anatomía de un ARN

El Amazon Resource Name es el identificador único y global de cualquier recurso de AWS. Aparece en todas las políticas, así que hay que saber leerlo carácter a carácter:

arn:partition:service:region:account-id:resource-type/resource-id
Campo Qué significa Valores típicos
arn Prefijo fijo Siempre arn
partition Partición de AWS aws (global), aws-cn (China), aws-us-gov (GovCloud)
service Espacio de nombres del servicio s3, ec2, iam, kms, lambda
region Región eu-west-1; vacío en servicios globales (IAM, S3, Route 53)
account-id Cuenta de 12 dígitos 111122223333; vacío en S3
resource Tipo e identificador Separados por /, : o nada, según el servicio

Ejemplos reales de MercadoFresco, con la peculiaridad de cada uno:

arn:aws:s3:::mercadofresco-catalogo-fotos
arn:aws:s3:::mercadofresco-catalogo-fotos/productos/tomate-rama.jpg
arn:aws:iam::111122223333:user/luis
arn:aws:iam::111122223333:role/rol-mercadofresco-tienda
arn:aws:iam::aws:policy/ReadOnlyAccess
arn:aws:ec2:eu-west-1:111122223333:instance/i-0abc123def4567890
arn:aws:rds:eu-west-1:111122223333:db:mercadofresco-pedidos
arn:aws:kms:eu-west-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab
arn:aws:kms:eu-west-1:111122223333:alias/mercadofresco-datos
arn:aws:lambda:eu-west-1:111122223333:function:mercadofresco-generar-miniaturas
arn:aws:sqs:eu-west-1:111122223333:mercadofresco-miniaturas-fallidas
arn:aws:elasticloadbalancing:eu-west-1:111122223333:loadbalancer/app/alb-mercadofresco-tienda/50dc6c495c0c9188

Cuatro observaciones que evitan errores frecuentes:

  • S3 no lleva región ni cuenta (arn:aws:s3:::, tres dos puntos seguidos). Los nombres de bucket son globalmente únicos, así que no hacen falta. Escribir arn:aws:s3:eu-west-1:111122223333:mercadofresco-catalogo-fotos es un error clásico y la política simplemente no coincidirá nunca.
  • El bucket y los objetos son recursos distintos. arn:aws:s3:::mercadofresco-catalogo-fotos sirve para s3:ListBucket; arn:aws:s3:::mercadofresco-catalogo-fotos/* sirve para s3:GetObject. Confundirlos produce el AccessDenied más común del mundo.
  • IAM es global: región vacía. Y las políticas gestionadas por AWS usan aws en el campo de cuenta: arn:aws:iam::aws:policy/ReadOnlyAccess.
  • RDS usa : como separador (:db:mercadofresco-pedidos), no /. Cada servicio elige, no hay regla general; hay que mirar la documentación o copiar el ARN desde la consola.

Usuarios, grupos y el problema de las claves de acceso

Un usuario de IAM es una identidad permanente con credenciales de larga duración: contraseña para la consola y, opcionalmente, un par de claves de acceso (AKIA... + clave secreta) para la API.

Y ahí está el problema. Una clave de acceso:

  • No caduca nunca por sí sola.
  • Es un secreto en texto plano que alguien tiene que guardar en algún sitio.
  • Si se filtra a un repositorio de Git, a un ticket de soporte o al portapapeles equivocado, quien la tenga es ese usuario, sin más comprobaciones.

Los bots que rastrean GitHub encuentran claves de AWS filtradas en cuestión de minutos y arrancan instancias para minar criptomonedas. La factura llega antes que el aviso.

De ahí la regla que gobierna el resto de la lección:

Las claves de acceso son el último recurso, no el primero. Una EC2, una Lambda, un contenedor o un pipeline nunca deben llevar claves de acceso. Para eso existen los roles.

Un grupo es una colección de usuarios que comparten permisos. No tiene credenciales, no puede asumirse y no puede anidarse dentro de otro grupo. Su única función es que cuando entre alguien nuevo al equipo de Luis no haya que recordar qué siete políticas hay que adjuntarle.

Roles: identidades que nadie posee

Un rol de IAM es una identidad con políticas de permisos pero sin credenciales permanentes. Nadie «es» un rol: las entidades lo asumen temporalmente y reciben credenciales que caducan.

flowchart LR
    A["Instancia<br/>mercadofresco-tienda-01"] -->|"1 pide credenciales<br/>al servicio de metadatos"| B["IMDSv2<br/>169.254.169.254"]
    B -->|"2 sts:AssumeRole"| C["rol-mercadofresco-tienda"]
    C -->|"3 credenciales temporales<br/>caducan en ~6 h"| A
    A -->|"4 llamada firmada"| D["S3<br/>mercadofresco-catalogo-fotos"]

Lo importante del diagrama es el paso 3: las credenciales caducan y se renuevan solas antes de caducar. Si alguien las roba, tiene una ventana de minutos u horas, no de años. Y no hay nada que rotar, ni nada que guardar, ni nada que se pueda filtrar a Git porque no existe en ningún fichero.

Los roles se usan en cuatro escenarios:

Escenario Quién asume Ejemplo
Rol de servicio Un servicio de AWS EC2, Lambda, ECS
Rol entre cuentas Un usuario o rol de otra cuenta La cuenta de producción y la de desarrollo (módulo 9)
Rol federado Un usuario autenticado por un proveedor externo Google Workspace, Entra ID, IAM Identity Center
Cambio de sombrero Un usuario de la misma cuenta Marta asume rol-mercadofresco-emergencia solo cuando lo necesita

sts:AssumeRole y la relación de confianza

Todo rol tiene dos políticas, y confundirlas es el error conceptual más frecuente de IAM:

Política Nombre técnico Responde a Cuántas
De confianza Trust policy / AssumeRolePolicyDocument ¿Quién puede asumir este rol? Exactamente 1
De permisos Políticas adjuntas ¿Qué puede hacer quien lo asuma? Hasta 10 gestionadas + inline

La política de confianza es la única política basada en recurso donde el recurso es el propio rol. Esta es la de rol-mercadofresco-tienda, que solo puede asumir el servicio EC2:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PermitirQueEC2AsumaElRol",
      "Effect": "Allow",
      "Principal": { "Service": "ec2.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}

Línea a línea:

  • Principal.Service: el que asume no es una persona sino el servicio EC2. Cuando lances una instancia con este rol, EC2 llamará a sts:AssumeRole en tu nombre.
  • Action: sts:AssumeRole: la acción especial que emite credenciales temporales. Existen variantes: sts:AssumeRoleWithWebIdentity (Cognito, OIDC) y sts:AssumeRoleWithSAML (federación empresarial).
  • No hay Resource: el recurso es implícitamente el rol al que pertenece la política.

Para roles entre cuentas hay una condición imprescindible, el ExternalId, que evita el ataque del confused deputy (un tercero que conoce el ARN de tu rol y consigue que alguien lo asuma en su nombre):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::444455556666:root" },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": { "sts:ExternalId": "mercadofresco-proveedor-logistica-2026" },
        "Bool": { "aws:MultiFactorAuthPresent": "true" }
      }
    }
  ]
}

"AWS": "arn:aws:iam::444455556666:root" no significa el usuario root de esa cuenta: significa «la cuenta 444455556666 puede delegar en quien quiera». Es la forma habitual de escribirlo, y la cuenta de destino debe además conceder sts:AssumeRole a sus propios usuarios. Los dos lados tienen que estar de acuerdo: sin eso no hay acceso entre cuentas.

Los seis tipos de política

Tipo Se adjunta a Efecto Uso en MercadoFresco
Basada en identidad Usuario, grupo, rol Concede permisos al principal La mayoría de las que escribiremos
Basada en recurso Bucket, clave KMS, cola, secreto, rol Concede acceso al recurso, indicando Principal Política de bucket, política de clave KMS
Límite de permisos Usuario o rol Techo máximo: no concede nada, solo limita El techo de los roles que cree Luis
SCP Cuenta u OU de Organizations Techo de toda la cuenta Se ve en detalle en 09-04
Política de sesión Se pasa al asumir un rol Reduce los permisos de esa sesión concreta Sesiones acotadas del pipeline (módulo 8)
ACL Bucket S3 (heredado) Mecanismo antiguo, anterior a IAM No usar; desactivadas por defecto desde 2023

Las dos primeras son las que se usan a diario, y hay una diferencia decisiva entre ellas:

  • Una política de identidad dice: «Luis puede leer ese bucket». Vive con Luis.
  • Una política de recurso dice: «ese bucket deja que lo lea Luis». Vive con el bucket, y además es la única forma de conceder acceso desde otra cuenta sin roles.

Y una consecuencia práctica: cuando el principal y el recurso están en la misma cuenta, basta con que una de las dos conceda el permiso. Cuando están en cuentas distintas, hacen falta las dos.

Las políticas gestionadas por AWS (AmazonS3ReadOnlyAccess, AdministratorAccess...) son cómodas para empezar y casi siempre demasiado amplias. AmazonS3ReadOnlyAccess concede lectura sobre todos los buckets de la cuenta, incluido mercadofresco-copias-basedatos. Sirven para prototipar; no para producción.

Anatomía de una política JSON

Todas las políticas tienen la misma estructura. Esta es la plantilla completa, con todos los elementos posibles:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "IdentificadorLegibleOpcional",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111122223333:role/rol-mercadofresco-tienda" },
      "Action": ["s3:GetObject", "s3:PutObject"],
      "NotAction": [],
      "Resource": "arn:aws:s3:::mercadofresco-catalogo-fotos/productos/*",
      "NotResource": [],
      "Condition": {
        "StringEquals": { "aws:PrincipalTag/Proyecto": "mercadofresco" }
      }
    }
  ]
}
Elemento Obligatorio Qué hace
Version Siempre 2012-10-17. No es la fecha de tu política: es la versión del lenguaje. Sin ella no funcionan las variables de política
Statement Lista de declaraciones. Se evalúan todas, no se para en la primera
Sid No Identificador legible. Útil para depurar y obligatorio único en políticas de recurso
Effect Allow o Deny
Principal Solo en políticas de recurso Quién. En políticas de identidad está prohibido (el principal es el dueño)
Action servicio:Operación. Admite comodines
Resource Sí (salvo en confianza) Sobre qué ARN
Condition No Cuándo se aplica

NotAction y NotResource significan «todo excepto». Son peligrosos: "NotAction": "s3:*" con "Effect": "Allow" concede todo lo demás en AWS. Úsalos casi exclusivamente con Deny.

Comodines, variables de política y condiciones

Comodines. * sustituye cualquier secuencia de caracteres, ? un solo carácter:

Patrón Coincide con
s3:Get* s3:GetObject, s3:GetBucketPolicy, s3:GetObjectAcl...
s3:* Todas las acciones de S3
* Todas las acciones de AWS (esto es AdministratorAccess)
arn:aws:s3:::mercadofresco-* Todos los buckets de MercadoFresco
arn:aws:s3:::mercadofresco-catalogo-fotos/miniaturas/* Solo los objetos bajo ese prefijo

Cuidado con s3:Get*: incluye s3:GetBucketPolicy, que permite leer la política del bucket, y s3:GetObjectVersion, que permite leer versiones antiguas incluso de objetos «borrados».

Variables de política. Se sustituyen en el momento de la evaluación:

Variable Valor
${aws:username} Nombre del usuario de IAM
${aws:userid} Identificador único del principal
${aws:PrincipalTag/Clave} Etiqueta del principal
${s3:prefix} Prefijo solicitado en una operación de S3

Con esto, una sola política da a cada persona su carpeta privada en el bucket de desarrollo:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "CadaUnoEnSuCarpeta",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::mercadofresco-tienda-web-desarrollo/personal/${aws:username}/*"
    }
  ]
}

Adjuntada al grupo mercadofresco-desarrollo, Luis solo puede tocar personal/luis/, y quien entre mañana solo personal/<su-nombre>/, sin escribir ni una política más.

Condiciones útiles. El bloque Condition tiene la forma { "Operador": { "ClaveDeCondición": "valor" } }:

Clave Operador típico Para qué
aws:SecureTransport Bool Exigir HTTPS: "Bool": {"aws:SecureTransport": "false"} con Deny
aws:SourceIp IpAddress Restringir a la IP de la oficina 192.168.10.0/24 (no funciona si la petición pasa por un endpoint de VPC)
aws:RequestedRegion StringEquals Confinar la actividad a eu-west-1
aws:PrincipalTag/Clave StringEquals Control de acceso por atributos (ABAC)
aws:MultiFactorAuthPresent Bool Exigir MFA
aws:CurrentTime DateGreaterThan Accesos temporales con caducidad
s3:x-amz-server-side-encryption StringEquals Obligar a que todo objeto se suba cifrado
kms:ViaService StringEquals Que la clave solo se use a través de un servicio concreto (04-02)

Un aviso importante sobre aws:SourceIp: si la petición viaja por un endpoint de VPC —como vpce-mercadofresco-s3, que montamos en 03-01—, la IP de origen es privada y la condición falla. Para ese caso la clave correcta es aws:SourceVpce.

Otro aviso: aws:MultiFactorAuthPresent es falso, no ausente, en las sesiones de rol de un servicio. Si aplicas esa condición a un rol de EC2, lo rompes.

La lógica de evaluación de políticas

Este es el corazón de IAM y merece memorizarse. AWS reúne todas las políticas aplicables y aplica tres reglas por este orden:

  1. Todo está denegado por defecto (denegación implícita).
  2. Un Allow explícito en cualquier política aplicable lo permite.
  3. Un Deny explícito en cualquier política gana siempre, sin excepción.

Dicho de otra forma: denegación explícita > permiso explícito > denegación implícita.

flowchart TD
    A["Petición autenticada"] --> B{"¿Hay un Deny explícito<br/>en ALGUNA política?"}
    B -->|"Sí"| Z["DENEGADA"]
    B -->|"No"| C{"¿Lo permite la SCP<br/>de Organizations?"}
    C -->|"No"| Z
    C -->|"Sí o no aplica"| D{"¿Lo permite la política<br/>de recurso?"}
    D -->|"Sí, y con Principal explícito"| Y["PERMITIDA"]
    D -->|"No concede"| E{"¿Está dentro del<br/>límite de permisos?"}
    E -->|"No"| Z
    E -->|"Sí o no aplica"| F{"¿Lo permite la política<br/>de sesión?"}
    F -->|"No"| Z
    F -->|"Sí o no aplica"| G{"¿Lo permite alguna política<br/>basada en identidad?"}
    G -->|"Sí"| Y
    G -->|"No"| Z

Consecuencias prácticas que hay que interiorizar:

  • No existe la «prioridad» entre políticas. No hay números de regla como en las NACL de 03-02. Un solo Deny perdido en una política olvidada bloquea todo, y encontrarlo es trabajo de detective.
  • El orden de las declaraciones dentro de una política es irrelevante. Se evalúan todas.
  • Ni el usuario root escapa a un Deny de una SCP. Es exactamente por eso que las SCP son la herramienta de gobierno de 09-04.
  • Límite de permisos y política de identidad se cruzan: el permiso efectivo es la intersección de ambos. Si el límite permite solo S3 y la política concede S3 y EC2, el resultado es solo S3.

Y la asimetría entre misma cuenta y cuentas distintas, resumida:

Situación Qué hace falta
Principal y recurso en la misma cuenta Basta con que una de las dos políticas conceda (identidad o recurso)
Cuentas distintas Hacen falta las dos: la de identidad en la cuenta origen y la de recurso en la de destino
Excepciones (KMS, IAM) La política de recurso siempre debe conceder, aunque sea la misma cuenta

Esa última fila es la fuente número uno de sorpresas en 04-02: si la política de la clave KMS no te nombra, no puedes usarla aunque seas administrador de la cuenta.

Formalizar rol-mercadofresco-tienda

Hasta ahora la instancia mercadofresco-tienda-01 funcionaba con permisos genéricos. Vamos a escribir la política que necesita exactamente, ni un permiso más. La tienda tiene que:

  1. Leer las fotos de mercadofresco-catalogo-fotos/productos/.
  2. Escribir las miniaturas generadas en miniaturas/.
  3. Publicar métricas de negocio (pedidos por hora) en CloudWatch.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "LeerFotosDeProducto",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": [
        "arn:aws:s3:::mercadofresco-catalogo-fotos/productos/*",
        "arn:aws:s3:::mercadofresco-catalogo-fotos/miniaturas/*"
      ]
    },
    {
      "Sid": "ListarSoloLosPrefijosNecesarios",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::mercadofresco-catalogo-fotos",
      "Condition": {
        "StringLike": {
          "s3:prefix": ["productos/*", "miniaturas/*"]
        }
      }
    },
    {
      "Sid": "EscribirMiniaturasCifradas",
      "Effect": "Allow",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::mercadofresco-catalogo-fotos/miniaturas/*",
      "Condition": {
        "StringEquals": {
          "s3:x-amz-server-side-encryption": "aws:kms"
        }
      }
    },
    {
      "Sid": "PublicarMetricasDeNegocio",
      "Effect": "Allow",
      "Action": "cloudwatch:PutMetricData",
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "cloudwatch:namespace": "MercadoFresco/Tienda"
        }
      }
    },
    {
      "Sid": "ProhibirTraficoSinCifrar",
      "Effect": "Deny",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::mercadofresco-catalogo-fotos",
        "arn:aws:s3:::mercadofresco-catalogo-fotos/*"
      ],
      "Condition": {
        "Bool": { "aws:SecureTransport": "false" }
      }
    }
  ]
}

Declaración por declaración:

  • LeerFotosDeProducto: solo s3:GetObject, y solo bajo dos prefijos. La tienda no puede leer nada fuera de productos/ y miniaturas/, ni siquiera dentro del mismo bucket. Tampoco puede borrar ni leer versiones antiguas.
  • ListarSoloLosPrefijosNecesarios: s3:ListBucket actúa sobre el bucket, no sobre los objetos, por eso el ARN va sin /*. La condición s3:prefix impide que alguien liste el bucket entero y descubra qué otros prefijos existen. Es una fuga de información sutil pero real.
  • EscribirMiniaturasCifradas: s3:PutObject limitado a miniaturas/, y exigiendo que la subida incluya la cabecera de cifrado con KMS. Si el código olvida cifrar, la subida se rechaza. Esto conecta directamente con alias/mercadofresco-datos en 04-02.
  • PublicarMetricasDeNegocio: cloudwatch:PutMetricData no admite ARN de recurso, hay que poner "Resource": "*". Pero se acota con cloudwatch:namespace, así que la tienda solo puede escribir en su propio espacio de nombres y no puede falsear las métricas de otro componente.
  • ProhibirTraficoSinCifrar: un Deny de refuerzo. Recuerda la regla: un Deny gana siempre, incluso frente a los Allow de arriba. Si por lo que sea se hiciera una llamada por HTTP sin TLS, se rechaza.

Fíjate en lo que no hay: ni s3:DeleteObject, ni s3:*, ni acceso a mercadofresco-copias-basedatos, ni a mercadofresco-informes-analitica, ni ec2:*. Si mañana alguien consigue ejecutar código en la instancia, esto es todo lo que se lleva.

Creación por CLI:

# 1. Política de confianza (quién puede asumir el rol)
cat > /tmp/confianza-ec2.json <<'JSON'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "ec2.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}
JSON

aws iam create-role \
  --role-name rol-mercadofresco-tienda \
  --assume-role-policy-document file:///tmp/confianza-ec2.json \
  --description "Rol de las instancias de la tienda de MercadoFresco" \
  --max-session-duration 3600 \
  --tags Key=Proyecto,Value=mercadofresco Key=Entorno,Value=produccion \
         Key=Componente,Value=tienda Key=Propietario,Value=marta \
         Key=CentroCoste,Value=tecnologia \
  --profile mercadofresco-dev

# 2. Política de permisos (qué puede hacer)
aws iam create-policy \
  --policy-name pol-mercadofresco-tienda \
  --policy-document file:///tmp/pol-tienda.json \
  --description "Permisos minimos de la tienda: leer catalogo, escribir miniaturas, metricas" \
  --profile mercadofresco-dev

# 3. Unir las dos cosas
aws iam attach-role-policy \
  --role-name rol-mercadofresco-tienda \
  --policy-arn arn:aws:iam::111122223333:policy/pol-mercadofresco-tienda \
  --profile mercadofresco-dev

--max-session-duration 3600 limita las sesiones a una hora. Para roles de EC2 el servicio de metadatos renueva automáticamente, así que reducirlo no rompe nada y acorta la ventana de un robo de credenciales.

Formalizar rol-lambda-miniaturas

La función mercadofresco-generar-miniaturas de 02-05 se dispara cuando aparece un objeto en productos/, genera la miniatura y la escribe en miniaturas/. Sus necesidades son parecidas pero no idénticas, y además necesita escribir sus registros y enviar los fallos a la cola.

Política de confianza —cambia solo el Principal:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "lambda.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}

Política de permisos:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EscribirRegistrosDeLaPropiaFuncion",
      "Effect": "Allow",
      "Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
      "Resource": "arn:aws:logs:eu-west-1:111122223333:log-group:/aws/lambda/mercadofresco-generar-miniaturas:*"
    },
    {
      "Sid": "LeerElOriginal",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::mercadofresco-catalogo-fotos/productos/*"
    },
    {
      "Sid": "EscribirLaMiniatura",
      "Effect": "Allow",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::mercadofresco-catalogo-fotos/miniaturas/*"
    },
    {
      "Sid": "UsarLaClaveDelBucket",
      "Effect": "Allow",
      "Action": ["kms:Decrypt", "kms:GenerateDataKey"],
      "Resource": "arn:aws:kms:eu-west-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab",
      "Condition": {
        "StringEquals": { "kms:ViaService": "s3.eu-west-1.amazonaws.com" }
      }
    },
    {
      "Sid": "EnviarLosFallosALaCola",
      "Effect": "Allow",
      "Action": "sqs:SendMessage",
      "Resource": "arn:aws:sqs:eu-west-1:111122223333:mercadofresco-miniaturas-fallidas"
    }
  ]
}

Cuatro detalles que importan:

  • logs:CreateLogGroup no está. Se crea el grupo una vez, a mano o con infraestructura como código, y la función solo necesita escribir. Es un permiso menos.
  • El ARN del grupo de registros acaba en :*: es la sintaxis de CloudWatch Logs para incluir los flujos. Sin ese sufijo la función no escribe nada y el fallo es silencioso.
  • kms:ViaService limita el uso de la clave a peticiones que lleguen a través de S3. Si alguien roba las credenciales de la función, no puede llamar a kms:Decrypt directamente para descifrar otra cosa. Es una de las condiciones más útiles de todo IAM y la desarrollamos en 04-02.
  • Se nombra la clave por su ARN de clave, no por el alias. Las políticas de identidad admiten alias en algunos contextos, pero el ARN es inequívoco y no cambia si alguien reasigna el alias.

Con el rol asignado, el código de la función no tiene ni una credencial. boto3.client("s3") encuentra las credenciales temporales por sí solo a través de las variables de entorno que Lambda inyecta. Esa es toda la magia.

Perfiles de instancia

Aquí hay una pieza que casi nadie entiende hasta que le falla: una instancia EC2 no puede recibir un rol directamente. Necesita un perfil de instancia (instance profile), que es un contenedor con exactamente un rol dentro.

flowchart LR
    A["mercadofresco-tienda-01"] --> B["Perfil de instancia<br/>rol-mercadofresco-tienda"]
    B --> C["Rol de IAM<br/>rol-mercadofresco-tienda"]
    C --> D["Política<br/>pol-mercadofresco-tienda"]

Cuando creas el rol desde la consola eligiendo el caso de uso «EC2», AWS crea el perfil de instancia con el mismo nombre automáticamente y no te enteras. Cuando lo creas por CLI, no. Hay que hacerlo a mano:

aws iam create-instance-profile \
  --instance-profile-name rol-mercadofresco-tienda \
  --profile mercadofresco-dev

aws iam add-role-to-instance-profile \
  --instance-profile-name rol-mercadofresco-tienda \
  --role-name rol-mercadofresco-tienda \
  --profile mercadofresco-dev

# Asociarlo a la plantilla de lanzamiento (nueva versión)
aws ec2 create-launch-template-version \
  --launch-template-name lt-mercadofresco-tienda \
  --source-version '$Latest' \
  --launch-template-data '{"IamInstanceProfile":{"Name":"rol-mercadofresco-tienda"}}' \
  --profile mercadofresco-dev

Si intentas lanzar una instancia con un rol que no tiene perfil, el error es Value (rol-mercadofresco-tienda) for parameter iamInstanceProfile.name is invalid, que no dice en absoluto lo que pasa. Ahora ya lo sabes.

Y una comprobación desde dentro de la instancia, usando IMDSv2 como vimos en 02-01:

TOKEN=$(curl -sX PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")

curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/iam/security-credentials/

# Devuelve: rol-mercadofresco-tienda
# Y con ese nombre se obtienen las credenciales temporales y su fecha de caducidad:
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/iam/security-credentials/rol-mercadofresco-tienda

Mínimo privilegio en la práctica: leer los AccessDenied

El principio de mínimo privilegio dice que cada identidad debe tener los permisos mínimos necesarios para su función, y nada más. Todo el mundo está de acuerdo y casi nadie lo aplica, porque adivinar de antemano qué permisos hace falta es imposible.

El método que sí funciona es el contrario:

flowchart TD
    A["Empezar SIN permisos"] --> B["Ejecutar el caso de uso real"]
    B --> C{"¿AccessDenied?"}
    C -->|"Sí"| D["Leer el mensaje:<br/>acción + recurso exactos"]
    D --> E["Añadir SOLO ese permiso"]
    E --> B
    C -->|"No"| F["Repetir con los casos<br/>menos frecuentes"]
    F --> G["Política mínima real"]

La clave está en que los mensajes de error de AWS son extraordinariamente precisos:

An error occurred (AccessDenied) when calling the PutObject operation:
User: arn:aws:sts::111122223333:assumed-role/rol-mercadofresco-tienda/i-0abc123def4567890
is not authorized to perform: s3:PutObject
on resource: "arn:aws:s3:::mercadofresco-catalogo-fotos/miniaturas/tomate-rama.jpg"
because no identity-based policy allows the s3:PutObject action

Ese mensaje te da los cuatro datos que necesitas: el principal exacto, la acción exacta, el recurso exacto y por qué falló. Esa última frase es oro:

Frase final del error Qué significa
because no identity-based policy allows Denegación implícita: falta un Allow. Añádelo
with an explicit deny in an identity-based policy Hay un Deny en una política adjunta al principal
with an explicit deny in a resource-based policy El Deny está en la política del bucket, cola o clave
with an explicit deny in a service control policy Lo bloquea una SCP de Organizations (09-04)
with an explicit deny in a permissions boundary El límite de permisos no lo incluye

Fíjate también en el ARN del principal: arn:aws:sts::111122223333:assumed-role/rol-.../i-0abc.... Ese es el aspecto de una sesión de rol asumido: empieza por sts, no por iam, y el último segmento es el nombre de sesión, que para EC2 es el identificador de la instancia. Verlo así confirma de inmediato que la instancia está usando el rol y no unas claves olvidadas.

IAM Access Analyzer y la generación de políticas

Hay dos herramientas que hacen este trabajo por ti.

Generación de políticas a partir de la actividad. Access Analyzer lee el historial de CloudTrail (el servicio de auditoría que veremos a fondo en 05-03) de un rol durante un periodo y escribe la política que ese rol realmente ha usado. Es la forma más rápida de recortar permisos existentes:

aws accessanalyzer start-policy-generation \
  --policy-generation-details '{"principalArn":"arn:aws:iam::111122223333:role/rol-mercadofresco-tienda"}' \
  --cloud-trail-details '{
    "trails": [{"cloudTrailArn":"arn:aws:cloudtrail:eu-west-1:111122223333:trail/mercadofresco-auditoria",
                "regions":["eu-west-1"],"allRegions":false}],
    "accessRole":"arn:aws:iam::111122223333:role/rol-access-analyzer",
    "startTime":"2026-07-01T00:00:00Z"
  }' \
  --profile mercadofresco-dev

Advertencia importante: la política generada refleja lo que se usó en la ventana analizada. Si el proceso de cierre mensual solo se ejecuta el día 30 y analizas del 1 al 15, ese permiso no aparecerá y romperás el cierre. Analiza al menos un ciclo completo de negocio, y para MercadoFresco eso significa incluir un viernes de pico y un fin de mes.

Hallazgos de acceso externo. El analizador revisa políticas de recurso (buckets, colas, claves, roles) y avisa de todo aquello a lo que puede acceder alguien de fuera de tu cuenta u organización. Es gratuito y detecta en segundos el bucket que alguien abrió «solo para una prueba»:

aws accessanalyzer create-analyzer \
  --analyzer-name mercadofresco-acceso-externo \
  --type ACCOUNT \
  --profile mercadofresco-dev

aws accessanalyzer list-findings \
  --analyzer-arn arn:aws:access-analyzer:eu-west-1:111122223333:analyzer/mercadofresco-acceso-externo \
  --query 'findings[?status==`ACTIVE`].[resource,principal,action]' \
  --output table \
  --profile mercadofresco-dev

Existe además el analizador de acceso no utilizado, que señala roles, usuarios y permisos que llevan meses sin usarse. Ese sí tiene coste (del orden de 0,20 USD por identidad analizada al mes) y es la herramienta natural para la limpieza semestral.

Las personas de MercadoFresco: usuarios y grupos

Hasta ahora solo existían mercadofresco-admin y el root. Marta crea la estructura humana:

Grupo Quién Qué puede hacer
mercadofresco-administracion Marta Administración completa con MFA obligatorio
mercadofresco-desarrollo Luis EC2, Lambda, S3 de desarrollo, lectura de registros; nada en producción
mercadofresco-analitica Sara Solo lectura de mercadofresco-informes-analitica y consultas
for g in mercadofresco-administracion mercadofresco-desarrollo mercadofresco-analitica; do
  aws iam create-group --group-name "$g" --profile mercadofresco-dev
done

aws iam create-user --user-name marta \
  --tags Key=Proyecto,Value=mercadofresco Key=Propietario,Value=marta \
  --profile mercadofresco-dev
aws iam add-user-to-group --user-name marta \
  --group-name mercadofresco-administracion --profile mercadofresco-dev

La política de Sara es el mejor ejemplo de mínimo privilegio para un perfil no técnico:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListarSoloElBucketDeInformes",
      "Effect": "Allow",
      "Action": ["s3:ListBucket", "s3:GetBucketLocation"],
      "Resource": "arn:aws:s3:::mercadofresco-informes-analitica"
    },
    {
      "Sid": "DescargarInformes",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:GetObjectVersion"],
      "Resource": "arn:aws:s3:::mercadofresco-informes-analitica/*"
    },
    {
      "Sid": "SoloDesdeEuWest1",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": ["eu-west-1", "us-east-1"]
        }
      }
    }
  ]
}
  • Sara puede listar y descargar, nunca subir ni borrar. Si su portátil se infecta, no puede destruir los informes.
  • La tercera declaración confina toda su actividad a eu-west-1. Se incluye us-east-1 porque los servicios globales (IAM, CloudFront, Route 53) firman sus peticiones contra esa región y sin ella Sara no podría ni cambiar su propia contraseña.
  • Es una política de identidad, así que no lleva Principal.

MFA obligatorio por condición

Exigir MFA en la consola es una casilla; exigirlo para la API requiere una política. Esta es la que Marta adjunta a los tres grupos, y es el patrón canónico de AWS:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PermitirGestionarLasPropiasCredenciales",
      "Effect": "Allow",
      "Action": [
        "iam:ChangePassword",
        "iam:GetUser",
        "iam:CreateVirtualMFADevice",
        "iam:EnableMFADevice",
        "iam:ListMFADevices",
        "iam:ResyncMFADevice"
      ],
      "Resource": [
        "arn:aws:iam::111122223333:user/${aws:username}",
        "arn:aws:iam::111122223333:mfa/${aws:username}"
      ]
    },
    {
      "Sid": "DenegarTodoLoDemasSinMFA",
      "Effect": "Deny",
      "NotAction": [
        "iam:CreateVirtualMFADevice",
        "iam:EnableMFADevice",
        "iam:GetUser",
        "iam:ListMFADevices",
        "iam:ListVirtualMFADevices",
        "iam:ResyncMFADevice",
        "sts:GetSessionToken",
        "iam:ChangePassword"
      ],
      "Resource": "*",
      "Condition": {
        "BoolIfExists": { "aws:MultiFactorAuthPresent": "false" }
      }
    }
  ]
}

Tres puntos delicados:

  • El primer Statement es imprescindible: sin él, un usuario nuevo sin MFA no podría registrar su MFA, y quedaría bloqueado para siempre. Es la paradoja del huevo y la gallina de esta política.
  • NotAction aquí sí es correcto porque va con Deny: «deniega todo excepto lo necesario para configurar el MFA».
  • BoolIfExists en lugar de Bool. Si la clave no existe en el contexto —cosa que ocurre en ciertas llamadas de servicio—, Bool haría que la condición no coincidiera y el Deny no se aplicaría. BoolIfExists trata la ausencia como false, es decir, como «sin MFA», que es el comportamiento seguro.

Ojo: adjuntar esto a un rol de servicio lo rompe todo, porque las sesiones de EC2 o Lambda nunca llevan MFA. Es exclusivamente para grupos de humanos.

Rotación de claves e informe de credenciales

Si un usuario necesita claves de acceso (por ejemplo Luis para el perfil mercadofresco-dev en su portátil), hay que rotarlas. IAM permite dos claves activas simultáneamente, y esa es exactamente la razón de que existan: rotar sin interrupción.

# 1. Crear la segunda clave
aws iam create-access-key --user-name luis --profile mercadofresco-dev

# 2. Actualizar ~/.aws/credentials con la nueva, probar durante unos días

# 3. Desactivar la antigua (sin borrarla: se puede revertir en segundos)
aws iam update-access-key --user-name luis \
  --access-key-id AKIAIOSFODNN7EXAMPLE --status Inactive --profile mercadofresco-dev

# 4. Confirmar que nada se ha roto y borrarla definitivamente
aws iam delete-access-key --user-name luis \
  --access-key-id AKIAIOSFODNN7EXAMPLE --profile mercadofresco-dev

El informe de credenciales es un CSV con el estado de todos los usuarios de la cuenta y es la revisión trimestral de Marta:

aws iam generate-credential-report --profile mercadofresco-dev
aws iam get-credential-report --query 'Content' --output text \
  --profile mercadofresco-dev | base64 --decode > /tmp/credenciales.csv

Las columnas que hay que mirar: mfa_active (debe ser true en todos los humanos), access_key_1_last_used_date (si es N/A, la clave nunca se ha usado: bórrala), access_key_1_last_rotated (más de 90 días: rótala) y password_last_used (más de 90 días: el usuario probablemente ya no trabaja aquí).

IAM Identity Center y federación

Todo lo anterior tiene un límite evidente: no escala a personas. Con tres empleados es manejable; con treinta, crear un usuario de IAM por persona y por cuenta es un desastre operativo, y cuando alguien se va hay que acordarse de borrarlo en todas partes.

La solución moderna es AWS IAM Identity Center (antes AWS SSO):

  • Los usuarios viven en un único directorio: el propio de Identity Center, Microsoft Entra ID, Okta o Google Workspace.
  • Se definen conjuntos de permisos (permission sets), que no son más que roles de IAM que Identity Center crea automáticamente en cada cuenta.
  • La persona entra por un portal, elige cuenta y rol, y recibe credenciales temporales. No hay claves de acceso permanentes en ningún sitio.
  • Cuando alguien deja la empresa se le desactiva en el directorio corporativo y pierde el acceso a todas las cuentas de AWS al instante.
  • Es gratuito y se integra con el CLI v2: aws configure sso y aws sso login.
Usuarios de IAM IAM Identity Center
Credenciales Permanentes Temporales
Alta/baja Manual y por cuenta Centralizada en el directorio
Multicuenta Un usuario por cuenta Un inicio de sesión, todas las cuentas
Coste Gratis Gratis
Cuándo Casos residuales Lo recomendado para personas

MercadoFresco tiene hoy tres personas y una cuenta, así que los usuarios de IAM son razonables. En cuanto haya una segunda cuenta —lo que ocurrirá en 09-04 al separar desarrollo de producción con Organizations— la migración a Identity Center deja de ser opcional.

Herramientas de diagnóstico

Cuatro herramientas que Marta usa cuando algo no cuadra.

1. sts get-caller-identity. La primera pregunta siempre es «¿quién soy ahora mismo?»:

aws sts get-caller-identity --profile mercadofresco-dev
{
    "UserId": "AIDAI23HXD2O5EXAMPLE",
    "Account": "111122223333",
    "Arn": "arn:aws:iam::111122223333:user/luis"
}

Si el Arn no es el que esperabas, el problema no es de permisos sino de credenciales: revisa la precedencia que vimos en 01-05 (variables de entorno antes que perfil, perfil antes que rol de instancia).

2. Simulador de políticas. Evalúa una acción sin ejecutarla:

aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::111122223333:role/rol-mercadofresco-tienda \
  --action-names s3:DeleteObject s3:GetObject \
  --resource-arns "arn:aws:s3:::mercadofresco-catalogo-fotos/productos/tomate-rama.jpg" \
  --query 'EvaluationResults[].[EvalActionName,EvalDecision]' \
  --output table \
  --profile mercadofresco-dev
-------------------------------------
|     SimulatePrincipalPolicy       |
+------------------+----------------+
|  s3:DeleteObject |  implicitDeny  |
|  s3:GetObject    |  allowed       |
+------------------+----------------+

Exactamente lo que queríamos: puede leer, no puede borrar. El simulador no evalúa políticas de recurso de otras cuentas ni todas las condiciones contextuales, así que es una ayuda, no una prueba.

3. --dry-run. Muchas operaciones de EC2 lo aceptan y comprueban permisos sin hacer nada:

aws ec2 terminate-instances --instance-ids i-0abc123def4567890 --dry-run \
  --profile mercadofresco-dev
# UnauthorizedOperation  -> no tiene permiso
# DryRunOperation        -> sí lo tiene (y no ha terminado nada)

4. get-account-authorization-details. El volcado completo de usuarios, grupos, roles y políticas de la cuenta. Es la foto para auditar o versionar en Git:

aws iam get-account-authorization-details --profile mercadofresco-dev \
  > /tmp/iam-mercadofresco-$(date +%F).json

# ¿Quién tiene permisos de administrador?
aws iam get-account-authorization-details --profile mercadofresco-dev \
  --query 'Policies[?PolicyName==`AdministratorAccess`]'

Y en boto3, un chequeo que Marta ejecuta en el repaso trimestral:

import boto3
from datetime import datetime, timezone

sesion = boto3.Session(profile_name="mercadofresco-dev", region_name="eu-west-1")
iam = sesion.client("iam")

ahora = datetime.now(timezone.utc)

for pagina in iam.get_paginator("list_users").paginate():
    for usuario in pagina["Users"]:
        nombre = usuario["UserName"]

        # ¿Tiene MFA?
        mfa = iam.list_mfa_devices(UserName=nombre)["MFADevices"]
        sin_mfa = "SIN MFA" if not mfa else "ok"

        # ¿Claves antiguas?
        for clave in iam.list_access_keys(UserName=nombre)["AccessKeyMetadata"]:
            dias = (ahora - clave["CreateDate"]).days
            aviso = "ROTAR" if dias > 90 else "ok"
            print(f"{nombre:20} {clave['AccessKeyId']} {dias:4} dias  {aviso}  {sin_mfa}")

El script recorre todos los usuarios, comprueba si tienen MFA registrado y calcula la antigüedad de cada clave de acceso. get_paginator es necesario porque list_users devuelve como máximo 100 usuarios por llamada; con paginador, boto3 se encarga de recorrer todas las páginas.

Coste y limpieza

IAM es gratuito. Usuarios, grupos, roles, políticas y llamadas a STS no cuestan nada. Tampoco cuesta IAM Identity Center ni el analizador de acceso externo de Access Analyzer. Lo único con precio es el analizador de acceso no utilizado, del orden de 0,20 USD por identidad analizada al mes: para las ~8 identidades de MercadoFresco, menos de 2 USD mensuales.

Que sea gratis no significa que no tenga coste: el coste de IAM es operativo. Cada política mal escrita es una brecha o una incidencia. Y hay cuotas que conviene conocer: 5.000 usuarios por cuenta, 1.000 roles, 10 políticas gestionadas por identidad, 6.144 caracteres por política gestionada y 2.048 por política inline de usuario.

Para deshacer lo de esta lección, el orden importa: no se puede borrar un rol con políticas adjuntas ni un perfil de instancia con un rol dentro.

aws iam remove-role-from-instance-profile \
  --instance-profile-name rol-mercadofresco-tienda --role-name rol-mercadofresco-tienda \
  --profile mercadofresco-dev
aws iam delete-instance-profile --instance-profile-name rol-mercadofresco-tienda \
  --profile mercadofresco-dev
aws iam detach-role-policy --role-name rol-mercadofresco-tienda \
  --policy-arn arn:aws:iam::111122223333:policy/pol-mercadofresco-tienda --profile mercadofresco-dev
aws iam delete-role --role-name rol-mercadofresco-tienda --profile mercadofresco-dev
aws iam delete-policy --policy-arn arn:aws:iam::111122223333:policy/pol-mercadofresco-tienda \
  --profile mercadofresco-dev

Para borrar un usuario hay que quitar antes contraseña, claves, dispositivos MFA, pertenencias a grupos y políticas adjuntas. Es tedioso a propósito.

Errores Comunes y Consejos

Confundir el ARN del bucket con el de los objetos. arn:aws:s3:::mi-bucket sirve para ListBucket; arn:aws:s3:::mi-bucket/* para GetObject. Casi todas las políticas de S3 necesitan las dos declaraciones. Si tu aplicación puede descargar un objeto cuyo nombre conoce pero no listar el bucket, te falta la primera.

Poner Principal en una política de identidad. Es un error de sintaxis: AWS lo rechaza. El principal de una política de identidad es la identidad a la que se adjunta.

Usar Bool donde hace falta BoolIfExists. Con Bool, si la clave de condición no está presente en el contexto de la petición, la condición no coincide y tu Deny no se aplica. Regla práctica: en condiciones de tipo Deny que dependen de una clave que puede faltar, usa siempre la variante IfExists.

Adjuntar AdministratorAccess «temporalmente» para desbloquear algo. Nunca es temporal. Si necesitas desbloquear una entrega a las once de la noche, usa el simulador para saber qué permiso falta y añade ese permiso concreto; tardas dos minutos más y no dejas una bomba en la cuenta.

Olvidar el perfil de instancia. Crear el rol por CLI no crea el perfil. Recuérdalo cuando el error hable de un iamInstanceProfile.name inválido.

Poner claves de acceso en user data o en variables de entorno de una instancia. Cualquiera con acceso a la instancia —o a los metadatos, si IMDSv1 está activo— las lee. Usa el rol. Siempre.

Creer que un Deny de una política se puede «anular» con un Allow en otra. No se puede. Si algo está denegado y no encuentras dónde, busca en este orden: SCP, límite de permisos, política de recurso, políticas de identidad. Y usa el mensaje de error, que te dice en cuál está.

Consejo: nombra las políticas con un prefijo propio. pol-mercadofresco-* se distingue de un vistazo de las gestionadas por AWS en cualquier listado y evita adjuntar la equivocada.

Consejo: versiona las políticas en Git. Un fichero .json por política, revisado por otra persona antes de aplicarse. Las políticas son código y merecen el mismo trato; en 09-01 y 09-02 las convertiremos directamente en plantillas de CloudFormation y en constructos de CDK.

Consejo: repasa el informe de credenciales cada tres meses. Cinco minutos que detectan claves de personas que ya no están, usuarios sin MFA y claves de dos años.

Ejercicios

Ejercicio 1: escribir la política de un rol nuevo

MercadoFresco añade una función Lambda llamada mercadofresco-informe-ventas que se ejecuta cada noche. Necesita: leer todos los objetos de mercadofresco-copias-basedatos bajo el prefijo exportaciones/, escribir el informe resultante en mercadofresco-informes-analitica bajo ventas/, escribir sus propios registros en CloudWatch Logs y publicar un mensaje en el tema SNS alertas-mercadofresco cuando termine. La cuenta es 111122223333 y la región eu-west-1.

Escribe la política de confianza y la política de permisos completas, aplicando mínimo privilegio. Justifica cada ARN.

Ejercicio 2: diagnosticar una denegación

Luis ejecuta un script desde la instancia mercadofresco-tienda-01 y recibe:

An error occurred (AccessDenied) when calling the GetObject operation:
User: arn:aws:sts::111122223333:assumed-role/rol-mercadofresco-tienda/i-0abc123def4567890
is not authorized to perform: s3:GetObject
on resource: "arn:aws:s3:::mercadofresco-informes-analitica/ventas/2026-07.csv"
because no identity-based policy allows the s3:GetObject action

Responde: (a) ¿qué principal está haciendo la llamada exactamente y cómo lo sabes?; (b) ¿el problema es una denegación explícita o implícita?; (c) ¿cuál es la solución correcta y cuál sería la solución cómoda y equivocada?; (d) ¿cambiaría algo si el bucket tuviera una política de bucket que concede s3:GetObject a arn:aws:iam::111122223333:root?

Ejercicio 3: resolver una evaluación de políticas

Sara pertenece al grupo mercadofresco-analitica. Sobre ella actúan estas cuatro políticas:

  1. Política del grupo: Allow de s3:GetObject sobre arn:aws:s3:::mercadofresco-informes-analitica/*.
  2. Política inline del usuario: Allow de s3:* sobre *.
  3. Política de MFA obligatorio (la del apartado correspondiente), y Sara ha iniciado sesión con MFA.
  4. Política de bucket de mercadofresco-copias-basedatos: Deny de s3:* a todo principal cuyo aws:PrincipalTag/Departamento no sea tecnologia. Sara tiene la etiqueta Departamento=analitica.

Determina si Sara puede hacer cada una de estas tres operaciones y por qué:

  • (a) s3:GetObject sobre mercadofresco-informes-analitica/ventas/2026-07.csv
  • (b) s3:GetObject sobre mercadofresco-copias-basedatos/dump-2026-07-01.sql
  • (c) s3:DeleteObject sobre mercadofresco-catalogo-fotos/productos/tomate-rama.jpg

Soluciones

Solución 1

Política de confianza (el principal es el servicio Lambda, igual que en rol-lambda-miniaturas):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "lambda.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}

Política de permisos:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Registros",
      "Effect": "Allow",
      "Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
      "Resource": "arn:aws:logs:eu-west-1:111122223333:log-group:/aws/lambda/mercadofresco-informe-ventas:*"
    },
    {
      "Sid": "LeerExportaciones",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::mercadofresco-copias-basedatos/exportaciones/*"
    },
    {
      "Sid": "ListarSoloEsePrefijo",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::mercadofresco-copias-basedatos",
      "Condition": { "StringLike": { "s3:prefix": "exportaciones/*" } }
    },
    {
      "Sid": "EscribirElInforme",
      "Effect": "Allow",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::mercadofresco-informes-analitica/ventas/*"
    },
    {
      "Sid": "AvisarAlTerminar",
      "Effect": "Allow",
      "Action": "sns:Publish",
      "Resource": "arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco"
    }
  ]
}

Justificación de los ARN:

  • El grupo de registros lleva :* final para abarcar los flujos, y se nombra la función concreta: esta Lambda no puede escribir en los registros de las demás.
  • La lectura es GetObject sobre exportaciones/* únicamente. No hay acceso al resto de mercadofresco-copias-basedatos, que contiene las copias completas de la base de datos.
  • ListBucket va sobre el ARN sin /* y acotado por s3:prefix.
  • La escritura es solo PutObject sobre ventas/*: no puede borrar informes anteriores.
  • SNS lleva el ARN del tema exacto, no *.
  • No hay logs:CreateLogGroup (se crea aparte) ni ninguna acción de KMS: si los buckets estuvieran cifrados con alias/mercadofresco-datos habría que añadir kms:Decrypt y kms:GenerateDataKey con kms:ViaService, como veremos en 04-02.

Solución 2

(a) El principal es una sesión de rol asumido: el ARN empieza por arn:aws:sts:: y tiene la forma assumed-role/<rol>/<nombre-de-sesión>. Para EC2 el nombre de sesión es el identificador de la instancia, i-0abc123def4567890. Es decir: el código se está ejecutando en la instancia usando rol-mercadofresco-tienda, exactamente como debe ser. No hay claves de acceso por medio.

(b) Implícita. Lo dice la última línea: because no identity-based policy allows. Si fuera explícita, el mensaje diría with an explicit deny in....

(c) La solución correcta es preguntarse si la tienda debe leer informes de analítica. Casi seguro que no: ese bucket es de Sara. Lo que hay que arreglar es el script, no la política. Si de verdad hiciera falta, se añadiría una declaración con s3:GetObject sobre el prefijo mínimo necesario. La solución cómoda y equivocada es adjuntar AmazonS3ReadOnlyAccess al rol, que concedería lectura sobre todos los buckets de la cuenta, incluidas las copias de la base de datos con datos personales de clientes.

(d) Sí, cambiaría. Con principal y recurso en la misma cuenta, basta con que una de las dos políticas conceda el permiso. Una política de bucket que concede a arn:aws:iam::111122223333:root delega en las políticas de identidad de la cuenta... pero ese root en una política de recurso significa «la cuenta», y por sí solo no concede: sigue haciendo falta que la política de identidad lo permita. Si en cambio la política de bucket nombrara explícitamente arn:aws:iam::111122223333:role/rol-mercadofresco-tienda con Allow de s3:GetObject, entonces sí funcionaría sin tocar la política de identidad. Es una distinción sutil y es la que más confunde en la práctica.

Solución 3

(a) Permitida. La política del grupo concede s3:GetObject sobre ese bucket, la política inline también, no hay ningún Deny aplicable (la política 4 solo afecta a mercadofresco-copias-basedatos) y la condición de MFA se cumple porque Sara inició sesión con MFA.

(b) Denegada. La política de bucket contiene un Deny explícito para principales cuyo Departamento no sea tecnologia, y el de Sara es analitica. Una denegación explícita gana siempre, en cualquier política, incluidas las de recurso. Da igual que la política inline le conceda s3:* sobre *.

(c) Permitida, y ese es justamente el problema. La política inline del punto 2 concede s3:* sobre *, lo que incluye s3:DeleteObject sobre el catálogo de fotos. Sara, que es analista de negocio, puede borrar las fotos de producto de la tienda. La política inline anula por completo el cuidadoso mínimo privilegio del grupo: hay que eliminarla. Este es el patrón real por el que las cuentas se degradan con el tiempo: alguien añade un permiso amplio «solo para probar» y nadie lo retira. Un límite de permisos sobre el usuario sara habría evitado el problema, porque el permiso efectivo es la intersección del límite y de las políticas.

Conclusión

IAM ha dejado de ser la parte que se configura a base de clics hasta que algo funciona. Ahora sabes que autenticación y autorización son dos cosas distintas y que el tipo de error te dice cuál ha fallado; sabes leer un ARN campo a campo, incluida la rareza de S3 sin región ni cuenta y la diferencia entre el ARN del bucket y el de sus objetos, que es el origen de la mitad de los AccessDenied del mundo.

Conoces los seis tipos de política y, sobre todo, la lógica de evaluación: denegación explícita por encima de permiso explícito, y ambos por encima de la denegación implícita que lo niega todo por defecto. Sabes que en la misma cuenta basta con que una política conceda, que entre cuentas hacen falta las dos, y que KMS es la excepción que obliga siempre a la política de recurso, algo que importará mucho dentro de una lección.

Has entendido por qué una EC2 o una Lambda nunca deben llevar claves de acceso: los roles y sts:AssumeRole entregan credenciales temporales que se renuevan solas, no se guardan en ningún fichero y no pueden filtrarse a Git. Y has formalizado por fin los dos roles que arrastrábamos desde el módulo 2: rol-mercadofresco-tienda, con la política pol-mercadofresco-tienda —lectura de productos/, escritura cifrada en miniaturas/, métricas acotadas a su propio espacio de nombres y un Deny que exige TLS— y rol-lambda-miniaturas, con kms:ViaService para que su clave solo sirva a través de S3. Sabes que a la instancia hay que darle un perfil de instancia, no el rol directamente.

En el plano humano, MercadoFresco tiene ya sus grupos: mercadofresco-administracion para Marta, mercadofresco-desarrollo para Luis y mercadofresco-analitica para Sara, esta última con solo lectura sobre mercadofresco-informes-analitica y confinada a eu-west-1; todos ellos con MFA obligatorio por condición aws:MultiFactorAuthPresent y con el BoolIfExists que evita el falso sentido de seguridad. Y tienes el método que de verdad produce mínimo privilegio: empezar sin permisos, ejecutar, leer el AccessDenied —que te dice acción, recurso y motivo exactos— y añadir solo lo que falta, ayudándote del simulador de políticas, de --dry-run, del informe de credenciales y de IAM Access Analyzer, que llega a escribir la política a partir de la actividad real registrada en CloudTrail (05-03).

Queda un cabo suelto, y es grande. En la política de la tienda hemos exigido que las miniaturas se suban con s3:x-amz-server-side-encryption: aws:kms, y en la de la Lambda hemos concedido kms:Decrypt y kms:GenerateDataKey sobre una clave identificada por un UUID. Pero nadie ha decidido todavía quién administra esa clave, quién puede usarla, cada cuánto rota ni qué ocurre exactamente cuando S3 cifra un objeto de 4 MB. La clave alias/mercadofresco-datos sigue siendo un nombre sin política. En la lección 04-02, «AWS Key Management Service (KMS)», la construiremos de verdad: veremos el cifrado de sobre que explica por qué tus datos nunca viajan a KMS, la diferencia real entre SSE-S3, SSE-KMS y SSE-C, la política de clave completa para alias/mercadofresco-datos con Marta como administradora y rol-mercadofresco-tienda como usuario, y cifraremos por fin el bucket mercadofresco-copias-basedatos, los volúmenes de EBS y la instancia mercadofresco-pedidos, con datos personales de clientes españoles y el RGPD mirando por encima del hombro.

Curso de AWS

Módulo 1: Introducción a AWS

Módulo 2: Servicios principales de AWS

Módulo 3: Redes y entrega de contenido

Módulo 4: Seguridad e identidad

Módulo 5: Monitorización y gestión

Módulo 6: Bases de datos

Módulo 7: Integración de aplicaciones

Módulo 8: Herramientas para desarrolladores

Módulo 9: Infraestructura como código y gobierno de cuentas

Módulo 10: Contenedores en AWS

Módulo 11: Mejores prácticas y gestión de costos

© Copyright 2026. Todos los derechos reservados