Ja tenim totes les peces: AEAD versionat, KDF, signatures, TLS, mTLS i, des de la lliçó anterior, un gestor de secrets amb envelope encryption. Aquesta lliçó canvia d'altitud: deixem de mirar primitives i adoptem la visió d'arquitecte. La pregunta ja no és "com funciona AES-GCM?" sinó "quines dades de MediNube estan xifrades, amb quina capa, i contra quin atacant protegeix cada capa?". Dibuixaràs el mapa complet de la dada — des del navegador d'Ana Pérez fins a la fila de PostgreSQL i la cinta de còpia de seguretat— i descobriràs que "està xifrat" és una frase que no vol dir res fins que respons: xifrat on i contra qui?
Contingut
- El mapa de dades de MediNube: estats i fronteres
- En trànsit: TLS extern i intern (la BD també parla TLS)
- En repòs: les quatre capes i quina amenaça cobreix cadascuna
- Cerca sobre dades xifrades: el dilema i l'índex cec
- Fer hash, xifrar o deixar en clar: la taula de decisió
- El bolcat robat: exercici mental guiat
- Xifratge d'extrem a extrem: el maximalisme i per què MediNube no ho és
El mapa de dades de MediNube: estats i fronteres
Tota dada està en un de dos estats: en trànsit (viatjant per una xarxa) o en repòs (escrita en un disc). Cada estat té les seves amenaces i les seves eines. Aquest és el mapa de MediNube amb el que ja hem construït:
flowchart LR
subgraph internet["Internet"]
NAV["Navegador d'Ana Perez"]
CLI["Sistemes de Clinica Sol<br/>(mTLS, 05-02)"]
end
subgraph dmz["Perimetre MediNube"]
NGINX["nginx<br/>TLS 1.3 (05-02)<br/>/etc/medinube/tls/"]
end
subgraph interna["Xarxa interna"]
APP["App MediNube<br/>xifratge d'aplicacio (02-03)<br/>DEK via KMS (06-01)"]
PG[("PostgreSQL<br/>TLS intern + disc xifrat")]
BK[("Copies de seguretat<br/>scrypt (02-04) + sobre (04-05)")]
S3[("Emmagatzematge d'objectes<br/>adjunts xifrats")]
end
NAV -->|"TLS 1.3"| NGINX
CLI -->|"mTLS"| NGINX
NGINX -->|"TLS intern"| APP
APP -->|"TLS: sslmode=verify-full"| PG
APP -->|"TLS"| S3
PG -->|"pg_dump xifrat"| BK
Lliçó d'arquitecte número u: les fletxes són trànsit (domini de TLS, mòdul 5) i els cilindres són repòs (domini dels mòduls 2 i 4). Lliçó número dos: l'interior de la xarxa "de confiança" també porta cadenats — l'època de "TLS només al perímetre" es va acabar quan els atacants van aprendre a moure's lateralment per xarxes internes.
En trànsit: TLS extern i intern
El tram extern ja el vam resoldre al mòdul 5 i aquí només el reutilitzem: TLS 1.3 a portal.medinube.example i api.medinube.example, mTLS per a les clíniques amb el seu SAN clinica-sol.clientes.medinube.example, HSTS, certificats de vida curta amb ACME. Res de nou.
El que sí que és nou és mirar cap dins: la connexió app ↔ PostgreSQL també travessa una xarxa, i en aquesta xarxa hi pot haver un contenidor compromès escoltant. PostgreSQL parla TLS de manera nativa, i el client decideix quant n'exigeix amb sslmode:
sslmode |
Xifra? | Verifica el certificat? | Verifica el nom del servidor? |
|---|---|---|---|
disable |
No | — | — |
require |
Sí | No | No |
verify-ca |
Sí | Sí (contra una CA) | No |
verify-full |
Sí | Sí | Sí |
Aquí hi ha el parany que sorprèn gairebé tothom: require xifra però no verifica. Accepta qualsevol certificat, inclòs l'autosignat que MalloryClinic presenti en un MITM dins la xarxa. És el verify=False de 05-02 disfressat d'opció raonable: canal xifrat amb un desconegut. La configuració correcta fa servir la CA interna que vam crear a 05-01:
# medinube/db.py — connexio a PostgreSQL amb TLS verificat de veritat
import psycopg
connexio = psycopg.connect(
host="pg.interna.medinube.example",
dbname="medinube",
user="app_medinube",
password=obtenir_del_gestor("medinube/produccio/db-password"), # 06-01
sslmode="verify-full", # xifratge + cadena + nom de l'amfitrio
sslrootcert="/etc/medinube/pki/ca.crt", # la nostra CA interna de 05-01
)Explicació: sslrootcert apunta a l'arrel MediNube Lab Root CA desplegada a 05-01; verify-full obliga que el certificat del servidor estigui signat per aquesta CA i que el seu SAN coincideixi amb pg.interna.medinube.example. És exactament la validació de cadena+nom del mòdul 5, aplicada a la base de dades. La contrasenya, per descomptat, surt del gestor de la lliçó anterior.
En repòs: les quatre capes i quina amenaça cobreix cadascuna
"La base de dades està xifrada" pot voler dir quatre coses molt diferents. La taula que segueix és probablement la més important de la lliçó — cada capa protegeix contra un atacant concret i és transparent per a tots els altres:
| Capa | Exemple | Protegeix contra | NO protegeix contra |
|---|---|---|---|
| Disc / volum | LUKS, xifratge per defecte del cloud | Robatori físic del disc, RMA/rebuig de maquinari | Qualsevol atacant amb el sistema operatiu arrencat (el SO veu el disc desxifrat) |
| Base de dades (TDE) | TDE de motors comercials; pgcrypto per columnes |
Robatori dels fitxers de dades i d'algunes còpies de seguretat en fred | Atacant amb accés SQL, injecció SQL, DBA maliciós (segons la variant) |
| Aplicació | medinube.cripto (02-03): AEAD abans de l'INSERT |
BD compromesa completament: bolcat, injecció, DBA curiós | Compromís de la mateixa app o del KMS; no permet consultar el camp |
| Còpies de seguretat | scrypt (02-04) + sobre híbrid (04-05) | Pèrdua/robatori del suport de còpia, repositoris de còpies aliens | Res més: és xifratge d'aplicació aplicat al fitxer de còpia |
Tres comentaris de profunditat:
-
El xifratge de disc és necessari i gairebé mai suficient. Quan el servidor està encès (sempre, en un SaaS), LUKS és transparent: qualsevol procés amb permisos llegeix les dades en clar. El seu escenari real és el disc que surt del centre de dades. Activa'l sempre (al cloud sol venir de sèrie), però no el posis a l'informe de seguretat com "els historials estan xifrats".
-
TDE i
pgcryptoviuen dins la BD. TDE (Transparent Data Encryption) xifra els fitxers del motor: mateix abast que el disc, més granular.pgcryptopermet xifrar columnes des de SQL:-- pgcrypto: xifratge simetric per columna, des del mateix SQL UPDATE pacients SET telefon = pgp_sym_encrypt('612345678', 'clau-secreta') WHERE id = 4471; SELECT pgp_sym_decrypt(telefon::bytea, 'clau-secreta') FROM pacients;El seu límit salta a la vista: la clau viatja a la consulta SQL, amb la qual cosa acaba als logs de la BD, a
pg_stat_activityi a mans de qualsevol atacant que ja estigui dins del motor... que és justament l'atacant del qual volíem protegir-nos. Útil en escenaris acotats; no per als historials de MediNube. -
El xifratge a nivell d'aplicació és el més granular i el més fort. És el que vam construir a 02-03: l'historial d'Ana Pérez es xifra amb AES-256-GCM (format
v1, AADpacient=...;format=v1) abans de sortir de l'app, i viatja ja xifrat fins a la fila de PostgreSQL. La BD només veu bytes opacs: un bolcat complet, una injecció SQL o un administrador curiós no obtenen absolutament res. La DEK arriba per l'envelope encryption de 06-01. El preu també és real, i li dediquem la secció següent: la BD ja no pot indexar ni cercar sobre aquest camp.
Les capes no competeixen: s'apilen. MediNube porta disc xifrat (contra el robatori físic), TLS intern (contra la xarxa), xifratge d'aplicació als camps sensibles (contra la BD compromesa) i còpies de seguretat xifrades amb el sobre híbrid de 04-05 quan el destinatari és extern (la clínica que demana la seva còpia: es xifra la DEK de la còpia amb la pública de la clínica).
Cerca sobre dades xifrades: el dilema i l'índex cec
Aquí apareix el conflicte central del xifratge d'aplicació. Un camp AEAD ben xifrat és indistingible de bytes aleatoris i, a més, xifrar dues vegades el mateix valor produeix blobs diferents (nonce aleatori). Conseqüència directa:
-- Aixo NO POT funcionar: cada xifratge del mateix DNI es diferent SELECT * FROM pacients WHERE dni_xifrat = '???';
No és un defecte: és la semàntica de seguretat que volíem. Però recepció necessita cercar Ana Pérez per DNI. El patró pràctic és l'índex cec (blind index): junt al camp xifrat es desa un HMAC del valor normalitzat, amb una clau dedicada del gestor:
# medinube/indexs.py — index cec per a cerca per igualtat exacta
import hmac, hashlib
CLAU_INDEX_DNI = obtenir_del_gestor("medinube/produccio/index-dni") # NOMES per a aixo
def index_cec_dni(dni: str) -> bytes:
normalitzat = dni.strip().upper().replace("-", "").replace(" ", "")
return hmac.new(CLAU_INDEX_DNI, normalitzat.encode(), hashlib.sha256).digest()
# En desar: INSERT ... (dni_xifrat, dni_idx) VALUES (aead(dni), index_cec_dni(dni))
# En cercar: SELECT ... WHERE dni_idx = %s ← determinista: mateix DNI, mateix HMACPer què cada peça és com és: la normalització garanteix que "12345678-Z" i "12345678z" produeixin el mateix índex (si no, la cerca falla per format); l'HMAC amb clau (i no un simple sha256) impedeix que qui robi la taula calculi l'índex de tots els DNI possibles per força bruta —els DNI tenen poquíssima entropia, regla d'or 5—; i la clau dedicada limita el dany si es filtra.
Honestedat obligada sobre el que l'índex cec filtra: és determinista, així que revela quines files comparteixen valor (igualtat) i permet confirmar un DNI concret a qui tingui la clau de l'índex. Només serveix per a igualtat exacta: res de LIKE 'PER%', rangs ni ordenació. Existeixen esquemes més ambiciosos (xifratge que preserva l'ordre, xifratge cercable), però les seves fuites són molt pitjors del que sembla i la seva història acadèmica és plena de trencaments: per a MediNube, igualtat exacta amb índex cec i qualsevol cerca més rica sobre camps no sensibles.
Fer hash, xifrar o deixar en clar: la taula de decisió
Amb les capes i l'índex cec sobre la taula, ja podem decidir camp a camp. Aquesta és la taula real de la taula pacients i companyia a MediNube — el criteri: cal recuperar el valor original? cal cercar-hi? què passa si es filtra?
| Columna | Tractament | Per què |
|---|---|---|
id (intern) |
Clar | Identificador opac sense significat extern; les FK i JOIN el necessiten |
dni |
AEAD + índex cec | Cal mostrar-lo (recuperable) i cercar-hi (índex); dada identificativa forta |
nom |
AEAD (xifrat) | Recuperable per mostrar-lo; no es cerca per igualtat exacta (la cerca de pacients fa servir altres camins) |
contrasenya |
Argon2id + pepper (03-03/06-01) | Mai no necessitem l'original: verificar ≠ recuperar. Fer hash, mai xifrar |
historial |
AEAD v1/v2 amb AAD |
La dada més sensible; mai no es consulta per contingut des de SQL |
email |
AEAD + índex cec | Recuperable (cal enviar-hi correus) i cercable (login) |
token_recuperacio |
sha256(token) (03-03) |
Només es compara, mai no es mostra; el token real només el té l'usuari |
data_alta, estat |
Clar | Necessaris per operar, ordenar i depurar; risc baix aïllats |
La fila de la contrasenya mereix subratllat: xifrar una contrasenya (recuperable) en lloc de fer-ne el hash és un error clàssic d'auditoria. Si el sistema pot recuperar la teva contrasenya, un atacant també.
El bolcat robat: exercici mental guiat
Posem a prova el disseny. MalloryClinic aconsegueix un pg_dump complet de la BD de MediNube (una injecció SQL, una còpia de seguretat mal protegida, tant se val el com). Recorrem què obté segons les capes presents:
- Sense cap capa (la MediNube del mòdul 1): tot en clar. Historials, DNI, contrasenyes. Bretxa catastròfica, notificació a l'AEPD, portada de premsa.
- Només disc xifrat: exactament el mateix que el punt 1. El bolcat es va fer amb el sistema arrencat; LUKS ni se'n va assabentar. Primera lliçó gravada a foc: el xifratge de disc no protegeix contra bolcats lògics.
- + TDE / xifratge de la BD: també gairebé el mateix: el bolcat lògic surt del motor, que desxifra de manera transparent. TDE protegiria el robatori dels fitxers del motor, no del dump.
- + xifratge d'aplicació (la MediNube actual): els historials i DNI són blobs AEAD sense les DEK (que són al KMS, no a la BD); les contrasenyes són Argon2id i a més falta el pepper (que viu al gestor); els índexs cecs no s'inverteixen sense la seva clau. Mallory té ids, dates i metadades. Continua sent un incident (les metadades també compten per al RGPD i cal notificar-lo), però el contingut clínic d'Ana Pérez està a salvo.
Aquest exercici —"què veu l'atacant amb aquest bolcat?"— és l'eina de disseny més barata que existeix. Fes-lo amb cada taula nova abans d'escriure la migració.
Xifratge d'extrem a extrem: el maximalisme i per què MediNube no ho és
Queda una capa per sobre de totes: el xifratge d'extrem a extrem (E2E), on ni tan sols el servidor pot llegir les dades — només els extrems tenen les claus. Signal, que ja vam esmentar al mòdul 4, és l'exemple canònic: el servidor de Signal transporta blobs que no pot obrir.
Podria MediNube ser E2E, amb els historials xifrats amb claus que només tinguessin les clíniques i els pacients? Tècnicament sí (les peces són les del mòdul 4: X25519, sobres, signatures). Però implicaria renunciar a funcions que avui són el producte: el servidor no podria generar informes agregats, ni indexar per a cerca, ni permetre que un metge d'urgències accedeixi a un historial si el pacient no hi és per "autoritzar" criptogràficament; i la pèrdua de la clau del pacient significaria la pèrdua irrecuperable del seu historial (o un sistema de recuperació... que reintrodueix la confiança en el servidor). E2E és l'elecció correcta quan el servidor és només un transport (missatgeria); MediNube és un processador de dades, i el seu compromís honest és un altre: xifratge d'aplicació amb claus en KMS auditat, de manera que llegir dades exigeixi comprometre dos sistemes i deixi rastre. Dir "som E2E" sense ser-ho seria màrqueting, no criptografia — i les auditories RGPD distingeixen perfectament les dues coses.
Errors Comuns i Consells
- Error:
sslmode=requirei sensació de seguretat. Xifra, però accepta qualsevol certificat. Contra un MITM intern és inútil:verify-full+ la teva CA interna, sempre. - Error: comptar el xifratge de disc com a xifratge de dades. Protegeix discos apagats. A l'informe de seguretat, especifica capa per capa què hi ha.
- Error: passar la clau de
pgcryptoa cada consulta i trobar-la després als logs de PostgreSQL. Si necessites protecció contra la mateixa BD, xifra a l'aplicació. - Error: índex cec amb hash sense clau (
sha256(dni)): amb ~10⁸ DNI possibles, es capgira per força bruta en minuts. HMAC amb clau del gestor, i normalitza abans. - Consell: apila capes segons l'atacant, no segons la moda. La pregunta de disseny sempre és "contra qui?": robatori físic → disc; xarxa interna → TLS amb
verify-full; BD compromesa → aplicació; suport extern → còpies de seguretat amb sobre. - Consell: documenta la taula de decisió de camps (clar/hash/xifrat/índex) al repositori. És or per al següent desenvolupador i per a l'auditor.
Exercicis
- Classifica la taula nova: MediNube afegeix la taula
citesamb columnesid,pacient_id,metge_id,data_hora,motiu_consulta(text lliure: "dolor toràcic..."),telefon_contacte. Assigna tractament a cada columna (clar / AEAD / AEAD+índex cec / hash) i justifica-ho. Pista: recepció necessita cercar cites per telèfon de contacte. - El bolcat, una altra vegada: amb el teu disseny de l'exercici 1, escriu què obté exactament MalloryClinic d'un
pg_dumpdecites, i quines metadades continuen filtrant-se encara que tot el sensible estigui xifrat. És "cita del pacient X amb el metge Y a les 9:00" una dada innòcua? - Caçant el
require: escriu la cadena de connexió (o el blocpsycopg.connect) incorrecta ambsslmode=requirei la seva correcció ambverify-full, i explica en dues frases l'atac concret que la versió incorrecta permet dins la xarxa de MediNube.
Solucions
id,pacient_id,metge_id: clar (claus de JOIN; són referències opaques).data_hora: clar (cal consultar agendes per rang:WHERE data_hora BETWEEN..., impossible sobre AEAD i l'índex cec no cobreix rangs).motiu_consulta: AEAD amb AAD (p. ex.f"cita={id};format=v2") — és contingut clínic, tan sensible com l'historial.telefon_contacte: AEAD + índex cec amb normalització (treure espais, prefix +34) perquè cal recuperar-lo (trucar al pacient) i cercar-hi.- Mallory veu el graf de cites: quin
pacient_idvisita quinmetge_idi quan, sense motius ni telèfons. No és innocu: si el metge Y és identificable com a oncòleg, la mera existència de cites freqüents revela informació de salut — per això les metadades també són dades personals sota el RGPD i l'incident igualment es notifica. Mitigacions possibles: pseudonimitzarmetge_ida nivell d'aplicació o xifrar també aquesta relació acceptant el cost en consultes. - Incorrecta:
psycopg.connect(..., sslmode="require"). Correcta:sslmode="verify-full", sslrootcert="/etc/medinube/pki/ca.crt". Atac: un pod compromès a la xarxa interna fa ARP/DNS spoofing cap apg.interna.medinube.example, presenta un certificat autosignat i el client ambrequirel'accepta sense rebufar: MalloryClinic acaba el TLS, llegeix credencials i consultes, i reenvia el trànsit a la BD real (MITM complet i invisible).
Conclusió
Ja no penses en primitives: penses en capes. Saps que "xifrat" sense cognoms no vol dir res, que cada capa (disc, BD, aplicació, còpia de seguretat) té el seu atacant, que l'app parla amb PostgreSQL amb verify-full contra la CA interna de 05-01, que cercar sobre dades xifrades es paga amb índexs cecs i honestedat sobre les seves fuites, i que l'historial d'Ana Pérez arriba xifrat fins a la fila de la base de dades amb DEK que viuen al KMS de 06-01. El mapa de dades de MediNube està complet.
Queda una dada viatgera que encara arrossega una vergonya del mòdul 1: el token de sessió del portal, aquell eyJ... que a 01-02 va resultar ser Base64 sense signatura — codificar no és xifrar... i tampoc és autenticar. A la propera lliçó el convertim en el que sempre hauria d'haver estat: un token signat. JWT, els seus atacs clàssics i com fer-lo servir bé. Ens veiem allà.
Curs de Criptografia Aplicada
Mòdul 1: Fonaments de la Criptografia
- Què és la criptografia i per a què serveix
- Codificació, ofuscació i xifratge
- Aleatorietat i entropia
- El principi de Kerckhoffs i les regles d'or
Mòdul 2: Criptografia Simètrica
- Xifratge simètric: AES i ChaCha20
- Modes d'operació
- Xifratge autenticat (AEAD)
- Derivació de claus (KDF)
Mòdul 3: Hashes, MAC i Contrasenyes
- Funcions hash criptogràfiques
- Autenticació de missatges amb HMAC
- Emmagatzematge segur de contrasenyes
Mòdul 4: Criptografia Asimètrica
- Fonaments de clau pública i RSA
- Criptografia de corba el·líptica
- Signatures digitals
- Intercanvi de claus: Diffie-Hellman
- Xifratge híbrid
Mòdul 5: PKI, Certificats i TLS
- Certificats X.509 i autoritats de certificació
- TLS a la pràctica
- Gestió del cicle de vida dels certificats
