IAM: Usuario, Rol y Política de forma correcta
1. Visión general
IAM (Identity and Access Management) es el servicio fundamental de AWS que determina “quién puede hacer qué con qué”. Muchos equipos lo usan mal desde el principio: crean usuarios con permisos de administrador para todo, asignan policies *:* para ir rápido, o confunden usuarios con roles. Este artículo te ayuda a diferenciar claramente los tres conceptos: usuario, rol y política; escribir JSON de policies con la sintaxis correcta; aplicar el principio de least privilege; y probar la asunción de rol usando el AWS SDK para Java.
Cuando termines de leer, tendrás confianza para diseñar un conjunto de IAM limpio, seguro, sin otorgar permisos “god-mode” a todos los servicios.
2. Tres componentes fundamentales
IAM gira alrededor de tres conceptos:
- Usuario: identidad para una persona o aplicación específica, con credenciales de larga duración (contraseña, access key).
- Rol: identidad temporal sin credenciales fijas, que es “asumida” (suplantada) por un usuario, servicio u otra cuenta.
- Política: documento JSON que describe permisos (allow/deny) adjuntos a un usuario, rol, grupo o recurso.
Nota: Un rol no es “un usuario sin contraseña”. Un rol genera credenciales temporales (con vencimiento, a través de STS), mientras que un usuario usa credenciales fijas hasta que las rotes.
3. Usuario: cuándo usarlo, cuándo no
El usuario es apropiado para:
- Personas que necesitan iniciar sesión en la Consola para trabajar manualmente.
- Casos excepcionales donde no puedas usar roles (raro, por ejemplo sistemas heredados fuera de AWS que necesitan access keys fijas).
El usuario no es apropiado para:
- Cargas de trabajo que corren en EC2, Lambda, ECS — estos lugares deben usar roles.
- Compartir un usuario entre múltiples personas (pierdes la capacidad de auditoría por CloudTrail).
Crear un usuario con CLI:
aws iam create-user --user-name order-service-deployer
aws iam create-login-profile \
--user-name order-service-deployer \
--password 'TempPass123!' \
--password-reset-required
La primera línea crea el usuario. El comando create-login-profile otorga permiso para iniciar sesión en la Consola con una contraseña temporal, obligando a cambiarla en el primer acceso — evita que la contraseña por defecto exista durante mucho tiempo.
Error común: crear un access key para el usuario y hard-codearlo en el código, subirlo a git. Si necesitas usar un access key de forma obligatoria, habilita la rotación periódica y usa Secrets Manager para almacenarla.
4. Rol: identidad temporal en el lugar correcto
4.1. EC2 instance role
Cuando una instancia de EC2 necesita llamar a S3, no incrustes un access key en .env. Crea un rol y adjunta un instance profile:
aws iam create-role \
--role-name OrderServiceEc2Role \
--assume-role-policy-document file://trust-policy.json
El archivo trust-policy.json especifica quién puede asumir este rol:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "ec2.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}
Principal.Service especifica que el servicio EC2 puede asumir este rol a través del instance profile. Esto se llama trust policy — diferente de la permission policy que describimos después.
4.2. Cross-account role
Los roles también se usan para permitir que otra cuenta de AWS acceda a la tuya sin compartir credenciales:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:root" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "sts:ExternalId": "order-partner-2024" }
}
}
]
}
La Condition con sts:ExternalId protege contra ataques de confused deputy — el tercero debe conocer el ExternalId secreto para asumir el rol, evitando que un cuarto intente aprovecharse del ARN público.
5. Política: estructura y cómo escribir
Una política JSON contiene Version, Statement — cada statement tiene Effect, Action, Resource, y opcionalmente Condition.
Ejemplo: permitir que PaymentService lea/escriba en un bucket S3 específico, pero no borre:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowReadWritePaymentBucket",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::payment-receipts-bucket/*"
},
{
"Sid": "DenyDeleteObject",
"Effect": "Deny",
"Action": "s3:DeleteObject",
"Resource": "arn:aws:s3:::payment-receipts-bucket/*"
}
]
}
El primer statement Allow autoriza dos acciones GetObject y PutObject solo en la ruta payment-receipts-bucket/*. El segundo statement Deny explícitamente deniega la acción de borrar — incluso si alguien añade después una policy s3:*, el deny explícito siempre gana.
Adjuntar esta política al rol:
aws iam put-role-policy \
--role-name PaymentServiceRole \
--policy-name PaymentBucketAccess \
--policy-document file://payment-bucket-policy.json
5.1. Identity-based vs resource-based policy
- Identity-based policy: se adjunta a usuario/rol/grupo, responde “¿qué puede hacer esta identidad?”.
- Resource-based policy: se adjunta directamente al recurso (política de bucket S3, política de cola SQS, política de clave KMS), responde “¿quién puede acceder a este recurso?”.
Ejemplo de política de bucket (resource-based) que solo permite que un rol específico acceda:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/PaymentServiceRole"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::payment-receipts-bucket/*"
}
]
}
Estos dos tipos de políticas pueden combinarse — el permiso final es la intersección de ambas (tanto identity-based como resource-based deben permitir; si uno no permite, se bloquea, excepto en casos cross-account donde se necesita allow en ambos).
6. Principio de least privilege
En lugar de escribir "Action": "s3:*" para ir rápido, lista exactamente las acciones que necesitas. Usa Condition para restringir el alcance aún más.
Ejemplo: permitir leer objetos solo si la solicitud viene de un VPC específico:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::order-exports-bucket/*",
"Condition": {
"StringEquals": {
"aws:SourceVpc": "vpc-0abc1234"
}
}
}
]
}
aws:SourceVpc es una global condition key que solo permite si la solicitud proviene del VPC endpoint del VPC especificado — útil cuando no quieres que los datos salgan de la red interna.
Enfoque práctico para lograr least privilege:
- Comienza con una política vacía (deny por defecto).
- Ejecuta la carga de trabajo, observa en IAM Access Analyzer o logs de CloudTrail qué acciones se deniegan.
- Añade exactamente esa acción a la política, sin añadir de más.
- Repite hasta que la carga de trabajo funcione estable sin bloqueos.
Nota: Usa
aws iam generate-service-last-accessed-detailspara ver qué acciones ha usado realmente un usuario/rol en los últimos 90 días — así puedes eliminar acciones innecesarias de la política actual.
7. Lógica de evaluación de políticas a recordar
AWS evalúa los permisos en el siguiente orden; necesitas recordarlo para debuguear rápido:
- Por defecto todo se deniega (implicit deny).
- Si hay algún statement que dice
Denyexplícitamente → el resultado final es deny, nada lo salva. - Si no hay deny explícito, busca un statement
Allowque coincida → allow. - Si no hay allow que coincida → sigue siendo deny (implicit deny por defecto).
Solicitud llega → ¿Hay Deny explícito que coincida?
├─ Sí → DENY (termina)
└─ No → ¿Hay Allow que coincida?
├─ Sí → ALLOW
└─ No → DENY (implicit)
Entender correctamente este orden te ayuda a evitar bugs clásicos: asignar AdministratorAccess a un usuario pero seguir siendo bloqueado en una acción, porque hay un SCP (Service Control Policy) en el nivel de Organization que deniega explícitamente esa acción.
8. Probar asunción de rol con AWS SDK para Java
Añade la dependencia del SDK v2:
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sts</artifactId>
<version>2.28.11</version>
</dependency>
El servicio llama a STS para asumir un rol y obtener credenciales temporales:
public class RoleAssumer {
private final StsClient stsClient;
public RoleAssumer(StsClient stsClient) {
this.stsClient = stsClient;
}
public Credentials assumePaymentServiceRole() {
AssumeRoleRequest request = AssumeRoleRequest.builder()
.roleArn("arn:aws:iam::123456789012:role/PaymentServiceRole")
.roleSessionName("payment-service-session")
.durationSeconds(900)
.build();
AssumeRoleResponse response = stsClient.assumeRole(request);
return response.credentials();
}
}
roleArn especifica el rol a asumir, roleSessionName pone nombre a la sesión para facilitar el rastreo en CloudTrail, durationSeconds limita el tiempo de vida de las credenciales temporales — cuanto más corto, más seguro.
Probar con JUnit 5 y Mockito para verificar que se llama al rol ARN correcto, sin necesidad de llamar realmente a AWS:
@Test
void assumePaymentServiceRole_shouldRequestCorrectRoleArn() {
StsClient stsClient = mock(StsClient.class);
Credentials fakeCredentials = Credentials.builder()
.accessKeyId("AKIAFAKE")
.secretAccessKey("secretFake")
.sessionToken("tokenFake")
.build();
when(stsClient.assumeRole(any(AssumeRoleRequest.class)))
.thenReturn(AssumeRoleResponse.builder()
.credentials(fakeCredentials)
.build());
RoleAssumer assumer = new RoleAssumer(stsClient);
Credentials result = assumer.assumePaymentServiceRole();
ArgumentCaptor<AssumeRoleRequest> captor = ArgumentCaptor.forClass(AssumeRoleRequest.class);
verify(stsClient).assumeRole(captor.capture());
assertThat(captor.getValue().roleArn())
.isEqualTo("arn:aws:iam::123456789012:role/PaymentServiceRole");
assertThat(result.accessKeyId()).isEqualTo("AKIAFAKE");
}
Este test mockea StsClient, captura la solicitud real que se envía, luego afirma que el roleArn es el esperado. De esta manera detectas temprano si alguien cambia accidentalmente el ARN del rol hard-codeado en el código.
assumePaymentServiceRole_shouldRequestCorrectRoleArn() PASSED
9. Errores comunes a evitar
- Asignar
AdministratorAccessa todas las cuentas de servicio porque “optimizaremos después”. La realidad es que nadie vuelve a optimizar, el riesgo persiste para siempre. - Usar access keys para cargas de trabajo en EC2/Lambda en lugar de roles — los access keys se filtran fácilmente, no rotan automáticamente, no tienen vencimiento.
- Olvidar revisar la trust policy del rol:
Principaldemasiado amplio (por ejemplo"AWS": "*") permite que cualquiera asuma el rol. - No usar
Conditioncon MFA para acciones sensibles (borrar bucket, modificar políticas de IAM). Añade"Bool": {"aws:MultiFactorAuthPresent": "true"}para acciones de alto riesgo. - Mezclar muchas responsabilidades en una única política grande dificulta la revisión. Separa por recurso/servicio, usa nombres claros (
PaymentBucketReadWrite, noMyPolicy1).
Cuándo NO necesitas IAM complejo: Si estás haciendo una sandbox personal, aprendiendo un nuevo servicio, crear roles/políticas detalladas desde el principio puede ralentizar tu aprendizaje. En ese caso usa un usuario temporal con una política managed lista (
AmazonS3ReadOnlyAccess), siempre y cuando lo elimines cuando termines.
10. Conclusión
El usuario es para personas que necesitan iniciar sesión manualmente, el rol es para cargas de trabajo y acceso temporal, la política es el conjunto de reglas JSON que decide allow/deny. Dominar el orden de evaluación (deny explícito siempre gana, implicit deny es el defecto) te ayuda a debuguear rápido cuando los permisos se comportan de forma “extraña”. Aplica least privilege comenzando desde cero, añadiendo gradualmente basándote en logs reales, y siempre escribe tests para la lógica de asunción de rol en tu código — esa es la forma de mantener tu sistema AWS seguro sin gastar tiempo en mantenimiento después.