Cierras el módulo con la lección integradora. Las seis anteriores te dieron las piezas por separado —primitivas, protocolos y gestión de claves—; esta las pone todas a la vez sobre el sistema real de Nimbus y responde a la pregunta que un profesional tiene que saber contestar delante de su dirección: qué dato está protegido, frente a qué amenaza, con qué primitiva y dónde vive la clave. Verás el contraste decisivo entre cifrado en tránsito y en reposo, los tres niveles del cifrado en reposo con el punto que casi nadie tiene claro —que el cifrado de disco no protege frente a una API comprometida—, el cifrado campo a campo con el problema práctico que introduce, la firma de artefactos en el CI/CD, las URL firmadas de 120 segundos explicadas por dentro y la diferencia real entre seudonimizar y anonimizar.

Contenido

  1. Mapa criptográfico de Nimbus de punta a punta
  2. En tránsito frente a en reposo
  3. Los tres niveles del cifrado en reposo
  4. Cifrado a nivel de campo: las notas clínicas
  5. El problema que introduce: ya no se puede buscar
  6. Cifrado de copias de seguridad
  7. Firma de código y artefactos en el CI/CD
  8. Tokens firmados y URL firmadas por dentro
  9. Seudonimización y anonimización
  10. Casos que suelen hacerse mal
  11. Tabla de referencia: dato, amenaza, protección y clave

  1. Mapa criptográfico de Nimbus de punta a punta

flowchart TB
    U["Navegador y app movil\nPasskeys en el modulo seguro\ndel dispositivo (02-05)"]
    U -->|"TLS 1.3: ECDHE + AES-GCM\nHSTS, forward secrecy"| API["API FastAPI\nValida el JWT firmado (kid)\nAutoriza por tenant"]
    API -->|"TLS + credenciales\ndel gestor de secretos"| DB["PostgreSQL\nCifrado de disco del proveedor\nArgon2id en credenciales\nAES-GCM en notas clinicas"]
    API -->|"URL firmadas con HMAC\ncaducidad 120 s"| S3["Bucket A-05\nAdjuntos con cifrado de sobre\nclave de datos por objeto"]
    API -->|"TLS + HMAC-SHA256\nen los webhooks"| PG["Pasarela de pago"]
    API -->|"TLS + DKIM en el saliente"| EM["Email transaccional"]
    DB --> BK["Copias 3-2-1-1-0\nCifradas e inmutables.\nClave FUERA de produccion"]
    S3 --> BK
    CI["CI/CD GitHub Actions\nSBOM, escaneo de secretos,\nartefactos FIRMADOS"] -->|"despliegue verificado"| API
    KMS["KMS / gestor de secretos\nClave maestra que no sale.\nAuditoria por operacion"] -.->|"claves de datos"| API
    KMS -.-> BK

Nueve tramos, nueve decisiones criptográficas. Fíjate en un detalle del diagrama: el KMS aparece con línea discontinua porque no está en el camino de los datos, sino en el de las claves. Esa separación es justo lo que hace que un compromiso de la API no entregue la clave maestra (03-06).

Y una advertencia que atraviesa toda la lección: en este mapa hay tramos donde la criptografía no protege nada, y saber cuáles es tan importante como saber cifrar. Un atacante que obtiene un token válido de la API entra por la puerta principal: TLS lo protege, el cifrado de disco lo descifra por él y el KMS le sirve las claves de datos con toda corrección. La criptografía no es la defensa contra ese ataque; lo son la autorización, el límite de tasa y la detección.


  1. En tránsito frente a en reposo

En tránsito En reposo
Qué protege Los datos mientras viajan por una red Los datos mientras están almacenados
Mecanismo TLS (03-05), SSH, VPN Cifrado simétrico: disco, base de datos o campo
Amenaza que detiene Escucha e interceptación en la red Robo del soporte, copia del volumen, acceso al fichero
Duración Milisegundos Años
En Nimbus Toda comunicación externa e interna PostgreSQL, bucket A-05, copias, portátiles

El cifrado en tránsito lo cubrió por completo la lección 03-05, así que aquí solo interesa su papel dentro del conjunto: protege el tramo y termina en el extremo. Cuando el dato llega al servidor de Nimbus, TLS ha cumplido y desaparece; a partir de ahí, la protección tiene que venir de otra parte. El resto de la lección trata precisamente de esa otra parte.


  1. Los tres niveles del cifrado en reposo

Este es el apartado que más malentendidos deshace. «Ciframos en reposo» puede significar tres cosas muy distintas, y cada una detiene una amenaza diferente.

Disco completo (LUKS, BitLocker, cifrado del proveedor) Nivel de base de datos (TDE, cifrado del volumen gestionado) Nivel de aplicación (campo a campo)
Quién cifra El sistema operativo o el proveedor El motor de base de datos Tu código, antes de guardar
Quién tiene la clave El sistema, al arrancar El motor Solo la aplicación, vía KMS
Detiene Robo del portátil o del disco; retirada de hardware; acceso al almacenamiento físico Además: copia del fichero de datos o del volumen; acceso al almacén de copias del motor Además: un administrador de base de datos, un volcado, una inyección SQL o una copia restaurada en otro sitio
NO detiene Nada de lo que ocurra con el sistema encendido Nada que llegue a través del motor: un SELECT legítimo devuelve texto claro Un atacante que controle la aplicación
Coste Nulo, transparente Bajo, transparente Alto: cambia el modelo de datos y rompe búsquedas
Uso Siempre. Es higiene básica Siempre que el proveedor lo ofrezca Solo para los datos más sensibles

El punto clave, dicho sin rodeos: el cifrado de disco protege frente a un ladrón que se lleva el hardware. No protege absolutamente nada frente a una API comprometida, una inyección SQL o un IDOR, porque en todos esos casos el sistema está encendido y el motor descifra encantado para quien pregunta. Es exactamente lo que ocurrió en la fuga por bucket mal configurado de 02-06: los datos estaban cifrados en reposo y salieron igual, porque el acceso era «legítimo».

De ahí la regla:

Cifrado de disco y de base de datos: siempre, porque son gratis. Pero no los cuentes como defensa frente a los ataques que realmente amenazan a Nimbus. Para eso hace falta cifrado a nivel de aplicación —para lo más sensible— y, sobre todo, control de acceso (02-05).


  1. Cifrado a nivel de campo: las notas clínicas

Nimbus guarda, junto a cada cita, un campo de notas que en las clínicas de fisioterapia puede contener información que revela el estado de salud del paciente. Es el dato más sensible del sistema y el candidato natural al cifrado a nivel de aplicación.

Nota de validación profesional. El tratamiento de datos que revelan información de salud está sujeto a exigencias reforzadas. El diseño técnico que sigue es necesario pero no suficiente: hay que documentar la base jurídica, la minimización, los plazos de conservación y el análisis de riesgos, y validarlo con el responsable de protección de datos y con asesoría legal. El marco normativo se trata en 06-03.

ALTER TABLE citas
    ADD COLUMN notas_cifradas   BYTEA,       -- version || nonce || ct+etiqueta
    ADD COLUMN notas_clave_dk   BYTEA,       -- clave de datos CIFRADA por el KMS
    ADD COLUMN notas_kms_kid    TEXT,        -- que clave maestra la cifro (03-06)
    ADD COLUMN notas_indice     BYTEA;       -- indice ciego (apartado 5)

-- La columna en claro se elimina DESPUES de migrar y verificar
-- ALTER TABLE citas DROP COLUMN notas;

CREATE INDEX idx_citas_notas_indice ON citas (tenant_id, notas_indice);
import secrets
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

VERSION = b"\x01"

def cifrar_notas(texto: str, tenant_id: str, cita_id: int, kms) -> dict:
    # 1. CIFRADO DE SOBRE (03-06): una clave de datos NUEVA por registro
    dk_claro, dk_cifrada, kid = kms.generar_clave_datos()

    # 2. AAD con el contexto: ata el ciphertext a su duenio (03-02)
    aad = f"v1|tenant:{tenant_id}|cita:{cita_id}".encode()
    nonce = secrets.token_bytes(12)
    ct = AESGCM(dk_claro).encrypt(nonce, texto.encode("utf-8"), aad)

    del dk_claro                      # 3. fuera de memoria cuanto antes
    return {
        "notas_cifradas": VERSION + nonce + ct,
        "notas_clave_dk": dk_cifrada,
        "notas_kms_kid": kid,
    }

def descifrar_notas(fila, tenant_id: str, cita_id: int, kms) -> str:
    dk_claro = kms.descifrar_clave_datos(fila["notas_clave_dk"],
                                         fila["notas_kms_kid"])
    blob = fila["notas_cifradas"]
    assert blob[:1] == VERSION, "version de cifrado desconocida"
    aad = f"v1|tenant:{tenant_id}|cita:{cita_id}".encode()
    try:
        return AESGCM(dk_claro).decrypt(blob[1:13], blob[13:], aad).decode()
    finally:
        del dk_claro

Las decisiones y por qué son así:

  • Cifrado de sobre por registro. Cada nota tiene su propia clave de datos, cifrada con la maestra del KMS. Así rotar la maestra no exige recifrar el contenido, y cada operación de descifrado deja auditoría en el KMS (03-06): si alguien descifra diez mil notas en una hora, se ve.
  • AAD con tenant_id y cita_id. Mover la fila a otro tenant o copiarla a otra cita invalida el descifrado. Es la extensión criptográfica del aislamiento multi-tenant de 02-05.
  • Byte de versión. Permite cambiar de algoritmo dentro de tres años sin migrar los datos antiguos de golpe.
  • del dk_claro. Reduce la ventana en que la clave está en memoria. No es una garantía fuerte en Python —el recolector decide—, pero es la práctica correcta y evita que la clave acabe en un volcado de excepción o en un registro de depuración.
  • La columna en claro se elimina al final, tras migrar y verificar. Y hay que recordar que borrarla no borra las copias de seguridad anteriores, que siguen conteniendo el texto claro hasta que expire su retención.

  1. El problema que introduce: ya no se puede buscar

Aquí está el coste real del cifrado a nivel de aplicación, y es la razón por la que no se aplica a todo:

-- Esto ya NO funciona: el motor solo ve bytes indistinguibles del azar
SELECT * FROM citas WHERE notas ILIKE '%rodilla%';

Al cifrar bien, el texto cifrado es indistinguible de datos aleatorios. Eso es precisamente lo que queremos —y lo que impide buscar, ordenar, indexar, agrupar o hacer coincidencias parciales—. Las opciones y sus compromisos:

Opción Qué permite Qué cuesta
Buscar solo por metadatos no cifrados Filtrar por tenant, fecha, profesional, estado Ninguna búsqueda por contenido. La opción más segura
Índice ciego con HMAC determinista Coincidencia exacta sobre valores concretos Revela igualdad: dos registros con el mismo valor comparten índice, lo que permite análisis de frecuencias
Descifrar en la aplicación y filtrar Búsqueda completa Inviable con volumen: hay que traer y descifrar todo
Cifrado que preserva el orden o cifrado buscable Rangos y búsquedas Fugas de información conocidas y complejidad alta. No recomendable para una PYME

El índice ciego, que es la solución práctica más habitual, funciona así: en lugar de cifrar el valor por el que se quiere buscar, se calcula un HMAC determinista con una clave dedicada, y se indexa ese resultado. Como el HMAC es determinista, el mismo valor produce siempre el mismo índice; y como lleva clave, nadie sin ella puede calcularlo ni construir un diccionario.

import hmac, hashlib

def indice_ciego(valor: str, tenant_id: str, clave_indice: bytes) -> bytes:
    """HMAC determinista para busqueda exacta. La clave es DISTINTA de la
    de cifrado, derivada con HKDF (03-02). El tenant_id entra en el mensaje
    para que el mismo valor en dos clinicas produzca indices distintos."""
    normalizado = valor.strip().casefold()          # misma forma -> mismo indice
    mensaje = f"{tenant_id}|{normalizado}".encode("utf-8")
    return hmac.new(clave_indice, mensaje, hashlib.sha256).digest()[:16]
-- Busqueda exacta sobre el indice, sin descifrar nada
SELECT id, fecha FROM citas
 WHERE tenant_id = $1 AND notas_indice = $2;

Tres detalles que hacen que esto sea correcto y no un adorno: la clave del índice es distinta de la de cifrado, derivada con HKDF, para que comprometer una no dé la otra; el tenant_id entra en el mensaje, de modo que el mismo valor en dos clínicas produce índices distintos y se corta el análisis entre tenants; y la normalización previa garantiza que "Rodilla" y " rodilla " coincidan, porque el HMAC no perdona ni un espacio.

Y su límite, que hay que asumir conscientemente: revela igualdad. Un atacante con acceso a la tabla ve qué filas comparten valor y puede hacer inferencias por frecuencia. Por eso el índice ciego se aplica a campos con alta cardinalidad —una referencia, un identificador— y no a campos con pocos valores posibles, donde la frecuencia lo delataría todo.

Decisión para Nimbus: cifrar el cuerpo de las notas clínicas y no permitir búsqueda por su contenido; la búsqueda se hace por metadatos (paciente, profesional, fecha) que no son el dato más sensible. El índice ciego se reserva para el identificador de paciente, donde hace falta coincidencia exacta.


  1. Cifrado de copias de seguridad

En 02-04 estableciste el esquema 3-2-1-1-0, con una copia inmutable. Esta lección añade el requisito que faltaba: esa copia debe además estar cifrada, y su clave debe vivir fuera del mismo entorno.

El razonamiento sale directamente del caso de ransomware de 02-06. Allí el atacante entró por el acceso remoto de la consultora, escaló con un secreto olvidado y destruyó unas copias que estaban en la misma cuenta que producción. Añade ahora la variante moderna, la doble extorsión: no basta con cifrar tus datos, también se los llevan y amenazan con publicarlos.

Propiedad de la copia Frente a qué amenaza
Inmutable (02-04) Que el atacante borre o cifre las copias
Cifrada Que el atacante lea y publique su contenido
Clave fuera del entorno Que el mismo compromiso que da acceso a la copia dé también la clave
Restauración probada Que la copia exista y no sirva

Las tres reglas prácticas: la clave de las copias vive en otra cuenta o proveedor, con acceso independiente y no accesible desde los roles de producción; nunca junto a la copia, que es el error más frecuente; y la restauración se ensaya al menos una vez al año, incluyendo el paso de recuperar la clave desde su custodia (03-06), porque una custodia no probada es una suposición.


  1. Firma de código y artefactos en el CI/CD

De SolarWinds (02-06) salió una lección incómoda: los atacantes comprometieron el proceso de compilación, así que las actualizaciones maliciosas iban firmadas con la clave legítima de la empresa y los clientes las instalaron con toda confianza. La conclusión que extrajiste entonces sigue vigente: una firma válida acredita el origen, no la ausencia de malicia.

Entonces, ¿para qué firmar? Porque sin firma no tienes ninguna de las dos cosas. Firmar da tres garantías concretas y verificables:

Garantía Qué significa
Integridad El artefacto no se ha modificado desde que se construyó
Origen Lo produjo tu CI/CD y no un tercero
Trazabilidad Con la procedencia firmada, puedes decir de qué commit, con qué dependencias y en qué ejecución salió
# .github/workflows/publicar.yml (fragmento)
permissions:
  contents: read
  id-token: write          # identidad efimera para firmar SIN claves guardadas
  packages: write

steps:
  - name: Construir imagen
    run: docker build -t ghcr.io/nimbus/api:${{ github.sha }} .

  - name: Generar SBOM (02-04)
    run: syft ghcr.io/nimbus/api:${{ github.sha }} -o spdx-json > sbom.json

  - name: Firmar imagen y SBOM
    run: |
      cosign sign --yes ghcr.io/nimbus/api:${{ github.sha }}
      cosign attest --yes --predicate sbom.json \
        ghcr.io/nimbus/api:${{ github.sha }}
# En el despliegue: NO desplegar nada que no verifique
cosign verify \
  --certificate-identity-regexp "https://github.com/nimbus/api/.github/workflows/.*" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  ghcr.io/nimbus/api:$SHA

Lo importante de este esquema: no hay una clave privada de firma guardada en ningún sitio. Se usa una identidad efímera del propio CI (el id-token), se firma con un par que existe unos segundos y la prueba queda anclada a un registro público de transparencia, con la misma lógica de la CT de 03-06. Es la mejor respuesta al problema del apartado anterior: elimina el riesgo de que roben la clave de firma, porque no hay clave que robar. Y la verificación en el despliegue comprueba la identidad del firmante, no solo que la firma sea válida: sin --certificate-identity-regexp, aceptarías la firma de cualquiera.

Integridad de dependencias. El otro extremo del mismo problema es lo que entra en tu compilación. La defensa es fijar hashes, de modo que una versión republicada con contenido distinto no se instale:

# requirements.txt con hashes fijados
cryptography==43.0.1 \
    --hash=sha256:8f1e2c9a...  \
    --hash=sha256:2d7b40f3...
pip install --require-hashes -r requirements.txt

--require-hashes hace que la instalación falle si el paquete descargado no coincide con el hash declarado. Es una aplicación directa de las funciones hash de 03-04 al riesgo de cadena de suministro.


  1. Tokens firmados y URL firmadas por dentro

Firmado no es cifrado: JWS frente a JWE

En 02-05 usaste JWT sin entrar en su criptografía. Aquí solo interesa esa parte, y una distinción que se confunde constantemente:

JWS (firmado) JWE (cifrado)
Qué garantiza Integridad y autenticidad Confidencialidad (y también integridad, si es AEAD)
¿El contenido es legible? Sí. Base64 es codificación, no cifrado (03-01) No
Uso habitual El 99 % de los JWT Cuando el token debe llevar datos que el cliente no puede ver
Algoritmos EdDSA, ES256, RS256, HS256 Combinaciones de gestión de clave + AEAD

La consecuencia práctica es muy concreta: un JWT normal es legible por cualquiera que lo tenga. Cualquiera puede pegarlo en un visor y ver su contenido. Por eso nunca se meten datos personales ni secretos en un token firmado: se meten identificadores opacos. Un sub: "u_88421" es correcto; un email: "ana.ruiz@..." es una fuga que viajará en cada petición y acabará en algún registro.

Y la elección de algoritmo: EdDSA o ES256 (asimétricos) cuando el verificador es otro sistema, porque solo necesita la clave pública y no puede fabricar tokens; HS256 (simétrico) solo si emisor y verificador son el mismo servicio, porque compartir la clave HMAC equivale a compartir la capacidad de emitir. Es la distinción entre MAC y firma de 03-03 aplicada a sesiones.

Las URL firmadas de 120 segundos, por dentro

Llevas todo el curso viendo esta pieza sin abrirla. Una URL firmada permite que el navegador descargue un adjunto directamente del bucket A-05, sin que el bucket sea público y sin que el fichero pase por la API. Lo que se firma es exactamente esto:

Mensaje que entra en la firma:
  metodo   : GET
  recurso  : /nimbus-adjuntos/CL-014/9f31c7.pdf
  caduca   : 1785938400          (marca de tiempo UNIX)
  cabeceras: host
import hmac, hashlib, time, base64

def url_firmada(recurso: str, clave: bytes, segundos: int = 120) -> str:
    caduca = int(time.time()) + segundos
    mensaje = f"GET\n{recurso}\n{caduca}".encode("utf-8")
    firma = hmac.new(clave, mensaje, hashlib.sha256).digest()
    firma_b64 = base64.urlsafe_b64encode(firma).decode().rstrip("=")
    return f"https://cdn.nimbusreservas.example{recurso}?exp={caduca}&sig={firma_b64}"

def validar(recurso: str, exp: str, sig: str, clave: bytes) -> bool:
    if int(exp) < time.time():            # 1. caducidad ANTES que la firma
        return False
    mensaje = f"GET\n{recurso}\n{exp}".encode("utf-8")
    esperada = base64.urlsafe_b64encode(
        hmac.new(clave, mensaje, hashlib.sha256).digest()
    ).decode().rstrip("=")
    return hmac.compare_digest(esperada, sig)   # 2. tiempo constante (03-04)

Por qué está diseñada así, punto por punto:

  • La caducidad está dentro del mensaje firmado. Si estuviera fuera, el usuario podría cambiar exp a un año vista y la URL seguiría valiendo. Al estar dentro, alterarla invalida la firma.
  • El recurso también. Sin él, una firma válida para un adjunto serviría para cualquier otro: sería un IDOR con esteroides.
  • El método. Impide convertir un permiso de lectura en uno de escritura.
  • 120 segundos. Es el tiempo justo para que el navegador inicie la descarga. Las URL acaban en el historial, en registros de proxy, en capturas de pantalla y en mensajes reenviados; una caducidad corta hace que todo eso sea inofensivo minutos después.
  • hmac.compare_digest. Tiempo constante, como en el webhook de 03-04.
  • La autorización se comprueba antes de firmar, no después. La API verifica que el usuario tiene acceso a ese adjunto de ese tenant, y solo entonces emite la URL. La firma no autoriza: transporta una autorización ya concedida.

  1. Seudonimización y anonimización

Nimbus necesita datos para el entorno de pruebas y para su analítica de producto, y la tentación es copiar producción «quitando los nombres». Aquí la precisión terminológica tiene consecuencias reales.

Seudonimización Anonimización
Definición Sustituir identificadores por otros valores, de forma que sigue siendo posible reidentificar con información adicional Transformar los datos de modo que la reidentificación sea imposible, para cualquiera y de forma irreversible
¿Reversible? Sí, con la clave o la tabla de correspondencia No
Técnicas Tokenización, hash con sal secreta, cifrado Agregación, generalización, supresión, privacidad diferencial
Estatus Siguen siendo datos personales Dejan de serlo
Uso en Nimbus Entornos de prueba, soporte, analítica interna Estadísticas publicables

Por qué el hash de un DNI no es anonimización. Es el error conceptual más caro de este apartado. Un DNI español tiene un espacio de valores pequeño y una estructura conocida: generar el hash de todos los DNI posibles y construir la tabla inversa es cuestión de minutos. Lo mismo vale para un teléfono, un correo o una matrícula. Un hash sin clave sobre un identificador de dominio acotado es reversible en la práctica, y por tanto sigue siendo un dato personal.

Qué hacer en su lugar, según el objetivo:

Objetivo Técnica correcta
Entorno de pruebas realista Generar datos sintéticos. La opción preferible: no hay dato personal que proteger
Si hay que partir de datos reales Tokenización reversible con tabla de correspondencia en un servicio aparte y de acceso muy restringido, o cifrado con clave que no esté en el entorno de pruebas
Analítica interna por usuario HMAC con clave secreta (no hash a secas), con clave distinta por finalidad y rotada
Estadística publicable Agregación con umbral mínimo (no publicar celdas con menos de N casos), generalización de rangos y supresión de valores raros

Nota de validación profesional. La frontera entre seudonimizar y anonimizar es jurídica además de técnica, y la anonimización real es difícil de alcanzar: la combinación de campos aparentemente inocuos —código postal, fecha de nacimiento y sexo— reidentifica a una parte sustancial de una población. Antes de declarar un conjunto de datos «anonimizado» y tratarlo como no personal, valídalo con el responsable de protección de datos y con asesoría legal. El marco del RGPD se trata en 06-03.


  1. Casos que suelen hacerse mal

Cifrar y guardar la clave al lado. El error número uno. La copia cifrada y su clave en el mismo bucket; la clave de campo en una columna de la misma base de datos; la clave del disco en el mismo repositorio que el docker-compose. En todos esos casos, el atacante que llega al dato llega a la clave, y el cifrado no ha añadido nada salvo una falsa tranquilidad. La pregunta de control es siempre la misma: ¿qué compromiso concreto tendría que ocurrir para que alguien obtuviera dato y clave a la vez? Si la respuesta es «uno solo», hay que rediseñar.

Cifrar por cumplir, sin modelo de amenaza. Se activa el cifrado de disco, se escribe «datos cifrados en reposo» en el cuestionario del cliente y se da el asunto por resuelto. Pero el cifrado de disco no detiene ninguna de las amenazas reales de Nimbus —IDOR, credenciales robadas, token filtrado, bucket mal configurado, acceso excesivo de la consultora—. Cifrar es una respuesta; la pregunta es frente a quién. Sin esa pregunta, se compra tranquilidad en lugar de seguridad.

Confundir cifrado con control de acceso. Son capas distintas y no se sustituyen. El IDOR de 01-01 ocurría sobre TLS impecable y con la base de datos cifrada en reposo; se corrigió con un WHERE tenant_id, no con más criptografía. El cifrado decide qué se puede leer sin la clave; el control de acceso decide quién obtiene la clave o el resultado. Un sistema que cifra todo y autoriza mal está roto.

Tres errores menores pero frecuentes: cifrar el campo y dejar el mismo dato en un registro o en un correo de notificación; no versionar el formato, lo que convierte cualquier cambio futuro en una migración de todo o nada; y no tener plan de rotación, que es lo que hace que una clave dure siete años.


  1. Tabla de referencia: dato, amenaza, protección y clave

Dato de Nimbus Amenaza principal Protección criptográfica Dónde vive la clave
Tráfico app/SPA ↔ API Escucha e interceptación en red pública TLS 1.3 con ECDHE y AEAD; HSTS Efímera por sesión (forward secrecy)
Contraseñas de usuarios Volcado de la tabla de credenciales Argon2id con sal única y rehash No hay clave; hay parámetros (pepper opcional en el gestor)
Sesiones y tokens Falsificación o manipulación de un token JWS con EdDSA y kid Privada en el gestor; pública en el JWKS
Adjuntos del bucket A-05 Acceso al almacenamiento; movimiento entre tenants AES-256-GCM con AAD del tenant, cifrado de sobre Clave de datos por objeto; maestra en el KMS
Descarga de un adjunto Enlace reenviado o filtrado en registros URL firmada con HMAC, caducidad de 120 s Clave de firma en el gestor de secretos
Notas clínicas Volcado, inyección SQL, copia restaurada fuera AES-256-GCM campo a campo con sobre Clave de datos por registro; maestra en el KMS
Resto de la base de datos Robo del soporte o del volumen Cifrado de disco y de base de datos Gestionada por el proveedor
Copias de seguridad Ransomware con doble extorsión Cifrado + inmutabilidad + verificación de hash Fuera de producción, con custodia repartida
Webhook de la pasarela Notificación de pago falsificada HMAC-SHA256 con marca de tiempo Secreto compartido, en el gestor
Correo saliente Suplantación del dominio DKIM (con SPF y DMARC) Privada en el proveedor de correo; pública en el DNS
Artefactos del CI/CD Cadena de suministro (SolarWinds) Firma con identidad efímera + procedencia + hashes fijados No hay clave persistente: identidad OIDC del CI
Acceso administrativo Robo de credenciales SSH con Ed25519; mTLS entre servicios Privada en el dispositivo; certificados de la PKI interna
Datos del entorno de pruebas Copia de producción sin proteger Datos sintéticos o tokenización reversible Correspondencia en un servicio aparte, fuera de pruebas

Esta tabla es el resumen ejecutivo del módulo entero y el artefacto que deberías saber producir para cualquier sistema en el que trabajes.


Errores Comunes y Consejos

  1. Contar el cifrado de disco como defensa frente a ataques a la aplicación. No lo es. Actívalo siempre, pero no lo apuntes en la columna equivocada.
  2. Cifrar y guardar la clave al lado. Aplica la pregunta de control: ¿un solo compromiso da dato y clave?
  3. Cifrar sin modelo de amenaza. Escribe primero de quién proteges.
  4. Confundir cifrado con autorización. El IDOR no se arregla cifrando.
  5. Meter datos personales en un JWT. Firmado no es cifrado: es legible por cualquiera.
  6. Usar HS256 cuando el verificador es otro sistema. Compartir la clave HMAC es regalar la capacidad de emitir.
  7. Firmar una URL sin incluir la caducidad y el recurso en el mensaje. Se puede alargar o reapuntar.
  8. Emitir una URL firmada sin comprobar antes la autorización. La firma transporta permiso, no lo concede.
  9. Llamar anonimización a un hash de un identificador. Es reversible y sigue siendo dato personal.
  10. Cifrar un campo y dejar el mismo dato en un registro, un correo o una copia antigua.
  11. No versionar el formato cifrado. Condena cualquier cambio futuro a una migración total.
  12. Verificar la firma de un artefacto sin comprobar la identidad del firmante.

Consejos

  • Construye la tabla del apartado 11 para tu propio sistema. Las casillas que no sepas rellenar son exactamente tu trabajo pendiente.
  • Cifra a nivel de aplicación solo lo que lo merece. Cada campo cifrado es funcionalidad perdida y complejidad ganada; pagarlo por todo es la vía más rápida a un sistema inmanejable.
  • Diseña desde el principio la rotación y el formato versionado. Añadirlos después cuesta diez veces más.
  • Recuerda la jerarquía real de eficacia para una PYME: control de acceso > gestión de claves > elección de algoritmo. El módulo ha ido en orden inverso por razones didácticas, pero el impacto va en este.

Ejercicios

Ejercicio 1 — Responder a la pregunta del cliente

Una clínica cliente envía un cuestionario de seguridad con estas afirmaciones y pide a Nimbus que confirme o matice cada una:

  1. «Los datos están cifrados en reposo, por lo que un atacante no puede leerlos.»
  2. «Usan HTTPS, por lo que los datos de nuestros pacientes están seguros.»
  3. «Las contraseñas están cifradas en su base de datos.»
  4. «Sus copias de seguridad están cifradas, por lo que un ransomware no es un riesgo.»
  5. «Los datos que usan para pruebas están anonimizados.»

Se pide: para cada afirmación, indica si es correcta, incorrecta o incompleta; explica con precisión técnica por qué; y redacta la respuesta que Nimbus debería dar —honesta, sin marketing y comprensible para un lector no técnico—.

Ejercicio 2 — Diseñar el cifrado de un campo nuevo

Nimbus añade un campo alergias a la ficha de paciente, visible para el profesional durante la cita.

Se pide: (a) decide si debe cifrarse a nivel de aplicación y justifícalo; (b) diseña el esquema completo: columnas, algoritmo, contenido del AAD, dónde vive la clave y cómo se rota; (c) resuelve el requisito de que el profesional pueda filtrar sus citas del día por presencia de alergias, sin exponer el contenido; (d) enumera los tres efectos colaterales que el cifrado provocará en el resto del sistema (registros, copias, soporte) y cómo los abordas; (e) indica qué validación no técnica hace falta antes de desplegarlo.

Ejercicio 3 — Auditar un flujo de descarga

Este es el código de descarga de adjuntos de Nimbus:

@app.get("/api/v1/adjuntos/{adjunto_id}/url")
def obtener_url(adjunto_id: str, usuario = Depends(usuario_actual)):
    recurso = f"/nimbus-adjuntos/{adjunto_id}.pdf"              # (1)
    exp = int(time.time()) + 86400                              # (2)
    sig = hashlib.sha256(f"{recurso}{CLAVE}".encode()).hexdigest()  # (3)
    logger.info("URL emitida: %s?exp=%s&sig=%s", recurso, exp, sig)  # (4)
    return {"url": f"https://cdn.nimbus.example{recurso}?exp={exp}&sig={sig}"}

Se pide: (a) identifica los problemas de las cuatro líneas marcadas y el ataque concreto de cada uno; (b) señala el problema más grave, que no está marcado; (c) reescribe el endpoint correctamente; (d) indica qué evento registrarías y con qué campos.


Soluciones

Ejercicio 1

# Veredicto Por qué Respuesta de Nimbus
1 Incompleta El cifrado en reposo detiene el robo del soporte, no un acceso a través de la aplicación con credenciales válidas. Con el sistema encendido, el motor descifra para quien pregunta «Sí, ciframos en reposo a nivel de disco y de base de datos, y además ciframos campo a campo los datos clínicos. Conviene precisar que el cifrado en reposo protege frente al robo del soporte; frente a un acceso indebido a través de la aplicación, la protección es el control de acceso, el aislamiento entre clínicas y la detección, que describimos aparte»
2 Incompleta HTTPS protege el tramo de red. No protege frente a fallos de la aplicación como IDOR o inyección, ni frente a un cliente comprometido «Sí, todo el tráfico va por TLS 1.3 con HSTS. HTTPS garantiza que nadie puede leer ni alterar los datos en el camino; la protección de los datos una vez llegan depende de otros controles, que detallamos»
3 Incorrecta Las contraseñas no se cifran: se derivan con una función lenta y unidireccional. Si se cifraran, existiría una clave capaz de recuperarlas todas «Corregimos el término: no las ciframos, las procesamos con Argon2id, con sal única por usuario y parámetros revisados periódicamente. Es un proceso irreversible: ni siquiera nosotros podemos recuperar una contraseña»
4 Incompleta El cifrado protege frente a la publicación de los datos (doble extorsión), no frente al borrado o cifrado de las copias. Para eso hace falta inmutabilidad, aislamiento y restauración probada «Las copias están cifradas y son inmutables, con la clave fuera del entorno de producción y restauraciones probadas periódicamente. El cifrado cubre el riesgo de publicación; la inmutabilidad, el de destrucción. Son medidas complementarias»
5 Probablemente incorrecta Si la técnica es un hash de identificadores, es seudonimización reversible en la práctica, y siguen siendo datos personales «Usamos datos sintéticos en pruebas y, cuando partimos de datos reales, tokenización con la correspondencia custodiada aparte. Evitamos el término "anonimizados" porque exige irreversibilidad demostrable; lo correcto es "seudonimizados", y por eso el entorno de pruebas mantiene controles equivalentes a producción»

Ejercicio 2

(a) ¿Debe cifrarse? Sí. Una alergia es información de salud, del mismo nivel de sensibilidad que las notas clínicas, y el criterio del apartado 3 es claro: el cifrado a nivel de aplicación se reserva para lo más sensible, y esto lo es.

(b) Esquema. Columnas alergias_cifradas BYTEA, alergias_clave_dk BYTEA, alergias_kms_kid TEXT y alergias_presencia BOOLEAN. Algoritmo AES-256-GCM con cifrado de sobre y clave de datos por registro. AAD: v1|tenant:{tenant_id}|paciente:{paciente_id}. Clave maestra en el KMS, rotada anualmente; como el sobre desacopla la maestra del contenido, rotarla solo recifra las claves de datos.

(c) Filtrar por presencia sin exponer el contenido. Una columna booleana no cifrada alergias_presencia. Revela únicamente si hay alergias registradas —un metadato de sensibilidad mucho menor— y permite un índice y un filtro triviales, sin descifrar nada. Es preferible a un índice ciego, que aquí no aportaría (el requisito no es coincidencia exacta) y revelaría igualdad entre pacientes. Importante: hay que decidir conscientemente que ese booleano es aceptable, porque también es información.

(d) Tres efectos colaterales. (1) Registros: cualquier logger que vuelque la fila completa reintroduce el dato en claro; hay que revisar el registro (02-04) y excluir el campo explícitamente. (2) Copias de seguridad: las anteriores a la migración contienen el texto claro hasta que expire su retención, lo que debe documentarse. (3) Soporte: Rubén dejará de ver el campo en las herramientas internas, lo que exige un procedimiento explícito —descifrado bajo permiso concreto, registrado y auditado— en lugar de acceso general.

(e) Validación no técnica. Antes de desplegar hay que validar con el responsable de protección de datos y con asesoría legal: base jurídica del tratamiento, información al interesado, plazo de conservación, quién puede acceder y en qué circunstancias, y actualización del registro de actividades y del análisis de riesgos (06-03).

Ejercicio 3

(a) Problemas marcados:

# Problema Ataque
1 El recurso se construye con el adjunto_id que envía el cliente, sin validar formato Recorrido de rutas: un identificador con ../ puede apuntar a otros objetos del bucket
2 Caducidad de 86.400 s (24 h) Una URL reenviada, guardada en el historial o filtrada en un registro sigue sirviendo un día entero
3 sha256(recurso + CLAVE) en lugar de HMAC, y sin incluir exp ni el método en el mensaje Construcción insegura (03-04) y, sobre todo, exp no está firmado: el usuario lo cambia a un año vista y la URL sigue siendo válida
4 Se registra la URL completa con la firma El registro se convierte en un almacén de credenciales de acceso: quien lea los logs descarga los adjuntos

(b) El problema más grave, no marcado: no hay ninguna comprobación de autorización. La función recibe el usuario_actual y no lo usa. Cualquier usuario autenticado de cualquier clínica puede pedir una URL para el adjunto de cualquier otra. Es el IDOR de 01-01 reaparecido en un endpoint nuevo, y ninguna mejora criptográfica lo arregla.

(c) Versión correcta:

@app.get("/api/v1/adjuntos/{adjunto_id}/url")
def obtener_url(adjunto_id: str, usuario = Depends(usuario_actual)):
    # 1. AUTORIZACION PRIMERO: el adjunto debe ser del tenant del usuario
    adjunto = repo.buscar_adjunto(adjunto_id, tenant_id=usuario.tenant_id)
    if adjunto is None:
        raise HTTPException(404, "No encontrado")     # mismo error que si no existe

    # 2. Recurso construido a partir de datos de CONFIANZA (de la BD)
    recurso = f"/nimbus-adjuntos/{adjunto.tenant_id}/{adjunto.clave_objeto}"

    # 3. Firma HMAC sobre metodo + recurso + caducidad, con 120 s de vida
    url = url_firmada(recurso, CLAVE_FIRMA_URL, segundos=120)

    # 4. Registro SIN la firma ni la URL completa
    registrar_evento("adjunto.url_emitida",
                     usuario_id=usuario.id, tenant_id=usuario.tenant_id,
                     adjunto_id=adjunto.id, caducidad_s=120)
    return {"url": url}

(d) Evento a registrar. Campos: marca de tiempo, usuario_id, tenant_id, adjunto_id, IP de origen, caducidad concedida y resultado. Nunca: la URL completa, la firma, el nombre del fichero si es descriptivo del contenido clínico, ni ningún dato del paciente. Sobre ese registro se construye una alerta útil: un mismo usuario solicitando muchas URL de adjuntos distintos en poco tiempo es el patrón de una exfiltración en curso, exactamente el tipo de señal que faltaba en el caso de 02-06.


Conclusión

Has cerrado el módulo integrando todo lo anterior sobre un sistema real. Tienes el mapa criptográfico de Nimbus de punta a punta, con sus nueve tramos y con el detalle que más importa: el KMS no está en el camino de los datos sino en el de las claves. Distingues tránsito y reposo, y sabes que TLS termina donde empieza el servidor. Y te llevas el apartado que deshace más malentendidos: los tres niveles del cifrado en reposo —disco, base de datos y aplicación—, con la conclusión de que el cifrado de disco protege frente al robo del hardware y no protege nada frente a una API comprometida, una inyección SQL o un IDOR, porque el sistema está encendido y el motor descifra encantado para quien pregunta.

Has cifrado campo a campo las notas clínicas con cifrado de sobre, AAD del tenant y formato versionado, y has pagado su precio honestamente: ya no se puede buscar, con las opciones sobre la mesa —metadatos, índice ciego con HMAC determinista y sus fugas por frecuencia, o descifrar y filtrar— y una decisión razonada para Nimbus. Has añadido el requisito que faltaba a las copias 3-2-1-1-0: cifradas, con la clave fuera del entorno, porque la inmutabilidad cubre la destrucción y el cifrado cubre la publicación en la doble extorsión. Has firmado los artefactos del CI/CD con identidad efímera —sin clave que robar— verificando la identidad del firmante y fijando hashes de dependencias, con SolarWinds como recordatorio de que una firma válida acredita el origen y no la ausencia de malicia. Has abierto por dentro los tokens firmados, con la distinción entre JWS y JWE y la regla de no meter datos personales en algo que cualquiera puede leer, y las URL firmadas de 120 segundos, entendiendo por qué la caducidad y el recurso van dentro del mensaje firmado y por qué la firma transporta una autorización, no la concede. Y has separado seudonimización de anonimización, con el motivo por el que el hash de un DNI es reversible en la práctica y sigue siendo un dato personal. Todo ello condensado en la tabla final dato → amenaza → protección → dónde vive la clave, que es el artefacto que deberías saber producir para cualquier sistema.

Cierra el módulo la advertencia que lo recorre entero: cifrar y guardar la clave al lado, cifrar por cumplir sin modelo de amenaza y confundir cifrado con control de acceso son los tres errores que convierten un despliegue criptográfico en tranquilidad falsa. Y con ellos, la jerarquía real de eficacia para una PYME como Nimbus: control de acceso, gestión de claves y, en último lugar, elección de algoritmo.

Con esto tienes el catálogo completo de lo que se puede proteger y cómo. Pero han quedado sin responder las preguntas que de verdad decide una dirección: de todo esto, ¿qué hacemos primero? Nimbus tiene 38 personas, un presupuesto limitado y una lista de mejoras que no cabe en un año. ¿Cuánto vale evitar la fuga de las notas clínicas frente a evitar una caída de dos días? ¿Qué riesgos se aceptan conscientemente y quién firma esa aceptación? ¿Cómo se pone por escrito para que la decisión sobreviva a la persona que la tomó?

En el Módulo 4: Gestión de Riesgos y Medidas de Protección dejamos de preguntar cómo se protege y empezamos a preguntar qué merece la pena proteger y con qué prioridad. Empezaremos por la evaluación de riesgos (04-01), que es el método para convertir esa lista inabarcable en un orden defendible, y seguiremos con las políticas que lo fijan por escrito, los controles, el riesgo de terceros —donde volverá la consultora del incidente de 02-06—, el plan de respuesta y la recuperación ante desastres.

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