L'incident amb què vam tancar el mòdul 5 ho va dir tot: la clau privada TLS de MediNube no va caure per una fallada matemàtica, va caure per un git push. Tota la criptografia que hem construït al llarg del curs —AEAD, Argon2id, signatures Ed25519, mTLS— comparteix un únic punt de fallada: les claus. Si una clau viu al lloc equivocat, tant se val com de perfecta sigui la primitiva que la fa servir. Aquesta lliçó respon la pregunta que portem ajornant des del mòdul 1: on haurien de viure els secrets de MediNube? I de pas salda diversos deutes pendents: el diccionari CLAUS_API de 01-04, la clau mestra d'historials de 02-03, el pepper de 03-03 i l'envelope encryption que vam anticipar a 04-05.
Contingut
- El problema real: els secrets es filtren per descuit
- Inventari de secrets de MediNube
- Jerarquia de claus: envelope encryption amb DEK i KEK
- L'escala de maduresa de la gestió de secrets
- Variables d'entorn: ús correcte i límits
- Fitxers xifrats al repositori: sops i age
- Gestors de secrets: Vault i KMS a la pràctica
- Rotació de la clau mestra: la criptoagilitat es cobra
- El pepper, per fi ben fet
- Prevenció: que el secret no arribi al repositori
El problema real: els secrets es filtren per descuit
Cap atacant realista factoritzarà el mòdul RSA-3072 de MediNube. El que sí que farà MalloryClinic és buscar als llocs on els desenvolupadors deixem secrets sense voler:
- Repositoris git: el
privkey.pemde l'incident de 05-03. El commit es va esborrar, però l'historial de git i els bots que escanegen GitHub no perdonen (un secret pujat a un repositori públic es considera compromès en minuts). - Logs: un
logger.debug(f"Connectant amb la clau {api_key}")escrit durant una depuració i oblidat. - Fitxers
.envcompartits: per correu, per Slack, "només aquesta vegada perquè puguis arrencar el projecte". - Imatges Docker: un
COPY .env /app/congela el secret a cada capa de la imatge, encara que després s'esborri el fitxer. - Codi font directament: el clàssic. El nostre vell conegut de 01-04:
# medinube/config.py — el deute original del curs (01-04)
CLAUS_API = {
"clinica-sol": "sk_live_9f8a7b6c5d4e3f2a",
"centro-medico-luna": "sk_live_1a2b3c4d5e6f7a8b",
}Aquest diccionari incompleix la regla d'or 2 (l'enemic coneix el sistema: qualsevol amb accés al repositori coneix les claus) i fa impossible la rotació sense desplegar codi nou. Avui el jubilem.
Inventari de secrets de MediNube
Abans de decidir on desar cada secret cal saber quins tenim. Aquest és l'inventari acumulat durant el curs — fer aquesta taula al teu propi projecte és el primer pas real de qualsevol pla de gestió de secrets:
| Secret | Origen al curs | Qui el necessita | Impacte si es filtra |
|---|---|---|---|
| Claus d'API de les clíniques | 01-04 (CLAUS_API) |
L'app en autenticar peticions | Suplantar Clínica Sol o Centre Mèdic Luna |
| Clau mestra d'historials | 02-03 / 02-04 (AEAD v1 + HKDF) |
Servei d'historials | Desxifrar tots els historials, inclòs el d'Ana Pérez |
| Claus de webhooks (una per clínica) | 03-02 (X-MediNube-Firma) |
Emissor de webhooks | Falsificar notificacions a les clíniques |
| Pepper de contrasenyes | 03-03 (ajornat fins avui) | medinube/auth.py |
Anul·la la defensa extra davant bolcats de BD |
| Clau privada de signatura de receptes | 04-03 (Ed25519, Dra. Ferrer CS-4471) | Servei de receptes | Falsificar receptes davant la Farmàcia Robles |
| Claus privades TLS | 05-02 / incident 05-03 | nginx / balancejador | Suplantar portal.medinube.example (MITM) |
| Credencials DNS de certbot | 05-03 (repte DNS-01) | Renovació ACME | Emetre certificats vàlids per als nostres dominis |
| Contrasenya de la base de dades | (implícita des del mòdul 2) | L'app en connectar a PostgreSQL | Accés directe a les dades xifrades i no xifrades |
Fixa't en un detall: no tots els secrets són iguals. La clau mestra d'historials protegeix dades sanitàries amb obligacions RGPD de dècades; la clau d'un webhook protegeix notificacions. El nivell de protecció pot (i ha de) ser proporcional a l'impacte.
Jerarquia de claus: envelope encryption amb DEK i KEK
A 04-05 vam construir el sobre híbrid i vam anticipar que el patró DEK/KEK era "just el que fan els KMS". Ha arribat el moment de saldar aquesta promesa. La idea de l'envelope encryption (xifratge de sobre) és organitzar les claus en jerarquia:
- DEK (Data Encryption Key): la clau que xifra les dades. Viu junt a les dades, però xifrada.
- KEK (Key Encryption Key): la clau que xifra les DEK. Viu al KMS i mai no en surt.
flowchart TD
KMS["KMS / Vault<br/>(la KEK mai no surt d'aquí)"]
KEK["KEK<br/>clau mestra arrel"]
DEK1["DEK historials<br/>(xifrada amb la KEK)"]
DEK2["DEK còpies de seguretat<br/>(xifrada amb la KEK)"]
DEK3["DEK adjunts<br/>(xifrada amb la KEK)"]
D1["Historials AEAD v1"]
D2["Còpies scrypt/sobre"]
D3["Adjunts (radiografies...)"]
KMS --- KEK
KEK -->|"xifra"| DEK1
KEK -->|"xifra"| DEK2
KEK -->|"xifra"| DEK3
DEK1 -->|"xifra"| D1
DEK2 -->|"xifra"| D2
DEK3 -->|"xifra"| D3
El flux operatiu és el punt clau, perquè sol malentendre's:
- L'app vol desxifrar un historial. Junt a la dada té desada la DEK xifrada.
- Envia la DEK xifrada al KMS: "desxifra'm això" (operació
decrypt). - El KMS verifica que l'app està autoritzada, desxifra la DEK amb la KEK i retorna la DEK en clar.
- L'app fa servir la DEK en memòria per a l'AES-GCM de
medinube.criptoi la descarta en acabar.
Les dades mai no passen pel KMS. Pel KMS només viatgen claus de 32 bytes, no historials de megabytes. Això fa el patró ràpid, barat i auditable: el KMS registra cada operació (qui ha demanat desxifrar quina DEK i quan), i rotar la KEK només obliga a rexifrar DEK (unes poques), no terabytes de dades.
L'escala de maduresa de la gestió de secrets
No totes les organitzacions necessiten un HSM. L'important és saber en quin esglaó estàs i quin és el següent:
| Nivell | On viuen els secrets | Quan n'hi ha prou | Risc residual |
|---|---|---|---|
| 0. Hardcoded | Al codi (CLAUS_API) |
Mai | Tothom que llegeixi el repositori té els secrets |
1. .env fora del repositori |
Fitxer local, a .gitignore |
Desenvolupament local, prototips | Es comparteix per canals insegurs; sense rotació ni auditoria |
| 2. Secrets de l'orquestrador/CI | Kubernetes Secrets, GitHub Actions secrets, credencials de systemd | Equips petits amb desplegament automatitzat | Qui administra la plataforma ho veu tot; auditoria limitada |
| 3. Gestor dedicat | Vault, KMS cloud (AWS KMS, GCP KMS, Azure Key Vault), step-ca per a certificats (05-03) | Producció seriosa; dades regulades com les de MediNube | L'app autenticada continua sent la frontera |
| 4. HSM | Maquinari a prova de manipulació; la clau no es pot extreure ni amb root | CA arrel, signatura eIDAS qualificada (04-03), requisits legals | Cost i complexitat operativa |
MediNube, amb historials mèdics i obligacions RGPD, hauria d'estar al nivell 3, amb la KEK idealment recolzada per HSM (els KMS cloud ja ho fan per sota). El .env del nivell 1 continua sent legítim... per a la màquina del desenvolupador amb dades de prova.
Variables d'entorn: ús correcte i límits
Les variables d'entorn són el mecanisme estàndard per lliurar un secret a un procés, i estan bé per a això. Però convé conèixer-ne els límits:
# medinube/config.py — versio corregida (adeu, diccionari de 01-04)
import os
def clau_api_clinica(clinica_id: str) -> str:
"""Recupera la clau d'API d'una clinica des de l'entorn.
En produccio, l'entorn el pobla el gestor de secrets
(nivell 3); en desenvolupament, un .env local que MAI es commiteja.
"""
valor = os.environ.get(f"MEDINUBE_API_KEY_{clinica_id.upper().replace('-', '_')}")
if valor is None:
raise RuntimeError(f"Falta la clau d'API de {clinica_id}: revisa el gestor de secrets")
return valorExplicació: en lloc de tenir els valors al codi, el codi només coneix el nom del secret. Fallar sorollosament (RuntimeError) si en falta un és deliberat: un secret absent ha d'impedir l'arrencada, no degenerar en un valor per defecte insegur.
Límits que has de conèixer:
- Visibles a
/proc: a Linux,cat /proc/<pid>/environmostra l'entorn d'un procés. Qualsevol usuari que pugui llegir aquest fitxer (root, o el mateix usuari) veu els secrets. - S'hereten: tot procés fill rep l'entorn complet. Aquell script de tercers que llances també veu
MEDINUBE_MASTER_KEY. - Acaben en logs i bolcats: molts frameworks imprimeixen l'entorn complet en informar d'un error. Configura el teu gestor d'errors perquè redacti variables amb noms com
*_KEY,*_SECRET,*_PASSWORD. - No es roten soles: canviar el valor exigeix reiniciar el procés.
Fitxers xifrats al repositori: sops i age
De vegades vols versionar la configuració amb secrets inclosos (per exemple, per a infraestructura). La resposta no és "commitejar el secret", és commitejar-lo xifrat. La parella sops + age és l'estàndard lleuger:
# Generar un parell de claus age (X25519 per sota — 04-04 en accio) age-keygen -o ~/.config/sops/age/keys.txt # Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p # Xifrar nomes els VALORS d'un YAML, deixant les claus llegibles per al diff sops --encrypt --age age1ql3z...ac8p secrets.yaml > secrets.enc.yaml git add secrets.enc.yaml # aixo SI pot anar al repositori # Desxifrar en el desplegament (la clau privada age viu al servidor, no al repositori) sops --decrypt secrets.enc.yaml
L'elegància de sops és que xifra valor a valor: el diff de git continua mostrant quina clau ha canviat, sense revelar el contingut. I pot fer servir com a backend age, un KMS cloud o PGP — criptoagilitat també aquí. El límit: descentralitza la rotació (rexifrar i redesplegar) i no dona auditoria d'accessos. És un nivell 2,5 en la nostra escala: perfecte per a configuració d'infraestructura, insuficient com a gestor central de MediNube.
Gestors de secrets: Vault i KMS a la pràctica
Un gestor de secrets (HashiCorp Vault, OpenBao, o el KMS/Secrets Manager del teu núvol) aporta el que cap nivell anterior té: autenticació de l'aplicació, secrets amb caducitat i auditoria central. El flux conceptual, comú a tots els proveïdors:
- L'app s'autentica davant el gestor. No amb "un altre secret hardcodejat" (seria el problema de sempre!), sinó amb una identitat de plataforma: el service account de Kubernetes, el rol IAM de la instància cloud, un certificat mTLS de la nostra PKI de 05-01.
- El gestor retorna un token amb TTL (temps de vida) i, per a molts secrets, un lease: el secret en si caduca (pensa en credencials de BD generades al vol, vàlides 1 hora).
- L'app renova el lease mentre viu; en morir, el secret expira sol. És la mateixa moralitat de les vides curtes dels certificats de 05-03 aplicada a tots els secrets.
Amb la llibreria hvac (client Python de Vault) el codi queda així — tracta aquest exemple com a flux, no com a lligam a un proveïdor:
import hvac
# 1. Autenticacio: aqui amb AppRole; a Kubernetes seria el JWT del service account
client = hvac.Client(url="https://vault.medinube.example:8200")
client.auth.approle.login(role_id=ROLE_ID, secret_id=SECRET_ID)
# 2. Llegir un secret estatic versionat (motor KV v2)
resposta = client.secrets.kv.v2.read_secret_version(
path="medinube/produccio/pepper",
)
pepper = resposta["data"]["data"]["valor"].encode()
# 3. Envelope encryption amb el motor "transit": la KEK viu a Vault
# i MAI no en surt; li enviem la DEK xifrada i ens la retorna en clar.
import base64
resultat = client.secrets.transit.decrypt_data(
name="kek-historials", # la KEK, identificada per nom
ciphertext=dek_xifrada_str, # "vault:v2:..." — fixa't en el versionat!
)
dek = base64.b64decode(resultat["data"]["plaintext"])Detalls a saborejar: el ciphertext del motor transit comença per vault:v2: — versionat del format, exactament com el nostre byte 0x01 de medinube.cripto. Tothom arriba a la mateixa conclusió: sense versió no hi ha rotació.
Nota de compliment: per a MediNube, el gestor de secrets no és només bona enginyeria: el RGPD exigeix "mesures tècniques adequades" (art. 32), i davant una auditoria, el registre d'accessos del gestor és l'evidència de qui va poder tocar la clau dels historials. Coordina l'elecció de nivell amb el teu responsable de seguretat i el teu DPO.
Rotació de la clau mestra: la criptoagilitat es cobra
A 02-03 vam dissenyar el format v1 = 0x01 || nonce(12) || xifrat+tag amb AAD f"pacient={id};format=v1" i vam dir que aquell byte de versió "algun dia ens salvaria". Avui és aquell dia: cal rotar la clau mestra d'historials (bona higiene periòdica, i obligatori si sospites d'un compromís). El pla, gràcies al versionat:
- Es crea la clau nova al gestor (o una nova versió de la KEK, que rexifra les DEK).
- Es defineix
v2: format idèntic, byte0x02, AADformat=v2, clau nova. - Escriptura: tot xifrat nou surt en
v2. - Lectura:
desxifrar_historialmira el primer byte i tria la clau.v1continua sent llegible. - Rexifrat progressiu: un job de fons llegeix cada historial
v1, el desxifra i el reescriu env2. Sense parada de servei. - Quan no queda cap
v1a la BD, la clau vella es destrueix (o s'arxiva segons política de retenció).
# medinube/cripto.py — desxifrat multi-versio
CLAUS = {
0x01: obtenir_del_gestor("kek-historials", version=1), # clau vella: nomes lectura
0x02: obtenir_del_gestor("kek-historials", version=2), # clau nova: lectura i escriptura
}
def desxifrar_historial(blob: bytes, pacient_id: str) -> bytes:
versio = blob[0]
if versio not in CLAUS:
raise HistorialManipulat(f"Versio de format desconeguda: {versio:#04x}")
aad = f"pacient={pacient_id};format=v{versio}".encode()
aesgcm = AESGCM(CLAUS[versio])
return aesgcm.decrypt(blob[1:13], blob[13:], aad)Sense aquell byte inicial, la rotació hauria exigit endevinar amb quina clau s'havia xifrat cada fila (provar i capturar excepcions: lent i lleig) o rexifrar-ho tot de cop amb el servei aturat. La regla d'or 8 —criptoagilitat— acaba de pagar-se sola.
El pepper, per fi ben fet
A 03-03 vam deixar el pepper "per quan tinguéssim un lloc segur on desar-lo". Ja el tenim. Recordatori: la sal va junt al hash a la BD (és pública); el pepper és un secret global que viu fora de la BD, de manera que un bolcat de la base de dades no basti per atacar els hashes fora de línia. La forma correcta no és concatenar, és HMAC (03-02):
# medinube/auth.py — Argon2id + pepper HMAC
import hmac, hashlib
from argon2 import PasswordHasher
ph = PasswordHasher()
PEPPER = obtenir_del_gestor("medinube/produccio/pepper") # 32 bytes del CSPRNG del gestor
def _amb_pepper(contrasenya: str) -> bytes:
# HMAC-SHA256(pepper, contrasenya): sortida uniforme de 32 bytes,
# sense els problemes de longitud/normalitzacio de concatenar.
return hmac.new(PEPPER, contrasenya.encode(), hashlib.sha256).digest()
def registrar(contrasenya: str) -> str:
return ph.hash(_amb_pepper(contrasenya))
def iniciar_sessio(hash_desat: str, contrasenya: str) -> bool:
ph.verify(hash_desat, _amb_pepper(contrasenya)) # llanca excepcio si falla
return TrueAra MalloryClinic necessita dues bretxes (la BD i el gestor de secrets) per muntar un atac de diccionari. Compte: el pepper és difícil de rotar (caldria refer el hash al següent inici de sessió de cada usuari, com la migració de 03-03), així que protegeix-lo bé des del principi.
Prevenció: que el secret no arribi al repositori
El runbook de l'incident de 05-03 era reactiu. La versió madura és impedir la filtració abans del commit i detectar-la a CI:
# gitleaks: escaner de secrets (detecta claus d'API, PEM, tokens...)
# 1) En local, com a hook pre-commit (fitxer .pre-commit-config.yaml):
# - repo: https://github.com/gitleaks/gitleaks
# rev: v8.24.0
# hooks: [{id: gitleaks}]
pre-commit install
# 2) A CI, escanejant TOT l'historial, no nomes l'ultim commit:
gitleaks git --redact -v .
# 3) git-secrets (alternativa d'AWS) amb patrons propis:
git secrets --add 'sk_live_[0-9a-f]{16}' # el format de les nostres claus d'APIComplements imprescindibles:
.gitignoredefensiu:.env,*.pem,*.key,credentials.jsonhan d'estar ignorats abans que existeixin.- Escaneig de l'historial ja publicat:
gitleakssobre tot l'historial descobreix secrets filtrats fa mesos. Si apareix alguna cosa, aplica el runbook de 05-03 generalitzat: rotar primer (el secret ja es considera compromès), després netejar l'historial, avisar[email protected]i buscar usos sospitosos als logs. Netejar l'historial sense rotar és teatre. - Redacció als logs: filtres al logger que emmascarin valors amb aspecte de secret.
Errors Comuns i Consells
- Error: esborrar el commit i donar-se per salvat. El secret continua al reflog, als forks, a les memòries cau dels escàners. L'única resposta vàlida a una filtració és la rotació.
- Error: autenticar l'app davant Vault amb un token hardcodejat. Has mogut el problema, no l'has resolt. Fes servir identitat de plataforma (IAM, service account, mTLS).
- Error: passar historials sencers pel KMS. El KMS xifra claus (DEK), no dades. L'envelope encryption existeix justament per a això.
- Error: un sol secret "per a tot". Una clau per propòsit (regla que ja apliquem amb HKDF a 02-04): així una filtració no arrossega tot el sistema, i cada clau es pot rotar per separat.
- Consell: mesura la teva maduresa amb la taula de nivells i puja un esglaó cada vegada. Passar del nivell 0 al nivell 1 en una tarda elimina el 80 % del risc immediat.
- Consell: tracta l'inventari de secrets com a documentació viva. Cada secret nou entra a la taula amb el seu propietari, el seu impacte i el seu pla de rotació.
Exercicis
- Auditoria expressa: clona (mentalment o de veritat) un projecte teu i localitza els seus secrets. Classifica'ls amb la taula de nivells de maduresa: en quin nivell està cadascun? Quin és el salt de nivell més urgent?
- Rotació amb versionat: partint del
desxifrar_historialmulti-versió d'aquesta lliçó, escriuxifrar_historial_v2i l'esquelet del job de rexifrat progressiurexifrar_pendents(lot=100)que busca blobs que comencen per0x01, els desxifra i els reescriu env2. - Caçador de secrets: escriu una expressió regular per detectar en pre-commit les claus d'API de MediNube (
sk_live_+ 16 hex) i els blocs PEM de claus privades. Per què convé afegir-la també a CI encara que ja estigui al pre-commit?
Solucions
-
Resposta oberta, però el patró típic: claus d'API al nivell 0 o 1, contrasenya de BD al nivell 1 o 2, certificats al nivell 2. El salt més urgent gairebé sempre és treure del codi el que estigui al nivell 0 (una tarda de feina) i afegir gitleaks a CI (una hora).
-
def xifrar_historial_v2(dades: bytes, pacient_id: str) -> bytes: nonce = os.urandom(12) aad = f"pacient={pacient_id};format=v2".encode() ct = AESGCM(CLAUS[0x02]).encrypt(nonce, dades, aad) return bytes([0x02]) + nonce + ct def rexifrar_pendents(lot: int = 100) -> int: files = bd.query("SELECT id, pacient_id, historial FROM historials " "WHERE get_byte(historial, 0) = 1 LIMIT %s", [lot]) for fila in files: dades = desxifrar_historial(fila.historial, fila.pacient_id) nou = xifrar_historial_v2(dades, fila.pacient_id) bd.execute("UPDATE historials SET historial = %s WHERE id = %s", [nou, fila.id]) return len(files)Claus del disseny: el job és idempotent (si s'interromp, la propera passada troba els que en falten), treballa per lots per no saturar la BD, i fa servir l'AAD correcte de cada versió (rexifrar canvia l'AAD de
v1av2, per això cal desxifrar i tornar a xifrar, no "canviar el byte"). -
Patrons:
sk_live_[0-9a-f]{16}i-----BEGIN( [A-Z]+)? PRIVATE KEY-----. Cal també a CI perquè els hooks de pre-commit són opcionals i locals: un company sense el hook instal·lat (o ungit commit --no-verify) se'ls salta; CI és la xarxa de seguretat que ningú pot eludir.
Conclusió
Hem tancat la ferida per la qual sagnava tot el curs: els secrets de MediNube ja no viuen en diccionaris del codi ni en .env compartits, sinó en un gestor amb autenticació, TTL i auditoria; la clau mestra d'historials rota gràcies al versionat v1/v2 que vam sembrar al mòdul 2; el pepper per fi existeix de veritat; i gitleaks vigila que la història del privkey.pem no es repeteixi. L'envelope encryption DEK/KEK ha passat de promesa (04-05) a arquitectura.
Però tenir les claus ben guardades és només la meitat del mapa: ara cal decidir què es xifra, on i contra quina amenaça a cada racó de l'arquitectura de MediNube — del navegador d'Ana Pérez a la fila de PostgreSQL, passant per les còpies de seguretat. Aquest mapa complet del xifratge en repòs i en trànsit és la propera lliçó. 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
