Hace unas semanas me senté a revisar el estado de IAM en una AWS Organization multicuenta. No era una auditoría formal con semanas de planificación. Era la pregunta simple que todo Cloud Security Engineer debería poder responder en cualquier momento:
¿Quién tiene acceso, con qué credenciales, y desde cuándo?La respuesta me sorprendió. No por lo compleja que fue encontrarla — sino por lo fácil que fue automatizarla, y por lo que salió a la luz cuando lo hice.
El contexto
Las organizaciones que llevan años en AWS acumulan deuda de seguridad sin darse cuenta. Empiezan con una cuenta, luego dos, luego un equipo pide su propio ambiente, llega otro proyecto, y de repente tienes una AWS Organization con decenas de cuentas, cada una con su propia historia de IAM.
El problema no es la escala — es la visibilidad. O mejor dicho, la falta de ella.
En entornos multicuenta, nadie tiene una vista consolidada de quién tiene qué acceso. Los equipos de infraestructura saben lo que desplegaron ellos. Los de desarrollo saben lo que necesitaron en su momento. Pero las Access Keys que se crearon hace tres años para un proceso de integración que ya no existe, que nunca se rotaron, que siguen activas — esas no aparecen en ningún dashboard. No generan alertas. No molestan a nadie. Simplemente esperan.
Eso es exactamente lo que encontré. Una AWS Organization activa, con múltiples cuentas en producción, y credenciales que llevaban años sin ser auditadas de forma centralizada. No porque el equipo fuera descuidado — sino porque nadie había construido el mecanismo para verlas todas a la vez.
Decidí automatizar esa búsqueda.
El riesgo
Las Access Keys de IAM son credenciales de largo plazo. A diferencia de los roles de IAM — que generan credenciales temporales que expiran automáticamente — una Access Key no tiene fecha de vencimiento. Si la creás hoy y no la rotás, sigue válida en 2030.
Eso las convierte en uno de los vectores de ataque más comunes en entornos AWS. No porque sean inseguras por diseño, sino porque el tiempo trabaja en su contra. Una key que lleva años activa acumula riesgo silenciosamente:
- Puede haber sido expuesta en un repositorio de código sin que nadie lo haya detectado
- Puede estar en manos de un empleado que ya no trabaja en la organización
- Puede tener permisos que se otorgaron para un proyecto puntual y nunca se revisaron
- Puede estar siendo usada por un atacante desde hace meses — y sin rotación, nadie lo sabe
El AWS Security Maturity Model v2 es preciso sobre esto. En su Phase 1 — Quick Wins, dentro del dominio de Identity and Access Management, uno de los controles clave es Multi-Factor Authentication — garantizar que todos los usuarios con acceso a consola tengan MFA activo. En Phase 2 — Foundational, el modelo va un paso más allá con Use Temporary Credentials — la recomendación de migrar hacia roles IAM y credenciales de corta duración, alejándose de Access Keys de largo plazo.
El script audita ambos controles en un solo run: qué usuarios tienen Access Keys activas, hace cuánto tiempo, y si tienen MFA configurado o no. Una key activa desde 2018 combinada con un usuario sin MFA no es solo una deuda técnica — es una superficie de ataque abierta por años.
La solución
Decisiones de diseño antes que códigoAntes de escribir una sola línea, tomé tres decisiones que definen cómo funciona el script.
Primera decisión: cross-account con STS, no con usuarios IAM.La tentación obvia es crear un usuario IAM con permisos de auditoría en cada cuenta. Pero eso resuelve un problema de visibilidad creando exactamente el problema que queremos eliminar — más credenciales de largo plazo. La solución correcta es sts:AssumeRole: el script asume un rol existente en cada cuenta, obtiene credenciales temporales, audita, y las credenciales expiran solas.
En lugar de mantener una lista manual de cuentas, el script consulta la Organization directamente y obtiene todas las cuentas activas en tiempo real. Si mañana se crea una cuenta nueva, la próxima ejecución la incluye automáticamente. Sin listas que mantener, sin cuentas que se escapan.
Tercera decisión: paginar siempre.Las APIs de IAM paginan. Si no usás get_paginator(), en una cuenta con muchos usuarios vas a ver solo los primeros resultados y nunca lo vas a saber. Es el tipo de bug silencioso que destruye la confiabilidad de una auditoría.
Con esas tres decisiones claras, el código se escribe solo.
El flujo principaldef main():
args = parse_args()
session = boto3.Session(profile_name=args.profile)
org_client = session.client('organizations')
# Obtener todas las cuentas activas de la Organization
accounts = get_accounts(org_client)
all_findings = []
for account in accounts:
print(f"Auditando cuenta: {account['name']} ({account['id']})")
try:
credentials = assume_role(
session,
account['id'],
args.role,
'SecurityAudit'
)
iam_client = boto3.client(
'iam',
aws_access_key_id=credentials['AccessKeyId'],
aws_secret_access_key=credentials['SecretAccessKey'],
aws_session_token=credentials['SessionToken']
)
findings = get_iam_users_with_keys(iam_client, account['id'], account['name'])
all_findings.extend(findings)
except Exception as e:
print(f"Error en cuenta {account['name']}: {e}")
return all_findings
Una cuenta falla — el script continúa. Eso es intencional: en una Organization real vas a encontrar cuentas donde el rol no está desplegado, o donde los permisos son distintos. El try/except garantiza que un error en una cuenta no detiene la auditoría completa.
def get_iam_users_with_keys(iam_client, account_id, account_name):
findings = []
paginator = iam_client.get_paginator('list_users')
for page in paginator.paginate():
for user in page['Users']:
# Access Keys del usuario
keys_response = iam_client.list_access_keys(UserName=user['UserName'])
# Estado de MFA
mfa_response = iam_client.list_mfa_devices(UserName=user['UserName'])
mfa_devices = mfa_response['MFADevices']
# Acceso a consola
try:
iam_client.get_login_profile(UserName=user['UserName'])
password_status = 'Configurada'
except iam_client.exceptions.NoSuchEntityException:
password_status = 'No configurada'
for key in keys_response['AccessKeyMetadata']:
last_used_response = iam_client.get_access_key_last_used(
AccessKeyId=key['AccessKeyId']
)
last_used = last_used_response['AccessKeyLastUsed']
findings.append({
'account_id': account_id,
'account_name': account_name,
'username': user['UserName'],
'password_status': password_status,
'access_key_id': key['AccessKeyId'],
'status': key['Status'],
'created_date': str(key['CreateDate']),
'last_used_date': str(last_used.get('LastUsedDate', 'Nunca utilizada')),
'service_name': last_used.get('ServiceName', 'N/A'),
'mfa_status': 'Virtual' if any('virtual' in d['SerialNumber'].lower()
for d in mfa_devices) else 'Hardware' if mfa_devices else 'None'
})
return findings
Por cada usuario el script captura: todas sus Access Keys con estado y fecha de creación, cuándo se usó cada key por última vez y para qué servicio, si tiene MFA activo y de qué tipo, y si tiene acceso a consola configurado. Todo eso en una sola pasada por cuenta.
Cómo ejecutarlopython iam_audit.py --profile tu-perfil-mgmt --role OrganizationAccountAccessRole
El script necesita dos cosas: un perfil AWS con acceso a la cuenta de management de la Organization, y un rol desplegado en cada cuenta miembro que permita asumir desde la cuenta de management.
Para la cuenta de management, la política IAM es mínima por diseño — solo los permisos estrictamente necesarios:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "organizations:ListAccounts",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::*:role/NOMBRE-DEL-ROL-EN-CHILD-ACCOUNTS"
}
]
}
Nada más. El rol en la cuenta de management no toca IAM directamente — toda la auditoría ocurre a través del rol que asume en cada cuenta hija. Si usás AWS Control Tower, el rol AWSControlTowerExecution ya existe en todas las cuentas y podés usarlo como punto de partida, aunque en producción recomiendo crear un rol de auditoría dedicado con permisos de solo lectura sobre IAM.
Esto no es un detalle menor — es el principio de mínimo privilegio aplicado a la herramienta que audita el mínimo privilegio. La superficie de ataque del script mismo queda reducida al mínimo.
El script no solo audita el estado actual — también reconstruye la línea de tiempo. Una vez que encontrás los hallazgos y empezás a remediar, necesitás poder responder una segunda pregunta: ¿cómo vamos avanzando?
Para eso, el script consulta CloudTrail en cada cuenta y extrae eventos clave de actividad IAM: DeleteAccessKey, CreateAccessKey, DeleteUser. Esto te permite ver, en el tiempo, si el equipo está efectivamente remediando — o si los hallazgos quedaron en un reporte que nadie ejecutó.
El output final son dos archivos CSV: uno con todos los hallazgos de IAM, y otro con esos eventos de CloudTrail que construyen la tendencia de remediación.
Los hallazgos
Corrí el script contra una AWS Organization activa en la región. Esto es lo que encontré:
| Hallazgo | Resultado |
|---|---|
| Cuentas auditadas | Más de 20 cuentas activas |
| Access Keys encontradas | Decenas |
| Key más antigua | Creada en 2018 — activa en producción |
| Usuarios sin MFA + acceso a consola | Decenas |
| Tiempo total de ejecución | Minutos |
Dos cuentas no fueron auditadas — el rol de auditoría no estaba desplegado en ellas. Eso en sí mismo es un hallazgo: si no podés auditar una cuenta, tampoco podés saber qué hay adentro.
El dato que más impacta no es el volumen — es la antigüedad. Una Access Key creada en 2018 y activa en producción significa que sobrevivió a todo. No porque alguien la protegiera — sino porque nadie la vio.
El script la vio en minutos.
¿Qué hacés con esto ahora?
Si tenés una AWS Organization, este script lo podés correr hoy. No necesitás un SIEM, no necesitás un equipo de seguridad dedicado, no necesitás un presupuesto especial. Necesitás Python, boto3, y un rol de auditoría con mínimo privilegio en tus cuentas.
Lo que sí necesitás es voluntad de ver lo que hay.
El resultado más incómodo de una auditoría no es el hallazgo técnico — es darte cuenta de que la información siempre estuvo ahí, esperando que alguien se tomara el trabajo de buscarla sistemáticamente. Las Access Keys viejas no aparecen en ningún dashboard por defecto. No generan alertas. No molestan a nadie. Simplemente esperan.
Automatizar la visibilidad es el primer paso de cualquier programa de seguridad serio. No el más glamoroso, pero sí el más honesto.
El script genera dos CSVs. El próximo paso natural es convertir esos datos en un dashboard visual — widgets de riesgo por cuenta, tendencia de remediación en el tiempo, estado de MFA consolidado. Eso viene en el próximo post.
El repositorio con el código completo está en GitHub — el link está abajo.
Si lo corres, si encuentrass algo interesante, o si tienes feedback — escribeme. La comunidad de CloudSec en LATAM se construye compartiendo lo que funciona en entornos reales.
Sobre el autor
Gerardo Castro es AWS Security Hero y Cloud Security Engineer con foco en LATAM. Fundador y Lead Organizer del AWS Security Users Group LatAm. Cree que la mejor forma de aprender seguridad en la nube es construyendo cosas reales — no memorizando frameworks. Escribe sobre lo que construye, lo que encuentra, y lo que aprende en el camino.⭐️ GitHub: gerardokaztro
🔗 LinkedIn: gerardokaztro

Comentarios