Módulo 5: AWS Services for AI (S3, Lambda, SageMaker Basics)

6. IAM y Permisos Mínimos

Descripción

En esta cápsula vas a aprender a configurar IAM (Identity and Access Management) para que tu sistema AI en AWS sea seguro en producción. En LocalStack no hay IAM real — todo funciona sin permisos. En AWS, cada acción requiere autorización explícita. Tu Lambda necesita permiso para leer de S3. Tu Lambda necesita permiso para invocar SageMaker. Y esos permisos deben ser los mínimos necesarios — no más. Un Lambda con rol admin en producción es un incidente de seguridad esperando suceder.

Contexto: Has construido el pipeline S3 + Lambda en las cápsulas anteriores. El código funciona, pero en AWS real necesita permisos para operar. Esta cápsula te enseña a crear roles IAM con least privilege: tu Lambda puede leer de ai-assets-dev/documents/* pero no de ai-assets-dev/secrets/*. Puede invocar gpt-4o-mini pero no borrar el bucket. Al terminar, tu servicio será funcional Y seguro.


IAM Concepts para AI Engineers

Los tres pilares de IAM

IAM (Identity and Access Management):
├── Users & Groups     → Quién eres (personas, equipos)
├── Roles              → Qué identidad puede asumir un servicio (Lambda, EC2)
└── Policies           → Qué permisos tiene esa identidad

Para tu Lambda de AI:
├── Lambda asume un Role
├── El Role tiene Policies adjuntas
└── Las Policies definen qué puede hacer
    ├── ✅ s3:GetObject en ai-assets-dev/documents/*
    ├── ✅ s3:PutObject en ai-assets-dev/responses/*
    ├── ❌ s3:DeleteBucket (no necesita esto)
    └── ❌ iam:* (definitivamente no)

Principal: quién actúa

# Un Principal es la entidad que realiza una acción
# Para Lambda, el principal es el servicio lambda.amazonaws.com

# Esto permite que Lambda asuma el rol
trust_policy = {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {"Service": "lambda.amazonaws.com"},
            "Action": "sts:AssumeRole",
        }
    ],
}

La anatomía de una Policy

policy = {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "ReadDocumentsFromS3",        # Identificador descriptivo
            "Effect": "Allow",                    # Allow o Deny
            "Action": [                           # Qué operaciones
                "s3:GetObject",
                "s3:ListBucket",
            ],
            "Resource": [                         # Sobre qué recursos
                "arn:aws:s3:::ai-assets-dev",
                "arn:aws:s3:::ai-assets-dev/documents/*",
            ],
        }
    ],
}

# Cada Statement tiene:
# Effect:   Allow (permite) o Deny (deniega, tiene prioridad)
# Action:   Operaciones API de AWS (s3:GetObject, lambda:InvokeFunction)
# Resource: ARN del recurso específico (no usar * salvo que sea necesario)

Least Privilege: El Principio Fundamental

Qué significa least privilege

Tu Lambda solo debe poder hacer lo que necesita para funcionar. Nada más.

❌ MAL: Policy demasiado permisiva
{
    "Effect": "Allow",
    "Action": "s3:*",           ← Puede hacer CUALQUIER cosa en S3
    "Resource": "*"             ← En CUALQUIER bucket de la cuenta
}

❌ PEOR: Admin access
{
    "Effect": "Allow",
    "Action": "*",              ← Puede hacer CUALQUIER cosa
    "Resource": "*"             ← En CUALQUIER recurso
}

✅ BIEN: Solo lo necesario
{
    "Effect": "Allow",
    "Action": ["s3:GetObject"],     ← Solo leer objetos
    "Resource": [
        "arn:aws:s3:::ai-assets-dev/documents/*",   ← Solo este prefijo
        "arn:aws:s3:::ai-assets-dev/prompts/*"       ← Y este
    ]
}

Por qué importa para AI

Imagina que tu Lambda tiene s3:* en *. Si un atacante compromete la API key de tu LLM provider y puede ejecutar código arbitrario (prompt injection → code execution en el peor caso), puede:

  • Leer TODOS los buckets de tu cuenta (datos sensibles, credenciales)
  • Eliminar buckets (sabotaje)
  • Subir malware a S3

Con least privilege, lo máximo que puede hacer es leer documentos de un prefijo específico y escribir responses. Daño contenido.


Crear Roles IAM para Lambda

Role con boto3

import boto3
import json
import os

iam = boto3.client("iam", endpoint_url=os.environ.get("AWS_ENDPOINT_URL"))

ACCOUNT_ID = os.environ.get("AWS_ACCOUNT_ID", "123456789012")
BUCKET_NAME = os.environ.get("AI_BUCKET", "ai-assets-dev")


def create_lambda_role(role_name: str) -> str:
    """Crea un IAM role que Lambda puede asumir."""
    trust_policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Principal": {"Service": "lambda.amazonaws.com"},
                "Action": "sts:AssumeRole",
            }
        ],
    }

    response = iam.create_role(
        RoleName=role_name,
        AssumeRolePolicyDocument=json.dumps(trust_policy),
        Description="Role for AI inference Lambda function",
    )

    role_arn = response["Role"]["Arn"]
    print(f"Role created: {role_arn}")
    return role_arn

Policies específicas para AI Lambda

def attach_s3_read_policy(role_name: str, bucket: str, prefixes: list[str]):
    """Adjunta policy para leer objetos de prefijos específicos en S3."""
    resources = [f"arn:aws:s3:::{bucket}"]
    resources.extend(
        f"arn:aws:s3:::{bucket}/{prefix}*" for prefix in prefixes
    )

    policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "ListBucket",
                "Effect": "Allow",
                "Action": ["s3:ListBucket"],
                "Resource": [f"arn:aws:s3:::{bucket}"],
                "Condition": {
                    "StringLike": {
                        "s3:prefix": [f"{p}*" for p in prefixes]
                    }
                },
            },
            {
                "Sid": "ReadObjects",
                "Effect": "Allow",
                "Action": ["s3:GetObject"],
                "Resource": [
                    f"arn:aws:s3:::{bucket}/{prefix}*" for prefix in prefixes
                ],
            },
        ],
    }

    iam.put_role_policy(
        RoleName=role_name,
        PolicyName="s3-read-ai-assets",
        PolicyDocument=json.dumps(policy),
    )
    print(f"S3 read policy attached to {role_name}")


def attach_s3_write_policy(role_name: str, bucket: str, prefixes: list[str]):
    """Adjunta policy para escribir objetos en prefijos específicos de S3."""
    policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "WriteObjects",
                "Effect": "Allow",
                "Action": ["s3:PutObject"],
                "Resource": [
                    f"arn:aws:s3:::{bucket}/{prefix}*" for prefix in prefixes
                ],
            },
        ],
    }

    iam.put_role_policy(
        RoleName=role_name,
        PolicyName="s3-write-responses",
        PolicyDocument=json.dumps(policy),
    )
    print(f"S3 write policy attached to {role_name}")


def attach_cloudwatch_logs_policy(role_name: str):
    """Adjunta policy para que Lambda pueda escribir logs a CloudWatch."""
    iam.attach_role_policy(
        RoleName=role_name,
        PolicyArn="arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
    )
    print(f"CloudWatch Logs policy attached to {role_name}")

Configurar el role completo

def setup_ai_lambda_role() -> str:
    """Configura un role IAM completo para Lambda de AI inference."""
    role_name = "ai-inference-lambda-role"

    role_arn = create_lambda_role(role_name)

    # Lambda puede LEER prompts y documentos
    attach_s3_read_policy(
        role_name,
        BUCKET_NAME,
        prefixes=["prompts/", "documents/", "config/"],
    )

    # Lambda puede ESCRIBIR responses
    attach_s3_write_policy(
        role_name,
        BUCKET_NAME,
        prefixes=["responses/"],
    )

    # Lambda puede escribir logs
    attach_cloudwatch_logs_policy(role_name)

    print(f"\nRole configured: {role_name}")
    print(f"  Read:  s3://{BUCKET_NAME}/prompts/*, documents/*, config/*")
    print(f"  Write: s3://{BUCKET_NAME}/responses/*")
    print(f"  Logs:  CloudWatch Logs")

    return role_arn

Inline vs Managed Policies

Tipos de policies

Managed Policies (AWS-managed):
├── AWSLambdaBasicExecutionRole    → Logs de CloudWatch
├── AmazonS3ReadOnlyAccess         → Leer TODOS los S3 (demasiado amplia)
├── AmazonS3FullAccess             → Todo en S3 (NUNCA en producción)
└── AmazonSageMakerFullAccess      → Todo en SageMaker (NUNCA en producción)

Managed Policies (customer-managed):
├── Tú las creas
├── Se versionan (hasta 5 versiones)
├── Se pueden adjuntar a múltiples roles
└── Ideales para policies que comparten varios roles

Inline Policies:
├── Adjuntas directamente al role
├── No se comparten entre roles
├── Se eliminan cuando se elimina el role
└── Ideales para policies únicas de un role específico

Cuándo usar cada una

TipoCuándoEjemplo
AWS ManagedPermisos estándar bien definidosAWSLambdaBasicExecutionRole para logs
Customer ManagedPermisos compartidos entre múltiples rolesPolicy de lectura S3 para 5 Lambdas diferentes
InlinePermisos específicos de un solo roleLambda X puede escribir SOLO en responses/

Customer Managed Policy

def create_ai_s3_managed_policy(bucket: str) -> str:
    """Crea una managed policy reutilizable para acceso AI a S3."""
    policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "ReadAIAssets",
                "Effect": "Allow",
                "Action": ["s3:GetObject", "s3:ListBucket"],
                "Resource": [
                    f"arn:aws:s3:::{bucket}",
                    f"arn:aws:s3:::{bucket}/prompts/*",
                    f"arn:aws:s3:::{bucket}/documents/*",
                    f"arn:aws:s3:::{bucket}/embeddings/*",
                ],
            },
            {
                "Sid": "WriteResponses",
                "Effect": "Allow",
                "Action": ["s3:PutObject"],
                "Resource": [f"arn:aws:s3:::{bucket}/responses/*"],
            },
        ],
    }

    response = iam.create_policy(
        PolicyName="ai-lambda-s3-access",
        PolicyDocument=json.dumps(policy),
        Description="S3 access policy for AI Lambda functions",
    )

    policy_arn = response["Policy"]["Arn"]
    print(f"Managed policy created: {policy_arn}")
    return policy_arn

SAM Template con IAM Roles

Definir permisos en template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: AI Service with least-privilege IAM

Parameters:
  BucketName:
    Type: String
    Default: ai-assets-dev

Resources:
  AIBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName

  DocProcessorFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: ai-doc-processor
      Handler: handler.handler
      Runtime: python3.11
      MemorySize: 512
      Timeout: 120
      Policies:
        # SAM simplifica IAM con policy templates
        - S3ReadPolicy:
            BucketName: !Ref BucketName
        - S3CrudPolicy:
            BucketName: !Ref BucketName
        - Statement:
            - Sid: RestrictWriteToResponses
              Effect: Allow
              Action:
                - s3:PutObject
              Resource:
                - !Sub "arn:aws:s3:::${BucketName}/responses/*"
      Events:
        NewDocument:
          Type: S3
          Properties:
            Bucket: !Ref AIBucket
            Events: s3:ObjectCreated:*
            Filter:
              S3Key:
                Rules:
                  - Name: prefix
                    Value: documents/inbox/

SAM Policy Templates

SAM ofrece policy templates predefinidos que simplifican IAM:

Policies:
  # Lectura de S3
  - S3ReadPolicy:
      BucketName: !Ref BucketName

  # CRUD completo en S3
  - S3CrudPolicy:
      BucketName: !Ref BucketName

  # Invocar otra Lambda
  - LambdaInvokePolicy:
      FunctionName: !Ref OtherFunction

  # Invocar SageMaker endpoint
  - Statement:
      - Effect: Allow
        Action: sagemaker:InvokeEndpoint
        Resource: !Sub "arn:aws:sagemaker:${AWS::Region}:${AWS::AccountId}:endpoint/*"

  # SSM Parameter Store
  - SSMParameterReadPolicy:
      ParameterName: /ai-service/*

  # Secrets Manager
  - Statement:
      - Effect: Allow
        Action: secretsmanager:GetSecretValue
        Resource: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:ai-service/*"

Security Best Practices

Checklist de seguridad IAM para AI

Permisos de Lambda:
├── ✅ Role específico por función (no compartir roles entre funciones no relacionadas)
├── ✅ Solo las acciones necesarias (s3:GetObject, no s3:*)
├── ✅ Recursos específicos (arn:aws:s3:::bucket/prefix/*, no *)
├── ✅ AWSLambdaBasicExecutionRole para logs
├── ❌ No usar AmazonS3FullAccess
├── ❌ No usar AdministratorAccess
└── ❌ No usar Resource: "*" si puedes especificar

Secrets:
├── ✅ API keys en Environment Variables (mínimo) o SSM/Secrets Manager
├── ✅ Encriptación at rest (habilitada por defecto en Lambda env vars)
├── ❌ No hardcodear API keys en código
├── ❌ No subir .env a Git
└── ❌ No compartir keys entre entornos (dev, staging, prod)

Buckets S3:
├── ✅ Block Public Access habilitado (default en buckets nuevos)
├── ✅ Encryption at rest (SSE-S3 o SSE-KMS)
├── ✅ Versioning para assets críticos (prompt templates)
├── ❌ No habilitar acceso público salvo que sea explícitamente necesario
└── ❌ No usar ACLs (usar policies en su lugar)

Auditoría de permisos

def audit_lambda_permissions(function_name: str):
    """Audita los permisos de un Lambda function."""
    lambda_client = boto3.client("lambda")

    config = lambda_client.get_function_configuration(FunctionName=function_name)
    role_arn = config["Role"]
    role_name = role_arn.split("/")[-1]

    print(f"Function: {function_name}")
    print(f"Role: {role_name}")
    print()

    # Managed policies
    attached = iam.list_attached_role_policies(RoleName=role_name)
    print("Managed policies:")
    for policy in attached["AttachedPolicies"]:
        print(f"  - {policy['PolicyName']}")
        if "FullAccess" in policy["PolicyName"]:
            print(f"    ⚠️ WARNING: FullAccess policy detected!")
    print()

    # Inline policies
    inline = iam.list_role_policies(RoleName=role_name)
    print("Inline policies:")
    for policy_name in inline["PolicyNames"]:
        policy_doc = iam.get_role_policy(RoleName=role_name, PolicyName=policy_name)
        document = policy_doc["PolicyDocument"]

        for stmt in document.get("Statement", []):
            actions = stmt.get("Action", [])
            resources = stmt.get("Resource", [])
            if isinstance(actions, str):
                actions = [actions]
            if isinstance(resources, str):
                resources = [resources]

            has_wildcard_action = any("*" in a for a in actions)
            has_wildcard_resource = any(r == "*" for r in resources)

            print(f"  {policy_name}:")
            print(f"    Actions: {actions}")
            print(f"    Resources: {resources}")
            if has_wildcard_action:
                print(f"    ⚠️ WARNING: Wildcard action detected!")
            if has_wildcard_resource:
                print(f"    ⚠️ WARNING: Wildcard resource detected!")
    print()

Deny Policies: Protección Adicional

def attach_deny_dangerous_actions(role_name: str):
    """Adjunta una deny policy para proteger contra acciones peligrosas."""
    deny_policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "DenyDangerousActions",
                "Effect": "Deny",
                "Action": [
                    "s3:DeleteBucket",
                    "s3:PutBucketPolicy",
                    "iam:*",
                    "organizations:*",
                    "account:*",
                ],
                "Resource": "*",
            },
            {
                "Sid": "DenyDeleteProductionAssets",
                "Effect": "Deny",
                "Action": ["s3:DeleteObject"],
                "Resource": [
                    "arn:aws:s3:::ai-assets-prod/prompts/*",
                    "arn:aws:s3:::ai-assets-prod/models/*",
                ],
            },
        ],
    }

    iam.put_role_policy(
        RoleName=role_name,
        PolicyName="deny-dangerous-actions",
        PolicyDocument=json.dumps(deny_policy),
    )
    print(f"Deny policy attached to {role_name}")

Troubleshooting

Problema 1: "AccessDenied" al hacer s3:GetObject

Tu Lambda no tiene permiso para leer del bucket o prefijo.

# Verifica las policies del role
aws iam list-role-policies --role-name ai-inference-lambda-role
aws iam get-role-policy --role-name ai-inference-lambda-role --policy-name s3-read-ai-assets

# Verifica que el Resource incluye el prefijo correcto
# "arn:aws:s3:::ai-assets-dev/documents/*" ← incluye el /*

Problema 2: "is not authorized to perform: sts:AssumeRole"

El trust policy del role no incluye Lambda como principal.

# Verifica el trust policy
response = iam.get_role(RoleName="ai-inference-lambda-role")
print(json.dumps(response["Role"]["AssumeRolePolicyDocument"], indent=2))

# Debe incluir:
# "Principal": {"Service": "lambda.amazonaws.com"}

Problema 3: Lambda funciona en LocalStack pero falla en AWS con permisos

LocalStack no enforcea IAM por defecto. Tu código funciona sin permisos en local pero falla en AWS real.

# En LocalStack: IAM no se enforcea (todo está permitido)
# En AWS: cada operación se valida contra la policy del role

# Solución: Antes de migrar de LocalStack a AWS,
# define las policies necesarias y pruébalas con IAM Policy Simulator:
# https://policysim.aws.amazon.com/

Problema 4: Policy demasiado grande

Las inline policies tienen límite de 10KB. Si tienes muchos prefijos, usa managed policies.

# Inline policy limit: 10,240 characters
# Managed policy limit: 6,144 characters per version, but can have 5 versions
# Si necesitas más: usa conditions en lugar de listar todos los ARNs

Ejercicios Prácticos

Ejercicio 1: Crear policy least-privilege para un servicio RAG

Tu servicio RAG necesita: leer documentos de documents/, leer embeddings de embeddings/, leer prompts de prompts/, escribir responses en responses/, y NO poder acceder a models/ ni borrar nada. Crea la policy completa.

Ver solución
import boto3
import json
import os

iam = boto3.client("iam", endpoint_url=os.environ.get("AWS_ENDPOINT_URL"))
BUCKET = "ai-assets-prod"


def create_rag_service_policy(role_name: str):
    """Crea policy least-privilege para servicio RAG."""
    policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "ListBucketWithPrefixes",
                "Effect": "Allow",
                "Action": ["s3:ListBucket"],
                "Resource": [f"arn:aws:s3:::{BUCKET}"],
                "Condition": {
                    "StringLike": {
                        "s3:prefix": [
                            "documents/*",
                            "embeddings/*",
                            "prompts/*",
                            "responses/*",
                        ]
                    }
                },
            },
            {
                "Sid": "ReadAIAssets",
                "Effect": "Allow",
                "Action": ["s3:GetObject", "s3:HeadObject"],
                "Resource": [
                    f"arn:aws:s3:::{BUCKET}/documents/*",
                    f"arn:aws:s3:::{BUCKET}/embeddings/*",
                    f"arn:aws:s3:::{BUCKET}/prompts/*",
                ],
            },
            {
                "Sid": "WriteResponses",
                "Effect": "Allow",
                "Action": ["s3:PutObject"],
                "Resource": [f"arn:aws:s3:::{BUCKET}/responses/*"],
            },
            {
                "Sid": "DenyModelsAccess",
                "Effect": "Deny",
                "Action": ["s3:*"],
                "Resource": [f"arn:aws:s3:::{BUCKET}/models/*"],
            },
            {
                "Sid": "DenyDeleteOperations",
                "Effect": "Deny",
                "Action": [
                    "s3:DeleteObject",
                    "s3:DeleteBucket",
                    "s3:PutBucketPolicy",
                ],
                "Resource": [
                    f"arn:aws:s3:::{BUCKET}",
                    f"arn:aws:s3:::{BUCKET}/*",
                ],
            },
        ],
    }

    iam.put_role_policy(
        RoleName=role_name,
        PolicyName="rag-service-s3-policy",
        PolicyDocument=json.dumps(policy),
    )
    print(f"RAG service policy attached to {role_name}")
    print(f"  ✅ Read:  documents/*, embeddings/*, prompts/*")
    print(f"  ✅ Write: responses/*")
    print(f"  ❌ Deny:  models/*, delete operations")

create_rag_service_policy("ai-inference-lambda-role")

Ejercicio 2: Auditar un role existente

Escribe una función que reciba un role name y retorne un reporte detallado de todos los permisos, marcando los que son demasiado amplios (wildcards en actions o resources).

Ver solución
import boto3
import json
import os

iam = boto3.client("iam", endpoint_url=os.environ.get("AWS_ENDPOINT_URL"))


def audit_role(role_name: str) -> dict:
    """Audita un role IAM y retorna reporte de seguridad."""
    report = {
        "role_name": role_name,
        "managed_policies": [],
        "inline_policies": [],
        "warnings": [],
        "score": "secure",
    }

    attached = iam.list_attached_role_policies(RoleName=role_name)
    for p in attached["AttachedPolicies"]:
        name = p["PolicyName"]
        report["managed_policies"].append(name)
        if "FullAccess" in name:
            report["warnings"].append(f"FullAccess managed policy: {name}")
        if name == "AdministratorAccess":
            report["warnings"].append(f"CRITICAL: AdministratorAccess attached!")
            report["score"] = "critical"

    inline_names = iam.list_role_policies(RoleName=role_name)["PolicyNames"]
    for policy_name in inline_names:
        doc = iam.get_role_policy(RoleName=role_name, PolicyName=policy_name)
        policy_doc = doc["PolicyDocument"]

        for stmt in policy_doc.get("Statement", []):
            actions = stmt.get("Action", [])
            resources = stmt.get("Resource", [])
            effect = stmt.get("Effect", "Allow")

            if isinstance(actions, str):
                actions = [actions]
            if isinstance(resources, str):
                resources = [resources]

            for action in actions:
                if action == "*" and effect == "Allow":
                    report["warnings"].append(f"Wildcard action in {policy_name}")
                    report["score"] = "critical"
                elif ":*" in action and effect == "Allow":
                    report["warnings"].append(f"Service wildcard: {action} in {policy_name}")
                    if report["score"] != "critical":
                        report["score"] = "warning"

            for resource in resources:
                if resource == "*" and effect == "Allow":
                    report["warnings"].append(f"Wildcard resource in {policy_name}")
                    if report["score"] != "critical":
                        report["score"] = "warning"

            report["inline_policies"].append({
                "name": policy_name,
                "effect": effect,
                "actions": actions,
                "resources": resources,
            })

    return report


report = audit_role("ai-inference-lambda-role")
print(json.dumps(report, indent=2, default=str))

if report["warnings"]:
    print(f"\n⚠️  {len(report['warnings'])} warning(s) found:")
    for w in report["warnings"]:
        print(f"  - {w}")
else:
    print("\n✅ No security warnings found")

Ejercicio 3: Policy para Lambda que invoca SageMaker

Crea una policy que permita a tu Lambda invocar UN endpoint SageMaker específico y leer de un bucket S3 específico, pero nada más.

Ver solución
import boto3
import json
import os

iam = boto3.client("iam", endpoint_url=os.environ.get("AWS_ENDPOINT_URL"))

ACCOUNT_ID = os.environ.get("AWS_ACCOUNT_ID", "123456789012")
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")


def create_lambda_sagemaker_policy(
    role_name: str,
    endpoint_name: str,
    bucket: str,
    read_prefixes: list[str],
):
    """Policy para Lambda que invoca SageMaker + lee S3."""
    endpoint_arn = f"arn:aws:sagemaker:{REGION}:{ACCOUNT_ID}:endpoint/{endpoint_name}"

    policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "InvokeSageMakerEndpoint",
                "Effect": "Allow",
                "Action": ["sagemaker:InvokeEndpoint"],
                "Resource": [endpoint_arn],
            },
            {
                "Sid": "ReadFromS3",
                "Effect": "Allow",
                "Action": ["s3:GetObject"],
                "Resource": [
                    f"arn:aws:s3:::{bucket}/{prefix}*"
                    for prefix in read_prefixes
                ],
            },
            {
                "Sid": "DenyOtherSageMaker",
                "Effect": "Deny",
                "Action": [
                    "sagemaker:CreateEndpoint",
                    "sagemaker:DeleteEndpoint",
                    "sagemaker:UpdateEndpoint",
                ],
                "Resource": "*",
            },
        ],
    }

    iam.put_role_policy(
        RoleName=role_name,
        PolicyName="lambda-sagemaker-inference",
        PolicyDocument=json.dumps(policy),
    )
    print(f"SageMaker inference policy attached to {role_name}")
    print(f"  ✅ Can invoke: {endpoint_name}")
    print(f"  ❌ Cannot create/delete/update endpoints")


create_lambda_sagemaker_policy(
    role_name="ai-inference-lambda-role",
    endpoint_name="sentiment-classifier-v1",
    bucket="ai-assets-prod",
    read_prefixes=["documents/", "embeddings/"],
)

Ejercicio 4: Script de setup completo para nuevo servicio AI

Crea un script que configure todo el IAM necesario para un nuevo servicio AI: role, trust policy, policies de S3 (read + write), policy de logs, y una deny policy de seguridad. El script debe ser idempotente (ejecutarlo dos veces no falla).

Ver solución
import boto3
import json
import os

iam = boto3.client("iam", endpoint_url=os.environ.get("AWS_ENDPOINT_URL"))


def setup_ai_service_iam(
    service_name: str,
    bucket: str,
    read_prefixes: list[str],
    write_prefixes: list[str],
) -> str:
    """Setup completo de IAM para un servicio AI. Idempotente."""
    role_name = f"{service_name}-lambda-role"

    # 1. Create role (idempotent)
    trust_policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Principal": {"Service": "lambda.amazonaws.com"},
                "Action": "sts:AssumeRole",
            }
        ],
    }

    try:
        response = iam.create_role(
            RoleName=role_name,
            AssumeRolePolicyDocument=json.dumps(trust_policy),
            Description=f"Lambda role for {service_name}",
        )
        role_arn = response["Role"]["Arn"]
        print(f"✅ Role created: {role_name}")
    except iam.exceptions.EntityAlreadyExistsException:
        response = iam.get_role(RoleName=role_name)
        role_arn = response["Role"]["Arn"]
        print(f"ℹ️  Role already exists: {role_name}")

    # 2. S3 read policy
    read_policy = {
        "Version": "2012-10-17",
        "Statement": [{
            "Sid": "ReadAIAssets",
            "Effect": "Allow",
            "Action": ["s3:GetObject", "s3:HeadObject", "s3:ListBucket"],
            "Resource": [f"arn:aws:s3:::{bucket}"]
            + [f"arn:aws:s3:::{bucket}/{p}*" for p in read_prefixes],
        }],
    }
    iam.put_role_policy(
        RoleName=role_name,
        PolicyName=f"{service_name}-s3-read",
        PolicyDocument=json.dumps(read_policy),
    )
    print(f"✅ S3 read policy: {read_prefixes}")

    # 3. S3 write policy
    write_policy = {
        "Version": "2012-10-17",
        "Statement": [{
            "Sid": "WriteResults",
            "Effect": "Allow",
            "Action": ["s3:PutObject"],
            "Resource": [f"arn:aws:s3:::{bucket}/{p}*" for p in write_prefixes],
        }],
    }
    iam.put_role_policy(
        RoleName=role_name,
        PolicyName=f"{service_name}-s3-write",
        PolicyDocument=json.dumps(write_policy),
    )
    print(f"✅ S3 write policy: {write_prefixes}")

    # 4. CloudWatch Logs
    try:
        iam.attach_role_policy(
            RoleName=role_name,
            PolicyArn="arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
        )
        print("✅ CloudWatch Logs policy attached")
    except iam.exceptions.ClientError:
        print("ℹ️  CloudWatch Logs policy already attached")

    # 5. Deny dangerous actions
    deny_policy = {
        "Version": "2012-10-17",
        "Statement": [{
            "Sid": "DenyDangerous",
            "Effect": "Deny",
            "Action": ["s3:DeleteBucket", "iam:*", "s3:PutBucketPolicy"],
            "Resource": "*",
        }],
    }
    iam.put_role_policy(
        RoleName=role_name,
        PolicyName=f"{service_name}-deny-dangerous",
        PolicyDocument=json.dumps(deny_policy),
    )
    print("✅ Deny policy for dangerous actions")

    print(f"\n{'='*50}")
    print(f"Service: {service_name}")
    print(f"Role ARN: {role_arn}")
    print(f"Read:  {', '.join(read_prefixes)}")
    print(f"Write: {', '.join(write_prefixes)}")
    print(f"{'='*50}")

    return role_arn


setup_ai_service_iam(
    service_name="rag-service",
    bucket="ai-assets-prod",
    read_prefixes=["documents/", "embeddings/", "prompts/"],
    write_prefixes=["responses/"],
)

Resumen

  • IAM controla quién puede hacer qué en AWS. Para Lambda, defines un Role con Policies que especifican acciones y recursos permitidos.
  • Least privilege: solo los permisos mínimos necesarios. s3:GetObject en bucket/prefix/*, no s3:* en *.
  • Trust policy permite que Lambda asuma el role. Permission policies definen qué puede hacer con ese role.
  • Inline policies para permisos específicos de un role. Managed policies para permisos compartidos entre roles.
  • Deny policies protegen contra acciones peligrosas (delete bucket, IAM changes) incluso si otra policy las permite.
  • LocalStack no enforcea IAM. Tus policies se validan cuando migras a AWS real. Define policies antes de migrar.
  • SAM Policy Templates simplifican IAM en templates — S3ReadPolicy, S3CrudPolicy, LambdaInvokePolicy.

Recursos Adicionales

  1. IAM Best Practices — Mejores prácticas oficiales
  2. IAM Policy Reference — Referencia completa de policies
  3. IAM Policy Simulator — Herramienta para validar policies
  4. SAM Policy Templates — Templates predefinidos
  5. S3 Bucket Policies — Policies a nivel bucket
  6. Lambda Execution Role — Roles de ejecución de Lambda
  7. IAM Access Analyzer — Herramienta para detectar permisos excesivos
  8. AWS Security Hub — Monitoreo de seguridad centralizado