La lección anterior cerró con doce medidas priorizadas para Nimbus, y la número uno era el MFA. No fue casualidad: varias de las restantes giran también alrededor de la misma pregunta —quién eres y qué puedes hacer—. Cuando la mitad de la plantilla trabaja fuera de la oficina, los datos están en la nube, hay una consultora con acceso remoto y la API la consume una app móvil, el perímetro deja de ser una línea en la red. Lo que queda separando a un atacante de las agendas de las clínicas es una credencial y una decisión de autorización. Esta lección desarrolla las dos: el ciclo de vida de una identidad y las cuentas huérfanas, qué dicen hoy las guías sobre contraseñas, las formas de MFA ordenadas por su resistencia real al phishing, SSO y los protocolos de identidad con la diferencia exacta entre OAuth 2.0 y OpenID Connect, sesiones y JWT con su validación correcta frente a la ingenua, los cinco modelos de autorización con el diseño concreto de Nimbus, y las cuentas privilegiadas con acceso just-in-time y recertificación.

Contenido

  1. La identidad como nuevo perímetro
  2. Ciclo de vida de la identidad y cuentas huérfanas
  3. Factores de autenticación y sus debilidades
  4. Contraseñas: qué dicen hoy las guías
  5. MFA ordenado por resistencia al phishing
  6. SSO, federación y protocolos de identidad
  7. Sesiones y tokens: cookies y JWT
  8. Modelos de autorización y el RBAC de Nimbus
  9. Multi-tenencia y aislamiento
  10. Cuentas privilegiadas, de servicio y de terceros
  11. Recertificación: la revisión periódica de accesos

  1. La identidad como nuevo perímetro

El modelo clásico dividía el mundo en dentro y fuera, y ponía el control en la frontera. Ese modelo describe cada vez peor la realidad de Nimbus:

Elemento Dónde está ¿Lo cubre un perímetro de red?
Base de datos y buckets (A-01, A-02, A-03) Nube del proveedor (A-05) No
19 empleados en remoto Casas, cafeterías, trenes No
App móvil de los clientes Miles de dispositivos ajenos No
Repositorio y CI/CD (A-06) SaaS externo No
Consultora externa (A-19) Su propia red No
Correo, pasarela de pago, email transaccional SaaS externos No

Lo único que atraviesa todos esos contextos es la identidad. Si un atacante obtiene las credenciales de Lucía, no necesita atravesar ningún cortafuegos: entra por la puerta principal de cada uno de esos sistemas, desde cualquier lugar del mundo, y todo lo que haga parecerá legítimo.

De ahí la formulación que resume el enfoque Zero Trust de 01-03 aplicado a este dominio:

La identidad es el nuevo perímetro. No se confía en una petición por venir «de dentro»; se confía por poder verificar quién la hace, con qué dispositivo, para qué recurso y bajo qué condiciones, en cada petición.

Tres conceptos que hay que separar con precisión, porque se confunden constantemente:

Concepto Pregunta Ejemplo en Nimbus
Identificación ¿Quién dices que eres? Introducir [email protected]
Autenticación ¿Puedes demostrarlo? Contraseña + llave FIDO2
Autorización ¿Qué puedes hacer? Lucía puede desplegar; no puede ver nóminas

Y la trampa recurrente: la mayoría de los incidentes graves no son fallos de autenticación, son fallos de autorización. El IDOR de 01-01 ocurría con una sesión perfectamente válida. Autenticar bien y autorizar mal es el patrón número uno del OWASP Top 10.


  1. Ciclo de vida de la identidad y cuentas huérfanas

Una identidad no es un registro estático: es un proceso con tres momentos, y el tercero es el que casi siempre falla.

flowchart LR
    A["ALTA\nQuien entra, con que\naccesos y quien lo aprueba"] --> B["CAMBIOS\nCambio de rol, proyecto,\npermisos temporales"]
    B --> C["BAJA\nRevocacion completa\nen todos los sistemas"]
    B -->|"si nadie retira\nlo anterior"| D["ACUMULACION\nDE PRIVILEGIOS"]
    C -->|"si se olvida\nalgun sistema"| E["CUENTA\nHUERFANA"]
    D --> F["Un usuario con\nmas permisos que\ncualquier administrador"]
    E --> G["Acceso valido sin\ndueno ni vigilancia"]

2.1 Alta

Los cuatro requisitos de un alta correcta:

  1. Basada en el rol, no en la copia. El error clásico es «dale los mismos permisos que a Iván». Se copian también los permisos que Iván acumuló por proyectos antiguos, y el privilegio se propaga como una infección. Debe existir un perfil de acceso por rol predefinido.
  2. Con aprobación registrada del propietario del activo (el campo «propietario» del inventario de 01-04 sirve exactamente para esto).
  3. Con caducidad si es temporal. Un becario, un contrato de tres meses o una consultora deben tener fecha de fin en el sistema, no en la memoria de alguien.
  4. Con identidad nominal. Nada de cuentas compartidas: sin identidad nominal no hay trazabilidad, y sin trazabilidad no hay no repudio (01-01).

2.2 Cambios: la acumulación de privilegios

Es el problema silencioso. Rubén entró en soporte, ayudó seis meses en facturación, participó en la migración de datos y ahora coordina el equipo. Si en cada cambio se añadieron permisos sin retirar los anteriores, Rubén tiene hoy más accesos que cualquier otra persona de Nimbus, y nadie lo ha decidido nunca.

La regla: todo cambio de rol es una baja del rol anterior y un alta del nuevo, no una suma. Y todo permiso temporal nace con fecha de fin.

2.3 Baja: el caso de Nimbus

Un desarrollador se marcha de Nimbus. Lucía desactiva su cuenta de correo el mismo día y da el proceso por terminado. Seis meses después, en la revisión de inventario, aparece esto:

Sistema Estado tras la «baja» Riesgo
Correo corporativo Desactivado
Cuenta del proveedor cloud (A-05) Activa, con permisos de lectura sobre los buckets Acceso a datos de clientes desde cualquier lugar
GitHub Activa como colaborador externo del repositorio Acceso al código y al historial, incluidos secretos antiguos
VPN Desactivada
Base de datos Usuario personal dev_carlos activo Acceso directo a datos de producción
Clave SSH en el servidor de aplicaciones Presente en authorized_keys Acceso al servidor sin pasar por ninguna autenticación centralizada
Herramienta de tickets (SaaS) Activa Datos de clientes en las incidencias
Grupo de WhatsApp de soporte Presente Capturas de pantalla con datos de clientes
Token personal de GitHub creado por él Activo y sin caducidad Acceso al código aunque se desactive la cuenta

Ocho accesos vivos de una persona que ya no trabaja aquí. Y ninguno de ellos aparece en un cortafuegos, porque todos son legítimos desde el punto de vista técnico.

Por qué las cuentas huérfanas son especialmente peligrosas, más allá de lo obvio:

  • Nadie las vigila. Un uso anómalo de la cuenta de Lucía se puede notar; nadie echa de menos a dev_carlos.
  • Nadie las mantiene. Su contraseña no se rota, no se les añade MFA cuando se despliega, no se revisan sus permisos.
  • Son el objetivo preferido del credential stuffing. Si esa persona reutilizaba la contraseña y aparece en una brecha de otro servicio, la puerta sigue abierta.
  • No siempre hay mala intención. El ex empleado que sigue accediendo «para ayudar» crea un problema igual de serio: acceso sin autorización vigente y sin control.

El procedimiento de baja correcto para Nimbus:

CHECKLIST DE BAJA — se ejecuta el ULTIMO DIA, no despues
[ ] Desactivar la identidad central (SSO) — bloquea todo lo federado
[ ] Revocar sesiones y tokens activos (no basta con desactivar la cuenta:
    una sesion viva sigue funcionando)
[ ] Revisar la lista de sistemas NO federados, uno a uno:
    [ ] Proveedor cloud   [ ] GitHub y tokens personales
    [ ] Base de datos     [ ] Claves SSH en authorized_keys
    [ ] SaaS de tickets   [ ] Pasarela de pago  [ ] Email transaccional
[ ] Retirar de grupos y canales de mensajeria
[ ] Reasignar la propiedad de sus activos del inventario (01-04)
[ ] Rotar los secretos compartidos que conocia
[ ] Recuperar el portatil y la clave de recuperacion de cifrado
[ ] Registrar la baja completada, con fecha y responsable

Los dos puntos que más se olvidan son la revocación de sesiones y tokens —desactivar una cuenta no invalida una sesión ya emitida, como viste en el pass-the-cookie de 02-03— y la rotación de secretos compartidos: si esa persona conocía la contraseña del NAS o la clave de la pasarela, esas credenciales están comprometidas por definición.

La medida estructural que reduce el problema a la mitad: cuantos más sistemas estén federados con un único proveedor de identidad, menos casillas hay que marcar. Es la razón principal por la que el SSO es una medida de seguridad y no solo de comodidad.


  1. Factores de autenticación y sus debilidades

Un factor es una categoría de prueba. La autenticación multifactor exige factores de categorías distintas; dos contraseñas no son dos factores.

Factor Qué es Ejemplos Debilidades
Algo que sabes Conocimiento Contraseña, PIN, respuesta a pregunta Se puede robar, adivinar, reutilizar, phishear y compartir sin dejar rastro
Algo que tienes Posesión Llave FIDO2, app de autenticación, tarjeta, teléfono Se pierde, se roba; el SMS es interceptable; los códigos son phisheables
Algo que eres Biometría Huella, rostro No se puede cambiar si se compromete; falsos positivos y negativos; en la práctica desbloquea un dispositivo, no autentica al servicio
(Contextual) Dónde y cómo IP, país, dispositivo conocido, hora No es un factor por sí solo: es una señal de riesgo que modula la exigencia

Tres precisiones que corrigen malentendidos frecuentes:

  1. La biometría casi nunca viaja al servidor. Cuando Iván desbloquea su móvil con la huella, el dato biométrico no sale del dispositivo: desbloquea una clave privada almacenada en el hardware seguro. Lo que el servicio recibe es una prueba criptográfica. Esa distinción es la que hace que las passkeys sean sólidas y no un problema de privacidad.
  2. Las preguntas de seguridad no son un factor, son una contraseña peor: la respuesta suele ser pública o adivinable, y no se puede cambiar el nombre del colegio de tu infancia.
  3. El contexto no sustituye a un factor, pero es enormemente valioso combinado: exigir reautenticación cuando cambia el país o el dispositivo es lo que convierte un token robado en un token inútil.

  1. Contraseñas: qué dicen hoy las guías

Las recomendaciones cambiaron sustancialmente en la última década, y muchas organizaciones siguen aplicando las de 2005 —que empeoran la seguridad—. La referencia actual es la guía NIST SP 800-63B.

Práctica tradicional Recomendación actual Por qué cambió
Mínimo 8 caracteres con mayúsculas, minúsculas, números y símbolos Mínimo 12-15 caracteres, sin exigir composición La complejidad obligatoria produce Empresa2026! de forma predecible; la longitud aporta mucha más resistencia
Caducidad cada 90 días Sin caducidad forzada, salvo indicio de compromiso La rotación obligatoria genera variaciones triviales (…2026!…2027!) y aumenta el uso de notas escritas
Prohibir pegar en el campo Permitir pegar siempre Prohibirlo impide el uso de gestores de contraseñas, que es lo que de verdad ayuda
Preguntas de seguridad como respaldo Eliminarlas Respuestas públicas o adivinables
Pistas de contraseña Eliminarlas Son una filtración parcial de la contraseña
Comprobar contra listas de contraseñas filtradas Impide de raíz el credential stuffing y el diccionario
Permitir todos los caracteres, incluidos espacios y unicode Facilita frases de paso largas
Longitud máxima ≥ 64 caracteres Un máximo bajo delata que la contraseña podría no almacenarse correctamente

La comprobación contra listas de filtradas es la medida individual más eficaz de esta tabla, y la menos implantada. Impide que alguien elija una contraseña que ya está en los diccionarios que usan los atacantes. Se implementa sin enviar la contraseña a ningún sitio: se envían los primeros caracteres del hash y se compara localmente el resto (modelo de k-anonimato).

Gestores de contraseñas. Para Nimbus son obligatorios, y el argumento es simple: hacen posible cumplir lo único que de verdad importa —una contraseña larga, aleatoria y distinta por servicio— sin memorizar nada. La objeción habitual («¿y si comprometen el gestor?») ignora la alternativa real, que no es memorizar 40 contraseñas fuertes: es reutilizar tres débiles. Un gestor con contraseña maestra fuerte y MFA es, con enorme diferencia, la opción más segura. Ventaja añadida y poco conocida: el gestor no rellena las credenciales en un dominio que no coincide, lo que lo convierte en un detector de phishing pasivo.

El almacenamiento seguro de contraseñas en el servidor —por qué nunca se guardan en claro ni con un hash simple, y qué son bcrypt, scrypt y Argon2 con su factor de coste y su sal— se estudia en detalle en 03-04. Aquí basta con la regla: Nimbus nunca almacena contraseñas recuperables, y ningún proceso legítimo puede devolverle a un cliente su contraseña actual.


  1. MFA ordenado por resistencia al phishing

El MFA es la medida más rentable de la lección, pero no todas las formas de MFA valen lo mismo. La diferencia decisiva es si el segundo factor puede ser capturado y reutilizado por un atacante interpuesto.

Método Cómo funciona Resistencia al phishing Otras debilidades Uso en Nimbus
SMS Código de 6 dígitos por mensaje Muy baja: el código se teclea y se puede reenviar en tiempo real Intercambio de SIM, interceptación, dependencia de cobertura Solo como último recurso; mejor que nada
Correo electrónico Código o enlace al buzón Muy baja Si el correo cae, cae todo Evitar
TOTP (app de autenticación) Código de 6 dígitos derivado de un secreto compartido y del tiempo Baja-media: sigue siendo un código que se puede pedir El secreto se puede copiar al configurarlo; ventana de 30 s Mínimo aceptable para cuentas normales
Push simple Notificación con «Aprobar / Denegar» Baja: vulnerable a MFA fatigue (02-03) Aprobación sin contexto Insuficiente por sí solo
Push con number matching Muestra un número en la pantalla que hay que introducir en la app Media: exige mirar el dispositivo original Aún phisheable con esfuerzo Aceptable
FIDO2 / WebAuthn y passkeys Criptografía de clave pública vinculada al dominio Alta: resistente al phishing por diseño Coste de la llave física; gestión de respaldo Objetivo para todas las cuentas administrativas
Certificado de cliente Certificado en el dispositivo Alta Gestión de PKI (03-06) Para servicio a servicio

5.1 Por qué FIDO2 es cualitativamente distinto

Todos los métodos anteriores comparten un defecto de fondo: producen un secreto que el usuario transmite. Si el usuario está en una web falsa, transmite el secreto al atacante, que lo reenvía al sitio real en segundos. Eso es exactamente lo que hacen los kits de phishing con proxy inverso, y por eso «tenemos MFA» dejó de ser una respuesta suficiente.

FIDO2 rompe ese esquema con dos propiedades:

  1. La clave privada nunca sale del dispositivo. No hay ningún secreto que el usuario pueda entregar, ni siquiera queriendo.
  2. La firma está vinculada al dominio (origin binding). El navegador incluye en la operación el origen real de la página. Si el usuario está en nimbusreservas.example.portal-facturas.example, la llave produce una firma para ese dominio, que el servidor legítimo rechaza. El ataque no falla porque el usuario se dé cuenta: falla porque es criptográficamente inválido.

Formulado de otro modo: con TOTP o SMS, la defensa depende de que el usuario detecte la web falsa. Con FIDO2, la defensa funciona aunque el usuario no detecte nada. Es la diferencia entre una medida que depende del criterio humano y una que no. Recuerda el principio de 02-03: si tu defensa exige que 38 personas acierten siempre, no tienes defensa.

Las passkeys son FIDO2 con la clave sincronizada entre los dispositivos del usuario a través de su ecosistema, lo que elimina la principal fricción (perder la llave) a cambio de confiar en el sincronizador.

5.2 Verificación de TOTP, explicada

Aunque el objetivo sea FIDO2, TOTP sigue siendo el mínimo realista para muchas cuentas. Conviene entender cómo funciona por dentro.

import hmac, hashlib, struct, time

def codigo_totp(secreto: bytes, momento: int | None = None,
                paso: int = 30, digitos: int = 6) -> str:
    """Genera el codigo TOTP de 6 digitos (RFC 6238)."""
    # 1. El "contador" es el numero de intervalos de 30 s desde 1970.
    #    Emisor y verificador lo calculan por separado: no se transmite nada.
    contador = int((momento or time.time()) // paso)

    # 2. HMAC-SHA1 del contador con el secreto compartido.
    #    HMAC garantiza que solo quien conoce el secreto genera el valor (03-04).
    resumen = hmac.new(secreto, struct.pack(">Q", contador), hashlib.sha1).digest()

    # 3. "Truncamiento dinamico": los 4 ultimos bits indican desde donde leer.
    desplazamiento = resumen[-1] & 0x0F
    valor = struct.unpack(">I", resumen[desplazamiento:desplazamiento + 4])[0]
    valor &= 0x7FFFFFFF                      # se descarta el bit de signo

    # 4. Se reducen a 6 digitos.
    return str(valor % (10 ** digitos)).zfill(digitos)


def verificar_totp(secreto: bytes, codigo: str, usuario_id: int,
                   ventana: int = 1) -> bool:
    """Verificacion correcta: tolerancia de reloj, tiempo constante y
       proteccion contra reutilizacion."""
    ahora = time.time()

    # (a) Se aceptan el intervalo actual y uno antes/despues: los relojes
    #     no estan perfectamente sincronizados. Ventanas mayores debilitan.
    for delta in range(-ventana, ventana + 1):
        esperado = codigo_totp(secreto, ahora + delta * 30)
        # (b) Comparacion en tiempo constante: evita deducir el codigo
        #     midiendo cuanto tarda la comparacion en fallar.
        if hmac.compare_digest(esperado, codigo):
            # (c) Un codigo solo puede usarse UNA vez: sin esto, un codigo
            #     capturado sirve durante los 30-90 s siguientes.
            if cache.get(f"totp:{usuario_id}:{codigo}"):
                return False
            cache.set(f"totp:{usuario_id}:{codigo}", "1", expira=120)
            return True
    return False

Las cuatro decisiones que separan esta implementación de una ingenua:

  • (a) Ventana de tolerancia acotada. Sin tolerancia, los usuarios con el reloj ligeramente desviado no pueden entrar. Con una ventana grande (por ejemplo ±5), el código vale casi cinco minutos y el ataque en tiempo real se vuelve trivial. ±1 es el equilibrio habitual.
  • (b) hmac.compare_digest. Una comparación normal de cadenas termina en cuanto encuentra una diferencia, y ese tiempo es medible. En tiempo constante no se filtra nada.
  • (c) Consumo de un solo uso. Es el control que más se olvida: sin él, un código capturado por phishing sigue siendo válido durante la ventana restante, que es justamente lo que necesita el atacante interpuesto.
  • El secreto se almacena cifrado en la base de datos, no en claro: quien lo lea puede generar códigos indefinidamente.

Y la conclusión honesta: por bien implementado que esté, TOTP sigue siendo phisheable, porque el usuario teclea un código en una página. Por eso la línea de trabajo para Nimbus es TOTP como mínimo general y FIDO2 para lo administrativo.

5.3 MFA fatigue y su corrección

Conectando con 02-03: el bombardeo de notificaciones explota que aprobar es un botón sin contexto. Las correcciones, por orden de eficacia:

  1. Number matching: obliga a mirar la pantalla del origen y a introducir un número, lo que hace imposible aprobar por inercia.
  2. Contexto en la notificación: aplicación, ubicación aproximada, dispositivo. Una petición desde otro país es evidente.
  3. Límite de intentos: tras 3 peticiones denegadas o ignoradas, se bloquea la cuenta y se alerta.
  4. Alerta a seguridad ante ráfagas: una ráfaga de peticiones MFA significa que alguien ya tiene la contraseña, con independencia de que la apruebe o no. Es una señal de alto valor que casi nadie vigila.

  1. SSO, federación y protocolos de identidad

6.1 Por qué el SSO es una medida de seguridad

Con inicio de sesión único, el usuario se autentica una vez ante un proveedor de identidad y accede a todas las aplicaciones federadas.

Ventaja Por qué importa en Nimbus
Una baja lo desactiva todo Reduce drásticamente la lista de casillas del apartado 2.3
MFA una vez, aplicado en todas partes No hay que configurarlo servicio a servicio
Un único punto de registro Todos los inicios de sesión en un log, correlacionables
Políticas centralizadas Exigir dispositivo conocido o reautenticación por riesgo, en un solo sitio
Menos contraseñas Menos reutilización, menos phishing eficaz

El contrapeso, que hay que asumir conscientemente: el proveedor de identidad se convierte en el activo más crítico de todos. Si cae, nadie entra a nada; si lo comprometen, se compromete todo. Por eso: MFA FIDO2 obligatorio para sus administradores, registro exhaustivo, y un procedimiento de acceso de emergencia con credenciales guardadas fuera de línea para el caso de indisponibilidad.

6.2 OAuth 2.0 frente a OpenID Connect: la diferencia exacta

Es la confusión más extendida del dominio, y produce sistemas inseguros.

OAuth 2.0 OpenID Connect (OIDC)
Para qué sirve Autorización: dar a una aplicación acceso limitado a recursos en nombre del usuario Autenticación: acreditar quién es el usuario
Pregunta que responde «¿Puede esta app leer tu calendario?» «¿Quién eres?»
Qué entrega Access token: una llave con permisos ID token (un JWT firmado) con la identidad
Quién lo consume La API que recibe el token La aplicación que hace el login
Relación Es la base Es una capa encima de OAuth 2.0

La frase que hay que fijar: OAuth 2.0 no es un protocolo de autenticación. Usar un token de acceso como prueba de identidad es un error clásico: ese token dice qué se puede hacer, no quién eres, y podría haber sido emitido para otra aplicación. Si lo que necesitas es «quién es este usuario», usa OIDC y valida el ID token.

SAML es el veterano: mismo objetivo que OIDC (autenticación federada), basado en XML y aserciones firmadas, muy implantado en el entorno corporativo. Para integraciones nuevas se prefiere OIDC por su simplicidad y su encaje natural con aplicaciones móviles y SPAs; SAML sigue siendo obligado cuando un cliente corporativo grande lo exige, y es una petición habitual en un SaaS B2B como Nimbus.

6.3 El flujo correcto para la SPA de Nimbus: Authorization Code + PKCE

sequenceDiagram
    participant U as Usuario
    participant SPA as SPA de Nimbus
    participant IDP as Proveedor de identidad
    participant API as API de Nimbus

    SPA->>SPA: Genera code_verifier aleatorio<br/>y code_challenge = SHA256(verifier)
    SPA->>U: Redirige al IdP con code_challenge
    U->>IDP: Se autentica (contrasena + FIDO2)
    IDP->>IDP: Valida credenciales y consentimiento
    IDP->>SPA: Redirige con un CODIGO de un solo uso
    SPA->>IDP: Canjea codigo + code_verifier
    IDP->>IDP: Comprueba SHA256(verifier) == challenge
    IDP->>SPA: ID token (quien eres) + Access token (que puedes)
    SPA->>API: GET /api/v1/reservas<br/>Authorization: Bearer <access token>
    API->>API: Valida firma, iss, aud, exp y scope
    API->>SPA: 200 con los datos del tenant del usuario

Por qué PKCE es imprescindible y no un extra. El código de autorización viaja por el navegador, y en una aplicación pública (SPA o app móvil) no hay forma de guardar un secreto de cliente: cualquiera puede descompilar la app o leer el JavaScript. Si un atacante intercepta el código, podría canjearlo por tokens.

PKCE lo impide con una idea sencilla: la aplicación genera un valor aleatorio (code_verifier), envía solo su hash al iniciar el flujo y presenta el valor original al canjear. Quien no tenga el code_verifier no puede usar el código robado, y ese valor nunca sale del navegador que inició el flujo.

Los cuatro errores frecuentes en este flujo:

  • Usar el flujo implícito (tokens en la URL): obsoleto y desaconsejado.
  • Guardar los tokens en localStorage: accesible desde JavaScript, luego robable con un XSS. Preferible una cookie HttpOnly gestionada por un intermediario de servidor.
  • No validar aud en la API: aceptar un token emitido para otra aplicación.
  • No validar el parámetro state: es la protección contra CSRF del propio flujo de autenticación.

  1. Sesiones y tokens: cookies y JWT

7.1 Cookies seguras

Set-Cookie: sesion=aB3xK9...; HttpOnly; Secure; SameSite=Lax;
            Path=/; Max-Age=3600; Domain=app.nimbusreservas.example
Atributo Qué hace Qué ataque para
HttpOnly El JavaScript no puede leerla Robo de sesión mediante XSS
Secure Solo se envía por HTTPS Captura en tránsito
SameSite=Lax No se envía en peticiones originadas por otro sitio CSRF (02-02)
Max-Age=3600 Vida corta con renovación Pass-the-cookie: reduce la ventana de la cookie robada
Path y Domain acotados Alcance mínimo Que la cookie viaje a subdominios innecesarios
Prefijo __Host- en el nombre Fuerza Secure, Path=/ y ausencia de Domain Fijación de cookie desde un subdominio comprometido

SameSite=Strict frente a Lax: Strict es más seguro pero rompe la navegación desde enlaces externos (el usuario llega al panel y aparece como no autenticado). Lax es el equilibrio habitual; para operaciones sensibles se combina con token anti-CSRF.

7.2 JWT: estructura y qué no meter dentro

Un JWT tiene tres partes separadas por puntos: cabecera, carga útil y firma, codificadas en base64url.

eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMjYtMDMifQ.eyJpc3MiOiJodHRwczovL2lkc...
└──── cabecera ────┘ └──────── carga util ────────┘ └── firma ──┘
{
  "iss": "https://identidad.nimbusreservas.example",
  "sub": "usr_8812",
  "aud": "https://api.nimbusreservas.example",
  "exp": 1774000000,
  "iat": 1773999100,
  "jti": "9f2a4c1e-77bb-4a10-9c31-7d2e5a6b1c04",
  "tenant_id": 42,
  "roles": ["recepcion"]
}

Lo primero que hay que entender, y lo que más se malinterpreta: un JWT está firmado, no cifrado. Cualquiera que lo tenga puede leer su contenido sin ninguna clave. La firma garantiza integridad y origen, no confidencialidad.

Qué NO debe ir nunca dentro de un JWT:

No incluir Por qué
Contraseñas o secretos Es texto legible por cualquiera
Datos personales sensibles (nombre del paciente, servicio médico) Viaja en cada petición, se registra en logs y proxies, queda en el navegador
Números de tarjeta o cuenta Mismo motivo, con obligaciones específicas
Datos que cambian a menudo El token es una foto: si se revoca un rol, el token viejo sigue diciendo lo contrario
Estructuras grandes El JWT viaja en cada petición: penaliza rendimiento

Lo que sí debe llevar: un identificador de sujeto (sub), el tenant, los roles o ámbitos mínimos, y siempre iss, aud, exp, iat y jti.

7.3 Validación correcta frente a validación ingenua

# ============ VALIDACION INGENUA Y VULNERABLE ============
import jwt

def usuario_de_token_MAL(token: str):
    # (1) Sin verificar la firma: cualquiera puede fabricar el token que quiera
    datos = jwt.decode(token, options={"verify_signature": False})
    # (2) Sin comprobar caducidad, emisor ni audiencia
    # (3) Se confia en los roles que vienen dentro sin mas
    return Usuario(id=datos["sub"], tenant=datos["tenant_id"],
                   roles=datos["roles"])

Los tres fallos y por qué cada uno es explotable:

  1. Sin verificar la firma, un atacante compone un JWT con "roles": ["admin"] y "tenant_id": 7 y entra como administrador de cualquier clínica. La variante histórica de este fallo es aceptar el algoritmo none que el propio token declara en su cabecera: nunca se debe confiar en el alg del token para decidir cómo validarlo.
  2. Sin comprobar exp, un token robado hace un año sigue sirviendo. Sin iss y aud, un token válido emitido por otro sistema o para otra aplicación se acepta aquí.
  3. Confiar ciegamente en los roles del token significa que un cambio de permisos o una baja no tienen efecto hasta que el token caduque.
# ============ VALIDACION CORRECTA ============
import jwt
from jwt import PyJWKClient
from fastapi import HTTPException

JWKS = PyJWKClient("https://identidad.nimbusreservas.example/.well-known/jwks.json")
EMISOR    = "https://identidad.nimbusreservas.example"
AUDIENCIA = "https://api.nimbusreservas.example"

def usuario_de_token(token: str):
    try:
        # (1) La clave publica se obtiene del emisor segun el 'kid' de la
        #     cabecera. Permite ROTAR claves sin desplegar la API.
        clave = JWKS.get_signing_key_from_jwt(token).key

        datos = jwt.decode(
            token,
            clave,
            algorithms=["RS256"],        # (2) algoritmo FIJADO por nosotros,
                                         #     nunca el que diga el token
            issuer=EMISOR,               # (3) quien lo emitio
            audience=AUDIENCIA,          # (4) para quien fue emitido
            options={"require": ["exp", "iat", "iss", "aud", "sub", "jti"],
                     "verify_exp": True},
            leeway=30,                   # (5) tolerancia de reloj: 30 s
        )
    except jwt.ExpiredSignatureError:
        raise HTTPException(401, "Sesion caducada")
    except jwt.InvalidTokenError:
        raise HTTPException(401, "Token no valido")

    # (6) REVOCACION: la firma valida no basta. Se comprueba que el token
    #     no este en la lista de revocados (baja, cierre de sesion, robo).
    if cache.get(f"revocado:{datos['jti']}"):
        raise HTTPException(401, "Sesion revocada")

    # (7) Los permisos SENSIBLES se resuelven contra la fuente de verdad,
    #     no contra lo que diga el token emitido hace 50 minutos.
    permisos = cargar_permisos_vigentes(datos["sub"], datos["tenant_id"])

    return Usuario(id=datos["sub"], tenant=datos["tenant_id"],
                   roles=permisos, jti=datos["jti"])

Las siete decisiones, explicadas:

  1. Claves desde JWKS por kid. Permite rotar la clave de firma sin tocar la API. Codificar la clave pública en el código convierte la rotación en un despliegue, y lo que es difícil no se hace.
  2. algorithms=["RS256"] fijado. Cierra el ataque de confusión de algoritmo (por ejemplo, hacer pasar la clave pública RSA como secreto HMAC).
  3. y 4. issuer y audience. Sin aud, un token legítimo para otro servicio del mismo emisor sería aceptado por la API de Nimbus.
  4. leeway. Evita fallos espurios por desfase de relojes; 30 s es razonable, minutos no.
  5. Revocación por jti. Es el punto débil estructural del JWT: al ser autocontenido, es válido hasta que caduca aunque el usuario haya sido dado de baja. La lista de revocados con vida igual a la del token resuelve el problema con un coste mínimo.
  6. Permisos vigentes desde la fuente de verdad. Los roles del token sirven como pista rápida; las decisiones sensibles se toman con datos actuales.

7.4 Vida de los tokens y refresh

Token Vida recomendada Dónde se guarda Notas
Access token 5-15 minutos Memoria de la aplicación Corto porque es difícil de revocar
Refresh token Horas o días, con rotación Cookie HttpOnly Secure SameSite Cada uso emite uno nuevo e invalida el anterior
Token de servicio (API) Máximo 90 días, rotación automatizada Gestor de secretos Nunca sin caducidad

Rotación de refresh con detección de reutilización, que es la pieza elegante: si un refresh token ya usado se presenta otra vez, significa que alguien tiene una copia robada. La respuesta correcta es invalidar toda la familia de tokens de esa sesión y forzar reautenticación. Es un mecanismo de detección de robo que funciona sin más infraestructura.

7.5 El token sin caducidad de 01-04, corregido

Recordemos el hallazgo: la cuenta de servicio nimbus-deploy-bot con un token personal sin caducidad y permisos de escritura en todos los repositorios. Fue además el primer eslabón del ataque encadenado de 02-02.

Todo lo que está mal en él:

Problema Consecuencia
Sin caducidad Robado una vez, sirve para siempre
Ámbito total sobre todos los repositorios El compromiso no está acotado
Ligado a una cuenta personal de tipo bot No hay dueño claro ni trazabilidad
Almacenado en la configuración del CI y probablemente en portátiles Múltiples copias, superficie multiplicada
Sin registro de uso revisado Nadie notaría un uso anómalo
Sin rotación Nunca ha cambiado desde su creación

La corrección, en orden de preferencia:

  1. Eliminarlo y usar credenciales efímeras del propio CI (identidad federada de corta duración emitida para cada ejecución). El mejor secreto es el que no existe.
  2. Si no es posible: token de aplicación con ámbito por repositorio, caducidad de 90 días, rotación automatizada y almacenamiento en el gestor de secretos.
  3. En cualquier caso: alerta ante cualquier uso desde una IP no esperada y revisión trimestral de tokens vivos.
-- Consulta de higiene: tokens sin caducidad o sin uso reciente.
-- Deberia ejecutarse mensualmente y devolver SIEMPRE cero filas.
SELECT id, nombre, propietario, creado_en, ultimo_uso, ambito
  FROM tokens_api
 WHERE caduca_en IS NULL                            -- nunca caducan
    OR caduca_en > now() + interval '90 days'       -- vida excesiva
    OR ultimo_uso < now() - interval '30 days'      -- creados y olvidados
 ORDER BY creado_en;

Los tokens sin uso en 30 días son tan peligrosos como los eternos, y por la misma razón que las cuentas huérfanas: nadie los echaría de menos si alguien empezara a usarlos.


  1. Modelos de autorización y el RBAC de Nimbus

Autenticar responde «quién eres». Autorizar responde «qué puedes hacer», y es donde ocurren la mayoría de los incidentes graves.

Modelo Cómo decide Ventaja Inconveniente Ejemplo
DAC (discrecional) El propietario del recurso decide quién accede Flexible, intuitivo Ingobernable a escala; origen de los enlaces «cualquiera con el enlace» La hoja de nóminas compartida por Sara (01-04)
MAC (obligatorio) Una política central e inmutable, por etiquetas de clasificación Muy estricto Rígido y caro Entornos militares; SELinux
RBAC (por roles) Permisos asociados a roles, roles asignados a personas Simple, auditable, escalable No expresa condiciones (hora, ubicación, propiedad) El modelo de Nimbus
ABAC (por atributos) Reglas sobre atributos de usuario, recurso y contexto Muy expresivo y granular Difícil de razonar y de auditar «Solo desde España, en horario laboral, si el recurso es de su tenant»
ReBAC (por relaciones) Según la relación entre sujeto y objeto en un grafo Natural para compartición y jerarquías Requiere infraestructura específica «Puede verlo porque es el profesional asignado a esa cita»

Qué le conviene a Nimbus: RBAC como base, con atributos para las condiciones. Es el modelo híbrido que usa casi todo el mundo: los roles definen el grueso de los permisos y unas pocas reglas contextuales cubren lo que los roles no expresan (el tenant, el horario, el dispositivo).

8.1 El diseño RBAC de Nimbus

Roles de cliente (dentro de una clínica o gimnasio):

Rol Puede No puede
Administrador de centro Gestionar usuarios de su centro, ver toda la agenda, configurar servicios, ver facturación Salir de su tenant; acceder a datos de otro centro
Recepción Crear, modificar y cancelar citas; consultar datos de contacto Ver notas clínicas; exportar en masa; gestionar usuarios
Profesional Ver su agenda y las notas de sus pacientes Ver la agenda de otros profesionales; gestionar usuarios; facturación

Roles internos de Nimbus:

Rol Puede No puede
Soporte (Rubén) Ver metadatos de incidencias; suplantar a un usuario solo con justificación y aprobación, con registro Acceder a datos de clientes sin ticket; exportar; ver notas clínicas
Desarrollo (Iván) Acceso total a preproducción con datos seudonimizados Acceso a producción sin aprobación explícita y temporal
Sistemas (Lucía) Administrar infraestructura; acceso a producción con MFA y registro Leer datos de negocio de forma rutinaria; borrar registros de auditoría
Administración (Sara) Facturación y RRHH Acceso a datos de clientes finales
# roles.yaml — definicion declarativa de la matriz de permisos.
# Ventaja de tenerlo en el repositorio: se revisa como codigo,
# se versiona y su cambio deja rastro en el historial.
roles:
  centro_admin:
    hereda: [recepcion]
    permisos:
      - usuarios:leer
      - usuarios:crear
      - usuarios:desactivar
      - facturacion:leer
      - configuracion:escribir
    condiciones:
      tenant: propio              # nunca fuera de su tenant

  recepcion:
    permisos:
      - citas:leer
      - citas:crear
      - citas:modificar
      - citas:cancelar
      - clientes:leer_contacto    # contacto SI, notas clinicas NO
    condiciones:
      tenant: propio
      exportacion_masiva: denegada

  profesional:
    permisos:
      - citas:leer_propias
      - notas_clinicas:leer_propias
      - notas_clinicas:escribir_propias
    condiciones:
      tenant: propio
      alcance: agenda_asignada    # atributo: solo SUS citas

  nimbus_soporte:
    permisos:
      - incidencias:leer
      - metadatos_tenant:leer
      - suplantar_usuario         # accion de alto riesgo
    condiciones:
      suplantar_usuario:
        requiere_ticket: true
        requiere_aprobacion: segundo_operador
        duracion_maxima: 30m
        registro: obligatorio
        notificar_al_cliente: true

  nimbus_sistemas:
    permisos:
      - infraestructura:administrar
      - despliegue:ejecutar
    condiciones:
      mfa: fido2
      acceso_produccion: just_in_time     # se solicita, caduca solo
      registro_sesion: obligatorio
    denegado:
      - auditoria:borrar                  # NADIE puede borrar auditoria

Cuatro decisiones de este diseño que merecen atención:

  1. profesional usa un atributo (agenda_asignada), no un rol. «Sus» pacientes no se puede expresar solo con roles: es una relación entre el usuario y el recurso. Aquí es donde RBAC puro se queda corto y se añade ABAC/ReBAC.
  2. suplantar_usuario es un permiso explícito con condiciones, no una capacidad implícita del rol de soporte. Es la corrección del «ver como cliente» que permitió acceder a 40 clínicas en el ejercicio de 02-02. Y se notifica al cliente: la transparencia es en sí misma un control.
  3. auditoria:borrar aparece como denegado explícito para todos, incluido sistemas. Es la separación de funciones de 01-03: quien administra no puede borrar la prueba de lo que hizo, coherente con el INSERT sin UPDATE de nimbus_api.
  4. El fichero está en el repositorio. Un cambio de permisos pasa por revisión y queda en el historial. Los permisos que se cambian a mano en una consola no dejan rastro comprensible.

Y en la base de datos, la contrapartida del mismo diseño, coherente con los roles de 01-03:

-- Rol de solo lectura para el panel de soporte: nunca ve notas clinicas
CREATE ROLE nimbus_soporte_rol NOLOGIN;
GRANT USAGE ON SCHEMA public TO nimbus_soporte_rol;

-- Vista que EXCLUYE las columnas sensibles: el minimo privilegio
-- se implementa en el esquema, no en la confianza en el codigo
CREATE VIEW v_incidencias_soporte AS
SELECT r.id, r.tenant_id, r.fecha_hora, r.estado, c.nombre AS cliente
  FROM reservas r JOIN clientes_finales c ON c.id = r.cliente_id;
-- deliberadamente sin r.notas_clinicas ni c.dni

GRANT SELECT ON v_incidencias_soporte TO nimbus_soporte_rol;
REVOKE ALL ON reservas, clientes_finales, nominas FROM nimbus_soporte_rol;

  1. Multi-tenencia y aislamiento

Nimbus es multi-tenant: una única base de datos con los datos de todas las clínicas. El aislamiento entre clientes es el producto: si falla, no hay negocio.

Ya has trabajado las dos piezas —el filtro por tenant_id que corrigió el IDOR en 01-01 y la seguridad a nivel de fila (RLS) que lo hace obligatorio desde el motor en 01-03—. Lo que corresponde aquí es cómo encaja el aislamiento en el control de acceso:

Capa Qué garantiza Qué pasa si falla sola
Token con tenant_id La API sabe a qué tenant pertenece la petición Si se manipula, las capas siguientes lo detienen
Filtro en la consulta Cada consulta se limita al tenant Un olvido expone datos ajenos
RLS en PostgreSQL El motor lo impone aunque el código lo olvide Es la red de seguridad
Rol de BD con mínimo privilegio Acota el daño de una inyección Limita el alcance
Auditoría con tenant_id Permite detectar accesos cruzados Es la capa detectiva

La regla de diseño, en una frase: el tenant_id nunca procede de un parámetro que envía el cliente; procede siempre del token validado en el servidor. Un endpoint que acepte ?tenant_id= del cliente es un IDOR con otro nombre.

Y la comprobación que conviene automatizar: una prueba en el CI que, con el token de un tenant, intente acceder a un recurso de otro y exija un 404. Es una prueba de cinco líneas que habría detectado el IDOR original antes de llegar a producción.


  1. Cuentas privilegiadas, de servicio y de terceros

Las cuentas con permisos elevados son el objetivo final de casi cualquier ataque. Merecen un tratamiento distinto del resto.

10.1 Separación de la cuenta administrativa

El problema: si Lucía navega, lee correo y administra la infraestructura con la misma cuenta, un phishing exitoso contra su correo entrega directamente el control de toda la producción.

La solución: dos identidades para la misma persona.

Cuenta Uso Restricciones
[email protected] Correo, documentos, navegación, reuniones Sin permisos administrativos
[email protected] Solo administración Sin correo ni navegación; MFA FIDO2 obligatorio; solo desde equipo gestionado; sesión registrada; acceso just-in-time

El coste es una molestia diaria moderada; el beneficio es que el vector más probable (el phishing al correo) deja de conducir al activo más crítico.

10.2 PAM y acceso just-in-time

PAM (gestión de accesos privilegiados) agrupa las prácticas de control de las cuentas con más poder:

Práctica Qué aporta Aplicación en Nimbus
Bóveda de credenciales Las contraseñas administrativas no las conoce nadie: se solicitan Contraseña de superusuario de PostgreSQL, cuenta raíz del proveedor cloud
Acceso just-in-time El permiso no existe hasta que se solicita, y caduca solo Acceso a producción concedido por 2 horas con justificación
Grabación de sesión Trazabilidad de lo que se hizo Sesiones del bastión
Aprobación de un segundo Separación de funciones Operaciones destructivas o acceso a datos de clientes
Rotación automática Reduce la ventana de una credencial filtrada Credenciales de servicio y de base de datos

El cambio mental que propone el just-in-time: en lugar de preguntar «¿quién debe tener acceso permanente?», se pregunta «¿por qué alguien tendría acceso permanente?». En un sistema bien diseñado, el estado normal de una cuenta administrativa es sin privilegios; los privilegios se conceden por minutos y desaparecen solos. Es el principio de mínimo privilegio de 01-03 llevado al eje del tiempo: no solo el mínimo de permisos, también el mínimo de duración.

10.3 El acceso remoto de la consultora (A-19)

Es el activo con criticidad crítica y propietario Marta del inventario de 01-04, y el patrón exacto del incidente de Target que analizaremos en 02-06.

Situación actual:

Aspecto Estado Riesgo
Cuentas Compartidas por varios técnicos Sin trazabilidad: no se sabe quién hizo qué
Permisos Administrativos totales y permanentes Sin límite de alcance ni de tiempo
MFA No Una contraseña filtrada da acceso pleno
Registro No revisado Un uso indebido pasaría inadvertido
Caducidad Ninguna El acceso sobrevivirá al contrato
Contrato Sin detalle de alcance ni obligaciones Sin base para exigir nada

Configuración objetivo:

  1. Cuentas nominales por técnico de la consultora. Sin excepciones: sin nominación no hay no repudio.
  2. MFA obligatorio en todos los accesos.
  3. Acceso just-in-time: sin privilegios de forma predeterminada; se solicitan con motivo, los aprueba Lucía o Marta y caducan en horas.
  4. Alcance acotado: solo los sistemas que realmente necesitan, nunca «administrador de todo».
  5. Sesiones registradas y revisadas por muestreo.
  6. Caducidad automática del acceso alineada con el contrato, con revisión trimestral.
  7. Alerta ante cualquier acceso fuera de la ventana pactada.

Nota: las obligaciones contractuales con el proveedor —alcance, confidencialidad, notificación de incidentes, subcontratación, auditoría— y la gestión del riesgo de terceros como proceso se tratan en 04-04, y sus implicaciones en materia de protección de datos en 06-03. Deben validarse con asesoría jurídica.

10.4 Cuentas de servicio

Las cuentas que usan los sistemas (no las personas) son el punto ciego habitual: no tienen quien las reclame ni quien las revise.

Regla Motivo
Una cuenta por servicio, nunca compartida entre varios Permite acotar el daño y saber quién hizo qué
Propietario humano asignado Alguien tiene que responder por ella en la recertificación
Sin acceso interactivo (NOLOGIN, sin shell) No debe poder usarse como puerta de entrada humana
Permisos mínimos y explícitos Como los roles nimbus_api y nimbus_informes de 01-03
Credenciales efímeras o rotadas Reduce la ventana de una filtración
Uso registrado y con línea base conocida Un servicio tiene un patrón muy predecible: cualquier desviación es sospechosa

  1. Recertificación: la revisión periódica de accesos

Todo lo anterior se degrada con el tiempo. La recertificación es la revisión periódica en la que el propietario de cada activo confirma —o retira— los accesos concedidos.

Cómo se hace en Nimbus, de forma sostenible:

Elemento Decisión
Frecuencia Semestral para accesos normales; trimestral para privilegiados y terceros
Quién revisa El propietario del activo del inventario de 01-04, no el equipo técnico
Qué se revisa Lista de personas con acceso, nivel, fecha de concesión y fecha de último uso
Regla por defecto Lo que no se confirma explícitamente, se retira. Si hay que actuar para retirar, no se retira nunca
Salida Lista firmada y fechada: es la evidencia que pedirá una auditoría (06-04)

El dato que hace eficiente la revisión es la fecha de último uso. Un permiso sin uso en seis meses casi nunca es necesario, y presentar la lista ordenada por ese campo convierte una revisión tediosa en una decisión rápida.

-- Insumo para la recertificacion: accesos sin uso reciente.
-- Se envia al propietario de cada activo para que confirme o retire.
SELECT u.email,
       u.rol,
       a.nombre                AS activo,
       p.concedido_en,
       p.concedido_por,
       MAX(l.ts)               AS ultimo_uso,
       CASE WHEN MAX(l.ts) IS NULL THEN 'NUNCA USADO'
            WHEN MAX(l.ts) < now() - interval '180 days' THEN 'SIN USO 6 MESES'
            ELSE 'ACTIVO' END  AS estado
  FROM permisos p
  JOIN usuarios u ON u.id = p.usuario_id
  JOIN activos  a ON a.id = p.activo_id
  LEFT JOIN accesos_log l ON l.usuario_id = u.id AND l.activo_id = a.id
 GROUP BY u.email, u.rol, a.nombre, p.concedido_en, p.concedido_por
HAVING MAX(l.ts) IS NULL OR MAX(l.ts) < now() - interval '180 days'
 ORDER BY a.nombre, ultimo_uso NULLS FIRST;

Un permiso «NUNCA USADO» es la mejor noticia posible en una recertificación: se retira sin discusión, sin riesgo de romper nada y con beneficio inmediato de reducción de superficie.


Errores Comunes y Consejos

Errores comunes:

  • Dar de baja solo el correo. Es el error del apartado 2.3: quedan vivos ocho accesos en sistemas no federados.
  • Desactivar la cuenta sin revocar sesiones ni tokens. Una sesión emitida sigue funcionando; un token sin caducidad, para siempre.
  • Copiar los permisos de otro empleado al dar de alta. Propaga privilegios acumulados que nadie decidió conceder.
  • Tratar todo el MFA como equivalente. SMS y FIDO2 se diferencian en si el ataque de phishing funciona o no.
  • Forzar caducidad de contraseñas cada 90 días. Produce variaciones triviales y contraseñas escritas en papel; las guías actuales lo desaconsejan salvo indicio de compromiso.
  • Usar OAuth 2.0 como si fuera autenticación. Un token de acceso dice qué se puede hacer, no quién eres. Para identidad, OIDC y validación del ID token.
  • Guardar tokens en localStorage. Un XSS los roba. Cookie HttpOnly o intermediario de servidor.
  • Confiar en un JWT sin verificar firma, exp, iss y aud, o dejar que el token elija el algoritmo.
  • Meter datos personales dentro del JWT. Está firmado, no cifrado: cualquiera lo lee.
  • Aceptar tenant_id como parámetro del cliente. Es un IDOR con otro nombre.
  • Conceder acceso privilegiado permanente porque «es más cómodo». El estado normal de una cuenta administrativa debería ser sin privilegios.
  • Recertificar con la regla «se retira lo que alguien pida retirar». Por defecto debe retirarse lo que no se confirma.

Consejos:

  • Empieza por lo que más rinde: FIDO2 en las cuentas administrativas y caducidad en todos los tokens. Son dos tardes de trabajo y cierran los dos vectores más explotados.
  • Escribe el checklist de baja del apartado 2.3 y ejecútalo el último día, no después. Guarda constancia firmada.
  • Separa la cuenta administrativa de la personal para Lucía y Marta. Es incómodo una semana y protege durante años.
  • Pon el fichero roles.yaml en el repositorio y trátalo como código: revisión, historial y despliegue controlado.
  • Añade al CI la prueba de aislamiento entre tenants. Cinco líneas que vigilan permanentemente el fallo más caro de un SaaS.
  • Ejecuta hoy la consulta de tokens sin caducidad. Si devuelve filas, ya tienes tu tarea de la semana.
  • En la recertificación, ordena siempre por fecha de último uso: convierte una revisión tediosa en decisiones de un segundo.

Ejercicios

Ejercicio 1 — Diseñar la baja de un empleado y detectar lo que falta

Iván deja Nimbus. Lucía ejecuta lo siguiente:

  1. Desactiva su cuenta de correo.
  2. Le quita del canal interno de mensajería.
  3. Recupera el portátil.
  4. Le retira del repositorio de GitHub como colaborador.

Dos meses después, en la revisión trimestral, se detecta actividad: alguien ha ejecutado un despliegue a las 4:10 de la madrugada.

Se pide: (a) enumera al menos seis accesos que probablemente sigan vivos y explica por qué cada uno es peligroso; (b) escribe el checklist de baja completo que debería haberse aplicado; (c) explica cómo pudo ejecutarse un despliegue si la cuenta de GitHub estaba retirada, con dos hipótesis técnicas distintas; (d) propón tres medidas estructurales que reduzcan este riesgo con independencia de la disciplina de quien ejecute la baja.

Ejercicio 2 — Auditar una implementación de tokens

Este es el código de autenticación de una nueva versión de la API de Nimbus:

import jwt, datetime
from fastapi import HTTPException

SECRETO = "nimbus-2026"

def emitir(usuario):
    carga = {
        "sub": usuario.email,
        "nombre": usuario.nombre_completo,
        "dni": usuario.dni,
        "tenant_id": usuario.tenant_id,
        "roles": usuario.roles,
        "notas_internas": usuario.notas,
    }
    return jwt.encode(carga, SECRETO, algorithm="HS256")

def validar(token: str):
    datos = jwt.decode(token, SECRETO,
                       algorithms=["HS256", "none"],
                       options={"verify_exp": False})
    return Usuario(id=datos["sub"], tenant=datos["tenant_id"],
                   roles=datos["roles"])

@app.post("/api/v1/sesion")
def login(email: str, password: str):
    u = autenticar(email, password)
    if not u:
        raise HTTPException(401, "Usuario no encontrado o clave incorrecta")
    return {"token": emitir(u), "expira": "nunca"}

Se pide: (a) identifica todos los problemas de seguridad, clasificándolos en críticos, altos y medios; (b) explica el impacto concreto de los dos más graves sobre los datos de las clínicas; (c) reescribe emisión y validación correctamente; (d) explica qué cambia si la API se despliega en tres instancias y se necesita revocar una sesión de inmediato.

Ejercicio 3 — Autorización de una funcionalidad nueva

Nimbus lanza los informes de ocupación por profesional. Requisitos de negocio:

  • El administrador de centro ve el informe de todos los profesionales de su centro.
  • El profesional ve solo su propio informe.
  • Recepción no accede a esta funcionalidad.
  • Soporte de Nimbus (Rubén) puede ver el informe de un centro solo con un ticket abierto y aprobación de un segundo operador, durante 30 minutos como máximo, y el centro debe ser notificado.
  • Los informes pueden exportarse a CSV, con un máximo de 5.000 filas y 6 peticiones por hora y usuario.
  • Un profesional dado de baja hace más de 90 días no debe aparecer.

Se pide: (a) decide qué modelo o combinación de modelos de autorización usarías y justifícalo; (b) escribe el fragmento de roles.yaml correspondiente; (c) escribe el código Python del endpoint con todas las comprobaciones de autorización, indicando en comentarios qué ataque previene cada una; (d) indica tres eventos que registrarías y una alerta que construirías sobre ellos.


Soluciones

Solución 1

(a) Accesos que probablemente sigan vivos:

Acceso Por qué es peligroso
Cuenta del proveedor cloud (A-05) Acceso a buckets, base de datos y a la propia infraestructura; es el activo del que dependen casi todos
Token personal de GitHub creado por Iván Sobrevive a la retirada de la cuenta: un token es una credencial independiente
Usuario personal en PostgreSQL (dev_ivan) Acceso directo a datos de producción sin pasar por la API ni por sus controles
Clave SSH en authorized_keys de los servidores Acceso a los servidores sin autenticación centralizada; invisible para el SSO
Secretos que conocía (contraseña de BD, claves de la pasarela, credencial del email transaccional) Siguen siendo válidos: los conoce alguien sin relación con la empresa
Herramientas SaaS no federadas (tickets, monitorización, gestor de errores) Contienen datos de clientes y trazas de producción
Sesiones activas en navegadores y app móvil Desactivar la cuenta no invalida una sesión emitida (pass-the-cookie, 02-03)
Copias locales de datos en su portátil o en almacenamiento personal Ni siquiera requieren acceso: los datos ya salieron

(b) El checklist del apartado 2.3, ejecutado el último día, con constancia firmada y con reasignación de la propiedad de sus activos del inventario.

(c) Dos hipótesis del despliegue nocturno:

  1. Token personal vivo. Iván creó un token de GitHub con permisos de escritura y sin caducidad —el patrón del hallazgo de 01-04—. Retirar la cuenta como colaborador no invalida el token, que sigue autenticando frente a la API del servicio. Es la hipótesis más probable.
  2. Clave SSH o credencial de infraestructura. El despliegue no pasó por GitHub: se ejecutó directamente contra el servidor con una clave SSH que quedó en authorized_keys, o con credenciales del proveedor cloud que conocía.

En ambos casos, la lección es la misma: la identidad no es solo la cuenta. Es la cuenta más todos los artefactos que ella creó y que viven de forma independiente.

(d) Tres medidas estructurales:

  1. Federar todo lo posible con el proveedor de identidad, de modo que una única baja desactive el máximo número de accesos. Reduce la dependencia de la disciplina humana.
  2. Prohibir por política los tokens sin caducidad y las claves SSH personales en servidores, sustituyéndolos por credenciales efímeras emitidas por el CI y acceso por bastión con identidad central. Un artefacto que no existe no se olvida.
  3. Recertificación trimestral con la consulta del apartado 11, que detecta lo que se escapó del proceso. Es la red de seguridad: asume que la baja fallará alguna vez y limita cuánto tiempo pasa hasta que se detecta.

Solución 2

(a) Problemas encontrados:

Gravedad Problema Detalle
Crítico algorithms=["HS256", "none"] Aceptar none permite fabricar tokens sin firma: cualquiera se convierte en administrador de cualquier tenant
Crítico SECRETO = "nimbus-2026" en el código Secreto débil, adivinable y presente en el repositorio; permite firmar tokens arbitrarios
Crítico verify_exp: False y "expira": "nunca" Los tokens no caducan jamás: robar uno equivale a acceso permanente
Alto Sin exp, iss, aud, iat ni jti en la carga Imposible acotar validez, origen, destinatario ni revocar
Alto dni, nombre y notas_internas dentro del JWT El JWT está firmado pero no cifrado: datos personales legibles por cualquiera y presentes en logs y navegador
Alto Sin mecanismo de revocación Una baja o un robo no tienen efecto
Medio HS256 con secreto compartido Cualquier servicio que valide puede también emitir; con RS256 solo el emisor firma
Medio sub es el correo Un cambio de correo rompe la identidad; mejor un identificador estable
Medio Mensaje de error que distingue casos «Usuario no encontrado o clave incorrecta» es aceptable, pero conviene un mensaje totalmente uniforme y un tiempo de respuesta constante para evitar la enumeración de usuarios (02-02)

(b) Impacto de los dos más graves. Con none aceptado, un atacante construye un token con "tenant_id": 7 y "roles": ["centro_admin"], sin firma, y la API lo acepta. Resultado: acceso completo a la agenda y los datos de cualquier clínica, sin necesidad de credenciales, sin phishing y sin explotar nada más. Combinado con la ausencia de caducidad, un token obtenido una sola vez —de un log, de una captura, de un portátil— da acceso indefinido. En términos de la tríada CIA, esto es una pérdida total de confidencialidad e integridad sobre datos que revelan información de salud, y desencadena obligaciones de notificación (06-03).

(c) Código correcto:

import jwt, uuid, datetime
from jwt import PyJWKClient
from fastapi import HTTPException

EMISOR    = "https://identidad.nimbusreservas.example"
AUDIENCIA = "https://api.nimbusreservas.example"
JWKS      = PyJWKClient(f"{EMISOR}/.well-known/jwks.json")
CLAVE_PRIVADA = gestor_secretos.leer("jwt/clave-privada")   # nunca en el codigo

def emitir(usuario):
    ahora = datetime.datetime.now(datetime.timezone.utc)
    carga = {
        "iss": EMISOR,
        "aud": AUDIENCIA,
        "sub": str(usuario.id),                 # identificador estable, no el correo
        "tenant_id": usuario.tenant_id,
        "roles": usuario.roles,                 # pista rapida; lo sensible se recarga
        "iat": ahora,
        "exp": ahora + datetime.timedelta(minutes=15),   # vida corta
        "jti": str(uuid.uuid4()),               # identificador para revocar
        # sin nombre, sin dni, sin notas internas
    }
    return jwt.encode(carga, CLAVE_PRIVADA, algorithm="RS256",
                      headers={"kid": "2026-03"})

def validar(token: str):
    try:
        clave = JWKS.get_signing_key_from_jwt(token).key
        datos = jwt.decode(token, clave,
                           algorithms=["RS256"],       # fijado, jamas del token
                           issuer=EMISOR, audience=AUDIENCIA,
                           options={"require": ["exp", "iat", "iss",
                                                "aud", "sub", "jti"]},
                           leeway=30)
    except jwt.ExpiredSignatureError:
        raise HTTPException(401, "Sesion caducada")
    except jwt.InvalidTokenError:
        raise HTTPException(401, "Token no valido")

    if cache.get(f"revocado:{datos['jti']}"):
        raise HTTPException(401, "Sesion revocada")

    permisos = cargar_permisos_vigentes(datos["sub"], datos["tenant_id"])
    return Usuario(id=datos["sub"], tenant=datos["tenant_id"],
                   roles=permisos, jti=datos["jti"])

(d) Revocación inmediata con tres instancias. Un JWT es autocontenido: las tres instancias lo validan sin consultar a nadie, así que una baja no tiene efecto hasta que caduca. Soluciones, combinables:

  1. Lista de revocación compartida (Redis) consultada por jti, con vida igual a la del token: máximo 15 minutos de entradas. Coste: una consulta rápida por petición; es lo que hace el código anterior.
  2. Vida corta del access token (5-15 min) con refresh rotatorio. La revocación real se aplica al refresh, que sí se comprueba contra la base de datos: la ventana máxima de exposición es la vida del access token.
  3. Marca de invalidación por usuario (tokens_validos_desde): se rechaza todo token con iat anterior a esa marca. Revoca todas las sesiones de una persona con un solo campo, ideal para una baja o un compromiso.

El compromiso de fondo: el JWT es rápido y sin estado, pero la revocación exige estado. La solución práctica no es elegir un extremo, sino acortar la vida del token para que la ventana sea aceptable y mantener un mecanismo de revocación para lo urgente.

Solución 3

(a) Modelo elegido: RBAC como base + ABAC/ReBAC para el alcance y las condiciones. Justificación:

  • Los tres perfiles (administrador de centro, profesional, recepción) son roles: RBAC los expresa de forma directa y auditable.
  • «Solo su propio informe» no es un rol: es una relación entre el usuario y el recurso (el profesional asignado). Requiere ReBAC o un atributo.
  • «Con ticket, aprobación, 30 minutos y notificación» son condiciones contextuales: ABAC.
  • «Profesional dado de baja hace más de 90 días» es un atributo temporal del recurso.

RBAC puro obligaría a crear un rol por profesional, lo que es ingobernable. ABAC puro sería expresivo pero muy difícil de auditar. El híbrido es lo que usan los sistemas reales.

(b) Fragmento de roles.yaml:

permisos_nuevos:
  informes_ocupacion:leer_centro:
    roles: [centro_admin]
    condiciones:
      tenant: propio
      profesionales_visibles: baja_hace_menos_de_90_dias

  informes_ocupacion:leer_propio:
    roles: [profesional]
    condiciones:
      tenant: propio
      alcance: profesional_id == usuario.profesional_id

  informes_ocupacion:exportar:
    roles: [centro_admin, profesional]
    limites:
      filas_maximas: 5000
      peticiones_por_hora: 6
    registro: obligatorio

  informes_ocupacion:leer_soporte:
    roles: [nimbus_soporte]
    condiciones:
      requiere_ticket: true
      requiere_aprobacion: segundo_operador
      duracion_maxima: 30m
      notificar_al_cliente: true
      registro: obligatorio
    denegado_para: [recepcion]

(c) Endpoint con las comprobaciones:

@app.get("/api/v1/informes/ocupacion")
@limitador.limit("6/hour")                     # abuso de API y DDoS aplicativo (02-02)
def informe_ocupacion(profesional_id: int | None = None,
                      formato: str = "json",
                      usuario = Depends(usuario_actual)):

    # 1. AUTORIZACION POR ROL. Recepcion no llega aqui.
    #    Previene: escalada vertical de privilegios.
    if not usuario.tiene_alguno(["centro_admin", "profesional",
                                 "nimbus_soporte"]):
        raise HTTPException(403)

    # 2. TENANT DESDE EL TOKEN, NUNCA DEL CLIENTE.
    #    Previene: IDOR entre tenants (01-01) y acceso cruzado.
    tenant = usuario.tenant_id

    # 3. ALCANCE SEGUN EL ROL (la parte ABAC/ReBAC).
    #    Previene: escalada horizontal entre profesionales del mismo centro.
    if usuario.tiene("profesional"):
        if profesional_id not in (None, usuario.profesional_id):
            raise HTTPException(404)          # 404, no 403: no revela existencia
        profesional_id = usuario.profesional_id

    # 4. SOPORTE: condiciones estrictas y trazabilidad.
    #    Previene: el abuso de "ver como cliente" del ejercicio de 02-02.
    if usuario.tiene("nimbus_soporte"):
        sesion = sesiones_soporte.vigente(usuario.id, tenant)
        if not sesion or not sesion.aprobada_por_segundo or sesion.caducada():
            raise HTTPException(403, "Requiere ticket aprobado y vigente")
        tenant = sesion.tenant_id             # el tenant lo fija la aprobacion
        notificar_al_cliente(tenant, usuario.id, "acceso_soporte_informe")

    # 5. CONSULTA ACOTADA: filtro de tenant, exclusion de bajas y LIMIT.
    #    Previene: exposicion de datos de mas y DDoS aplicativo.
    filas = db.execute("""
        SELECT p.id, p.nombre, COUNT(r.id) AS citas,
               SUM(CASE WHEN r.estado='completada' THEN 1 ELSE 0 END) AS completadas
          FROM profesionales p
          LEFT JOIN reservas r ON r.profesional_id = p.id
                              AND r.tenant_id = :t
         WHERE p.tenant_id = :t
           AND (p.baja_en IS NULL OR p.baja_en > now() - interval '90 days')
           AND (:pid::int IS NULL OR p.id = :pid)
         GROUP BY p.id, p.nombre
         LIMIT 5000""",
        {"t": tenant, "pid": profesional_id}).fetchall()

    # 6. AUDITORIA: quien, que, cuanto, para quien.
    registrar_auditoria(usuario_id=usuario.id, tenant_id=tenant,
                        accion="informe_ocupacion",
                        formato=formato, num_registros=len(filas),
                        profesional_id=profesional_id)

    if formato == "csv":
        return exportar_csv(filas, maximo=5000)
    return {"datos": [dict(f) for f in filas]}

(d) Tres eventos a registrar y una alerta:

Evento Campos
informe_ocupacion_consultado usuario_id, tenant_id, rol, profesional_id, num_registros, formato
acceso_soporte_iniciado usuario_id, tenant_id, ticket, aprobado_por, caduca_en
exportacion_csv usuario_id, tenant_id, num_registros, ip

Alerta: acceso de soporte a más de 3 tenants distintos en 24 horas, o cualquier acceso de soporte sin ticket asociado. Es exactamente la señal que habría detectado en horas —en lugar de en tres días— el incidente del panel de soporte del ejercicio de 02-02, y es barata de construir porque los campos ya están en el registro. Una segunda alerta útil: cualquier exportación CSV fuera del horario laboral del tenant, que aprovecha que Nimbus conoce el horario de cada centro.


Conclusión

Has trabajado el dominio que más incidentes decide hoy. Sabes por qué la identidad es el nuevo perímetro: ninguno de los activos relevantes de Nimbus está detrás de una frontera de red, y lo único que atraviesa todos los contextos es quién eres. Has separado con precisión identificación, autenticación y autorización, y has fijado la trampa que se repite en todo el curso: la mayoría de los incidentes graves no son fallos de autenticación, sino de autorización. Has recorrido el ciclo de vida de la identidad y su punto débil, la baja, con el caso concreto de los ocho accesos que sobreviven a una desactivación de correo, el checklist que lo evita y las dos cosas que siempre se olvidan: revocar sesiones y tokens, y rotar los secretos compartidos.

En contraseñas te llevas la actualización de las guías —longitud sobre complejidad, sin caducidad forzada, permitir pegar, y la medida más eficaz y menos implantada: comprobar contra listas de filtradas— y el argumento definitivo a favor de los gestores. En MFA has aprendido la jerarquía que de verdad importa, la de resistencia al phishing, y la razón por la que FIDO2 es cualitativamente distinto: la clave nunca sale del dispositivo y la firma está vinculada al dominio, de modo que la defensa funciona aunque el usuario no detecte nada. Has visto por dentro la verificación TOTP con sus cuatro decisiones —ventana acotada, comparación en tiempo constante, consumo de un solo uso y secreto cifrado— y la corrección del MFA fatigue con number matching.

En federación has fijado la diferencia exacta entre OAuth 2.0, que autoriza, y OpenID Connect, que autentica, junto con el flujo Authorization Code + PKCE y la razón por la que PKCE es imprescindible en aplicaciones públicas. En sesiones y tokens has recorrido los atributos de cookie que neutralizan XSS, CSRF y captura en tránsito, y has comparado una validación de JWT ingenua con una correcta: firma verificada con clave del emisor, algoritmo fijado por nosotros, iss, aud y exp comprobados, revocación por jti y permisos sensibles recargados de la fuente de verdad. Y has corregido el token sin caducidad de 01-04, el primer eslabón del ataque encadenado de 02-02.

En autorización has comparado DAC, MAC, RBAC, ABAC y ReBAC y has construido el diseño real de Nimbus: roles de cliente y roles internos en un roles.yaml versionado, con suplantar_usuario como permiso condicionado y notificado, y auditoria:borrar denegado a todos. Has cerrado el aislamiento multi-tenant con su regla de oro —el tenant_id viene siempre del token, nunca del cliente— y su prueba automatizada en el CI. Y has terminado con las cuentas privilegiadas: la separación de la cuenta administrativa, PAM y el acceso just-in-time que convierte el mínimo privilegio en mínima duración, la configuración objetivo para el acceso de la consultora (A-19) y la recertificación periódica cuya regla decisiva es que lo que no se confirma, se retira.

Tienes ya el arsenal completo del módulo: sabes qué se ataca, cómo, con qué se defiende y cómo se controla el acceso. Falta la prueba de realidad. En la última lección del módulo, Casos de Estudio de Incidentes de Ciberseguridad (02-06), aplicaremos un método de análisis a incidentes reales y públicos —Target, WannaCry, Equifax, SolarWinds, Colonial Pipeline y una fuga por almacenamiento en la nube mal configurado—, mapearemos cada uno a los conceptos del curso, extraeremos los patrones que se repiten sin excepción y reconstruiremos hora a hora un incidente ficticio de ransomware en Nimbus que entra por el acceso remoto de la consultora, para responder a la única pregunta que importa: qué habría cambiado el desenlace.

Curso de Fundamentos de Seguridad Informática

Módulo 1: Introducción a la Seguridad Informática

Módulo 2: Ciberseguridad

Módulo 3: Criptografía

Módulo 4: Gestión de Riesgos y Medidas de Protección

Módulo 5: Herramientas y Técnicas de Seguridad

Módulo 6: Buenas Prácticas y Normativas

Módulo 7: Proyecto Final

© Copyright 2026. Todos los derechos reservados