La lliçó anterior va deixar inventari exigint un token signat abans de moure una unitat d'estoc. Però aquest token, el telèfon de l'Anna a la comanda P-2026-000123, l'esdeveniment comanda.creada que viatja per comandes.esdeveniments i les còpies de seguretat de km0_inventari a MinIO continuen circulant i reposant en clar. Qualsevol amb accés a un segment de xarxa, a un disc reciclat, a un volum de Docker o a un bucket mal configurat els llegeix o, pitjor, els modifica sense que ningú se n'adoni. Saber qui és qui no serveix si la conversa es pot escoltar i alterar.

Aquesta lliçó tracta de la criptografia aplicada a una plataforma distribuïda: no com es dissenyen els algorismes, sinó què garanteix cadascun, quan fer servir quin i com integrar-lo sense cometre els errors que converteixen un xifratge correcte en un d'inútil. Veurem xifratge simètric i asimètric, hashes, HMAC i signatures; TLS per a les dades en trànsit, amb gRPC entre comandes i inventari com a cas pràctic; xifratge en repòs a disc, base de dades i columna, amb envelope encryption per al telèfon de l'Anna; xifratge del costat del servidor a MinIO i als backups; i la protecció de dades personals: minimització, pseudonimització per al llac de dades i xifratge com a base del dret a l'oblit. Queden per a 06-04 l'autenticació mútua amb certificats (mTLS) i la custòdia operativa de les claus (Vault).

Contingut

  1. Tres propietats: confidencialitat, integritat, autenticitat
  2. Xifratge simètric: AES-GCM, claus, nonces i etiquetes
  3. Xifratge asimètric i xifratge híbrid
  4. Hashes, HMAC i signatures digitals
  5. Taula resum: quina primitiva per a quin problema
  6. Dades en trànsit: TLS
  7. TLS a gRPC entre comandes i inventari
  8. TLS a PostgreSQL, Kafka, Redis i MinIO
  9. Dades en repòs: disc, base de dades, columna
  10. Envelope encryption: el telèfon de l'Anna
  11. Gestió de claus: rotació, entorns i on no van
  12. Dades personals: minimització, pseudonimització, anonimització i dret a l'oblit
  13. Mapa de protecció de Quilòmetre Zero
  14. Errors Comuns i Consells
  15. Exercicis
  16. Conclusió

  1. Tres propietats: confidencialitat, integritat, autenticitat

Quan diem "protegir una dada" barregem tres garanties diferents que s'aconsegueixen amb eines diferents:

Propietat Pregunta Amenaça Eina
Confidencialitat Només ho llegeix qui toca? Escolta a la xarxa, disc robat, bucket públic Xifratge (simètric, asimètric, TLS)
Integritat Ha canviat pel camí? Alteració d'un esdeveniment, d'un fitxer, d'un preu en trànsit Hash, MAC, signatura; el tag d'AES-GCM
Autenticitat Ho ha produït qui diu? Suplantació de comandes a Kafka, servidor inventari fals MAC (amb clau compartida), signatura digital, certificat TLS

Xifrar no dona integritat per si sol: amb alguns modes de xifratge clàssics (AES-CBC sense MAC) un atacant pot capgirar bits del text xifrat i produir canvis controlats al text en clar sense conèixer la clau. Per això la pràctica moderna és el xifratge autenticat (AEAD), que combina totes dues en una sola operació. I a les tres se n'hi sol afegir una quarta, el no-repudi (que l'autor no pugui negar l'autoria), que només la signatura digital proporciona, perquè el MAC el poden generar tots els que comparteixen la clau.

  1. Xifratge simètric: AES-GCM, claus, nonces i etiquetes

En el xifratge simètric la mateixa clau xifra i desxifra. És ràpid (AES té instruccions dedicades a les CPU: diversos GB/s per nucli) i és el que es fa servir per a tot el volum de dades: discos, fitxers, columnes, i el cos d'una connexió TLS. El seu problema és la distribució de la clau: els dos extrems l'han de tenir, i fer-los-la arribar de manera segura és precisament el problema que resol el xifratge asimètric.

L'estàndard actual és AES-256-GCM (o ChaCha20-Poly1305 on no hi ha acceleració AES). Té cinc peces:

  • Clau: 32 bytes aleatoris. Del seu secret en depèn tot.
  • Nonce (number used once): 12 bytes que no es repeteixen mai amb la mateixa clau. No és secret: es desa al costat del text xifrat. La seva funció és que xifrar dues vegades el mateix missatge doni resultats diferents.
  • Text xifrat: de la mateixa mida que l'original.
  • Etiqueta d'autenticació (tag): 16 bytes que garanteixen la integritat. En desxifrar, si un sol bit del xifrat (o de les dades associades) ha canviat, la verificació falla i no es retorna res.
  • Dades associades (AAD): informació en clar que no es xifra però sí que s'autentica: per exemple, el comanda_id a què pertany un telèfon xifrat, perquè ningú no pugui copiar el telèfon xifrat de l'Anna a la comanda del Marc.
# km0/serveis/comu/cripto.py
# pip install cryptography
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

def generar_clau() -> bytes:
    return AESGCM.generate_key(bit_length=256)          # 32 bytes d'os.urandom

def xifrar(clau: bytes, text: bytes, aad: bytes = b"") -> bytes:
    nonce = os.urandom(12)                              # NOU a cada xifratge
    xifrat_i_tag = AESGCM(clau).encrypt(nonce, text, aad)
    return nonce + xifrat_i_tag                         # el nonce viatja davant, en clar

def desxifrar(clau: bytes, blob: bytes, aad: bytes = b"") -> bytes:
    nonce, xifrat_i_tag = blob[:12], blob[12:]
    return AESGCM(clau).decrypt(nonce, xifrat_i_tag, aad)     # InvalidTag si alguna cosa ha canviat

if __name__ == "__main__":
    k = generar_clau()
    blob = xifrar(k, b"+34 600 123 456", aad=b"P-2026-000123")
    print(len(blob))                                    # 12 + 15 + 16 = 43 bytes
    print(desxifrar(k, blob, aad=b"P-2026-000123"))     # b'+34 600 123 456'
    try:
        desxifrar(k, blob, aad=b"P-2026-000124")        # una altra comanda: l'AAD no quadra
    except Exception as e:
        print(type(e).__name__)                         # InvalidTag

Per què no reutilitzar mai un nonce. GCM genera internament un flux de bits (keystream) a partir de la clau i el nonce, i fa XOR amb el missatge. Si dos missatges es xifren amb el mateix parell clau-nonce, el XOR dels dos textos xifrats és igual al XOR dels dos textos en clar: l'atacant elimina el xifratge sense conèixer la clau, i a més pot recuperar la clau d'autenticació i falsificar etiquetes. Amb nonces aleatoris de 96 bits, la probabilitat de col·lisió esdevé preocupant cap als 2³² missatges amb la mateixa clau (uns 4.000 milions): més que suficient per a una clau per servei i per any, però és la raó per la qual les claus es roten i per la qual un comptador (nonce seqüencial) és preferible quan hi ha un únic escriptor que pot garantir que no es repeteix.

  1. Xifratge asimètric i xifratge híbrid

En el xifratge asimètric hi ha un parell de claus: el que es xifra amb la pública només es desxifra amb la privada, i el que se signa amb la privada ho verifica qualsevol amb la pública. Resol la distribució: inventari publica la seva clau pública i qualsevol li pot enviar un secret que només ell obre. Dues famílies:

Família Exemples Mida de clau per a ~128 bits de seguretat Ús
RSA RSA-2048, RSA-3072 2048-3072 bits Signatures (RS256 de 06-01), certificats; xifratge de claus petites
Corbes el·líptiques ECDSA P-256, Ed25519 (signatura), X25519 (intercanvi de claus) 256 bits Signatures (ES256, EdDSA), intercanvi de claus a TLS 1.3; més ràpid i compacte

El que no es fa és xifrar dades voluminoses amb RSA: és centenars de vegades més lent que AES i només pot xifrar missatges més curts que la clau. La solució universal és el xifratge híbrid: es genera una clau simètrica aleatòria per al missatge, es xifra el missatge amb AES-GCM, i es xifra aquesta clau (32 bytes) amb la clau pública del destinatari. TLS fa exactament això a cada connexió (apartat 6), i l'envelope encryption de l'apartat 10 és el mateix patró aplicat a l'emmagatzematge.

  1. Hashes, HMAC i signatures digitals

Un hash criptogràfic (SHA-256, SHA-3, BLAKE2) resumeix qualsevol entrada en un valor fix de manera que és inviable trobar l'entrada a partir del hash, o dues entrades amb el mateix hash. Dona integritat només si el hash arriba per un canal fiable: un atacant que pot canviar el fitxer pot canviar el hash que l'acompanya. Per això hi ha dues construccions amb clau:

  • HMAC (hash-based message authentication code): HMAC(clau, missatge). Només qui té la clau el pot generar o verificar. Dona integritat i autenticitat entre parts que comparteixen un secret; no dona no-repudi (qualsevol de les parts el va poder generar). És el que signa els URL presignats de MinIO (04-03) i els JWT HS256.
  • Signatura digital (RSA-PSS, ECDSA, Ed25519): hash del missatge xifrat amb la clau privada. Qualsevol la verifica amb la pública; només el signant la va poder produir. Integritat, autenticitat i no-repudi. És RS256 a 06-01 i la base dels certificats TLS.

Un cas pràctic d'HMAC a Quilòmetre Zero: l'embolcall d'esdeveniments de 02-04. Avui qualsevol procés amb accés a Kafka pot publicar un comanda.creada fals a comandes.esdeveniments, o modificar un esdeveniment reproduït des del llac. Afegir un HMAC de l'embolcall, amb una clau que només tenen els productors legítims i els consumidors, permet a inventari i analitica descartar esdeveniments manipulats:

# km0/serveis/comu/esdeveniments_signats.py
import hmac, hashlib, json

def _canonic(esdeveniment: dict) -> bytes:
    """Serialització determinista: mateixes claus, mateix ordre, sense espais.
    Sense això, dos JSON equivalents donarien HMAC diferents."""
    sense_signatura = {k: v for k, v in esdeveniment.items() if k != "signatura"}
    return json.dumps(sense_signatura, sort_keys=True, separators=(",", ":"),
                      ensure_ascii=False).encode()

def signar_esdeveniment(esdeveniment: dict, clau: bytes) -> dict:
    esdeveniment["signatura"] = hmac.new(clau, _canonic(esdeveniment), hashlib.sha256).hexdigest()
    return esdeveniment

def verificar_esdeveniment(esdeveniment: dict, clau: bytes) -> bool:
    esperada = hmac.new(clau, _canonic(esdeveniment), hashlib.sha256).hexdigest()
    # compare_digest: temps constant, per no filtrar quants bytes coincideixen
    return hmac.compare_digest(esperada, esdeveniment.get("signatura", ""))

Al productor de comandes (02-04), abans de producer.send, es crida signar_esdeveniment(esdeveniment, CLAU_ESDEVENIMENTS); a cada consumidor, if not verificar_esdeveniment(esdeveniment, CLAU_ESDEVENIMENTS): descartar_i_alertar(). La clau es distribueix amb el gestor de secrets de 06-04. Quan els consumidors no hagin de poder produir (per exemple, un tercer que llegeix repartiment.panell), l'embolcall se signa amb Ed25519 en lloc d'HMAC: el mateix codi amb cryptography.hazmat.primitives.asymmetric.ed25519.

  1. Taula resum: quina primitiva per a quin problema

Necessitat Primitiva Clau Exemple a Quilòmetre Zero
Xifrar volum de dades AES-256-GCM (AEAD) Simètrica Telèfon de l'Anna a km0_comandes; volums; backups
Enviar una clau a algú / acordar-la RSA-OAEP, X25519 (ECDH) Asimètrica Handshake TLS; envelope encryption amb KMS
Detectar alteració amb secret compartit HMAC-SHA256 Simètrica Embolcall de comandes.esdeveniments; URL presignats
Detectar alteració sense secret compartit, amb autoria Signatura Ed25519 / ECDSA / RSA-PSS Asimètrica JWT RS256 (06-01); certificats; esdeveniments cap a tercers
Empremta d'un contingut SHA-256 Cap Deduplicar fotos a km0-fotos; ETag; hash encadenat d'auditoria (06-05)
Desar contrasenyes Argon2id / bcrypt Cap (sal) identitat (06-01)
Derivar claus d'una de mestra HKDF Simètrica Una clau per servei i propòsit a partir d'una arrel

  1. Dades en trànsit: TLS

TLS (Transport Layer Security) és el protocol que xifra i autentica una connexió TCP: HTTPS és HTTP sobre TLS, i gRPC, PostgreSQL, Kafka i Redis el suporten de manera nativa. Combina tot l'anterior: xifratge asimètric per acordar una clau, simètric per al trànsit, signatures per autenticar el servidor, hashes per a la integritat.

El handshake de TLS 1.3, resumit:

sequenceDiagram
    participant C as Client (comandes)
    participant S as Servidor (inventari)
    C->>S: ClientHello: versions, suites, clau pública efímera (X25519)
    S->>C: ServerHello: suite triada, clau pública efímera
    Note over C,S: Tots dos deriven la mateixa clau simètrica (ECDH).<br/>A partir d'aquí tot va xifrat.
    S->>C: Certificate (cadena) + CertificateVerify (signatura amb la privada) + Finished
    C->>C: Verifica la cadena fins a una CA de confiança,<br/>que el nom coincideix (SAN), dates, revocació
    C->>S: Finished
    C->>S: Dades d'aplicació (gRPC) xifrades amb AES-GCM / ChaCha20

Conceptes que convé fixar:

  • Certificat: la clau pública del servidor més la seva identitat (nom DNS al camp Subject Alternative Name, SAN), signades per una autoritat de certificació (CA). El client confia en la CA (té el seu certificat arrel al magatzem de confiança) i, per transitivitat, en el servidor.
  • Cadena de confiança: arrel → intermèdia → servidor. L'arrel es guarda fora de línia; les intermèdies signen els certificats dels servidors. A Internet, les arrels les distribueixen els sistemes operatius i els navegadors; dins de Quilòmetre Zero, la CA serà pròpia (aquesta lliçó la crea amb OpenSSL; 06-04 la converteix en una PKI amb emissió automàtica i certificats per a clients).
  • Secret perfecte cap endavant (forward secrecy): a TLS 1.3 la clau de sessió s'acorda amb claus efímeres que es destrueixen; robar la clau privada del servidor demà no permet desxifrar el trànsit capturat avui.
  • TLS 1.3 (2018) va eliminar les suites febles, va reduir el handshake a un viatge d'anada i tornada i xifra el certificat del servidor. TLS 1.0 i 1.1 estan obsolets; 1.2 s'accepta si està ben configurat. Configura sempre mínim 1.2, preferit 1.3.
  • Terminació: la connexió TLS pot acabar al mateix servei o en un component davant seu (el gateway de 06-05 terminarà el TLS d'Internet). Entre el terminador i el servei, si hi ha xarxa, cal TLS un altre cop.

El que TLS no fa per defecte: autenticar el client. inventari sabrà que parla amb xifratge, i comandes sabrà que parla amb el veritable inventari, però inventari no sap si el client és comandes o un procés qualsevol. Això és mTLS, i és la lliçó 06-04.

  1. TLS a gRPC entre comandes i inventari

Substituirem l'add_insecure_port que 02-03 va deixar "sense TLS de moment". Primer, una CA interna i un certificat de servidor amb OpenSSL:

# km0/certs/generar.sh  (desenvolupament; en producció ho farà la PKI de 06-04)
set -e
mkdir -p km0/certs && cd km0/certs

# 1. CA arrel de Quilòmetre Zero (clau EC P-256, vàlida 10 anys, només en desenvolupament)
openssl ecparam -name prime256v1 -genkey -noout -out km0-ca.key
openssl req -x509 -new -key km0-ca.key -sha256 -days 3650 \
    -subj "/CN=Quilometre Zero CA interna" -out km0-ca.crt

# 2. Clau i CSR per a inventari, amb SAN (nom DNS a Compose i localhost per a proves)
openssl ecparam -name prime256v1 -genkey -noout -out inventari.key
openssl req -new -key inventari.key -subj "/CN=inventari" -out inventari.csr

cat > inventari.ext <<EOF
subjectAltName = DNS:inventari, DNS:inventari.km0.internal, DNS:localhost
extendedKeyUsage = serverAuth
EOF

# 3. La CA signa el certificat d'inventari (90 dies: els certificats curts es roten, no es vigilen)
openssl x509 -req -in inventari.csr -CA km0-ca.crt -CAkey km0-ca.key -CAcreateserial \
    -days 90 -sha256 -extfile inventari.ext -out inventari.crt

openssl x509 -in inventari.crt -noout -subject -dates -ext subjectAltName
rm inventari.csr inventari.ext

El detall que més fallades produeix és el SAN: el client compara el nom al qual s'ha connectat (inventari, el nom del servei a Compose) amb els del certificat; si no coincideix, rebutja la connexió encara que la signatura sigui vàlida. El CN ja no compta per a aquesta comprovació.

Servidor (serveis/inventari/servidor.py), només canvia l'arrencada:

import grpc
from concurrent import futures

def _llegir(ruta):
    with open(ruta, "rb") as f:
        return f.read()

credencials = grpc.ssl_server_credentials(
    private_key_certificate_chain_pairs=[(_llegir("certs/inventari.key"),
                                          _llegir("certs/inventari.crt"))],
    # root_certificates i require_client_auth arriben a 06-04 (mTLS)
)
servidor = grpc.server(futures.ThreadPoolExecutor(max_workers=10),
                       interceptors=[AuthInterceptor()])          # el de 06-01 continua aquí
inventari_pb2_grpc.add_InventariServicer_to_server(InventariServicer(), servidor)
servidor.add_secure_port("0.0.0.0:50051", credencials)           # abans: add_insecure_port

Client a comandes:

# km0/serveis/comandes/client_inventari.py (fragment)
credencials = grpc.ssl_channel_credentials(root_certificates=_llegir("certs/km0-ca.crt"))
canal = grpc.secure_channel("inventari:50051", credencials)     # el nom ha de ser al SAN
stub = inventari_pb2_grpc.InventariStub(canal)
# El JWT continua viatjant a les metadades, ara xifrat per TLS:
stub.ReservarEstoc(req, metadata=(("authorization", f"Bearer {token}"),), timeout=2)

Al client només se li dona la CA, no el certificat d'inventari: així, quan el certificat d'inventari es renovi d'aquí a 90 dies, comandes continuarà confiant-hi sense canviar res. Si comandes es connecta per localhost:50051 des del host, funciona perquè localhost és al SAN; si es connectés per IP, fallaria (Ssl handshake failed ... Hostname mismatch).

Al docker-compose.yml, inventari i comandes munten ./certs en només lectura; el fitxer km0-ca.key no es munta a cap servei (només cal per signar):

  inventari:
    build: ./serveis/inventari
    volumes:
      - ./certs/inventari.crt:/app/certs/inventari.crt:ro
      - ./certs/inventari.key:/app/certs/inventari.key:ro
  comandes:
    build: ./serveis/comandes
    volumes:
      - ./certs/km0-ca.crt:/app/certs/km0-ca.crt:ro

Pots comprovar que el canal va xifrat capturant el trànsit: docker compose exec comandes tcpdump -A port 50051 mostrava abans formatge-curat i el JWT en clar; ara només bytes il·legibles després del ClientHello.

  1. TLS a PostgreSQL, Kafka, Redis i MinIO

Cada magatzem dels Mòduls 2 i 4 té la seva manera d'activar TLS. No la desenvolupem aquí, però sí que la deixem localitzada, perquè tots els clients de Quilòmetre Zero hauran de canviar la seva cadena de connexió:

Sistema Costat servidor Costat client (Python) Nota
PostgreSQL (km0_analitica, km0_inventari) ssl = on, ssl_cert_file, ssl_key_file a postgresql.conf; hostssl a pg_hba.conf per obligar psycopg.connect(..., sslmode="verify-full", sslrootcert="certs/km0-ca.crt") sslmode=require xifra però no verifica el servidor; verify-full
Kafka (comandes.esdeveniments, repartiment.posicions...) Listener SSL://:9093, ssl.keystore.location, ssl.truststore.location; SASL_SSL si a més hi ha autenticació KafkaProducer(security_protocol="SSL", ssl_cafile="certs/km0-ca.crt") Kafka fa servir keystores JKS/PKCS12; converteix-los amb keytool/openssl pkcs12
Redis (memòria cau, sessions, revocats) tls-port 6379, port 0, tls-cert-file, tls-key-file, tls-ca-cert-file redis.Redis(ssl=True, ssl_ca_certs="certs/km0-ca.crt") o rediss:// Redis ≥ 6; els clients de Redis Cluster de 04-05 també ho suporten
Cassandra (km0_comandes) client_encryption_options: enabled: true a cassandra.yaml Cluster(ssl_context=ctx) amb un ssl.SSLContext carregant la CA La comunicació entre nodes es xifra a part (server_encryption_options)
MinIO (km0-fotos...) Certificats a ~/.minio/certs/public.crt i private.key boto3.client("s3", endpoint_url="https://minio:9000", verify="certs/km0-ca.crt") Els URL presignats de 04-03 passen a https:// sense més canvis
HDFS (/km0/esdeveniments/...) dfs.http.policy=HTTPS_ONLY, dfs.encrypt.data.transfer=true WebHDFS per https:// Kerberos per a l'autenticació és un món a part, fora de l'abast del curs

La regla comuna: verificar sempre el certificat del servidor contra la CA interna. Un client que xifra sense verificar (sslmode=require, verify=False) protegeix contra escoltes passives però no contra un servidor impostor, que és l'atac d'"home al mig" que TLS existeix per impedir.

  1. Dades en repòs: disc, base de dades, columna

Les dades passen molt més temps en repòs que en trànsit, i allà les amenaces són unes altres: un disc retirat sense esborrar, un volum de núvol clonat, una còpia de seguretat en un bucket accessible, un administrador de base de dades curiós, o un SELECT * d'un servei compromès. Hi ha tres nivells, que s'acumulen:

Nivell Què protegeix Contra què Contra què no Cost
Disc / volum (LUKS, dm-crypt, xifratge de volums EBS/PD) Tot el que hi ha al disc Disc robat o reciclat, volum clonat, accés físic Qualsevol amb accés al sistema en marxa: la base de dades veu les dades en clar Gairebé nul (AES-NI); transparent
Base de dades (TDE: transparent data encryption, xifratge dels fitxers de dades i del WAL) Els fitxers de la BD i els seus backups Fitxers copiats, backups filtrats Administradors de la BD, qualsevol SELECT autoritzat Baix; cal gestionar la clau fora de la BD
Columna / camp (l'aplicació xifra abans de l'INSERT) Camps concrets: telèfon, adreça, IBAN DBA, dumps, un servei compromès que llegeix la taula, filtracions de rèpliques i analítica Un compromís del servei que posseeix la clau Alt: no es pot indexar ni cercar pel camp; cal gestionar claus per servei

Quilòmetre Zero aplica els tres: volums xifrats a tota la infraestructura (una línia de configuració al núvol o a la instal·lació), TDE on el motor ho ofereixi (Cassandra i PostgreSQL amb extensions o xifratge de fitxers), i xifratge de camp per a les dades personals sensibles que només un servei necessita en clar. El telèfon de l'Anna el necessita repartiment perquè el repartidor truqui en arribar; analitica no l'hauria de veure mai. Amb xifratge de camp i clau pròpia de repartiment, encara que analitica llegeixi la taula, hi veu bytes.

  1. Envelope encryption: el telèfon de l'Anna

Si cada servei xifra els seus camps amb una clau, on viu aquesta clau? Desar-la al costat de les dades anul·la el xifratge; desar-la a cada procés fa la rotació impossible (caldria rexifrar tota la taula). El patró estàndard és el xifratge per sobres (envelope encryption):

  1. Existeix una clau mestra (KEK, key encryption key) que mai no surt d'un KMS (key management service): AWS KMS, Google Cloud KMS, Azure Key Vault, o el motor transit de HashiCorp Vault. El KMS ofereix dues operacions: xifrar(bytes) i desxifrar(bytes), amb control d'accés i auditoria.
  2. Per a cada dada (o cada lot, o cada client) es genera una clau de dades (DEK) aleatòria, es xifra la dada amb AES-GCM i la DEK, i es xifra la DEK amb la KEK al KMS.
  3. Es desen juntes la dada xifrada i la DEK xifrada. La DEK en clar es descarta.
  4. Per llegir: es demana al KMS que desxifri la DEK, i amb ella es desxifra la dada.
flowchart LR
    T[Telèfon de l'Anna<br/>+34 600 123 456] -->|AES-GCM amb DEK| TC[Telèfon xifrat]
    DEK[DEK aleatòria<br/>32 bytes] -->|KMS.xifrar amb KEK| DEKC[DEK xifrada]
    TC --> BD[(km0_comandes<br/>telefon_xif, dek_xif, kek_id)]
    DEKC --> BD
    KMS[KMS / Vault transit<br/>KEK 'km0-comandes-2026' no surt mai]
    DEK -.generada localment.-> DEK

L'avantatge decisiu: rotar la KEK no obliga a rexifrar les dades, només a rexifrar les DEK (32 bytes cadascuna, i es pot fer de manera mandrosa), i revocar l'accés d'un servei a la KEK deixa totes les seves dades il·legibles de cop, encara que tingui la taula sencera. A Quilòmetre Zero no tenim un KMS de núvol; a 06-04 Vault ocuparà aquest paper amb el seu motor transit. Per a aquesta lliçó, simulem el KMS amb una classe que exposa només xifrar_dek/desxifrar_dek i guarda la KEK fora de l'abast del codi de negoci:

# km0/serveis/comandes/xifratge_camps.py
import os, base64, json
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

class KMSLocal:
    """Substitut de desenvolupament d'un KMS: la KEK viu aquí i no s'exposa mai.
    A 06-04 aquesta classe se substitueix pel motor 'transit' de Vault."""
    def __init__(self, kek_per_id: dict[str, bytes], kek_activa: str):
        self._keks = kek_per_id
        self.kek_activa = kek_activa

    def xifrar_dek(self, dek: bytes) -> tuple[str, bytes]:
        nonce = os.urandom(12)
        return self.kek_activa, nonce + AESGCM(self._keks[self.kek_activa]).encrypt(nonce, dek, b"dek")

    def desxifrar_dek(self, kek_id: str, dek_xifrada: bytes) -> bytes:
        kek = self._keks[kek_id]                  # KeyError si la KEK s'ha retirat: crypto-shredding
        return AESGCM(kek).decrypt(dek_xifrada[:12], dek_xifrada[12:], b"dek")


class XifradorCamps:
    """Xifra camps d'un registre amb envelope encryption i AAD = id del registre."""
    def __init__(self, kms: KMSLocal):
        self._kms = kms

    def xifrar(self, valor: str, registre_id: str) -> str:
        dek = AESGCM.generate_key(bit_length=256)
        nonce = os.urandom(12)
        xifrat = AESGCM(dek).encrypt(nonce, valor.encode(), registre_id.encode())
        kek_id, dek_xifrada = self._kms.xifrar_dek(dek)
        sobre = {"v": 1, "kek": kek_id,
                 "dek": base64.b64encode(dek_xifrada).decode(),
                 "n": base64.b64encode(nonce).decode(),
                 "c": base64.b64encode(xifrat).decode()}
        return json.dumps(sobre, separators=(",", ":"))      # es desa com a TEXT/BLOB

    def desxifrar(self, sobre_json: str, registre_id: str) -> str:
        s = json.loads(sobre_json)
        dek = self._kms.desxifrar_dek(s["kek"], base64.b64decode(s["dek"]))
        return AESGCM(dek).decrypt(base64.b64decode(s["n"]), base64.b64decode(s["c"]),
                                   registre_id.encode()).decode()


if __name__ == "__main__":
    # En desenvolupament, la KEK ve d'una variable d'entorn carregada des del gestor de secrets (06-04)
    kms = KMSLocal({"km0-comandes-2026": base64.b64decode(os.environ["KM0_KEK_COMANDES"])},
                   kek_activa="km0-comandes-2026")
    xif = XifradorCamps(kms)
    sobre = xif.xifrar("+34 600 123 456", "P-2026-000123")
    print(sobre[:80], "...")
    print(xif.desxifrar(sobre, "P-2026-000123"))       # +34 600 123 456
    # Copiar el sobre a la comanda del Marc no serveix: l'AAD el delata
    try:
        xif.desxifrar(sobre, "P-2026-000124")
    except Exception as e:
        print("Rebutjat:", type(e).__name__)            # InvalidTag

A Cassandra (km0_comandes), la columna telefon passa a ser telefon_xif text, i a serveis/comandes només el codi que atén repartiment crida desxifrar. Els requisits de consulta canvien: no es pot fer WHERE telefon = ... sobre un camp xifrat amb nonce aleatori. Si cal cercar per telèfon (per exemple, perquè el suport localitzi una comanda), s'afegeix una columna telefon_hmac amb HMAC(clau_index, telefon normalitzat): determinista, cercable, i no reversible. És el mateix truc de la pseudonimització de l'apartat 12.

Per als camps de pagaments, la regla és no tenir-los: el número de targeta (PAN) no arriba mai a Quilòmetre Zero; el navegador el lliura directament a la passarel·la de pagament, que retorna un token de pagament que pagaments emmagatzema. Xifrar un PAN propi és assumir PCI DSS complet; delegar és l'única opció sensata per a un marketplace.

  1. Gestió de claus: rotació, entorns i on no van

El xifratge desplaça el problema: de protegir dades a protegir claus, que són poques i petites. Principis:

  • Mai al codi ni al repositori. Ni en un .py, ni en un docker-compose.yml versionat, ni en una imatge de contenidor. Un secret a Git ho és per sempre a l'historial. La variable d'entorn de l'exemple anterior és un pas intermedi que 06-04 substitueix per injecció des de Vault.
  • Una clau per entorn: desenvolupament, proves i producció no comparteixen cap clau. Un bolcat de la base de proves no ha de ser mai desxifrable amb la clau de producció.
  • Una clau per servei i per propòsit: comandes no pot desxifrar els camps de repartiment, i la clau d'HMAC d'esdeveniments no és la de xifratge de camps. Amb HKDF se'n deriven diverses d'una arrel si cal, però les KEK de producció viuen al KMS.
  • Rotació programada: la KEK anualment (o abans si hi ha sospita), les DEK per registre (no cal rotar-les: es regeneren en reescriure), els certificats TLS cada 90 dies o menys, la clau de signatura de JWT cada pocs mesos amb kid. Tot xifrat guarda l'identificador de la clau amb què es va fer, per poder desxifrar durant la transició.
  • Separació de funcions: qui administra la base de dades no administra el KMS; qui desplega no veu les claus de producció.
  • Còpia de seguretat de les claus, xifrades i en un lloc diferent de les dades: unes dades xifrades la clau de les quals s'ha perdut són dades esborrades.

El que aquí és principi, a 06-04 serà operació: Vault, credencials dinàmiques, leases i auditoria d'accessos a cada clau.

  1. Dades personals: minimització, pseudonimització, anonimització i dret a l'oblit

L'Anna, el Marc i la Llúcia són persones, i els seus noms, adreces, telèfons, correus, historial de compres i (a repartiment.posicions) la posició dels repartidors són dades personals subjectes al RGPD. La criptografia és una de les eines, no l'única, i les decisions de disseny importen més que els algorismes:

  • Minimització: no recollir ni conservar el que no cal. El llac de dades de 04-02 desava a /km0/esdeveniments/<dia>/comandes.jsonl l'esdeveniment complet, amb correu i adreça. analitica necessita saber que un mateix client va comprar tres vegades, no qui és. I vendes_diaries no necessita ni això.
  • Pseudonimització: substituir els identificadors directes per un pseudònim estable que només es pot revertir amb informació guardada a part. client_id en lloc del correu és el mínim; encara millor, un HMAC del client_id amb una clau que analitica no té, de manera que ni tan sols un JOIN accidental amb km0_comandes reidentifiqui. Les dades pseudonimitzades continuen sent personals, però el risc d'una fuita del llac baixa radicalment.
  • Anonimització: transformar les dades de manera que la reidentificació sigui raonablement impossible: agregar (vendes per producte i dia, no per client), generalitzar (ciutat en lloc d'adreça; franja horària en lloc d'instant), suprimir valors rars (una única comanda de vi-crianca a Lleida un dimarts identifica algú encara que no hi hagi nom). Els agregats de km0_analitica són anònims; els esdeveniments pseudonimitzats no.
  • Tokenització: reemplaçar un valor per un token sense cap relació matemàtica amb ell, amb una cambra cuirassada que guarda la correspondència; és el que fa la passarel·la de pagament amb la targeta.
  • Dret de supressió ("a l'oblit"): quan la Llúcia demana que s'esborrin les seves dades, cal esborrar-les de km0_comandes, de les rèpliques, dels backups, del llac i dels tòpics de Kafka amb retenció llarga. Esborrar d'un backup immutable o d'un log de Kafka és impracticable. El crypto-shredding ho resol: si les dades personals de la Llúcia es van xifrar amb una DEK pròpia de la Llúcia (una per client, desada xifrada per la KEK), destruir aquesta DEK converteix totes les seves dades, a tot arreu, en soroll irrecuperable, sense tocar els backups. És la raó de pes per triar "una DEK per client" en lloc d'"una per registre" a l'apartat 10.

La pseudonimització d'esdeveniments per al llac, en codi:

# km0/serveis/analitica/pseudonimitzar.py
# S'executa al consumidor que escriu /km0/esdeveniments/<dia>/comandes.jsonl (04-02),
# de manera que el llac no rebi mai identificadors directes.
import hmac, hashlib

CAMPS_DIRECTES = {"email", "nom", "telefon", "adreca"}               # s'eliminen
CAMPS_PSEUDONIM = {"client_id"}                                      # se substitueixen

def pseudonimitzar(esdeveniment: dict, clau_pseudonims: bytes) -> dict:
    dades = dict(esdeveniment["dades"])
    for camp in CAMPS_DIRECTES:
        dades.pop(camp, None)
    for camp in CAMPS_PSEUDONIM:
        if camp in dades:
            dades[camp] = hmac.new(clau_pseudonims, dades[camp].encode(),
                                   hashlib.sha256).hexdigest()[:24]
    if "adreca_lliurament" in dades:                # generalitzar: només el mercat
        dades["mercat"] = dades.pop("adreca_lliurament").get("mercat")   # 'girona'
    return {**esdeveniment, "dades": dades}

# Entrada:  {"tipus": "comanda.creada", "dades": {"comanda_id": "P-2026-000125", "client_id": "u-llucia",
#            "email": "[email protected]", "telefon": "+34 6...", "linies": [...],
#            "adreca_lliurament": {"carrer": "...", "mercat": "lleida"}}}
# Sortida:  {"tipus": "comanda.creada", "dades": {"comanda_id": "P-2026-000125",
#            "client_id": "9f1c2a...b7", "linies": [...], "mercat": "lleida"}}

clau_pseudonims la custodia identitat (o el KMS), no analitica: així l'equip de dades pot comptar clients recurrents i calcular recomanacions (05-03) sense poder saber qui és 9f1c2a...b7. Si un dia cal contactar amb aquest client (per exemple, per a una retirada de producte), la petició passa per identitat, que sí que pot recalcular l'HMAC de cada client i trobar-ne la correspondència, amb auditoria de qui ho ha demanat (06-05).

Advertència de compliment normatiu. El RGPD (i la LOPDGDD a Espanya) exigeix molt més que xifratge: base jurídica, informació a l'interessat, registre d'activitats de tractament, avaluació d'impacte quan hi ha seguiment de posicions, contractes amb encarregats (la passarel·la de pagament, el proveïdor de núvol), i notificació de bretxes en 72 hores. PCI DSS regula qualsevol contacte amb dades de targeta. El que es descriu aquí és el disseny tècnic; la política de retenció, l'abast de la pseudonimització i el procediment de supressió s'han de definir i revisar amb un professional de protecció de dades i seguretat.

  1. Mapa de protecció de Quilòmetre Zero

Amb tot l'anterior, cada dada de la plataforma queda localitzada i protegida:

Dada On viu En trànsit En repòs Personal
Contrasenyes identitat (PostgreSQL) TLS Argon2id (irreversible)
JWT d'accés Memòria de l'app; metadades gRPC / Authorization TLS (gRPC i HTTP) No es persisteix Conté sub
Catàleg, preus, fotos cataleg (PostgreSQL), Redis, km0-fotos TLS; URL presignats per HTTPS Volum xifrat; SSE a MinIO No
Comandes (línies, imports) km0_comandes (Cassandra) TLS client-node i entre nodes Volum xifrat + TDE Sí (vinculades a client_id)
Telèfon i adreça de lliurament km0_comandes TLS Xifratge de camp (envelope, DEK per client, KEK al KMS) Sí, sensible
Dades de targeta No s'emmagatzemen: token de la passarel·la a pagaments TLS navegador → passarel·la Token opac PCI DSS delegat
Estoc km0_inventari (inv-bcn, inv-vlc) TLS Volum xifrat No
Esdeveniments (comandes.esdeveniments, inventari.alertes) Kafka TLS + HMAC de l'embolcall Volum xifrat; retenció limitada Sí fins a pseudonimitzar
Posicions (repartiment.posicions, repartiment.panell) Kafka, Flink TLS + HMAC Retenció curta (hores) Sí (repartidors): minimitzar i agregar
Llac d'esdeveniments HDFS /km0/esdeveniments/... HTTPS / transferència xifrada Volum xifrat; pseudonimitzat en escriure Pseudònims
Agregats km0_analitica (PostgreSQL) TLS Volum xifrat Anònims
Factures PDF km0-factures (MinIO) HTTPS presignat SSE-S3 amb clau de MinIO + object lock
Backups km0-backups (MinIO) HTTPS Xifrats pel procés de backup (AES-GCM, clau del KMS) abans de pujar-los, a més de SSE
Configuració d'etcd etcd TLS entre parells i clients Volum xifrat; sense secrets a dins (06-04) No

Sobre el xifratge del costat del servidor a MinIO/S3 (SSE): el magatzem xifra cada objecte en escriure'l i el desxifra en servir-lo. SSE-S3 (clau gestionada pel magatzem) protegeix contra discos i volums; SSE-KMS (clau al KMS, amb auditoria per accés) protegeix a més contra administradors del magatzem; SSE-C (el client envia la clau a cada petició) deixa la custòdia al client. Per a km0-fotos n'hi ha prou amb SSE-S3; per a km0-factures i km0-backups, SSE-KMS i, en el cas dels backups, xifratge addicional al client perquè un compromís de MinIO no els exposi.

Errors Comuns i Consells

  • Xifrar sense autenticar. AES-CBC o AES-CTR "a pèl" permeten manipular el text xifrat. Fes servir sempre un mode AEAD (GCM, ChaCha20-Poly1305) o xifratge + HMAC (encrypt-then-MAC).
  • Reutilitzar nonces. Un nonce fix, o derivat d'alguna cosa repetible (l'id de la comanda), destrueix AES-GCM. os.urandom(12) per xifratge, o un comptador que es persisteix.
  • Inventar criptografia. Ni algorismes propis ni combinacions creatives de primitives. cryptography amb la seva API d'alt nivell (AESGCM, Fernet) i protocols estàndard (TLS) són el límit del que un equip de producte ha de tocar.
  • verify=False en desenvolupament que arriba a producció. Un client TLS que no verifica el certificat és una escolta passiva més. Fes servir la CA interna també en desenvolupament.
  • Certificat sense el SAN correcte. "Hostname mismatch" és l'error de TLS més freqüent; el SAN ha de contenir tots els noms pels quals s'accedeix al servei.
  • Desar la clau al costat de les dades (a la mateixa base de dades, al mateix bucket, al docker-compose.yml). Envelope encryption amb la KEK a fora, al KMS.
  • Xifrar un camp i després cercar-hi. No és possible amb nonce aleatori; planifica una columna HMAC determinista si necessites cercar.
  • Comparar HMAC amb ==. La comparació normal s'atura al primer byte diferent i filtra informació pel temps; hmac.compare_digest sempre.
  • Confondre pseudonimitzar amb anonimitzar. Substituir el correu per un hash continua sent una dada personal; només l'agregació o la generalització suficient anonimitza.
  • Backups en clar "perquè el bucket és privat". Els buckets es fan públics per error amb una freqüència sorprenent. Xifra abans de pujar.
  • Consell: desa amb cada dada xifrada l'identificador de la clau i la versió del format ({"v":1,"kek":"km0-comandes-2026",...}). Sense això, la primera rotació de clau és una migració a cegues.
  • Consell: prova la recuperació: un backup xifrat que mai no s'ha restaurat amb la clau d'un altre entorn és un backup que no existeix.

Exercicis

Exercici 1: Un esdeveniment manipulat

Un procés desconegut dins de la xarxa de Quilòmetre Zero publica a comandes.esdeveniments un esdeveniment comanda.creada per a P-2026-000126 amb una línia de 200 unitats de vi-crianca a 0,01 €. Explica què passa a inventari i analitica (a) amb el disseny de 02-04 sense canvis, (b) amb TLS a Kafka però sense HMAC, (c) amb l'HMAC de l'apartat 4. Després, indica què protegeix l'HMAC que TLS no protegeix, i viceversa, i per què l'HMAC no impedeix que el procés continuï publicant esdeveniments brossa al tòpic (quina lliçó ho resol).

Exercici 2: La Llúcia exerceix el seu dret de supressió

La Llúcia demana que s'esborrin les seves dades. Enumera on són (fes servir la taula de l'apartat 13) i, per a cada lloc, si es poden esborrar directament, si cal crypto-shredding, o si la dada ja no és personal. Explica què s'hauria d'haver decidit a l'apartat 10 perquè el crypto-shredding funcioni, i què queda a km0_analitica després.

Exercici 3: Rotar la KEK

L'equip de seguretat decideix rotar km0-comandes-2026 a km0-comandes-2027. Descriu pas a pas, fent servir les classes KMSLocal i XifradorCamps, què cal canviar perquè (1) les dades noves es xifrin amb la nova KEK, (2) les antigues es continuïn llegint, (3) les antigues es migrin a la nova sense rexifrar els telèfons, i (4) l'antiga es retiri. Quin camp del sobre fa possible el pas 2? Quants bytes cal rexifrar per registre al pas 3?

Solucions

Exercici 1.

(a) L'esdeveniment arriba, inventari el consumeix i reserva 200 unitats de vi-crianca de l'estoc del Celler Roure Alt (o esgota l'estoc i dispara inventari.alertes), analitica l'escriu al llac i vendes_diaries comptabilitza 2 € de vendes de vi; ningú no detecta res perquè l'esdeveniment és sintàcticament correcte. (b) TLS xifra el canal entre cada client i el broker i autentica el broker, però no el productor: el procés desconegut obre la seva pròpia connexió TLS vàlida i publica igualment; només protegeix contra algú que escolti o modifiqui el trànsit d'un altre client. (c) Tots dos consumidors calculen HMAC(clau, embolcall canònic), no coincideix amb el camp signatura (l'atacant no té la clau) i descarten l'esdeveniment amb una alerta; l'estoc no es toca i el llac no el rep. L'HMAC protegeix la integritat i l'autenticitat del missatge d'extrem a extrem, fins i tot davant del broker, d'un reproductor des del llac o d'un productor no autoritzat; TLS protegeix la connexió (confidencialitat i que el broker és l'autèntic), cosa que l'HMAC no fa (l'esdeveniment continua sent llegible al tòpic). L'HMAC no impedeix publicar perquè Kafka no exigeix que el productor s'identifiqui: això requereix autenticació del client al broker (SASL o certificats de client, és a dir, mTLS i ACL), que és la identitat de càrrega de treball de 06-04.

Exercici 2.

identitat (compte, hash de contrasenya): esborrat directe. km0_comandes: línies i imports vinculats a client_id: es poden esborrar o dissociar (substituir client_id per un marcador), segons el que exigeixi l'obligació fiscal de conservar factures; telèfon i adreça xifrats: crypto-shredding destruint la DEK de la Llúcia, cosa que també invalida les còpies a les rèpliques i als backups de km0-backups, on l'esborrat directe és impracticable. km0-factures: les factures tenen retenció legal d'anys amb object lock; no s'esborren, es justifica la conservació per obligació legal (i les seves dades de contacte poden estar xifrades amb la mateixa DEK). Kafka comandes.esdeveniments: els esdeveniments amb dades directes dins de la retenció no es poden editar; si la retenció és curta caduquen sols; si és llarga, és un motiu més perquè els esdeveniments portin els camps personals xifrats amb la DEK del client o pseudonimitzats. Llac HDFS: només conté el pseudònim HMAC; continua sent una dada personal mentre identitat conservi la clau i el client_id; en esborrar el compte a identitat, el pseudònim deixa de ser vinculable i es pot argumentar que queda anonimitzat (a revisar amb el professional de protecció de dades). km0_analitica: agregats anònims, no es toca. La decisió necessària a l'apartat 10 és una DEK per client (no per registre), desada xifrada per la KEK en un lloc central: destruir aquesta única DEK és l'esborrat.

Exercici 3.

(1) Afegir "km0-comandes-2027": nova_kek al diccionari de KMSLocal i posar kek_activa = "km0-comandes-2027": xifrar_dek farà servir la nova per a tot sobre nou. (2) Res més: desxifrar_dek rep s["kek"] del sobre i cerca aquesta KEK al diccionari, que continua contenint la 2026. El camp "kek" del sobre és el que ho fa possible. (3) Un procés de migració que, per a cada registre, fa dek = kms.desxifrar_dek(s["kek"], s["dek"]) i s["kek"], s["dek"] = kms.xifrar_dek(dek), i desa el sobre; el telèfon xifrat ("c") i el seu nonce no es toquen. Es rexifren 32 bytes (la DEK) més els 12 del nonce i 16 del tag del sobre de la DEK: uns 60 bytes per registre, davant de rexifrar el camp sencer i, sobretot, sense necessitat que el procés de migració vegi els telèfons en clar. (4) Quan cap sobre a la base de dades ni als backups que es vulguin mantenir llegibles no tingui "kek": "km0-comandes-2026", eliminar l'entrada del diccionari (a Vault: deshabilitar la versió). Qualsevol sobre oblidat donarà KeyError, que és exactament el comportament del crypto-shredding: per això es verifica amb una consulta abans de retirar-la.

Conclusió

Protegir una dada és garantir tres coses diferents, confidencialitat, integritat i autenticitat, i cadascuna té la seva eina: AES-GCM per xifrar volum amb nonce únic i etiqueta d'autenticació; RSA i corbes el·líptiques per acordar claus i signar, sempre en mode híbrid; hashes per a empremtes, HMAC per a integritat amb secret compartit, signatures per a autoria verificable per tothom. TLS les reuneix totes per al trànsit, amb certificats, cadena de confiança, SAN i TLS 1.3, i Quilòmetre Zero el té ara entre comandes i inventari amb una CA interna, i localitzat a la configuració de PostgreSQL, Kafka, Redis, Cassandra, MinIO i HDFS. En repòs, el xifratge s'acumula per capes (volum, base de dades, camp) i l'envelope encryption amb una KEK en un KMS fa possible rotar claus, revocar accessos i esborrar per destrucció de clau. Les dades personals de l'Anna, el Marc i la Llúcia es minimitzen, es pseudonimitzen abans d'entrar al llac, s'agreguen per a l'analítica, i es xifren amb una DEK per client que converteix el dret de supressió en una operació de 32 bytes. Les dades de targeta, senzillament, no hi entren. I tot això sota la revisió d'un professional de compliment normatiu, perquè el RGPD i PCI DSS són força més que criptografia.

Queda una peça que aquesta lliçó ha donat per resolta amb un fitxer .pub copiat a mà i un formulari de login propi: d'on surten les identitats. Els clients es registren a la web, els empleats tenen comptes corporatius, els productors volen entrar amb el seu compte de Google, l'app de repartidors necessita un flux sense contrasenya visible, i comandes necessita un token de màquina per parlar amb inventari. Gestionar tot això servei per servei és inviable. La següent lliçó centralitza la identitat en un proveïdor (Keycloak), connecta els empleats amb LDAP, i explica els protocols que fan que un token emès allà valgui a tota la plataforma: SAML, OAuth 2.0 i OpenID Connect.

Curs d'Arquitectures Distribuïdes

Mòdul 1: Introducció als Sistemes Distribuïts

Mòdul 2: Comunicació en Sistemes Distribuïts

Mòdul 3: Consistència i Replicació

Mòdul 4: Emmagatzematge Distribuït

Mòdul 5: Computació Distribuïda

Mòdul 6: Seguretat en Sistemes Distribuïts

Mòdul 7: Monitoratge i Manteniment

Mòdul 8: Casos d'Estudi i Aplicacions

© Copyright 2026. Tots els drets reservats