Demo real: permisos efectivos, SCPs, RCPs y una comparación que no salió como esperaba
En la parte anterior expliqué el problema: en entornos con múltiples cuentas, roles y políticas en capas, saber quién tiene acceso real a un recurso no es algo que puedas responder abriendo la consola y revisando una IAM policy. Si todavía no leíste la primera parte, te recomiendo empezar por ahí.
En este post voy directo al laboratorio. Configuré un escenario con dos cuentas dentro de la misma AWS Organization, un bucket S3 en una cuenta, varios roles IAM en otra y distintas capas de control sobre el acceso: IAM, bucket policy, SCP, RCP y un permissions boundary sobre el rol administrador.
El objetivo no fue solo comprobar si una acción funcionaba o no, sino comparar qué estaba configurado, qué reportaba IAM Access Analyzer Internal Access y qué ocurría realmente al ejecutar las APIs.
⚠️ Nota sobre costos antes de empezar
IAM Access Analyzer External Access no tiene costo adicional, pero Internal Access Analyzer sí es una capacidad pagada. AWS cobra $9.00 USD por recurso monitoreado, por analizador y por región, al mes. En este lab solo se monitorea un bucket S3, así que el costo estimado fue $9.00 USD/mes mientras el analizador estuvo activo. Si lo habilitas sobre múltiples recursos en producción, el costo escala linealmente: 10 recursos = $90 USD/mes, 38 recursos en 5 cuentas = $342 USD/mes (ejemplo publicado en la página oficial de precios de AWS).
Lab: validando acceso cross-account dentro de la misma AWS Organization con IAM Access Analyzer Internal Access
En este laboratorio validé cómo IAM Access Analyzer Internal Access representa el acceso de roles ubicados en una cuenta hacia un bucket S3 ubicado en otra cuenta de la misma organización.
El escenario empezó con dos roles:
RoleReadOnlyRoleAdmin
Después añadí dos roles de control para comparar si la forma de declarar las acciones influía en el resultado:
RoleDiagnosticExplicitNoBoundaryRoleAdminWildcardNoBoundary
También añadí tres controles:
SCP para bloquear
s3:DeleteObjectys3:DeleteObjectVersionRCP para bloquear
s3:PutObjectPermissions boundary sobre
RoleAdmin
Arquitectura
AWS Organization
└── OU del laboratorio
├── Cuenta A — propietaria del recurso
│ └── Bucket S3: demo-internal-aa-2026
│
└── Cuenta B — propietaria de los principals
├── RoleReadOnly
├── RoleAdmin
├── RoleDiagnosticExplicitNoBoundary
└── RoleAdminWildcardNoBoundary
La Cuenta A contiene el bucket S3 y la Cuenta B contiene los roles que intentarán acceder a él. Ambas cuentas están dentro de la misma OU, y tanto la SCP como la RCP se adjuntaron a esa OU. La SCP limita las acciones disponibles para los principals de las cuentas que contiene, mientras que la RCP limita el acceso sobre sus recursos; en este lab, el ARN definido en la RCP restringe el deny al bucket de la Cuenta A. El permissions boundary se adjunta directamente a RoleAdmin.
La imagen anterior corresponde a la primera fase del laboratorio, cuando todavía trabajaba únicamente con RoleReadOnly y RoleAdmin. Los otros dos roles se añadieron después para controlar mejor la comparación.
Objetivo
El laboratorio comprueba siete cosas:
Qué roles dentro de la organización aparecen con acceso al bucket.
Qué acciones reporta el finding para cada rol.
Cómo una SCP bloquea una acción aunque IAM y la bucket policy la permitan.
Cómo una RCP bloquea una acción desde el lado del recurso.
Cómo un permissions boundary limita el máximo permiso de un rol.
Qué cambia al declarar acciones explícitas frente a utilizar
s3:*.Si el resultado del finding coincide con la ejecución real de las APIs.
| Rol | Permiso configurado | Boundary |
|---|---|---|
| RoleReadOnly | ListBucket y GetObject | No |
| RoleAdmin | s3:* | Sí: limita a ListBucket, GetObject, PutObject y DeleteObject |
| RoleAdminWildcardNoBoundary | s3:* | No |
| RoleDiagnosticExplicitNoBoundary | ListBucket, GetBucketPolicy, GetObject, PutObject y DeleteObject | No |
La SCP bloquea s3:DeleteObject y s3:DeleteObjectVersion.
La RCP bloquea s3:PutObject sobre el bucket del lab.
Paso 1: Crear los roles en la Cuenta B
Desde CloudShell en la Cuenta B, se definen las variables de entorno:
export ACCOUNT_B_ID="<ACCOUNT_B_ID>"
export BUCKET_NAME="demo-internal-aa-2026" # ejemplo
Se valida la identidad actual:
aws sts get-caller-identity
Luego se crea la trust policy para permitir asumir los roles desde la misma Cuenta B:
cat > trust-account-b.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowAssumeRoleFromAccountB",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::$ACCOUNT_B_ID:root"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
Se crean los cuatro roles:
for ROLE in \
RoleReadOnly \
RoleAdmin \
RoleDiagnosticExplicitNoBoundary \
RoleAdminWildcardNoBoundary
do
aws iam create-role \
--role-name "$ROLE" \
--assume-role-policy-document file://trust-account-b.json
done
Paso 2: Asignar permisos IAM a los roles
RoleReadOnly — solo puede listar y leer:
cat > role-readonly-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::$BUCKET_NAME"
},
{
"Sid": "ReadObjects",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::$BUCKET_NAME/*"
}
]
}
EOF
aws iam put-role-policy \
--role-name RoleReadOnly \
--policy-name DemoBucketReadOnlyAccess \
--policy-document file://role-readonly-policy.json
RoleAdmin — Permite s3:* sobre el bucket y sus objetos:
cat > role-admin-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AdminBucketAccess",
"Effect": "Allow",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::$BUCKET_NAME",
"arn:aws:s3:::$BUCKET_NAME/*"
]
}
]
}
EOF
aws iam put-role-policy \
--role-name RoleAdmin \
--policy-name DemoBucketAdminAccess \
--policy-document file://role-admin-policy.json
RoleDiagnosticExplicitNoBoundary
Este rol declara de manera explícita las acciones que me interesaba comparar:
cat > role-diagnostic-explicit-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DiagnosticBucketActions",
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetBucketPolicy"
],
"Resource": "arn:aws:s3:::$BUCKET_NAME"
},
{
"Sid": "DiagnosticObjectActions",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::$BUCKET_NAME/*"
}
]
}
EOF
aws iam put-role-policy \
--role-name RoleDiagnosticExplicitNoBoundary \
--policy-name DiagnosticExplicitNoBoundaryAccess \
--policy-document file://role-diagnostic-explicit-policy.json
RoleAdminWildcardNoBoundary
Este rol mantiene el wildcard y no tiene boundary:
cat > role-admin-wildcard-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AdminWildcardBucketAccess",
"Effect": "Allow",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::$BUCKET_NAME",
"arn:aws:s3:::$BUCKET_NAME/*"
]
}
]
}
EOF
aws iam put-role-policy \
--role-name RoleAdminWildcardNoBoundary \
--policy-name AdminWildcardNoBoundaryAccess \
--policy-document file://role-admin-wildcard-policy.json
Paso 2.1: Añadir permissions boundary a RoleAdmin
Además de su inline policy con s3:*, RoleAdmin tiene un permissions boundary.
La idea no fue reemplazar la SCP ni la RCP. Quería añadir una capa que materializara un conjunto explícito de acciones máximas para ese rol.
El boundary permite:
s3:ListBuckets3:GetObjects3:PutObjects3:DeleteObject
No permite, por ejemplo:
s3:GetBucketPolicys3:GetBucketAcls3:GetBucketVersioning
cat > role-admin-boundary.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::$BUCKET_NAME"
},
{
"Sid": "PowerUserObjects",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::$BUCKET_NAME/*"
}
]
}
EOF
aws iam create-policy \
--policy-name boundary-adminuser \
--policy-document file://role-admin-boundary.json
aws iam put-role-permissions-boundary \
--role-name RoleAdmin \
--permissions-boundary \
"arn:aws:iam::$ACCOUNT_B_ID:policy/boundary-adminuser"
Validación:
aws iam get-role \
--role-name RoleAdmin \
--query 'Role.PermissionsBoundary' \
--output json
Paso 3: Configurar la bucket policy en la Cuenta A
Desde CloudShell en la Cuenta A:
export ACCOUNT_B_ID="<ACCOUNT_B_ID>"
export BUCKET_NAME="demo-internal-aa-2026"
La bucket policy concede a cada rol las mismas acciones utilizadas en su identity policy:
cat > bucket-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowReadOnlyListBucket",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::$ACCOUNT_B_ID:role/RoleReadOnly"
},
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::$BUCKET_NAME"
},
{
"Sid": "AllowReadOnlyGetObject",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::$ACCOUNT_B_ID:role/RoleReadOnly"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::$BUCKET_NAME/*"
},
{
"Sid": "AllowAdminFullBucketAccess",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::$ACCOUNT_B_ID:role/RoleAdmin"
},
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::$BUCKET_NAME",
"arn:aws:s3:::$BUCKET_NAME/*"
]
},
{
"Sid": "AllowAdminWildcardNoBoundaryFullBucketAccess",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::$ACCOUNT_B_ID:role/RoleAdminWildcardNoBoundary"
},
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::$BUCKET_NAME",
"arn:aws:s3:::$BUCKET_NAME/*"
]
},
{
"Sid": "AllowDiagnosticExplicitNoBoundaryBucketAccess",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::$ACCOUNT_B_ID:role/RoleDiagnosticExplicitNoBoundary"
},
"Action": [
"s3:ListBucket",
"s3:GetBucketPolicy"
],
"Resource": "arn:aws:s3:::$BUCKET_NAME"
},
{
"Sid": "AllowDiagnosticExplicitNoBoundaryObjectAccess",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::$ACCOUNT_B_ID:role/RoleDiagnosticExplicitNoBoundary"
},
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::$BUCKET_NAME/*"
}
]
}
EOF
aws s3api put-bucket-policy \
--bucket "$BUCKET_NAME" \
--policy file://bucket-policy.json
Validación:
aws s3api get-bucket-policy \
--bucket "$BUCKET_NAME" \
--query Policy \
--output text |
jq .
En la cuenta real también quedó una entrada anterior para
RolePowerUser. No la utilicé en la comparación final porque los cuatro roles anteriores ya cubrían los casos que necesitaba aislar.
Paso 4: Crear la SCP en AWS Organizations
Desde la management account (o una con permisos de Organizations), se crean las variables:
export BUCKET_NAME="demo-internal-aa-2026" # Nombre del bucket seteado al inicio
export OU_ID="<OU_ID>"
export SCP_NAME="DenyDeleteObjectDemoBucket"
La SCP niega el borrado de objetos sobre el bucket del lab:
cat > deny-delete-demo-bucket-scp.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyDeleteObjectOnDemoBucket",
"Effect": "Deny",
"Action": [
"s3:DeleteObject",
"s3:DeleteObjectVersion"
],
"Resource": "arn:aws:s3:::$BUCKET_NAME/*"
}
]
}
EOF
Se crea la SCP y se adjunta a la OU, donde viven tanto la Cuenta A como la Cuenta B:
POLICY_ID=$(aws organizations create-policy \
--name "$SCP_NAME" \
--description "Deny object deletion on demo S3 bucket for IAM Access Analyzer lab" \
--type SERVICE_CONTROL_POLICY \
--content file://deny-delete-demo-bucket-scp.json \
--query "Policy.PolicySummary.Id" \
--output text)
echo "$POLICY_ID"
aws organizations attach-policy \
--policy-id "$POLICY_ID" \
--target-id "$OU_ID"
Para validar que quedó adjunta:
aws organizations list-policies-for-target \
--target-id "$OU_ID" \
--filter SERVICE_CONTROL_POLICY \
--output table
Paso 4.1: Añadir una RCP sobre el bucket
Después de probar la SCP, amplié el ejercicio con una Resource Control Policy (RCP) para bloquear s3:PutObject directamente sobre el bucket del lab.
La intención de esta fase ya no era solo ver un deny desde el lado del principal, sino comparar qué pasa cuando el deny viene del lado del recurso.
La RCP quedó así:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyPutObjectOnLabBucket",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::demo-internal-aa-2026/*"
}
]
}
Un detalle importante: en RCP el elemento correcto es Principal con mayúscula, y el valor permitido para este tipo de policy es "*".
Se creó la policy y se adjuntó a la OU, donde viven tanto la cuenta del bucket como la cuenta de los roles:
export OU_ID="<OU_ID>"
cat > deny-putobject-rcp.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyPutObjectOnLabBucket",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::demo-internal-aa-2026/*"
}
]
}
EOF
RCP_ID=$(aws organizations create-policy \
--name "DenyPutObjectOnLabBucketRCP" \
--description "RCP to deny PutObject on the lab bucket" \
--type RESOURCE_CONTROL_POLICY \
--content file://deny-putobject-rcp.json \
--query "Policy.PolicySummary.Id" \
--output text)
echo "$RCP_ID"
aws organizations attach-policy \
--policy-id "$RCP_ID" \
--target-id "$OU_ID"
Para validar que quedó adjunta:
aws organizations list-policies-for-target \
--target-id "$OU_ID" \
--filter RESOURCE_CONTROL_POLICY \
--output table
Con esto, el laboratorio quedó así:
-
SCP → bloquea
DeleteObjectyDeleteObjectVersion -
RCP → bloquea
PutObject -
Permissions boundary → limita el máximo permiso de
RoleAdmin
Y eso dejó una comparación bastante útil porque ya no estaba probando una sola capa organizacional, sino varias a la vez.
Paso 5: Crear el IAM Access Analyzer Internal Access
Desde la consola:
IAM > Access Analyzer > Analyzer settings > Create analyzer
Configuración:
Analysis: Resource analysis - Internal access
Zone of trust: Entire organization
Recurso: arn:aws:s3:::demo-internal-aa-2026
Un detalle importante: para S3, el recurso debe ser el ARN del bucket.
Correcto:
arn:aws:s3:::demo-internal-aa-2026
Los object ARNs con prefijos o wildcards no están soportados como recurso monitoreado en Internal Access Analyzer. Solo el ARN del bucket sin ruta.
Después de crear el analizador, esperar hasta que aparezcan los primeros findings. Para realizar la comparación final conviene dejar tiempo suficiente para el reanálisis, como se explica más adelante.
Antes de las pruebas: resumen rápido de qué hace cada capa
Si vienes del post anterior, esta tabla resume rápido el alcance de cada mecanismo en este lab:
| Capa | Qué controla | Alcance principal | En este lab |
|---|---|---|---|
| IAM policy | Lo que el usuario o rol puede intentar hacer | Principal | Define los permisos base de los cuatro roles |
| Bucket policy | Qué principals pueden acceder al bucket | Recurso | Habilita el acceso cross-account desde la Cuenta B |
| SCP | Máximo permiso disponible para principals en cuentas miembro | Cuenta / OU / organización | Bloquea DeleteObject y DeleteObjectVersion
|
| RCP | Máximo permiso disponible sobre recursos en cuentas miembro | Recurso en cuenta miembro / OU / organización | Bloquea PutObject sobre el bucket |
| Permissions boundary | Máximo permiso que puede tener un principal IAM | Principal IAM | Limita a RoleAdmin; no permite GetBucketPolicy
|
La forma corta de leerlo es esta:
IAM dice qué intenta hacer el rol
bucket policy abre el camino cross-account
SCP limita desde el lado del principal
RCP limita desde el lado del recurso
boundary limita el máximo permiso del principal
Paso 6: Probar acceso asumiendo los roles
Desde CloudShell en la Cuenta B:
export ACCOUNT_B_ID="<ACCOUNT_B_ID>"
export BUCKET_NAME="demo-internal-aa-2026"
export PREFIX="aa-lab"
Se definen funciones para manejar las credenciales y asumir roles fácilmente:
clear_role() {
unset AWS_ACCESS_KEY_ID
unset AWS_SECRET_ACCESS_KEY
unset AWS_SESSION_TOKEN
unset AWS_SECURITY_TOKEN
}
assume_role() {
ROLE_NAME="$1"
clear_role
CREDS=$(aws sts assume-role \
--role-arn "arn:aws:iam::$ACCOUNT_B_ID:role/$ROLE_NAME" \
--role-session-name "lab-$ROLE_NAME" \
--output json)
export AWS_ACCESS_KEY_ID=$(echo "$CREDS" | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo "$CREDS" | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo "$CREDS" | jq -r '.Credentials.SessionToken')
echo "Asumiendo rol: $ROLE_NAME"
aws sts get-caller-identity
}
Prueba con RoleReadOnly
assume_role RoleReadOnly
aws s3 ls "s3://$BUCKET_NAME/$PREFIX/"
aws s3 cp "s3://$BUCKET_NAME/$PREFIX/seed.txt" readonly-seed.txt
cat readonly-seed.txt
echo "test readonly put" > readonly-put.txt
aws s3 cp readonly-put.txt "s3://$BUCKET_NAME/$PREFIX/readonly-put.txt"
Resultado observado: PutObject falla con AccessDenied por RCP.
aws s3 rm "s3://$BUCKET_NAME/$PREFIX/seed.txt"
Resultado observado: DeleteObject falla con AccessDenied por IAM / identity-based policy.
Prueba con RoleAdmin
assume_role RoleAdmin
aws s3 ls "s3://$BUCKET_NAME/$PREFIX/"
aws s3 cp "s3://$BUCKET_NAME/$PREFIX/seed.txt" admin-seed.txt
cat admin-seed.txt
echo "test admin put" > admin-put.txt
aws s3 cp admin-put.txt "s3://$BUCKET_NAME/$PREFIX/admin-put.txt"
Resultado observado: PutObject falla con AccessDenied por RCP.
aws s3 rm "s3://$BUCKET_NAME/$PREFIX/seed.txt"
Resultado observado: DeleteObject falla con AccessDenied por SCP.
Prueba específica para validar el permissions boundary
Para comprobar cómo aparecía el boundary en el mensaje de denegación, ejecuté una acción que:
-
RoleAdminsí tendría pors3:* - la bucket policy también permite
- no estaba siendo bloqueada por SCP ni por RCP
- pero el permissions boundary no permite
La acción elegida fue:
assume_role RoleAdmin
aws s3api get-bucket-policy --bucket "$BUCKET_NAME"
Y ahí sí apareció la evidencia que faltaba: el AccessDenied indica que RoleAdmin no está autorizado para **s3:GetBucketPolicy** porque ningún permissions boundary permite esa acción.
El mensaje de AccessDenied señaló expresamente que ningún permissions boundary permitía s3:GetBucketPolicy, por lo que confirmó que el boundary participó en la evaluación.
Pruebas adicionales: acciones explícitas frente a s3:*
Para aislar mejor el comportamiento del finding, añadí dos roles sin permissions boundary:
-
RoleDiagnosticExplicitNoBoundary, con las acciones declaradas individualmente. -
RoleAdminWildcardNoBoundary, cons3:*.
Ambos tenían acceso concedido tanto por su identity policy como por la bucket policy, y estaban sujetos a la misma SCP y RCP.
RoleDiagnosticExplicitNoBoundary tenía permitidos GetBucketPolicy, GetObject, PutObject, DeleteObject y ListBucket. Sin embargo, su finding mostró únicamente:
s3:GetBucketPolicys3:GetObjects3:ListBucket
Es decir, PutObject y DeleteObject no aparecieron, tal como esperaba después de aplicar la RCP y la SCP.
En cambio, RoleAdminWildcardNoBoundary tenía s3:* y su finding mantuvo una lista amplia de acciones, incluyendo:
s3:PutObjects3:DeleteObjects3:DeleteObjectVersion
Esto ocurrió aunque el mismo finding mostraba las restricciones RCP y SCP como APPLIED.
En la captura se observa que el finding de RoleAdminWildcardNoBoundary conserva PutObject, DeleteObject y DeleteObjectVersion, aunque las restricciones SCP y RCP aparecen como aplicadas.
Para comprobar el acceso real, asumí ambos roles y ejecuté las cuatro operaciones principales.
En los dos casos:
-
ListBucketyGetObjectfueron permitidos. -
PutObjectdevolvióAccessDeniedpor un explicit deny de la RCP. -
DeleteObjectdevolvióAccessDeniedpor un explicit deny de la SCP.
| Rol | Forma de declarar permisos | Resultado esperado | Finding observado | Runtime |
|---|---|---|---|---|
RoleDiagnosticExplicitNoBoundary |
Acciones explícitas | Sin Put ni Delete | No muestra Put ni Delete | List y Get permitidos; Put denegado por RCP y Delete por SCP |
RoleAdminWildcardNoBoundary |
s3:* |
Sin Put, Delete ni DeleteObjectVersion | Conserva esas acciones | List y Get permitidos; Put denegado por RCP y Delete por SCP |
La comparación muestra una diferencia observable entre ambos findings. No determina por sí sola la causa interna del comportamiento, pero sí permite contrastar lo reportado por el analyzer con el resultado real de las APIs.
Ver los findings en IAM Access Analyzer
Con los roles ya creados y el analizador activo, el siguiente paso es revisar qué detectó IAM Access Analyzer sobre el bucket.
En la consola de IAM, dentro de Access Analyzer, puedes ver todos los analizadores activos. En este lab, el que interesa es el de internal access.
Al entrar al analizador y seleccionar el bucket, aparece una lista de findings. Y aquí viene un detalle importante: no vas a ver solo los roles del laboratorio. También pueden aparecer roles internos de la misma cuenta dueña del bucket, como roles administrativos, roles de AWS IAM Identity Center (SSO) o service roles.
Si esperabas encontrar únicamente los cuatro roles creados para el laboratorio, aquí es donde te das cuenta de que olvidaste considerar algo más: el analizador muestra acceso efectivo al recurso, no solo lo que agregaste manualmente en la bucket policy del lab.
Y acá está una de las claves del escenario: los principals de la misma cuenta del bucket no necesitan estar expresamente nombrados en la bucket policy para tener acceso. Si un rol o usuario de la cuenta propietaria ya tiene permisos por su propia IAM identity policy, puede acceder igual. AWS lo documenta de forma explícita para S3 aquí: Granting access to an IAM principal in the same account does not require updating the bucket policy.
La bucket policy se vuelve realmente obligatoria cuando quieres habilitar acceso cross-account, como sí pasa con los roles del laboratorio.
Qué muestra la lista de findings
En la vista resumida, estas son las columnas más útiles:
| Campo | Qué significa |
|---|---|
| ID de resultado | Identificador único del finding |
| Recurso | El bucket analizado |
| Cuenta del propietario del recurso | La cuenta dueña del bucket |
| Entidad principal | El rol o principal que tiene acceso |
| Condición | Si el acceso depende de alguna condición |
| Compartido a través de | Qué capa origina el acceso, por ejemplo bucket policy o bucket ACL |
| Nivel de acceso | Categorías generales de acciones: Read, List, Write, Permissions, Tagging |
| Restricción RCP | Si el analizador consideró una Resource Control Policy |
| Restricción SCP | Si el analizador consideró una Service Control Policy |
| Estado | Si el finding sigue activo o fue archivado |
Dos columnas valen especial atención en este lab:
Restricción SCP = Aplicado
Restricción RCP = Aplicado
Eso no significa por sí solo que una acción específica como s3:DeleteObject o s3:PutObject haya desaparecido de la lista. Lo que sí significa es que esas capas fueron consideradas en la evaluación.
Ver el detalle de un finding
La lista resumida ayuda, pero el valor real aparece cuando entras al detalle de cada finding.
En el caso de RoleReadOnly, el resultado es bastante limpio:
Principal:
Account B / RoleReadOnlyCuenta del principal:
Account BCompartido a través de:
Política del bucket-
Acceso reportado por el finding:
-
Read→s3:GetObject -
List→s3:ListBucket
-
Ese finding ya no muestra solo categorías amplias. Muestra directamente las acciones que el analizador está reportando para ese principal sobre el bucket.
Qué demuestra esto sobre la SCP, la RCP y el permissions boundary
Acá apareció una de las partes más interesantes del ejercicio.
Al comienzo solo tenía RoleReadOnly y RoleAdmin, pero después añadí dos roles para comparar mejor el resultado:
-
RoleDiagnosticExplicitNoBoundary, con las acciones declaradas individualmente. -
RoleAdminWildcardNoBoundary, cons3:*y sin permissions boundary.
Los cuatro roles estaban sujetos a la misma SCP y RCP, pero los findings no se presentaron exactamente igual.
| Rol | Configuración | Finding observado | Resultado runtime |
|---|---|---|---|
RoleReadOnly |
GetObject y ListBucket
|
Muestra GetObject y ListBucket
|
Lectura permitida; escritura y borrado denegados |
RoleDiagnosticExplicitNoBoundary |
Get, Put, Delete, List y GetBucketPolicy declarados explícitamente | No muestra PutObject ni DeleteObject
|
List y Get permitidos; Put denegado por RCP y Delete por SCP |
RoleAdmin |
s3:* limitado por un permissions boundary |
Muestra únicamente GetObject y ListBucket
|
Put denegado por RCP, Delete por SCP y GetBucketPolicy por boundary |
RoleAdminWildcardNoBoundary |
s3:* sin permissions boundary |
Mantiene PutObject, DeleteObject y DeleteObjectVersion
|
List y Get permitidos; Put denegado por RCP y Delete por SCP |
Tomando como referencia las cuatro operaciones principales del laboratorio (ListBucket, GetObject, PutObject y DeleteObject), en los tres primeros casos el finding coincidió con el conjunto de acciones que esperaba después de considerar las distintas capas.
La diferencia apareció con RoleAdminWildcardNoBoundary. Su finding seguía incluyendo acciones que las pruebas runtime confirmaron como denegadas:
-
PutObjectdevolvióAccessDeniedpor un explicit deny de la RCP. -
DeleteObjectdevolvióAccessDeniedpor un explicit deny de la SCP.
Además, en los findings las dos restricciones aparecían como:
resourceControlPolicyRestriction: APPLIED
serviceControlPolicyRestriction: APPLIED
Para todo el set de pruebas, después de completar los cambios esperé al menos 24 horas antes de volver a revisar los findings. También confirmé mediante get-finding-v2 que los resultados habían sido reanalizados.
De esta manera evité comparar findings que todavía pudieran estar pendientes de actualización.
Esto no significa que la SCP o la RCP hayan fallado. Las pruebas runtime muestran precisamente lo contrario: ambas políticas bloquearon las operaciones.
Lo que sí puedo documentar es una diferencia entre las acciones reportadas por el finding y las acciones que el rol pudo ejecutar realmente en este escenario específico.
Por eso, mi lectura final del laboratorio es esta:
- El finding es muy útil para identificar principals, rutas de acceso y acciones reportadas sobre el recurso.
- Las columnas SCP y RCP permiten saber si esas capas participaron en la evaluación.
- Cuando necesitas validar una API concreta, la prueba runtime sigue siendo una evidencia importante.
- Si el finding y runtime no coinciden, conviene revisar directamente IAM, bucket policy, boundary, SCP, RCP y confirmar que el analyzer tuvo tiempo suficiente para actualizarse antes de sacar una conclusión.
No intento determinar desde fuera cuál es la causa interna de esta diferencia ni generalizar el resultado a todos los servicios o configuraciones. El objetivo es dejar la configuración y la evidencia del laboratorio de forma reproducible.
La referencia oficial utilizada para interpretar los campos APPLIED es: InternalAccessDetails – AWS IAM Access Analyzer API
Por qué aparecen otros roles aparte de los del lab
También aparecen findings para roles internos como administradores federados, roles de Control Tower y service roles de la propia cuenta propietaria del bucket.
Eso no significa que la bucket policy del lab les haya dado acceso a todos ellos.
Lo que significa es que el bucket también es accesible por principals internos de la misma cuenta propietaria del recurso, y Access Analyzer los muestra porque su trabajo no es enseñarte solo el acceso cross-account del ejercicio, sino el acceso efectivo total dentro del alcance del analizador.
Dicho de otra forma:
Account A es la cuenta dueña del bucket.
Account B es la cuenta donde viven los cuatro roles utilizados en el laboratorio.
Los roles del lab explican el escenario cross-account.
Los otros findings muestran acceso intra-account de roles que ya existen en Account A y que tienen permisos por otras capas del entorno.
En un laboratorio pequeño esto ya se vuelve fácil de perder de vista. Ahora imagina el mismo problema en una organización real con decenas o cientos de cuentas, miles de roles, roles federados de SSO, service roles, roles de Control Tower, políticas administradas, inline policies, bucket policies, ACLs, SCPs, RCPs, permissions boundaries y además múltiples recursos aparte de un solo bucket. En ese punto, responder “quién puede acceder realmente a qué” deja de ser una revisión manual y pasa a ser un problema de correlación entre varias capas de permisos.
Y esa es precisamente la complicación: el acceso efectivo no vive en una sola policy. Sale de la combinación de identity policies, resource policies, permisos intra-account, permisos cross-account y restricciones organizacionales. Si revisas solo una bucket policy, puedes pensar que entiendes el acceso del recurso, pero en realidad te puedes estar perdiendo principals internos de la misma cuenta, roles heredados del entorno, service roles o accesos que siguen siendo válidos por otras rutas. Mientras más cuentas, roles y recursos agregas, más difícil se vuelve distinguir entre acceso esperado, acceso heredado, acceso indirecto y acceso realmente riesgoso.
Por eso herramientas como IAM Access Analyzer toman valor en escenarios grandes: no porque reemplacen entender IAM, sino porque ayudan a reducir la complejidad operativa cuando ya no estás mirando cuatro roles de laboratorio, sino cientos o miles de identidades con permisos cruzados sobre muchos recursos.
El botón Archivar: para qué sirve
Archivar un finding no modifica el acceso ni cambia las policies. Solo cambia la forma en que administras ese resultado dentro del analyzer.
Cuando archivas un finding:
- deja de mostrarse entre los findings activos;
- permanece disponible como archivado;
- puedes utilizar archive rules para archivar automáticamente nuevos findings que coincidan con criterios definidos;
- al crear una regla puedes elegir aplicarla solo a nuevos findings o también archivar findings activos existentes.
Si desaparece la ruta de acceso, el finding pasa a Resolved. Si la ruta cambia de forma relevante, Access Analyzer puede resolver el finding anterior y generar uno nuevo.
Resultados del laboratorio
| Rol | ListBucket | GetObject | PutObject | DeleteObject |
|---|---|---|---|---|
RoleReadOnly |
✅ Permitido | ✅ Permitido | ❌ RCP | ❌ IAM |
RoleAdmin |
✅ Permitido | ✅ Permitido | ❌ RCP | ❌ SCP |
RoleDiagnosticExplicitNoBoundary |
✅ Permitido | ✅ Permitido | ❌ RCP | ❌ SCP |
RoleAdminWildcardNoBoundary |
✅ Permitido | ✅ Permitido | ❌ RCP | ❌ SCP |
La tabla muestra únicamente las llamadas realmente ejecutadas. La comparación de los findings se encuentra en la sección anterior.
Qué demuestra este lab
Para que una acción cross-account funcione en este escenario se necesitan estas condiciones:
- Una identity policy en el rol de la Cuenta B que permita la acción.
- Una bucket policy en la Cuenta A que conceda esa acción al rol externo.
- Que no exista un explicit deny aplicable a esa acción y a ese recurso.
- Si el rol tiene un permissions boundary, que la acción esté incluida dentro de su máximo permitido.
Esto no significa que no puedan existir SCP o RCP. En este laboratorio existen ambas, pero bloquean operaciones concretas: la SCP bloquea DeleteObject y DeleteObjectVersion, mientras que la RCP bloquea PutObject. Las demás acciones continúan disponibles según las otras policies aplicables.
La primera parte del lab muestra esto con SCP: el rol tiene permiso IAM, la bucket policy también se lo da, pero el explicit deny organizacional gana.
La ampliación con RCP refuerza la misma idea desde el lado del recurso: aunque la ruta de acceso exista, el control organizacional sigue pudiendo cortar la operación en runtime.
Y la prueba con GetBucketPolicy añade una tercera lección importante: un permissions boundary también puede participar como capa limitante y aparecer expresamente identificado en el mensaje de AccessDenied.
Lo importante es que el lab no solo muestra el deny, sino también algo más útil para producción: te obliga a entender cómo leer el finding y cómo interpretar los errores runtime sin asumir que uno de los dos es un espejo perfecto del otro.
Antes de habilitarlo en producción: el pricing
Acá hay algo que casi ningún tutorial menciona y que conviene saber antes de habilitar esto en producción.
IAM Access Analyzer Internal Access no es gratuito para el análisis de permisos efectivos.
Algunas consideraciones prácticas:
No lo habilites sobre todos los recursos por default. Empieza por los buckets que contengan datos sensibles o regulados.
Usa etiquetas para identificar recursos críticos (
DataClassification: Confidential, por ejemplo) y limita el scope del analizador a esos recursos.Tiene más sentido en recursos específicos de alto riesgo: buckets con datos PCI, repositorios de logs de seguridad, buckets compartidos cross-account, tablas de DynamoDB, snapshots de bases de datos RDS y snapshots de clústeres RDS.
Para el resto, IAM Access Advisor puede ayudar a priorizar una revisión manual periódica y reducir el número de recursos sobre los que necesitas habilitar Internal Access Analyzer.
La recomendación concreta: define primero qué es un recurso crítico en tu organización, aplica tags de clasificación de datos, y usa Access Analyzer Internal Access solo sobre ese subconjunto. No sobre todo.
Nota práctica: si pensaste en activarlo un momento y luego borrarlo, ojo: en mi caso el cobro se reflejó el mismo día que lo activé y por la unidad mensual completa del recurso monitoreado. Eso ya de por sí te obliga a pensar mejor el experimento antes de habilitarlo.
Y acá sumaría otra recomendación basada en lo que me pasó en este lab: antes de habilitarlo sobre recursos críticos, entiende bien qué tipo de preguntas quieres responder con él.
Si tu pregunta es “quién puede llegar a este recurso y por qué camino”, el analyzer te aporta muchísimo.
Si tu pregunta es “esta API exacta quedó efectivamente bloqueada por esta capa organizacional o de identidad”, entonces probablemente también vas a querer hacer validación runtime para no llevarte sorpresas al interpretar el finding.
Perspectiva Red Team
Access Analyzer no es solamente útil para equipos defensivos, pero esta parte necesita contexto.
Un usuario o rol necesita permisos para consultar el analyzer y sus findings. Además, las llamadas realizadas desde la consola o mediante las APIs de IAM Access Analyzer se registran en AWS CloudTrail.
Por tanto, no es correcto asumir que cualquier rol comprometido en una cuenta miembro puede consultar el analyzer organizacional o que su uso sea invisible.
En un ejercicio Red Team interno y autorizado, una identidad que ya tenga acceso al analyzer podría utilizar los findings para:
- Revisar rutas de acceso sin llamar directamente al recurso objetivo.
- Priorizar relaciones cross-account relevantes.
- Comparar acceso reportado frente a acceso ejecutable.
- Evaluar el impacto de SCP, RCP y permissions boundaries.
Lo que Access Analyzer muestra (y lo que no)
Para dejarlo claro:
✅ Qué identidades tienen acceso a un recurso específico
✅ Qué capas participaron en la evaluación, incluyendo IAM, resource policies, SCPs y RCPs
✅ Findings exportables para evidencia de auditoría
✅ Visibilidad centralizada a escala de organización desde la cuenta que administra el analyzer
Y también algo que este lab me dejó mucho más claro:
⚠️ No siempre lo usaría como única evidencia para afirmar que una API concreta desapareció del acceso efectivo solo porque una SCP o una RCP la bloquean en runtime
⚠️ El mensaje de AccessDenied no siempre te va a mostrar todas las capas que podrían estar contribuyendo a la denegación
⚠️ Para denegaciones puntuales por capa organizacional o por boundary, conviene complementar con una prueba runtime controlada
Conclusión
La pregunta con la que empezó esta serie era:
¿Quién puede acceder realmente a tus recursos en AWS?
Después de este lab, la respuesta práctica sigue siendo que no puedes resolverlo revisando una sola policy.
IAM Access Analyzer Internal Access aporta valor para descubrir principals, acciones y rutas de acceso sobre recursos críticos. También permite centralizar los resultados a nivel organizacional y consultar los findings mediante API.
El laboratorio dejó además una observación concreta: tomando como referencia las cuatro operaciones principales, en tres casos controlados el finding coincidió con el conjunto de acciones esperado. En el rol con s3:* sin permissions boundary, el finding siguió mostrando PutObject, DeleteObject y DeleteObjectVersion, aunque las restricciones aparecían como APPLIED. Las pruebas runtime confirmaron que PutObject era denegado por la RCP y DeleteObject por la SCP.
No voy a especular sobre la causa exacta. Lo que sí puedo hacer es dejar la configuración, los findings y las pruebas runtime para que el resultado sea reproducible.
Y debido a que Internal Access Analyzer es una capacidad pagada, también recomiendo pensar bien qué preguntas quieres responder antes de habilitarlo. Si buscas identificar quién puede acceder a un bucket crítico y por qué ruta, resulta especialmente útil. Pero si solo necesitas validar una API puntual, quizá una revisión de policies y una prueba runtime controlada sean suficientes.
El costo se calcula por recurso monitoreado, por analyzer y por región, así que habilitarlo sin definir previamente el alcance puede terminar generando un gasto mayor al esperado. En producción empezaría por recursos realmente críticos, como buckets con datos sensibles, logs de seguridad o recursos compartidos entre cuentas, y ampliaría el alcance solo cuando exista una necesidad clara.
Para mí, esa es la lectura más útil del ejercicio:
- usa el analyzer para ganar visibilidad;
- entiende qué significa cada campo;
- valida las APIs críticas cuando necesites una respuesta puntual;
- define el alcance antes de activarlo;
- evita interpretar una sola pantalla sin revisar las demás capas.
💡 ¿Encontraste algún acceso que no esperabas al analizar tus recursos? Ese suele ser el momento en que este tipo de herramientas pasan de “interesante” a “necesaria”.
🔗 Si llegaste directo aquí, te recomiendo leer la Parte 1 primero para entender el contexto.
📌 Y si quieres el contexto de SCPs, RCPs y boundaries desde cero, tengo un post anterior donde explico cada tipo de política con ejemplos concretos.











Top comments (0)