---
name: iam-secretos
description: >
  Identity and Access Management y gestión de secretos: RBAC, ABAC, HashiCorp Vault,
  AWS Secrets Manager, Azure Key Vault, rotación de secretos, principio de mínimo
  privilegio, cuentas de servicio, gestión de API keys, zero trust.
version: "1.1.0"
evolved: true
evolved-from: "1.0.0"
evolved-at: "2026-04-24"
evolved-by: "aprender"
evolved-note: "Sección nueva: logs con PII → gitignore + hard-block env flag en producción (defense in depth)"
herramientasPermitidas: [Read, Grep]
evolvable: true  # default para skill estandar
nist_csf: [PR.AA-01, PR.AA-02, PR.AA-05, PR.DS-02]
attack_techniques: [T1552, T1078]
d3fend_techniques: [D3-MFA]
exclusiones:
  - "No cargar para autenticación y autorización de usuarios en el frontend (login, JWT en browser, OAuth PKCE) — para auth de usuario final cargar `auth-patrones`."
  - "No cargar para configuración de IAM en nubes específicas (AWS IAM roles, GCP Service Accounts, Azure Managed Identity) sin relación con la aplicación — para cloud IAM cargar el skill de la nube correspondiente (`cloud-aws`, `gcp-cloud`, `azure-cloud`)."
  - "No cargar para auditoría de dependencias con CVEs (vulnerabilidades en paquetes npm/pip) — para auditoría de dependencias cargar `dependencias-auditoria`."
  - "No cargar para cifrado de datos en reposo o en tránsito sin relación con secretos o identidades (TLS, AES en columnas de BD) — para cifrado de datos cargar el skill de seguridad del stack correspondiente."
---
# IAM y Gestión de Secretos — Guía de Producción

## Cuándo NO cargar

- La tarea es autenticación de usuario en el frontend (login, JWT en browser, OAuth PKCE): cargar `auth-patrones`.
- La tarea es configuración de IAM en la nube (AWS IAM, GCP Service Accounts, Azure Managed Identity) sin relación con la aplicación: cargar el skill de la nube correspondiente.
- La tarea es auditoría de CVEs en paquetes de dependencias: cargar `dependencias-auditoria`.
- La tarea es cifrado de datos en reposo o en tránsito sin relación con identidades o secretos: cargar el skill de seguridad del stack.

## Principios Base

### Zero Trust — "Nunca confíes, siempre verifica"

```
Modelo perimetral tradicional:    Modelo Zero Trust:
  [Firewall externo]                Toda petición se autentica
  └── Interior "seguro"            Toda petición se autoriza
       └── Sin verificación        Acceso mínimo siempre
                                   Red, identidad y datos cifrados
```

**Reglas de oro:**
1. Ningún secreto en código fuente, variables de entorno de CI/CD, ni logs.
2. Todo acceso privilegiado tiene duración máxima y se rota periódicamente.
3. Las cuentas de servicio tienen permisos mínimos necesarios (least privilege).
4. Toda acción privilegiada deja registro de auditoría.

---

## RBAC — Control de Acceso Basado en Roles

```python
from enum import Enum
from typing import FrozenSet


class Permiso(str, Enum):
    USUARIO_LEER     = "usuario:leer"
    USUARIO_CREAR    = "usuario:crear"
    USUARIO_EDITAR   = "usuario:editar"
    USUARIO_ELIMINAR = "usuario:eliminar"
    REPORTE_LEER     = "reporte:leer"
    REPORTE_EXPORTAR = "reporte:exportar"
    ADMIN_CONFIGURAR = "admin:configurar"
    ADMIN_AUDITAR    = "admin:auditar"


class Rol(str, Enum):
    LECTURA   = "LECTURA"
    OPERADOR  = "OPERADOR"
    ADMIN     = "ADMIN"
    AUDITOR   = "AUDITOR"
    SUPER     = "SUPER"


PERMISOS_POR_ROL: dict[Rol, FrozenSet[Permiso]] = {
    Rol.LECTURA: frozenset({Permiso.USUARIO_LEER, Permiso.REPORTE_LEER}),
    Rol.OPERADOR: frozenset({
        Permiso.USUARIO_LEER, Permiso.USUARIO_CREAR, Permiso.USUARIO_EDITAR,
        Permiso.REPORTE_LEER, Permiso.REPORTE_EXPORTAR,
    }),
    Rol.ADMIN: frozenset({
        Permiso.USUARIO_LEER, Permiso.USUARIO_CREAR, Permiso.USUARIO_EDITAR,
        Permiso.USUARIO_ELIMINAR, Permiso.REPORTE_LEER,
        Permiso.REPORTE_EXPORTAR, Permiso.ADMIN_CONFIGURAR,
    }),
    Rol.AUDITOR: frozenset({Permiso.USUARIO_LEER, Permiso.REPORTE_LEER, Permiso.ADMIN_AUDITAR}),
    Rol.SUPER: frozenset(Permiso),
}


def requiere_permiso(permiso: Permiso):
    """Decorador FastAPI para verificación de permisos granulares."""
    from fastapi import Depends, HTTPException, status

    def verificar(usuario=Depends(get_current_user)):
        if permiso not in PERMISOS_POR_ROL.get(usuario.rol, frozenset()):
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail={"permiso_requerido": permiso.value, "rol_actual": usuario.rol.value},
            )
        return usuario

    return Depends(verificar)
```

---

## Anti-Patrones de IAM y Secretos

| Anti-patrón | Riesgo | Solución |
|-------------|--------|----------|
| Secretos en variables de entorno del proceso | Visibles en `/proc/environ` | Usar Vault/AWS SM con acceso programático |
| Secretos en código fuente o Git | Exposición permanente | Detección con `git-secrets`, `truffleHog` en CI |
| Credenciales de larga duración sin rotación | Ventana de ataque infinita | Credenciales dinámicas o rotación automática |
| Rol de servicio con permisos de admin | Un compromiso = acceso total | Least privilege: solo los permisos necesarios |
| API keys sin expiración ni scopes | Key comprometida da acceso total | Expiración obligatoria + scopes granulares |
| Logs que contienen secretos | Secretos filtrados en logs | Nunca loguear headers de Authorization ni tokens |
| Sin auditoría de acceso a secretos | Sin visibilidad | Habilitar audit logs en Vault/AWS SM |

---

## Implementaciones Completas

Para implementaciones de ABAC, HashiCorp Vault, AWS Secrets Manager con caché,
gestión de API Keys, estrategias de rotación de secretos y configuración de
cuentas de servicio en Kubernetes, ver
[recursos/implementaciones-completas.md](recursos/implementaciones-completas.md).

---

## Checklist de Revisión — IAM y Secretos

- [ ] Cero secretos en código fuente, `.env` de producción, o CI/CD variables sin cifrar.
- [ ] Toda cuenta de servicio tiene permisos mínimos (least privilege).
- [ ] Los secretos se leen de Vault/AWS SM en tiempo de ejecución, no en tiempo de build.
- [ ] Las API keys se almacenan sólo como hash (SHA-256), nunca en texto plano.
- [ ] Hay un plan de rotación documentado con frecuencia para cada tipo de secreto.
- [ ] El acceso a recursos sensibles pasa por verificación ABAC (no sólo RBAC) donde aplica.
- [ ] Los audit logs de acceso a secretos están habilitados y retenidos por 1 año mínimo.
- [ ] Los Kubernetes ServiceAccounts tienen RBAC restringido a sus propios recursos.
- [ ] Los certificados TLS tienen alerta de expiración 30 días antes.
- [ ] El código tiene pruebas que verifican que usuarios sin rol no pueden acceder a recursos protegidos.

## Gotchas / Errores comunes no obvios

**Las variables de entorno de proceso no son más seguras que los archivos `.env` en contenedores Docker porque `docker inspect` expone todas las variables de entorno en texto plano**: configurar secretos con `-e DATABASE_PASSWORD=secreto` en `docker run` o en `docker-compose.yml` los hace visibles para cualquiera con acceso al daemon Docker (`docker inspect <container>`). Causa: las variables de entorno de proceso en Linux son visibles en `/proc/<pid>/environ` y en la API de Docker sin autenticación adicional. Fix: usar Docker Secrets (`docker secret create`) o montar el secreto como archivo en un volumen `tmpfs` y leerlo desde el código, nunca como variable de entorno de proceso.

**HashiCorp Vault con `AppRole` en CI/CD usa el mismo `role_id` + `secret_id` para todos los pipelines, eliminando la trazabilidad de auditoría**: si el `secret_id` es estático y compartido entre todos los jobs de CI, el audit log de Vault solo muestra "appRole autenticó" sin identificar cuál pipeline específico accedió al secreto. Causa: el propósito de `AppRole` es autenticación por instancia, pero se usa como credencial global. Fix: usar `secret_id` de un solo uso (`use_limit=1`) generado por cada job de CI al inicio del pipeline con un `wrapped token`. Esto garantiza trazabilidad y elimina la ventana de reutilización.

**La rotación automática de secretos en AWS Secrets Manager puede romper conexiones de larga duración a la BD que no leen el secreto en cada conexión**: las aplicaciones que leen `DATABASE_URL` una sola vez al arrancar y la cachean en memoria durante la vida del proceso siguen usando la credencial antigua después de la rotación. Causa: AWS SM rota el secreto pero no reinicia la aplicación ni invalida el caché del proceso. Fix: implementar un mecanismo de reconexión que re-lee el secreto desde SM cuando la BD devuelve un error de autenticación (código `PGCONN_ERROR` en PostgreSQL o `ER_ACCESS_DENIED_ERROR` en MySQL). No cachear la URL de conexión más allá de la frecuencia de rotación.

**RBAC implementado solo en el frontend produce IDOR (Insecure Direct Object Reference) cuando los endpoints no validan permisos server-side**: si el botón "Eliminar" se oculta en la UI para usuarios sin el rol `ADMIN` pero el endpoint `DELETE /api/usuarios/:id` no verifica el rol del JWT, cualquier usuario autenticado puede enviar el request directamente con curl y eliminará el registro. Causa: la verificación de RBAC en el frontend es UX, no seguridad. Fix: el decorador `requiere_permiso(Permiso.USUARIO_ELIMINAR)` o equivalente DEBE estar en CADA endpoint mutante, sin excepción. Verificar en code review que no hay endpoint de escritura sin `Depends(require_role([...]))` en FastAPI o equivalente en el framework del stack.

---

## Logs con PII: gitignore + hard-block env flag en producción (defense in depth)

### Regla

Cuando la aplicación tiene capacidad de loggear contenido que puede incluir
PII (prompts de usuario, payloads de request, body de webhooks de terceros,
stack traces con datos concretos), aplicar **defensa en profundidad** con dos
capas independientes:

1. **Capa 1 — Infraestructura**: `logs/*.jsonl` (o el path equivalente) fuera
   del tracking de git via `.gitignore`. Previene fuga accidental al publicar
   el repo.
2. **Capa 2 — Runtime**: flag de feature (`llm_prompt_audit_log_text=True` o
   similar) que **rechaza duro** en producción, sin importar si la capa 1
   funcionó o no.

Una sola capa no basta: `.gitignore` no protege contra un bucket mal configurado
ni contra un desarrollador que copia logs a otro destino. El env flag por sí
solo no protege contra un commit accidental del log mismo.

### Implementación canónica

```python
# .gitignore (capa 1)
logs/
*.jsonl
*.log
data/local/
```

```python
# config.py (capa 2: runtime guard)
from pydantic import field_validator, Field
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    environment: str = Field("development")
    llm_prompt_audit_log_text: bool = Field(False)

    @field_validator("llm_prompt_audit_log_text")
    @classmethod
    def _bloquear_en_produccion(cls, v: bool, info) -> bool:
        ambiente = info.data.get("environment", "development")
        if v and ambiente == "production":
            raise ValueError(
                "llm_prompt_audit_log_text=True está prohibido con "
                "environment=production. Los logs contienen PII y no "
                "pueden persistir fuera de dev/staging. "
                "Para auditoría en prod, usar Presidio + redaction."
            )
        return v
```

```python
# En el servicio que loggea
from config import settings

def log_llm_prompt(prompt: str, user_id: str) -> None:
    if not settings.llm_prompt_audit_log_text:
        # Log sin el cuerpo del prompt: solo metadata
        logger.info("llm_prompt_sent", extra={
            "user_id_hash": _hash_estable(user_id),
            "prompt_len": len(prompt),
            "tokens_estimados": estimar_tokens(prompt),
        })
        return

    # Este path es inalcanzable en production (validator lanza al cargar settings).
    # Solo se alcanza en dev/staging con flag explícito.
    logger.debug("llm_prompt_content", extra={
        "user_id_hash": _hash_estable(user_id),
        "prompt": prompt,
    })
```

### Reglas

- **`.gitignore` es capa 1**: cubre el filesystem del repo. Verificar con `git check-ignore -v logs/foo.jsonl` que el path está cubierto.
- **El env flag es capa 2**: validar con test de integración que `environment=production + flag=True` lanza al cargar settings, **no** en runtime cuando se intenta loggear.
- **Nombres del flag explícitos**: `llm_prompt_audit_log_text`, `http_body_log_full`, `stacktrace_log_full_data` — el nombre debe dejar claro que habilita log de contenido sensible. Nombres genéricos como `debug_mode` no comunican el riesgo.
- **Tercera capa opcional pero recomendada**: en el logger central, aplicar filtro Presidio o regex para redactar patrones PII (email, RFC, teléfono, CURP) antes de escribir a disco. Esto convierte "si los 2 gates fallan, el daño es total" en "si los 2 gates fallan, el daño es parcial".
- **Test de capa 2 en CI**: test que crea `Settings(environment="production", llm_prompt_audit_log_text=True)` y verifica `ValidationError`. Sin este test, la capa 2 puede desactivarse silenciosamente en un refactor futuro.

### Anti-patrón

```python
# MAL — solo capa 1 (gitignore), sin hard-block en runtime
# Si un dev sube los logs a un bucket S3 o a Slack para debugging,
# la PII sale del perímetro sin alarma.

# MAL — solo capa 2, sin gitignore
# Si el flag se activó una vez y los logs quedaron en disco,
# un `git add .` en un pull request los filtra al repo público.

# MAL — log fuente de verdad para auditoría regulatoria
# Los logs JSONL no son un sistema de auditoría conforme. Para compliance
# (GDPR Art. 30, SOC 2 CC6.2), usar un audit trail dedicado con retención,
# integridad (merkle, firmas) y control de acceso separado del log operacional.
```

### Frameworks de seguridad

- OWASP Top 10 A02:2021 — Cryptographic Failures (incluye log exposure de PII).
- NIST CSF PR.DS-01 (data-at-rest protection), PR.DS-02 (data-in-transit).
- NIST AI RMF MANAGE-3.1 — AI system information, including sensitive data, is protected.
- GDPR Art. 5 principle of data minimization.
