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
- La identidad como nuevo perímetro
- Ciclo de vida de la identidad y cuentas huérfanas
- Factores de autenticación y sus debilidades
- Contraseñas: qué dicen hoy las guías
- MFA ordenado por resistencia al phishing
- SSO, federación y protocolos de identidad
- Sesiones y tokens: cookies y JWT
- Modelos de autorización y el RBAC de Nimbus
- Multi-tenencia y aislamiento
- Cuentas privilegiadas, de servicio y de terceros
- Recertificación: la revisión periódica de accesos
- 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.
- 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:
- 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.
- Con aprobación registrada del propietario del activo (el campo «propietario» del inventario de 01-04 sirve exactamente para esto).
- 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.
- 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 responsableLos 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.
- 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:
- 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.
- 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.
- 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.
- 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.
- 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:
- La clave privada nunca sale del dispositivo. No hay ningún secreto que el usuario pueda entregar, ni siquiera queriendo.
- 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 FalseLas 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:
- Number matching: obliga a mirar la pantalla del origen y a introducir un número, lo que hace imposible aprobar por inercia.
- Contexto en la notificación: aplicación, ubicación aproximada, dispositivo. Una petición desde otro país es evidente.
- Límite de intentos: tras 3 peticiones denegadas o ignoradas, se bloquea la cuenta y se alerta.
- 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.
- 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 cookieHttpOnlygestionada por un intermediario de servidor. - No validar
auden 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.
- 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:
- Sin verificar la firma, un atacante compone un JWT con
"roles": ["admin"]y"tenant_id": 7y entra como administrador de cualquier clínica. La variante histórica de este fallo es aceptar el algoritmononeque el propio token declara en su cabecera: nunca se debe confiar en elalgdel token para decidir cómo validarlo. - Sin comprobar
exp, un token robado hace un año sigue sirviendo. Sinissyaud, un token válido emitido por otro sistema o para otra aplicación se acepta aquí. - 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:
- 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. algorithms=["RS256"]fijado. Cierra el ataque de confusión de algoritmo (por ejemplo, hacer pasar la clave pública RSA como secreto HMAC).- y 4.
issueryaudience. Sinaud, un token legítimo para otro servicio del mismo emisor sería aceptado por la API de Nimbus. leeway. Evita fallos espurios por desfase de relojes; 30 s es razonable, minutos no.- 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. - 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:
- 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.
- 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.
- 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.
- 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 auditoriaCuatro decisiones de este diseño que merecen atención:
profesionalusa 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.suplantar_usuarioes 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.auditoria:borraraparece 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 elINSERTsinUPDATEdenimbus_api.- 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;
- 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.
- 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:
- Cuentas nominales por técnico de la consultora. Sin excepciones: sin nominación no hay no repudio.
- MFA obligatorio en todos los accesos.
- Acceso just-in-time: sin privilegios de forma predeterminada; se solicitan con motivo, los aprueba Lucía o Marta y caducan en horas.
- Alcance acotado: solo los sistemas que realmente necesitan, nunca «administrador de todo».
- Sesiones registradas y revisadas por muestreo.
- Caducidad automática del acceso alineada con el contrato, con revisión trimestral.
- 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 |
- 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. CookieHttpOnlyo intermediario de servidor. - Confiar en un JWT sin verificar firma,
exp,issyaud, o dejar que el token elija el algoritmo. - Meter datos personales dentro del JWT. Está firmado, no cifrado: cualquiera lo lee.
- Aceptar
tenant_idcomo 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.yamlen 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:
- Desactiva su cuenta de correo.
- Le quita del canal interno de mensajería.
- Recupera el portátil.
- 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:
- 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.
- 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:
- 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.
- 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.
- 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:
- 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. - 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.
- Marca de invalidación por usuario (
tokens_validos_desde): se rechaza todo token coniatanterior 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
- Conceptos Básicos de Seguridad Informática
- Tipos de Amenazas y Vulnerabilidades
- Principios de la Seguridad Informática
- Activos, Superficie de Ataque y Actores de Amenaza
Módulo 2: Ciberseguridad
- Definición y Alcance de la Ciberseguridad
- Tipos de Ataques Cibernéticos
- Ingeniería Social y Phishing
- Medidas de Protección en Ciberseguridad
- Identidad, Autenticación y Control de Acceso
- Casos de Estudio de Incidentes de Ciberseguridad
Módulo 3: Criptografía
- Introducción a la Criptografía
- Criptografía Simétrica
- Criptografía Asimétrica
- Funciones Hash, HMAC y Almacenamiento de Contraseñas
- Protocolos Criptográficos
- Gestión de Claves, Certificados y PKI
- Aplicaciones de la Criptografía
Módulo 4: Gestión de Riesgos y Medidas de Protección
- Evaluación de Riesgos
- Políticas de Seguridad
- Controles de Seguridad
- Riesgo de Terceros y Cadena de Suministro
- Plan de Respuesta a Incidentes
- Recuperación ante Desastres y Continuidad de Negocio
Módulo 5: Herramientas y Técnicas de Seguridad
- Herramientas de Análisis de Vulnerabilidades
- Técnicas de Monitoreo y Detección
- Pruebas de Penetración
- Seguridad en Redes
- Seguridad en Aplicaciones
- Hardening de Sistemas y Seguridad del Endpoint
- Seguridad en la Nube y en Contenedores
Módulo 6: Buenas Prácticas y Normativas
- Buenas Prácticas en Seguridad Informática
- Normativas y Estándares de Seguridad
- Protección de Datos Personales y RGPD en la Práctica
- Cumplimiento y Auditoría
- Formación y Concienciación
- Ética, Aspectos Legales y Divulgación Responsable
