DEV Community

Cover image for 🛡️ AWS IAM Access Analyzer — Parte 3/4
Terry Quispe Paniagua
Terry Quispe Paniagua

Posted on

🛡️ AWS IAM Access Analyzer — Parte 3/4

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:

  • RoleReadOnly

  • RoleAdmin

Después añadí dos roles de control para comparar si la forma de declarar las acciones influía en el resultado:

  • RoleDiagnosticExplicitNoBoundary

  • RoleAdminWildcardNoBoundary

También añadí tres controles:

  • SCP para bloquear s3:DeleteObject y s3:DeleteObjectVersion

  • RCP para bloquear s3:PutObject

  • Permissions 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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Qué roles dentro de la organización aparecen con acceso al bucket.

  2. Qué acciones reporta el finding para cada rol.

  3. Cómo una SCP bloquea una acción aunque IAM y la bucket policy la permitan.

  4. Cómo una RCP bloquea una acción desde el lado del recurso.

  5. Cómo un permissions boundary limita el máximo permiso de un rol.

  6. Qué cambia al declarar acciones explícitas frente a utilizar s3:*.

  7. 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
Enter fullscreen mode Exit fullscreen mode

Se valida la identidad actual:

aws sts get-caller-identity
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:ListBucket

  • s3:GetObject

  • s3:PutObject

  • s3:DeleteObject

No permite, por ejemplo:

  • s3:GetBucketPolicy

  • s3:GetBucketAcl

  • s3: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"
Enter fullscreen mode Exit fullscreen mode

Validación:

aws iam get-role \
  --role-name RoleAdmin \
  --query 'Role.PermissionsBoundary' \
  --output json
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Validación:

aws s3api get-bucket-policy \
  --bucket "$BUCKET_NAME" \
  --query Policy \
  --output text |
jq .
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Para validar que quedó adjunta:

aws organizations list-policies-for-target \
  --target-id "$OU_ID" \
  --filter SERVICE_CONTROL_POLICY \
  --output table
Enter fullscreen mode Exit fullscreen mode

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/*"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Para validar que quedó adjunta:

aws organizations list-policies-for-target \
  --target-id "$OU_ID" \
  --filter RESOURCE_CONTROL_POLICY \
  --output table
Enter fullscreen mode Exit fullscreen mode

Con esto, el laboratorio quedó así:

  • SCP → bloquea DeleteObject y DeleteObjectVersion
  • 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
Enter fullscreen mode Exit fullscreen mode

Configuración:

Analysis:     Resource analysis - Internal access
Zone of trust: Entire organization
Recurso:      arn:aws:s3:::demo-internal-aa-2026
Enter fullscreen mode Exit fullscreen mode

Un detalle importante: para S3, el recurso debe ser el ARN del bucket.

Correcto:

arn:aws:s3:::demo-internal-aa-2026
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Prueba con RoleReadOnly

assume_role RoleReadOnly
Enter fullscreen mode Exit fullscreen mode
aws s3 ls "s3://$BUCKET_NAME/$PREFIX/"
Enter fullscreen mode Exit fullscreen mode
aws s3 cp "s3://$BUCKET_NAME/$PREFIX/seed.txt" readonly-seed.txt
cat readonly-seed.txt
Enter fullscreen mode Exit fullscreen mode
echo "test readonly put" > readonly-put.txt
aws s3 cp readonly-put.txt "s3://$BUCKET_NAME/$PREFIX/readonly-put.txt"
Enter fullscreen mode Exit fullscreen mode

Resultado observado: PutObject falla con AccessDenied por RCP.

aws s3 rm "s3://$BUCKET_NAME/$PREFIX/seed.txt"
Enter fullscreen mode Exit fullscreen mode

Resultado observado: DeleteObject falla con AccessDenied por IAM / identity-based policy.

Prueba con RoleAdmin

assume_role RoleAdmin
Enter fullscreen mode Exit fullscreen mode
aws s3 ls "s3://$BUCKET_NAME/$PREFIX/"
Enter fullscreen mode Exit fullscreen mode
aws s3 cp "s3://$BUCKET_NAME/$PREFIX/seed.txt" admin-seed.txt
cat admin-seed.txt
Enter fullscreen mode Exit fullscreen mode
echo "test admin put" > admin-put.txt
aws s3 cp admin-put.txt "s3://$BUCKET_NAME/$PREFIX/admin-put.txt"
Enter fullscreen mode Exit fullscreen mode

Resultado observado: PutObject falla con AccessDenied por RCP.

aws s3 rm "s3://$BUCKET_NAME/$PREFIX/seed.txt"
Enter fullscreen mode Exit fullscreen mode

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:

  • RoleAdmin sí tendría por s3:*
  • 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"
Enter fullscreen mode Exit fullscreen mode

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, con s3:*.

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:GetBucketPolicy
  • s3:GetObject
  • s3: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:PutObject
  • s3:DeleteObject
  • s3: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:

  • ListBucket y GetObject fueron permitidos.
  • PutObject devolvió AccessDenied por un explicit deny de la RCP.
  • DeleteObject devolvió AccessDenied por 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 / RoleReadOnly

  • Cuenta del principal: Account B

  • Compartido a través de: Política del bucket

  • Acceso reportado por el finding:

    • Reads3:GetObject
    • Lists3: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, con s3:* 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:

  • PutObject devolvió AccessDenied por un explicit deny de la RCP.
  • DeleteObject devolvió AccessDenied por un explicit deny de la SCP.

Además, en los findings las dos restricciones aparecían como:

resourceControlPolicyRestriction: APPLIED
serviceControlPolicyRestriction: APPLIED
Enter fullscreen mode Exit fullscreen mode

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:

  1. El finding es muy útil para identificar principals, rutas de acceso y acciones reportadas sobre el recurso.
  2. Las columnas SCP y RCP permiten saber si esas capas participaron en la evaluación.
  3. Cuando necesitas validar una API concreta, la prueba runtime sigue siendo una evidencia importante.
  4. 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:

  1. Una identity policy en el rol de la Cuenta B que permita la acción.
  2. Una bucket policy en la Cuenta A que conceda esa acción al rol externo.
  3. Que no exista un explicit deny aplicable a esa acción y a ese recurso.
  4. 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:

  1. Revisar rutas de acceso sin llamar directamente al recurso objetivo.
  2. Priorizar relaciones cross-account relevantes.
  3. Comparar acceso reportado frente a acceso ejecutable.
  4. 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.

AWS #CloudSecurity #IAM #AccessAnalyzer #SCPs #RCPs #PermissionsBoundary #CloudGovernance #AWSOrganizations #LeastPrivilege #RedTeam

Top comments (0)