Permission Boundaries en IAM: el techo que tus roles admin necesitan + código CDK

Aprende qué son los Permission Boundaries, por qué son esenciales para evitar el uso del usuario root y cómo implementarlos con AWS CDK (TypeScript y Python).

Los Permission Boundaries (límites de permiso) son una de las mejores prácticas de IAM para limitar el alcance de los permisos, incluso cuando se usan políticas amplias como *. En este post explicamos qué son, por qué son cruciales para evitar el uso del usuario root, y cómo crearlos y asignarlos usando AWS CDK en TypeScript y Python. Incluye ejemplos listos para desplegar y recomendaciones para entornos multi‑account y AWS Identity Center.

¿Qué es un Permission Boundary?

Un Permission Boundary es una política gerenciada de IAM que establece el máximo nivel máximo de permisos que una entidad IAM (rol o usuario) puede tener. Incluso si se le adjuntan políticas de permisos que otorgan * (todos los permisos), el boundary actúa como un techo: el permiso efectivo es la intersección entre la política de permisos y el boundary.

💡 Analogía
Piensa en el Permission Boundary como el techo de un edificio: puedes tener tantas habitaciones (políticas de permisos) como quieras, pero nunca podrás salir por encima del techo.

¿Por qué son importantes para evitar el usuario root?

El usuario root de AWS tiene acceso ilimitado e irrevocable. Usarlo para operaciones diarias es el error de seguridad más crítico que puedes cometer. Sin embargo, a veces se necesita un usuario con altos privilegios para tareas administrativas. En lugar de crear un usuario con políticas AdministratorAccess (que implica *), puedes:

Así, incluso si se comprometen las credenciales, el atacante no podrá realizar acciones que el boundary haya denegado.

Diseño de un Permission Boundary seguro

Un buen bloque de permissions debería:

Ejemplo de Permission Boundary (política gerenciada)

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "*",
            "Resource": "*"
        },
        {
            "Effect": "Deny",
            "Action": [
                "iam:DeleteAccountPasswordPolicy",
                "iam:DeleteVirtualMFADevice",
                "iam:AttachUserPolicy",
                "iam:DetachUserPolicy",
                "iam:PutUserPolicy",
                "iam:DeleteUserPolicy",
                "iam:AttachGroupPolicy",
                "iam:DetachGroupPolicy",
                "iam:PutGroupPolicy",
                "iam:DeleteGroupPolicy",
                "iam:AttachRolePolicy",
                "iam:DetachRolePolicy",
                "iam:PutRolePolicy",
                "iam:DeleteRolePolicy",
                "iam:CreateLoginProfile",
                "iam:DeleteLoginProfile",
                "iam:UpdateLoginProfile",
                "iam:CreateAccessKey",
                "iam:DeleteAccessKey",
                "iam:UpdateAccessKey",
                "iam:CreateSigningCertificate",
                "iam:DeleteSigningCertificate",
                "iam:UpdateSigningCertificate",
                "iam:CreateServiceSpecificCredential",
                "iam:DeleteServiceSpecificCredential",
                "iam:ResetServiceSpecificCredential",
                "iam:UpdateServiceSpecificCredential",
                "iam:AddUserToGroup",
                "iam:RemoveUserFromGroup",
                "iam:CreateGroup",
                "iam:DeleteGroup",
                "iam:CreateRole",
                "iam:DeleteRole",
                "iam:CreatePolicy",
                "iam:DeletePolicy",
                "iam:CreatePolicyVersion",
                "iam:DeletePolicyVersion",
                "iam:SetDefaultPolicyVersion",
                "organizations:DeleteOrganization",
                "organizations:DeleteOrganizationalUnit",
                "organizations:DeleteAccount",
                "organizations:LeaveOrganization",
                "account:CloseAccount",
                "billing:*",
                "aws-portal:*"
            ],
            "Resource": "*"
        }
    ]
}

Implementación con AWS CDK

A continuación se muestra cómo definir el Permission Boundary como una política gerenciada y luego crear un rol que lo utilice. Se proporcionan ejemplos en TypeScript y Python.

TypeScript

import * as iam from 'aws-cdk-lib/aws-iam';
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class PermissionBoundaryStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 1. Definir el Permission Boundary (policy gerenciada)
    const adminBoundary = new iam.ManagedPolicy(this, 'AdminPermissionBoundary', {
      managedPolicyName: 'AdminPermissionBoundary',
      description: 'Techo de permisos para roles admin - bloquea acciones destructivas',
      statements: [
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          actions: ['*'],
          resources: ['*'],
        }),
        new iam.PolicyStatement({
          effect: iam.Effect.DENY,
          actions: [
            'iam:DeleteAccountPasswordPolicy',
            'iam:DeleteVirtualMFADevice',
            'iam:AttachUserPolicy',
            'iam:DetachUserPolicy',
            'iam:PutUserPolicy',
            'iam:DeleteUserPolicy',
            'iam:AttachGroupPolicy',
            'iam:DetachGroupPolicy',
            'iam:PutGroupPolicy',
            'iam:DeleteGroupPolicy',
            'iam:AttachRolePolicy',
            'iam:DetachRolePolicy',
            'iam:PutRolePolicy',
            'iam:DeleteRolePolicy',
            'iam:CreateLoginProfile',
            'iam:DeleteLoginProfile',
            'iam:UpdateLoginProfile',
            'iam:CreateAccessKey',
            'iam:DeleteAccessKey',
            'iam:UpdateAccessKey',
            'iam:CreateSigningCertificate',
            'iam:DeleteSigningCertificate',
            'iam:UpdateSigningCertificate',
            'iam:CreateServiceSpecificCredential',
            'iam:DeleteServiceSpecificCredential',
            'iam:ResetServiceSpecificCredential',
            'iam:UpdateServiceSpecificCredential',
            'iam:AddUserToGroup',
            'iam:RemoveUserFromGroup',
            'iam:CreateGroup',
            'iam:DeleteGroup',
            'iam:CreateRole',
            'iam:DeleteRole',
            'iam:CreatePolicy',
            'iam:DeletePolicy',
            'iam:CreatePolicyVersion',
            'iam:DeletePolicyVersion',
            'iam:SetDefaultPolicyVersion',
            'organizations:DeleteOrganization',
            'organizations:DeleteOrganizationalUnit',
            'organizations:DeleteAccount',
            'organizations:LeaveOrganization',
            'account:CloseAccount',
            'billing:*',
            'aws-portal:*'
          ],
          resources: ['*'],
        }),
      ],
    });

    // 2. Rol de administración que usa el boundary
    const adminRole = new iam.Role(this, 'SecurityAdminRole', {
      roleName: 'SecurityAdminRole',
      assumedBy: new iam.AccountRootPrincipal(), // o bien un principal de Identity Center
      permissionsBoundary: adminBoundary, // ← aquí se aplica el boundary
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('job-function/SecurityAudit')
      ],
    });

    // 3. Output para referencia
    new cdk.CfnOutput(this, 'BoundaryArn', {
      value: adminBoundary.managedPolicyArn,
      description: 'ARN del Permission Boundary para auditar',
    });
  }
}

Python

from aws_cdk import (
    aws_iam as iam,
    core as cdk,
)

class PermissionBoundaryStack(cdk.Stack):
    def __init__(self, scope: cdk.Construct, id: str, **kwargs):
        super().__init__(scope, id, **kwargs)

        # 1. Permission Boundary
        admin_boundary = iam.ManagedPolicy(
            self, 'AdminPermissionBoundary',
            managed_policy_name='AdminPermissionBoundary',
            description='Techo de permisos para roles admin - bloquea acciones destructivas',
            statements=[
                iam.PolicyStatement(
                    effect=iam.Effect.ALLOW,
                    actions=['*'],
                    resources=['*'],
                ),
                iam.PolicyStatement(
                    effect=iam.Effect.DENY,
                    actions=[
                        'iam:DeleteAccountPasswordPolicy',
                        'iam:DeleteVirtualMFADevice',
                        'iam:AttachUserPolicy',
                        'iam:DetachUserPolicy',
                        'iam:PutUserPolicy',
                        'iam:DeleteUserPolicy',
                        'iam:AttachGroupPolicy',
                        'iam:DetachGroupPolicy',
                        'iam:PutGroupPolicy',
                        'iam:DeleteGroupPolicy',
                        'iam:AttachRolePolicy',
                        'iam:DetachRolePolicy',
                        'iam:PutRolePolicy',
                        'iam:DeleteRolePolicy',
                        'iam:CreateLoginProfile',
                        'iam:DeleteLoginProfile',
                        'iam:UpdateLoginProfile',
                        'iam:CreateAccessKey',
                        'iam:DeleteAccessKey',
                        'iam:UpdateAccessKey',
                        'iam:CreateSigningCertificate',
                        'iam:DeleteSigningCertificate',
                        'iam:UpdateSigningCertificate',
                        'iam:CreateServiceSpecificCredential',
                        'iam:DeleteServiceSpecificCredential',
                        'iam:ResetServiceSpecificCredential',
                        'iam:UpdateServiceSpecificCredential',
                        'iam:AddUserToGroup',
                        'iam:RemoveUserFromGroup',
                        'iam:CreateGroup',
                        'iam:DeleteGroup',
                        'iam:CreateRole',
                        'iam:DeleteRole',
                        'iam:CreatePolicy',
                        'iam:DeletePolicy',
                        'iam:CreatePolicyVersion',
                        'iam:DeletePolicyVersion',
                        'iam:SetDefaultPolicyVersion',
                        'organizations:DeleteOrganization',
                        'organizations:DeleteOrganizationalUnit',
                        'organizations:DeleteAccount',
                        'organizations:LeaveOrganization',
                        'account:CloseAccount',
                        'billing:*',
                        'aws-portal:*'
                    ],
                    resources=['*'],
                ),
            ]
        )

        # 2. Rol que usa el boundary
        admin_role = iam.Role(
            self, 'SecurityAdminRole',
            role_name='SecurityAdminRole',
            assumed_by=iam.AccountRootPrincipal(),  # o iam.ServicePrincipal('...') para Identity Center
            permissions_boundary=admin_boundary,
            managed_policies=[
                iam.ManagedPolicy.from_aws_managed_policy_name('job-function/SecurityAudit')
            ]
        )

        # 3. Output
        cdk.CfnOutput(
            self, 'BoundaryArn',
            value=admin_boundary.managed_policy_arn,
            description='ARN del Permission Boundary para auditar'
        )

Buenas prácticas

Conclusión

Los Permission Boundaries son una capa de protección esencial que te permite otorgar permisos amplios para tareas administrativas sin exponer tu cuenta a riesgos innecesarios. Al combinarlos con AWS CDK, puedes definir, versionar y desplegar estos límites como código, asegurando consistencia y reproducibilidad en todos tus entornos y cuentas.

Implementar esta práctica es un paso significativo hacia una postura de seguridad sólida en AWS, y encaja perfectamente dentro de los pilares de identidad y gestión de acceso que valoran los evaluadores del programa AWS Community Builder (Security).