Les quatre lliçons anteriors han assegurat l'interior de Quilòmetre Zero: tokens verificables, canals xifrats, identitats centralitzades, mTLS entre serveis i secrets a Vault. Però la plataforma té un exterior. Des d'Internet arriben els navegadors de l'Anna, el Marc i la Llúcia, l'app dels 140 repartidors de furgoneta-3, els panells dels productors, i també els bots que rastregen preus, els scripts que proven contrasenyes robades, i les 40.000 peticions per segon de la primera hora de la Setmana de la Verema. Fins ara, cada servei exposat (cataleg, comandes, repartiment) feia la seva pròpia terminació TLS, la seva pròpia verificació de tokens, la seva pròpia defensa contra abusos, i cap no deixava registre fiable de qui havia fet què.

Aquesta lliçó construeix la vora: una passarel·la (API gateway) com a punt únic d'entrada que termina TLS, valida els tokens de 06-01 i 06-03, encamina cap a cada servei, versiona les APIs i frena els abusos; la limitació de taxa amb els seus algorismes i una implementació distribuïda a Redis; les proteccions addicionals de la vora (validació d'esquemes, mides, timeouts, WAF); i l'auditoria: què registrar, com fer-la immutable amb hash encadenat i emmagatzematge amb bloqueig d'objectes, i com respondre a "qui ha consultat la comanda de l'Anna?". Acaba amb la idea de la seguretat com a procés (modelatge d'amenaces STRIDE aplicat a comandes, gestió de vulnerabilitats) i tanca el mòdul amb el mapa complet de qui pot fer què a Quilòmetre Zero. El monitoratge de mètriques, els logs operatius i els patrons interns de resiliència són el Mòdul 7.

Contingut

  1. La vora de la plataforma: gateway contra balancejador
  2. Què fa un API gateway
  3. Opcions: Kong, Envoy Gateway, NGINX, gateways gestionats; BFF
  4. Limitació de taxa: per què i contra què
  5. Algorismes: token bucket, leaky bucket, finestra fixa, finestra lliscant
  6. Token bucket distribuït a Redis amb Lua
  7. Capçaleres, límits per identitat i quotes
  8. Protecció addicional a la vora
  9. Auditoria: què registrar i per què no és un log més
  10. Logs d'auditoria immutables: hash encadenat i object lock
  11. Auditoria d'accés a dades personals i detecció d'anomalies
  12. Seguretat com a procés: STRIDE, vulnerabilitats i dependències
  13. Quilòmetre Zero: Kong a docker-compose.yml, limitador.py i auditoria.py
  14. Errors Comuns i Consells
  15. Exercicis
  16. Conclusió

  1. La vora de la plataforma: gateway contra balancejador

Un balancejador de càrrega (L4 o L7) reparteix connexions entre instàncies d'un mateix servei i, com a molt, termina TLS. No sap què és un JWT, no distingeix l'Anna d'un bot, i no pot dir "aquesta ruta va a comandes i aquesta altra a cataleg v2". Al monòlit n'hi havia prou; amb sis serveis exposats, cadascun hauria de repetir la mateixa dotzena de responsabilitats transversals, i els clients haurien de conèixer sis adreces.

Un API gateway és un proxy invers especialitzat que es col·loca davant de tots els serveis i concentra aquestes responsabilitats:

flowchart LR
    subgraph Internet
        N[Navegador de l'Anna]
        R[App repartidors]
        B[Bots / abusos]
    end
    N & R & B -->|HTTPS| G[API Gateway<br/>TLS · JWT · rutes · rate limit<br/>CORS · esquemes · auditoria]
    G -->|mTLS| C[cataleg]
    G -->|mTLS| P[comandes]
    G -->|mTLS| RP[repartiment]
    G -.->|auditoria.esdeveniments| K[(Kafka)]
    G -.->|comptadors| RD[(Redis)]
    P -->|mTLS| I[inventari]
    style B stroke-dasharray: 5 5

Res de fora no arriba a un servei sense passar pel gateway; els serveis només accepten connexions mTLS del gateway i d'altres serveis (06-04). I el gateway no substitueix la seguretat de cada servei: és la primera barrera, no l'única. inventari continua verificant el token (06-01) perquè la crida que li arriba de comandes no ha passat per la vora.

  1. Què fa un API gateway

Responsabilitat Què vol dir Sense gateway
Terminació TLS Un sol lloc amb el certificat públic d'api.km0.example, renovat per Let's Encrypt/ACME; cap a dins, mTLS amb la CA interna Un certificat públic per servei
Autenticació Verifica el JWT (RS256 contra el JWKS de Keycloak, 06-03) i rebutja a la vora el que és invàlid; opcionalment introspecció de tokens opacs Cada servei ho fa, i el que és invàlid consumeix recursos interns
Encaminament /api/cataleg/*cataleg, /api/comandes/*comandes; per ruta, mètode, capçalera, versió Els clients coneixen sis hosts
Transformació Afegir capçaleres (X-Request-Id, identitat verificada), treure capçaleres internes de les respostes, adaptar formats Repetit
Versionat d'APIs /api/v1/comandes i /api/v2/comandes a serveis o rutes diferents; retirada gradual Trencaments per a clients antics
CORS Capçaleres Access-Control-* perquè el navegador de km0.example pugui cridar api.km0.example Configuració per servei, sovint * per descuit
Protecció contra abusos Rate limiting, quotes, mida màxima, timeouts, bloqueig per IP El servei se satura abans de poder defensar-se
Observabilitat Un punt pel qual passa tot el trànsit extern: mètriques (07-01), traces (07-02), auditoria Fragmentada
Memòria cau de respostes Per a GET públics del catàleg, complementa Redis (04-05)

El que no hauria de fer: lògica de negoci, agregació complexa de diversos serveis (aquest és el paper del BFF, apartat 3), ni autorització fina (l'ABAC de "els seus productes" necessita dades que només té el servei).

  1. Opcions: Kong, Envoy Gateway, NGINX, gateways gestionats; BFF

Opció Naturalesa Configuració Punts forts A tenir en compte
Kong Gateway Proxy sobre NGINX/OpenResty amb plugins (Lua, Go, Python) Declarativa (YAML, mode sense base de dades) o API d'administració Ecosistema de plugins (JWT, OIDC, rate limiting, auditoria), madur, versió oberta completa Plugins avançats a l'edició comercial
Envoy Gateway / Envoy Proxy L7 d'alt rendiment (C++), base d'Istio Kubernetes Gateway API o xDS Rendiment, filtres (JWT, ext_authz per a OPA, rate limit global), el mateix Envoy que la malla Configuració verbosa fora de Kubernetes
NGINX / OpenResty Servidor web i proxy nginx.conf Ubic, ràpid, simple per encaminar i terminar TLS JWT i rate limiting distribuït requereixen mòduls comercials o Lua propi
Traefik, KrakenD, Tyk, APISIX Gateways de codi obert Diversa Traefik: descobriment automàtic de contenidors; KrakenD: agregació; APISIX: rendiment
Gestionats (AWS API Gateway, Google Apigee/Cloud Endpoints, Azure API Management) Servei de núvol Consola/IaC Sense operar; integració amb IAM, WAF, quotes i facturació per ús Cost per petició, dependència del proveïdor (08-03)

BFF (Backend for Frontend): quan cada tipus de client (web, app mòbil, panell de productors) necessita agregacions diferents (la pantalla d'inici de l'app combina catàleg, comandes en curs i posició del repartidor), es col·loca un servei per client que compon aquestes respostes, darrere del gateway. El gateway continua sent transversal; el BFF és específic. Quilòmetre Zero, en aquesta lliçó, fa servir Kong sense BFF; el BFF de l'app de repartidors apareixerà al projecte final (08-05).

  1. Limitació de taxa: per què i contra què

La limitació de taxa (rate limiting) acota quantes peticions pot fer un client en un interval. Protegeix contra:

  • Abús deliberat: força bruta contra /login (milers de contrasenyes per minut), scraping del catàleg complet cada minut, denegació de servei amb peticions vàlides.
  • Errors de clients: una app amb un bucle de reintents sense backoff (07-04) que, després d'una fallada, dispara mil peticions per segon des de cada dispositiu.
  • Sobrecàrrega en pics legítims: la Setmana de la Verema omple la web; sense límit, comandes se satura, totes les peticions fallen, i ningú no compra. Amb límit, el 95 % compra i el 5 % rep un "torna-ho a provar d'aquí a uns segons".
  • Equitat: que un integrador que consumeix l'API no monopolitzi cataleg a costa de la resta.
  • Cost: si darrere hi ha crides a la passarel·la de pagament o a un proveïdor de mapes facturats per ús.

S'aplica a la vora (per client, token o IP, abans de tocar un servei) i de vegades també a dins (un servei que es protegeix d'un altre, cosa que a 07-04 s'anomenarà bulkhead). I es distingeix de la quota: el límit de taxa és a curt termini (100 per minut: suavitzar), la quota a llarg termini (10.000 al dia: contracte).

  1. Algorismes: token bucket, leaky bucket, finestra fixa, finestra lliscant

Algorisme Com funciona Ràfegues Memòria per clau Precisió Ús típic
Finestra fixa Un comptador per clau i per interval (10:04 → 37); es reinicia en canviar el minut Permet el doble del límit al canvi de finestra (100 a les 10:04:59 + 100 a les 10:05:00) 1 enter Baixa Quotes diàries, on la vora de finestra no importa
Finestra lliscant (log) Guarda la marca de temps de cada petició; compta les dels últims 60 s Exacte, sense efecte vora Una entrada per petició (car) Alta Límits baixos i estrictes (login)
Finestra lliscant (comptador ponderat) Comptador de la finestra actual + comptador de l'anterior ponderat per la fracció transcorreguda Aproxima bé, sense el pic de la vora 2 enters Mitjana-alta El més usat als gateways (Kong, Cloudflare)
Token bucket Un cubell de capacitat C que s'omple a r tokens/s; cada petició en consumeix un; sense tokens, rebuig Permet ràfegues de fins a C i després el ritme sostingut r 2 valors (tokens, última recàrrega) Alta APIs: tolera ràfegues naturals (carregar una pàgina llança 20 peticions)
Leaky bucket Una cua de capacitat C que es buida a r/s; la petició entra si hi ha lloc i se serveix a ritme constant Suavitza: la sortida és sempre r, les ràfegues esperen o es descarten Cua Alta Modelar trànsit cap a un servei que no tolera pics (la passarel·la de pagament)

Token bucket i leaky bucket són duals: el primer limita l'admissió permetent ràfegues, el segon limita la sortida eliminant-les. Per a una API pública, token bucket és la tria habitual: l'Anna carrega la pàgina d'inici (20 peticions en un segon, que caben al cubell) i després navega a un ritme baix que el degoteig de recàrrega cobreix de sobres; un bot que en fa 50 per segon esgota el cubell en un instant i queda limitat a r.

flowchart LR
    R[Recàrrega: r tokens/s] --> B[(Cubell<br/>capacitat C)]
    P[Petició] -->|hi ha token?| B
    B -->|sí: en consumeix 1| OK[200 → servei]
    B -->|no| KO[429 Too Many Requests<br/>Retry-After]

  1. Token bucket distribuït a Redis amb Lua

El gateway té diverses instàncies; l'estat del cubell ha de ser compartit, i l'operació "llegir tokens, recarregar segons el temps transcorregut, decidir, escriure" ha de ser atòmica, o dues instàncies concedirien el mateix últim token. Redis (04-05) resol totes dues coses: l'estat viu allà, i un script Lua s'executa atòmicament al servidor.

# km0/serveis/vora/limitador.py
import time, redis

# L'script s'executa sencer a Redis sense que cap altra ordre s'hi intercali:
# llegir, recarregar, decidir i escriure són una sola operació.
_LUA_TOKEN_BUCKET = """
local clau       = KEYS[1]
local capacitat  = tonumber(ARGV[1])
local taxa       = tonumber(ARGV[2])   -- tokens per segon
local ara        = tonumber(ARGV[3])   -- segons amb decimals (el passa el client perquè
                                       -- l'script sigui determinista i replicable)
local cost       = tonumber(ARGV[4])   -- tokens que consumeix aquesta petició (1 normalment)

local estat  = redis.call('HMGET', clau, 'tokens', 'ts')
local tokens = tonumber(estat[1])
local ts     = tonumber(estat[2])
if tokens == nil then tokens = capacitat; ts = ara end     -- primer cop: cubell ple

-- Recàrrega proporcional al temps transcorregut, sense superar la capacitat
tokens = math.min(capacitat, tokens + (ara - ts) * taxa)

local permes = 0
if tokens >= cost then
    tokens = tokens - cost
    permes = 1
end

redis.call('HSET', clau, 'tokens', tokens, 'ts', ara)
-- El cubell s'omple sol en capacitat/taxa segons: després d'això, la clau sobra
redis.call('EXPIRE', clau, math.ceil(capacitat / taxa) + 1)

-- Segons fins que hi haurà 'cost' tokens (per a Retry-After); 0 si permès
local espera = 0
if permes == 0 then espera = math.ceil((cost - tokens) / taxa) end
return {permes, math.floor(tokens), espera}
"""

class LimitadorTaxa:
    def __init__(self, r: redis.Redis, capacitat: int, taxa_per_s: float, prefix="rl"):
        self._r = r
        self._script = r.register_script(_LUA_TOKEN_BUCKET)   # es carrega un cop (EVALSHA després)
        self.capacitat, self.taxa, self.prefix = capacitat, taxa_per_s, prefix

    def permetre(self, identitat: str, cost: int = 1) -> tuple[bool, int, int]:
        """Retorna (permes, tokens_restants, segons_espera)."""
        permes, restants, espera = self._script(
            keys=[f"{self.prefix}:{identitat}"],
            args=[self.capacitat, self.taxa, time.time(), cost])
        return bool(permes), int(restants), int(espera)


if __name__ == "__main__":
    r = redis.Redis(host="localhost", port=6379)
    lim = LimitadorTaxa(r, capacitat=20, taxa_per_s=5)      # ràfega de 20, després 5/s sostingut
    ok = ko = 0
    for i in range(60):                                     # 60 peticions tan ràpid com sigui possible
        permes, restants, espera = lim.permetre("u-anna")
        ok += permes; ko += not permes
    print(f"permeses={ok} rebutjades={ko}")                 # permeses=20 rebutjades=40 (aprox.)
    time.sleep(2)
    print(lim.permetre("u-anna"))                           # (True, 9, 0): 2 s × 5/s = 10 tokens recarregats

Detalls que importen:

  • time.time() el passa el client, no redis.call('TIME'): els scripts Lua han de ser deterministes perquè la replicació de Redis els reprodueixi; i un desfasament de rellotge entre instàncies del gateway (01-05) només introdueix un error de mil·lisegons a la recàrrega, irrellevant aquí.
  • Una clau per identitat (rl:u-anna) amb EXPIRE: els clients inactius no ocupen memòria.
  • cost permet que les operacions cares (una cerca de text al catàleg) consumeixin més tokens que un GET simple.
  • A Redis Cluster (04-05), cada clau s'encamina al seu node per hash slot; l'script només toca una clau, així que funciona sense canvis.
  • Cost: un viatge a Redis per petició a la vora (~0,3 ms a la mateixa xarxa). Per a pics extrems, es fa limitació local aproximada a cada instància del gateway (un cubell en memòria amb C/n) i se sincronitza amb Redis de manera asíncrona; és el que trien les polítiques (policy) local/redis/cluster del plugin de limitació de Kong.

Com a middleware en un servei Flask (per a comandes, que a més de la protecció del gateway vol un límit propi més estricte a POST /comandes):

# km0/serveis/comandes/api.py (fragment)
from flask import Flask, request, g, jsonify
from serveis.vora.limitador import LimitadorTaxa
import redis

app = Flask(__name__)
lim_crear = LimitadorTaxa(redis.Redis(host="redis"), capacitat=5, taxa_per_s=0.1, prefix="rl:crear")

@app.before_request
def limitar_creacio():
    if request.method == "POST" and request.path == "/comandes":
        subjecte = getattr(g, "claims", {}).get("sub") or request.remote_addr    # per usuari; si no, per IP
        permes, restants, espera = lim_crear.permetre(subjecte)
        if not permes:
            resp = jsonify(error="massa peticions", reintentar_en_s=espera)
            resp.status_code = 429
            resp.headers["Retry-After"] = str(espera)
            resp.headers["RateLimit-Limit"] = "5"
            resp.headers["RateLimit-Remaining"] = "0"
            resp.headers["RateLimit-Reset"] = str(espera)
            return resp
        g.rl_restants = restants

@app.after_request
def capcaleres_rl(resp):
    if hasattr(g, "rl_restants"):
        resp.headers["RateLimit-Limit"] = "5"
        resp.headers["RateLimit-Remaining"] = str(g.rl_restants)
    return resp

Cinc comandes de cop i després una cada deu segons: ningú no crea comandes legítimes més de pressa, i un script que intenti centenars de compres amb targetes robades s'atura a la cinquena.

  1. Capçaleres, límits per identitat i quotes

  • 429 Too Many Requests és el codi; Retry-After (segons o data) diu al client quan tornar, i un client ben fet ho respecta (07-04 tractarà els reintents amb backoff del costat client).
  • RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset (esborrany IETF, ja d'ús comú; Kong i altres fan servir variants X-RateLimit-*) informen a cada resposta, no només al 429, perquè el client s'autoreguli abans de topar-hi.
  • Per quina identitat limitar, de millor a pitjor: per sub del token (l'Anna, o svc-integrador-x) → per client_id d'OAuth (tota l'app de repartidors com a conjunt, útil per protegir-se d'un bug de l'app) → per clau d'API → per IP (l'únic possible abans d'autenticar-se, a /login; imprecisa per NAT: una oficina sencera comparteix IP; i evitable amb proxies). Combinar: /login per IP i per compte destí; la resta per sub.
  • Límits diferents per ruta i rol: GET /api/cataleg generós (200/min) i emmagatzemable a la memòria cau; POST /api/comandes estricte; operadors amb límits més grans; integradors amb quotes contractuals (100.000/dia) que es comptabilitzen amb finestra fixa diària a més del token bucket.
  • Què retornar quan Redis no respon: fail open (permetre-ho tot, i alertar) sol ser el correcte a la vora d'un marketplace (millor vendre sense límit uns minuts que no vendre); fail closed a /login i a les operacions de pagament.

  1. Protecció addicional a la vora

Protecció Què fa A Quilòmetre Zero
Validació d'esquema (OpenAPI) El gateway (o el servei) rebutja cossos que no compleixen el contracte: tipus, camps obligatoris, rangs, longituds contractes/comandes.openapi.yaml; una comanda amb quantitat: -5 o un producte de 10.000 caràcters mor a la vora
Mida màxima de cos Rebutja peticions més grans de N KB/MB abans de llegir-les 64 KB per a l'API; les fotos van per URL presignat a MinIO (04-03), no pel gateway
Timeouts a la vora Temps màxim de connexió, de lectura i total cap a cada servei 5 s a cataleg, 10 s a comandes; evita que connexions lentes ocupin el gateway (slowloris). Els timeouts interns, reintents i circuit breakers són 07-04
Límit de connexions concurrents per client i per servei Complementa el rate limit (peticions lentes i moltes alhora) 50 per IP
WAF (web application firewall: ModSecurity amb l'OWASP Core Rule Set, Cloudflare, AWS WAF) Regles contra patrons d'atac: injecció SQL, XSS, rutes d'escàner, agents maliciosos Davant del gateway, gestionat per la CDN/núvol; només s'anomena: la seva afinació és un ofici propi
Capçaleres de seguretat a les respostes Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options Afegides pel gateway a tota resposta
Bloqueig per reputació / geografia Llistes d'IPs, països, ASN Bloquejar rangs amb abús reiterat
Protecció de bots Desafiaments, empremtes de navegador Servei de la CDN; s'anomena

La validació d'entrada és la més important i la que menys es fa: la majoria de vulnerabilitats d'aplicació comencen per una entrada que ningú no ha validat. L'esquema OpenAPI, versionat a contractes/ al costat dels .proto de 02-03, és a l'HTTP públic el que protobuf és a l'interior: el contracte com a font de veritat, i el gateway com qui el fa complir.

  1. Auditoria: què registrar i per què no és un log més

Un log d'auditoria respon, mesos després i davant d'un auditor, un jutge o un client, a la pregunta: qui ha fet què, quan, des d'on i amb quin resultat? No és el mateix que els logs operatius (07-02), encara que tècnicament tots dos siguin línies amb marca de temps:

Log operatiu (07-02) Log d'auditoria
Propòsit Depurar, entendre el comportament del sistema Retre comptes, investigar incidents, complir obligacions
Contingut El que el programador va considerar útil: stack traces, temps, variables Un esquema fix: subjecte, acció, recurs, resultat, context
Volum Alt; es mostreja i es descarta Tot esdeveniment rellevant, sense mostreig
Retenció Dies o setmanes Anys (segons obligació: fiscal, RGPD, PCI DSS)
Mutabilitat Es roten, s'esborren, s'editen sense drama Immutable: ningú, ni un administrador, no pot alterar-lo ni esborrar-lo
Accés Tot l'equip Restringit: seguretat, compliment; accés al seu torn auditat
Exemple WARN comandes: reintent 2/3 a inventari (deadline 1.8s) 2026-09-15T10:04:12Z sub=u-marc jti=7f4… accio=comandes.llegir recurs=P-2026-000123 resultat=DENEGAT ip=…

Què registrar (cada esdeveniment, amb esquema fix):

  • Qui: sub del JWT i jti (per correlacionar amb el token concret), client_id, rols en aquell moment; per als serveis, el SPIFFE ID (06-04).
  • Què: acció (comandes.crear, comandes.llegir, productes.editar, login.fallit, token.revocat, secret.llegit) i recurs (P-2026-000123, formatge-curat, kv/km0/comandes/keycloak).
  • Quan: marca de temps del gateway/servei en UTC (01-05 explica per què es registra també l'origen del rellotge).
  • Des d'on: IP d'origen, User-Agent, servei intermedi, X-Request-Id.
  • Resultat: PERMES/DENEGAT/ERROR, amb el motiu de la denegació.
  • Mai: contrasenyes, tokens complets, dades personals que no siguin imprescindibles (la comanda P-2026-000123 sí; el telèfon de l'Anna no).

Quins esdeveniments: tota autenticació (èxit i fallada), tota denegació d'autorització, tot accés a dades personals (apartat 11), tota operació administrativa (canvis de rol, aprovació de productors, canvis de configuració del gateway), tot accés a secrets (Vault ja ho fa), i les operacions de negoci sensibles (cancel·lacions, reemborsaments, ajustos d'estoc).

  1. Logs d'auditoria immutables: hash encadenat i object lock

Un log d'auditoria que l'atacant (o un administrador) pot editar no val res: la primera acció d'un intrús competent és esborrar les seves empremtes. Tres capes d'immutabilitat:

  1. Append-only a l'origen: el servei publica cada esdeveniment al tòpic Kafka auditoria.esdeveniments (02-04) i no l'escriu enlloc que pugui editar. Kafka és un log de només annexió; amb ACL (06-04), els serveis només tenen permís d'escriptura en aquest tòpic, no de lectura ni d'administració.
  2. Hash encadenat: cada registre inclou el hash del registre anterior. Modificar-ne o eliminar-ne un trenca la cadena a partir d'ell, i un verificador ho detecta. És l'estructura d'una blockchain sense res de la resta: un prev_hash per registre i, periòdicament, una àncora (el hash de l'últim registre del dia, signat i publicat en un lloc extern: un tòpic diferent, un notari de marques de temps, un tercer) perquè ni tan sols reescriure tota la cadena des del principi passi desapercebut.
  3. Emmagatzematge amb bloqueig d'objectes: un consumidor dedicat escriu els esdeveniments en lots (una hora, un dia) al bucket km0-auditoria de MinIO amb object lock en mode compliance (04-03): ni el propietari del bucket ni l'administrador no poden esborrar ni sobreescriure l'objecte fins que expiri la retenció (5 anys, per exemple). Juntament amb SSE-KMS (06-02) i un lease d'accés de només lectura per a l'equip de seguretat.
flowchart LR
    S[Servei / gateway] -->|esdeveniment signat, prev_hash| K[Kafka: auditoria.esdeveniments<br/>ACL: només escriptura]
    K --> C[Consumidor auditoria<br/>verifica la cadena]
    C -->|lots per hora| M[(MinIO km0-auditoria<br/>object lock 5 anys, SSE-KMS)]
    C -->|àncora diària signada| A[Àncora externa]
    C --> Q[(Índex de consulta<br/>només lectura, accés auditat)]

  1. Auditoria d'accés a dades personals i detecció d'anomalies

El RGPD exigeix poder demostrar qui ha accedit a les dades d'una persona. "Qui ha consultat la comanda de l'Anna?" s'ha de respondre amb una consulta a l'índex d'auditoria:

sub              accio             recurs           resultat   quan                   des_de
u-anna           comandes.llegir   P-2026-000123    PERMES     2026-09-14T18:02:11Z   web-km0 / 83.x.x.x
mpuig            comandes.llegir   P-2026-000123    PERMES     2026-09-15T09:41:03Z   panell-operadors / oficina
u-marc           comandes.llegir   P-2026-000123    DENEGAT    2026-09-15T10:04:12Z   web-km0 / 90.x.x.x
svc-repartiment  comandes.llegir   P-2026-000123    PERMES     2026-09-15T11:20:45Z   spiffe://km0.internal/repartiment

La Marta (operadora) va llegir la comanda a les 9:41: legítim si hi havia un tiquet de suport obert (i el registre hauria de portar el ticket_id com a context); el Marc va intentar llegir-la i va ser denegat (l'ABAC de 06-01 funcionant); repartiment la va llegir per assignar repartidor. La correlació amb la identitat es fa per sub i, quan importa distingir sessions, per jti: si el token de l'Anna va ser robat (exercici 1 de 06-01), tots els accessos amb aquest jti des d'una IP diferent són l'empremta de l'atacant.

Sobre aquest registre es construeix la detecció d'anomalies bàsica, sense aprenentatge automàtic: regles sobre finestres de temps que produeixen alertes (07-01 les portarà al sistema d'alertes general):

Regla Senyal de
> 10 login.fallit per al mateix compte en 5 min, o > 100 des de la mateixa IP Força bruta, credential stuffing
Un sub amb rol operador llegeix > 200 comandes diferents en una hora Exfiltració, curiositat indeguda
Un jti usat des de dos països en 10 minuts Token robat
DENEGAT repetit del mateix sub sobre recursos d'altres usuaris Enumeració d'ids (P-2026-000123, 124, 125...)
Accés a Vault fora de l'horari de desplegament, o des d'una identitat nova Compromís d'un servei
Un productor ajusta l'estoc de > 50 productes en un minut Compte de productor compromès

  1. Seguretat com a procés: STRIDE, vulnerabilitats i dependències

Cap de les peces d'aquest mòdul no és definitiva: es descobreixen vulnerabilitats, canvien els serveis, s'afegeixen rutes. La seguretat és un procés, amb tres pràctiques mínimes:

Modelatge d'amenaces lleuger. Abans de dissenyar o canviar un servei, seure mitja hora amb el seu diagrama i preguntar-se, per cada flux i component, què podria sortir malament. STRIDE és la llista de comprovació clàssica. Aplicada a comandes:

Amenaça Significat Exemple a comandes Contramesura Lliçó
Spoofing Suplantar una identitat Un procés es fa passar per pagaments per marcar P-2026-000125 com a pagada mTLS + política servei a servei; JWT verificat per a usuaris 06-01, 06-04
Tampering Alterar dades Canviar l'import d'una comanda en trànsit; modificar un esdeveniment a comandes.esdeveniments TLS; HMAC d'esdeveniments; validació d'esquema; l'import es recalcula al servidor 06-02, 06-05
Repudiation Negar haver fet alguna cosa L'Anna diu que mai no va fer la comanda; un operador nega haver cancel·lat Auditoria immutable amb sub/jti; signatura d'accions crítiques 06-05
Information disclosure Revelar informació El Marc llegeix la comanda de l'Anna; el telèfon apareix en un log; un error mostra un stack trace amb la cadena de connexió ABAC; xifratge de camp; pseudonimització; missatges d'error genèrics; mai secrets als logs 06-01, 06-02, 06-04
Denial of service Impedir el servei 40.000 comandes per segon falses a la Setmana de la Verema; cossos de 100 MB Rate limiting; mida màxima; timeouts; quotes; (07-04 per a la resiliència interna) 06-05
Elevation of privilege Obtenir més permisos Un client s'afegeix operador al token; una injecció en un paràmetre de cerca executa SQL Signatura RS256 i verificació estricta; consultes parametritzades; validació d'entrada; mínim privilegi a la BD (credencials dinàmiques amb només els GRANT necessaris) 06-01, 06-04, 06-05

Gestió de vulnerabilitats i dependències. El codi de Quilòmetre Zero és una fracció del que executa: grpcio, PyJWT, cryptography, redis, Kong, Keycloak, Vault, Kafka, PostgreSQL, imatges base de Docker. Cadascun publica vulnerabilitats (CVE) amb regularitat. Procés mínim: fixar versions (requirements.txt amb hashes, imatges per digest), un escàner de dependències a CI (pip-audit, Dependabot/Renovate per a les actualitzacions, Trivy o Grype per a les imatges), una finestra de pedaçat definida per severitat (crític: 48 h), i un inventari de què corre on (SBOM). I revisió: de codi per a canvis en autenticació i autorització, de configuració del gateway i de Vault, i proves de penetració periòdiques per part d'algú extern a l'equip.

  1. Quilòmetre Zero: Kong a docker-compose.yml, limitador.py i auditoria.py

Kong com a gateway, en mode declaratiu

Kong en mode DB-less llegeix tota la seva configuració d'un fitxer YAML: rutes, serveis, plugins i consumidors. És versionable i revisable, com el realm de Keycloak.

# km0/docker-compose.yml (fragment)
  kong:
    image: kong:3.8
    environment:
      KONG_DATABASE: "off"
      KONG_DECLARATIVE_CONFIG: /kong/kong.yaml
      KONG_PROXY_LISTEN: "0.0.0.0:8443 ssl"
      KONG_SSL_CERT: /certs/api.km0.example.crt      # certificat públic (ACME en producció)
      KONG_SSL_CERT_KEY: /certs/api.km0.example.key
      KONG_ADMIN_LISTEN: "127.0.0.1:8001"            # l'API d'administració MAI exposada
      KONG_LUA_SSL_TRUSTED_CERTIFICATE: /certs/km0-ca.crt   # per verificar els serveis (TLS intern)
      KONG_CLIENT_SSL: "on"                          # mTLS cap als serveis (06-04)
      KONG_CLIENT_SSL_CERT: /certs/kong.crt
      KONG_CLIENT_SSL_CERT_KEY: /certs/kong.key
    volumes:
      - ./vora/kong.yaml:/kong/kong.yaml:ro
      - ./certs:/certs:ro
    ports: [ "8443:8443" ]
    depends_on: [ redis, cataleg, comandes ]
# km0/vora/kong.yaml
_format_version: "3.0"

services:
  - name: cataleg
    url: https://cataleg:8000
    connect_timeout: 2000
    read_timeout: 5000
    routes:
      - name: cataleg-v1
        paths: [ "/api/v1/cataleg" ]
        strip_path: true
    plugins:
      - name: rate-limiting
        config: { minute: 200, policy: redis, redis_host: redis, limit_by: consumer,
                  fault_tolerant: true, hide_client_headers: false }   # fail open
      - name: request-size-limiting
        config: { allowed_payload_size: 64, size_unit: kilobytes }

  - name: comandes
    url: https://comandes:8000
    connect_timeout: 2000
    read_timeout: 10000
    routes:
      - name: comandes-v1
        paths: [ "/api/v1/comandes" ]
        strip_path: true
    plugins:
      - name: jwt                               # rebutja a la vora el que no signi Keycloak
        config:
          key_claim_name: iss                   # cerca el consumidor pel claim 'iss'
          claims_to_verify: [ exp ]
          maximum_expiration: 900               # tokens de més de 15 min: rebutjats
      - name: rate-limiting
        config: { minute: 60, policy: redis, redis_host: redis, limit_by: consumer,
                  fault_tolerant: false }        # fail closed: comandes és sensible
      - name: request-size-limiting
        config: { allowed_payload_size: 64, size_unit: kilobytes }

consumers:
  - username: keycloak-km0
    jwt_secrets:
      - key: "http://localhost:8080/realms/km0"   # = claim 'iss' dels tokens
        algorithm: RS256
        rsa_public_key: |                          # clau pública del realm (o el plugin openid-connect amb JWKS)
          -----BEGIN PUBLIC KEY-----
          MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
          -----END PUBLIC KEY-----

plugins:                                          # globals
  - name: cors
    config: { origins: [ "https://km0.example" ], credentials: true, max_age: 3600 }
  - name: correlation-id
    config: { header_name: X-Request-Id, generator: uuid, echo_downstream: true }
  - name: response-transformer
    config:
      add:
        headers:
          - "Strict-Transport-Security: max-age=31536000; includeSubDomains"
          - "X-Content-Type-Options: nosniff"
      remove:
        headers: [ "Server", "X-Powered-By" ]
  - name: http-log                                # cada petició → consumidor d'auditoria (apartat següent)
    config: { http_endpoint: "http://auditoria:9000/vora", timeout: 1000, queue_size: 100 }

Amb això, https://api.km0.example/api/v1/cataleg/productes/formatge-curat arriba a cataleg sense token (catàleg públic) però limitat a 200 per minut; https://api.km0.example/api/v1/comandes/P-2026-000123 sense Authorization rep 401 del mateix Kong sense tocar comandes, i amb un token vàlid arriba a comandes amb X-Request-Id, que comandes propaga a inventari a les metadades gRPC (07-02 el farà servir per a les traces). El plugin jwt de l'edició oberta verifica signatura i exp; la verificació d'aud i de rols la continua fent requereix_rol a comandes (06-01), i per descobrir claus per JWKS i validar aud a la vora es faria servir el plugin openid-connect o el filtre JWT d'Envoy, que sí que ho fan. El principi no canvia: la vora filtra l'evident; el servei decideix.

serveis/vora/auditoria.py

El registre d'auditoria amb hash encadenat, usat tant pels serveis (una funció registrar) com pel consumidor que arxiva a MinIO:

# km0/serveis/vora/auditoria.py
import hashlib, json, time, uuid, threading
from datetime import datetime, timezone
from kafka import KafkaProducer            # 02-04
import boto3                               # 04-03

TOPIC = "auditoria.esdeveniments"

class RegistreAuditoria:
    """Emet esdeveniments d'auditoria amb hash encadenat a Kafka. Un emissor per procés;
    la cadena és per emissor (camp 'origen'), i el consumidor verifica cada cadena."""
    def __init__(self, origen: str, productor: KafkaProducer):
        self.origen, self._p = origen, productor
        self._prev = "0" * 64                          # gènesi d'aquesta cadena
        self._lock = threading.Lock()

    @staticmethod
    def _hash(ev: dict) -> str:
        canonic = json.dumps({k: v for k, v in ev.items() if k != "hash"},
                             sort_keys=True, separators=(",", ":")).encode()
        return hashlib.sha256(canonic).hexdigest()

    def registrar(self, sub: str, accio: str, recurs: str, resultat: str,
                  jti: str | None = None, des_de: str | None = None, context: dict | None = None):
        ev = {
            "id": str(uuid.uuid4()),
            "quan": datetime.now(timezone.utc).isoformat(timespec="milliseconds"),
            "origen": self.origen,                     # 'comandes', 'kong', 'spiffe://.../inventari'
            "sub": sub, "jti": jti, "accio": accio, "recurs": recurs,
            "resultat": resultat, "des_de": des_de,
            "context": context or {},                  # ticket_id, request_id... MAI dades personals ni tokens
        }
        with self._lock:                               # la cadena exigeix ordre: un esdeveniment cada cop
            ev["prev_hash"] = self._prev
            ev["hash"] = self._hash(ev)
            self._prev = ev["hash"]
            # Clau = origen: mateix origen → mateixa partició → ordre preservat (02-04)
            self._p.send(TOPIC, key=self.origen.encode(), value=json.dumps(ev).encode())
        return ev["id"]


def verificar_cadena(esdeveniments: list[dict]) -> tuple[bool, int | None]:
    """Recorre esdeveniments d'un mateix origen en ordre; retorna (ok, índex de la primera fallada)."""
    prev = "0" * 64
    for i, ev in enumerate(esdeveniments):
        if ev["prev_hash"] != prev or RegistreAuditoria._hash(ev) != ev["hash"]:
            return False, i
        prev = ev["hash"]
    return True, None


class ArxivadorAuditoria:
    """Consumidor: acumula esdeveniments per origen i hora, verifica la cadena i escriu a
    km0-auditoria amb object lock (retenció 5 anys, mode COMPLIANCE)."""
    def __init__(self, s3, bucket="km0-auditoria"):
        self._s3, self._bucket = s3, bucket
        self._lots: dict[tuple[str, str], list[dict]] = {}

    def acumular(self, ev: dict):
        hora = ev["quan"][:13]                         # '2026-09-15T10'
        self._lots.setdefault((ev["origen"], hora), []).append(ev)

    def tancar_lots_anteriors_a(self, hora_actual: str):
        for (origen, hora), evs in list(self._lots.items()):
            if hora >= hora_actual:
                continue
            ok, idx = verificar_cadena(evs)
            clau = f"{origen.replace('/', '_')}/{hora[:10]}/{hora[11:13]}.jsonl"
            cos = "\n".join(json.dumps(e) for e in evs).encode()
            self._s3.put_object(
                Bucket=self._bucket, Key=clau, Body=cos,
                ObjectLockMode="COMPLIANCE",
                ObjectLockRetainUntilDate=datetime.fromtimestamp(time.time() + 5 * 365 * 86400, timezone.utc),
                ServerSideEncryption="aws:kms",         # SSE-KMS (06-02)
                Metadata={"cadena_ok": str(ok), "primera_fallada": str(idx),
                          "ultim_hash": evs[-1]["hash"]})
            if not ok:
                print(f"[auditoria] ALERTA: cadena trencada a {origen} {hora} posició {idx}")
            del self._lots[(origen, hora)]

Ús a comandes, a la ruta que 06-01 va protegir amb ABAC:

# km0/serveis/comandes/api.py (fragment)
auditoria = RegistreAuditoria("comandes", KafkaProducer(bootstrap_servers="kafka:19092", acks="all"))

@app.get("/comandes/<comanda_id>")
@requereix_rol("client", "operador", audiencia="comandes")
def veure_comanda(comanda_id):
    comanda = repositori.carregar(comanda_id)
    permes = comanda is not None and ("operador" in g.claims["roles"] or comanda.client_id == g.claims["sub"])
    auditoria.registrar(sub=g.claims["sub"], jti=g.claims.get("jti"), accio="comandes.llegir",
                        recurs=comanda_id, resultat="PERMES" if permes else "DENEGAT",
                        des_de=request.headers.get("X-Forwarded-For", request.remote_addr),
                        context={"request_id": request.headers.get("X-Request-Id"),
                                 "ticket_id": request.headers.get("X-Ticket-Id")})
    if not permes:
        abort(403 if comanda is not None else 404)
    return comanda.a_json()

I el docker-compose.yml guanya el tòpic i el bucket, creats amb object lock des del principi (no es pot activar després):

docker compose exec kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 \
    --create --topic auditoria.esdeveniments --partitions 6 --config retention.ms=-1 --config cleanup.policy=delete
mc mb --with-lock local/km0-auditoria
mc retention set --default COMPLIANCE 1825d local/km0-auditoria

L'intent del Marc de llegir P-2026-000123 produeix ara un esdeveniment DENEGAT a auditoria.esdeveniments, encadenat a l'anterior de comandes, arxivat a km0-auditoria/comandes/2026-09-15/10.jsonl amb retenció de cinc anys, i visible a l'índex per a la consulta de l'apartat 11. Si algú altera el fitxer (no pot, per l'object lock) o intenta reescriure el tòpic, verificar_cadena ho detecta.

Errors Comuns i Consells

  • Exposar l'API d'administració del gateway. KONG_ADMIN_LISTEN a 0.0.0.0 és lliurar la configuració de la vora a Internet. Només a localhost o en una xarxa de gestió amb mTLS.
  • Confiar que "el gateway ja valida". Les crides internes no hi passen. Cada servei verifica el token i la identitat de servei.
  • Rate limiting només per IP. Un NAT corporatiu bloqueja cent clients legítims i un atacant amb mil proxies ni se n'adona. Per identitat quan n'hi ha; per IP només abans d'autenticar.
  • Sense Retry-After ni capçaleres RateLimit-*. Els clients no es poden autoregular i reintenten en bucle, agreujant el problema que es volia evitar.
  • Comptador en memòria en un gateway amb diverses instàncies. Cada instància deixa passar el límit complet. Estat a Redis amb script atòmic.
  • Auditoria al mateix log que l'operatiu, amb la mateixa retenció. Als 14 dies s'ha esborrat la prova. Esquema propi, canal propi, retenció pròpia.
  • Auditoria que pot editar qui l'escriu. Només escriptura a Kafka (ACL), hash encadenat, object lock. Si l'administrador la pot esborrar, no és auditoria.
  • Dades personals o tokens als esdeveniments d'auditoria. El registre d'auditoria és al seu torn un tresor per a un atacant; identificadors, no continguts; jti, no el token.
  • Validar l'entrada només al navegador. El navegador és de l'usuari; l'atacant no el fa servir. L'esquema es fa complir a la vora i al servei.
  • Dependències sense fixar ni escanejar. La vulnerabilitat més probable a Quilòmetre Zero no és al seu codi, sinó en una llibreria amb una versió de fa dos anys.
  • Consell: desa kong.yaml, el realm de Keycloak, les polítiques de Vault i l'esquema OpenAPI al repositori i revisa'ls com a codi: un canvi de minute: 60 a minute: 600000 ha de passar per revisió.
  • Consell: assaja la consulta "qui ha accedit a les dades de X?" abans que la faci un auditor; si triga més d'uns minuts a respondre's, l'índex d'auditoria no està ben dissenyat.
  • Advertència de compliment normatiu. La retenció dels logs d'auditoria, el seu contingut (que inclou identificadors de persones i adreces IP, dades personals al seu torn), l'accés a ells i la resposta a sol·licituds d'accés o supressió estan regulats pel RGPD; PCI DSS imposa requisits concrets d'auditoria a qualsevol component que toqui pagaments. Tot el que s'ha descrit s'ha de revisar amb un professional de seguretat i compliment normatiu.

Exercicis

Exercici 1: La primera hora de la Setmana de la Verema

A les 10:00 obre la campanya. En els primers 60 segons arriben al gateway 2.400.000 peticions: 1.900.000 GET /api/v1/cataleg/* de 60.000 clients diferents, 300.000 POST /api/v1/comandes de 40.000 clients, i 200.000 peticions de 50 IPs que fan GET /api/v1/cataleg/productes/vi-crianca en bucle. Amb la configuració de kong.yaml i el lim_crear de comandes (capacitat 5, 0,1/s), calcula aproximadament quantes peticions de cada grup arriben als serveis i quantes reben 429, i què veu un client legítim que carrega la pàgina d'inici (18 peticions al catàleg). Què canviaries per al tercer grup, i per què el límit per consumidor no els frena?

Exercici 2: Un operador curiós

Se sospita que un operador ha estat consultant comandes de clients sense motiu. Descriu (a) quina consulta faries sobre l'índex d'auditoria i quins camps faries servir, (b) com demostraries que els registres no han estat alterats pel mateix operador (que té accés a la consola de Kafka i a MinIO com a administrador), (c) quina regla d'anomalia hauria alertat abans, i (d) quines dues coses del disseny de registrar fan que la consulta no exposi al seu torn les dades dels clients.

Exercici 3: STRIDE per a repartiment

Aplica la taula STRIDE al servei repartiment (rep posicions de 140 repartidors per l'app, publica a repartiment.posicions, serveix el panell en temps real des de repartiment.panell als operadors, i mostra a l'Anna la posició aproximada del seu repartidor). Dona un exemple concret per lletra i la contramesura amb la seva lliçó. Assenyala l'amenaça que consideres més greu i per què.

Solucions

Exercici 1.

Grup 1 (catàleg, 60.000 clients, ~32 peticions cadascun en el minut): el límit és 200/min per consumidor, així que totes passen (~1,9 M arriben a cataleg, que les serveix des de la memòria cau de Redis de 04-05); un client que carrega la pàgina d'inici (18 peticions) veu RateLimit-Remaining: 182 i cap espera. Grup 2 (comandes, 40.000 clients, ~7,5 POST cadascun): Kong deixa passar 60/min per consumidor (cap no hi arriba), però lim_crear a comandes en concedeix 5 de cop i després 1 cada 10 s: en un minut, com a molt 5 + 6 = 11 per client; els que n'han fet 7-8 passen gairebé tots (5 immediats + 1 o 2 recarregats), i una fracció rep 429 amb Retry-After: 10: unes 40.000 × ~1,5 ≈ 60.000 rebutjades i ~240.000 arriben a comandes. Aquest és el comportament desitjat: ningú legítim no fa 8 comandes en un minut llevat d'un doble clic, que el 429 amb reintent suau resol. Grup 3 (50 IPs, 4.000 peticions cadascuna al catàleg): limit_by: consumer no s'aplica perquè el catàleg no exigeix token; Kong cau al límit per IP (200/min): passen 50 × 200 = 10.000 i se'n rebutgen 190.000; però 10.000 peticions idèntiques continuen arribant i, sobretot, aquests 50 hosts poden ser 5.000 demà. Canvis: memòria cau de resposta al gateway per a GET públics (proxy-cache: les 10.000 ni tan sols toquen cataleg), un límit més baix per IP per a rutes públiques sense token (60/min és més que suficient per a un humà), bloqueig per reputació dels rangs reincidents i, si persisteix, protecció de bots a la CDN. El límit per consumidor no els frena perquè no són consumidors: no s'autentiquen, i contra trànsit anònim només queden IP, empremta i memòria cau.

Exercici 2.

(a) SELECT recurs, quan, context->>'ticket_id' FROM auditoria WHERE sub = 'mpuig' AND accio = 'comandes.llegir' AND quan BETWEEN ... ORDER BY quan, i després unir-ho amb els tiquets de suport: cada lectura sense ticket_id (o amb un tiquet que no esmenta aquesta comanda) és sospitosa; a més, comptar comandes diferents per hora i comparar-ho amb la mitjana d'altres operadors. (b) La cadena: cada esdeveniment d'origen comandes porta prev_hash; verificar_cadena sobre els fitxers horaris de km0-auditoria/comandes/ detecta qualsevol esborrat o modificació; els fitxers són amb object lock en mode compliance, així que ni com a administrador de MinIO no va poder alterar-los ni esborrar-los; a Kafka, l'ACL d'auditoria.esdeveniments només concedeix escriptura als serveis i lectura a l'arxivador, i encara que com a administrador pogués esborrar el tòpic, els fitxers arxivats i l'àncora diària signada (l'últim hash del dia publicat externament) demostrarien la manipulació; finalment, el seu propi accés a la consola de Kafka i a MinIO està auditat als logs d'aquests sistemes. (c) "Un sub amb rol operador llegeix > 200 comandes diferents en una hora", o la seva variant més fina "lectures sense ticket_id associat". (d) registrar desa l'identificador de la comanda (P-2026-000123) i no el seu contingut, i el context és només request_id/ticket_id: la consulta revela quines comandes va consultar, no el telèfon ni l'adreça de ningú; i l'índex d'auditoria té accés restringit i al seu torn auditat, així que la investigació deixa el seu propi rastre.

Exercici 3.

Spoofing: una app modificada publica posicions com a furgoneta-3-017 sense ser aquell repartidor: el token amb repartidor_id (06-01, exercici 2) i el servei ignora l'id del cos i fa servir el del token. Tampering: un procés intern injecta posicions falses a repartiment.posicions perquè el panell mostri lliuraments on no n'hi ha: autenticació del productor a Kafka amb certificat i ACL (06-04) més HMAC de l'embolcall (06-02). Repudiation: un repartidor nega haver marcat com a lliurada la comanda P-2026-000124: auditoria de repartiment.marcar_lliurada amb sub, jti, posició en aquell moment i hash encadenat (06-05). Information disclosure: l'Anna veu la posició exacta del seu repartidor, i amb ella la casa del client anterior; o repartiment.panell amb les rutes de 140 persones és llegible per qualsevol operador i es filtra: mostrar a l'Anna només la posició aproximada i només mentre la seva comanda estigui "en repartiment" (minimització, 06-02), retenció d'hores a repartiment.posicions, panell amb rol operador i auditoria d'accés; les posicions dels repartidors són dades personals d'empleats amb obligacions específiques (avaluació d'impacte). Denial of service: 140 apps en bucle per un bug (o una app maliciosa) publiquen 1.000 posicions per segon cadascuna: rate limiting per sub a la vora (una posició cada 5 s és suficient), mida màxima, i repartiment que descarta silenciosament el que excedeixi. Elevation of privilege: un repartidor crida LlistarLliuraments d'un altre, o descobreix que MarcarLliurada no comprova l'assignació: ABAC al servei (06-01) i una prova automàtica per cada regla. La més greu és la d'informació: és l'única amb dany irreversible i directe sobre persones (l'adreça de l'Anna, la ruta diària d'un empleat); les altres es detecten i es reverteixen. És també la que menys es veu en un diagrama, perquè no hi ha cap "atacant": només un disseny que mostra més del necessari.

Conclusió

La vora és on la plataforma es troba amb el món, i per això concentra les responsabilitats transversals que cap servei no hauria de repetir: un API gateway (Kong, Envoy, NGINX, o un de gestionat) termina TLS, valida els tokens de Keycloak, encamina i versiona, aplica CORS i capçaleres de seguretat, fa complir el contracte OpenAPI, limita mides i temps, i frena els abusos amb limitació de taxa. Dels algorismes, token bucket és el que millor encaixa amb una API (tolera ràfegues humanes, talla les màquines), i a Redis amb un script Lua atòmic funciona per a totes les instàncies del gateway; 429, Retry-After i RateLimit-* diuen al client com comportar-se, i limitar per identitat (no només per IP) fa que el límit recaigui sobre qui correspon. L'auditoria és una categoria diferent dels logs operatius: esquema fix (qui, què, quan, des d'on, resultat), sense dades personals innecessàries, amb sub i jti per correlacionar, immutable per construcció (només escriptura a Kafka, hash encadenat, object lock a MinIO), amb accés restringit, retenció llarga, i regles d'anomalia que converteixen el registre en alerta. I tot això com a procés: STRIDE abans de cada canvi, dependències fixades i escanejades, revisió de la configuració de seguretat com a codi.

Amb això es tanca el Mòdul 6. Quilòmetre Zero té ara una resposta explícita a "qui pot fer què":

Subjecte S'autentica amb Pot No pot
Anna, Marc, Llúcia (client) Keycloak (contrasenya Argon2 o Google) + JWT RS256 de 15 min; MFA opcional Veure el catàleg; crear comandes (5 de cop, 1 cada 10 s); veure les seves comandes i la posició aproximada del seu repartidor Veure comandes alienes (ABAC); editar productes; més de 60 crides/min a comandes
Horta La Vega, Formatgeria Montblanc, Celler Roure Alt (productor) Keycloak + MFA obligatòria; grup amb productor_id Editar els seus productes i ajustar el seu estoc; veure comandes amb els seus productes; pujar fotos per URL presignat Tocar productes d'un altre productor; veure clients; reservar estoc
Repartidors de furgoneta-3 (repartidor) app-repartidors (OIDC + PKCE) amb repartidor_id Publicar la seva posició; veure els seus lliuraments; marcar com a lliurades les seves comandes Suplantar un altre repartidor; veure el panell complet
Jordi, Marta (operador) OpenLDAP → Keycloak, MFA obligatòria Veure qualsevol comanda (auditat, amb tiquet); cancel·lar; aprovar productors; veure el panell de repartiment Publicar posicions; llegir telèfons sense que en quedi registre; alterar l'auditoria
comandes (spiffe://km0.internal/comandes) Certificat mTLS de 24 h emès per Vault PKI; token client credentials per a compensacions ReservarEstoc, AlliberarReserva, ConsultarEstoc; Cobrar, Reemborsar; escriure comandes.esdeveniments i auditoria.esdeveniments AjustarEstoc; llegir repartiment.posicions; llegir secrets d'altres serveis a Vault
cataleg Certificat mTLS ConsultarEstoc; llegir inventari.alertes; signar URL de km0-fotos Reservar, cobrar, publicar comandes
repartiment Certificat mTLS Escriure repartiment.posicions; llegir comandes.esdeveniments; llegir i desxifrar telèfons de comandes en repartiment Cancel·lar comandes; llegir factures
analitica (Airflow, Spark, Flink) Certificat mTLS; AppRole a Vault; credencials dinàmiques de km0_analitica amb TTL de 2 h Llegir comandes.esdeveniments i repartiment.posicions; llegir el llac pseudonimitzat; escriure vendes_diaries i repartiment.panell Llegir km0_comandes; veure correus o telèfons; escriure a comandes.esdeveniments; conservar una contrasenya
Kong (vora) Certificat públic ACME cap enfora; mTLS cap endins Terminar TLS, verificar JWT, encaminar, limitar, registrar Decidir autorització fina; veure l'administració des d'Internet
Qualsevol a la xarxa interna sense identitat Res Res: sense certificat no hi ha handshake, sense token no hi ha crida

I els controls implantats, lliçó a lliçó: contrasenyes amb Argon2id, JWT RS256 amb claims estrictes, refresh amb rotació i revocació per jti, RBAC i ABAC amb mínim privilegi (06-01); AES-GCM, HMAC d'esdeveniments, TLS a gRPC i a tots els magatzems, xifratge de camp amb envelope encryption, pseudonimització del llac, crypto-shredding, res de targetes (06-02); Keycloak com a IdP amb OIDC + PKCE, client credentials, JWKS, OpenLDAP per als empleats, MFA per rol, cicle de vida centralitzat (06-03); zero trust amb mTLS, PKI interna, autorització servei a servei, Vault amb credencials dinàmiques i auditoria de secrets (06-04); gateway, rate limiting, validació d'esquemes, auditoria immutable i STRIDE (06-05). Cap d'ells no és infal·lible; junts, en capes, fan que comprometre una peça no sigui comprometre la plataforma.

La plataforma és segura. Però sabem si funciona? Un 429 de més a la Setmana de la Verema, una cadena d'auditoria que es trenca, un lease de Vault que no es renova, un certificat que caduca un diumenge: res del que s'ha construït en sis mòduls no avisa per si sol quan va malament. Sense instruments, un sistema distribuït falla en silenci fins que ho nota un client. El Mòdul 7 tracta el monitoratge i el manteniment de la plataforma, i comença pel monitoratge amb mètriques: què mesurar a cada servei, com recollir-ho amb Prometheus, com definir els objectius de nivell de servei de Quilòmetre Zero i com alertar abans que l'Anna se n'assabenti.

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