Las cuatro lecciones anteriores han asegurado el interior de Kilómetro Cero: tokens verificables, canales cifrados, identidades centralizadas, mTLS entre servicios y secretos en Vault. Pero la plataforma tiene un exterior. Desde Internet llegan los navegadores de Ana, Marc y Lucía, la app de los 140 repartidores de furgoneta-3, los paneles de los productores, y también los bots que rastrean precios, los scripts que prueban contraseñas robadas, y las 40 000 peticiones por segundo de la primera hora de la Semana de la Vendimia. Hasta ahora, cada servicio expuesto (catalogo, pedidos, reparto) hacía su propia terminación TLS, su propia verificación de tokens, su propia defensa contra abusos, y ninguno dejaba registro fiable de quién hizo qué.

Esta lección construye el borde: una puerta de enlace (API gateway) como punto único de entrada que termina TLS, valida los tokens de 06-01 y 06-03, enruta a cada servicio, versiona las APIs y frena los abusos; la limitación de tasa con sus algoritmos y una implementación distribuida en Redis; las protecciones adicionales del borde (validación de esquemas, tamaños, timeouts, WAF); y la auditoría: qué registrar, cómo hacerlo inmutable con hash encadenado y almacenamiento con bloqueo de objetos, y cómo responder a "¿quién consultó el pedido de Ana?". Termina con la idea de seguridad como proceso (modelado de amenazas STRIDE aplicado a pedidos, gestión de vulnerabilidades) y cierra el módulo con el mapa completo de quién puede qué en Kilómetro Cero. El monitoreo de métricas, los logs operativos y los patrones internos de resiliencia son el Módulo 7.

Contenido

  1. El borde de la plataforma: gateway frente a balanceador
  2. Qué hace un API gateway
  3. Opciones: Kong, Envoy Gateway, NGINX, gateways gestionados; BFF
  4. Limitación de tasa: por qué y contra qué
  5. Algoritmos: token bucket, leaky bucket, ventana fija, ventana deslizante
  6. Token bucket distribuido en Redis con Lua
  7. Cabeceras, límites por identidad y cuotas
  8. Protección adicional en el borde
  9. Auditoría: qué registrar y por qué no es un log más
  10. Logs de auditoría inmutables: hash encadenado y object lock
  11. Auditoría de acceso a datos personales y detección de anomalías
  12. Seguridad como proceso: STRIDE, vulnerabilidades y dependencias
  13. Kilómetro Cero: Kong en docker-compose.yml, limitador.py y auditoria.py
  14. Errores Comunes y Consejos
  15. Ejercicios
  16. Conclusión

  1. El borde de la plataforma: gateway frente a balanceador

Un balanceador de carga (L4 o L7) reparte conexiones entre instancias de un mismo servicio y, como mucho, termina TLS. No sabe qué es un JWT, no distingue a Ana de un bot, y no puede decir "esta ruta va a pedidos y esta otra a catalogo v2". En el monolito bastaba; con seis servicios expuestos, cada uno tendría que repetir la misma docena de responsabilidades transversales, y los clientes tendrían que conocer seis direcciones.

Un API gateway es un proxy inverso especializado que se coloca delante de todos los servicios y concentra esas responsabilidades:

flowchart LR
    subgraph Internet
        N[Navegador de Ana]
        R[App repartidores]
        B[Bots / abusos]
    end
    N & R & B -->|HTTPS| G[API Gateway<br/>TLS · JWT · rutas · rate limit<br/>CORS · esquemas · auditoría]
    G -->|mTLS| C[catalogo]
    G -->|mTLS| P[pedidos]
    G -->|mTLS| RP[reparto]
    G -.->|auditoria.eventos| K[(Kafka)]
    G -.->|contadores| RD[(Redis)]
    P -->|mTLS| I[inventario]
    style B stroke-dasharray: 5 5

Nada de fuera llega a un servicio sin pasar por el gateway; los servicios solo aceptan conexiones mTLS del gateway y de otros servicios (06-04). Y el gateway no sustituye la seguridad de cada servicio: es la primera barrera, no la única. inventario sigue verificando el token (06-01) porque la llamada que le llega de pedidos no pasó por el borde.

  1. Qué hace un API gateway

Responsabilidad Qué significa Sin gateway
Terminación TLS Un solo lugar con el certificado público de api.km0.example, renovado por Let's Encrypt/ACME; hacia dentro, mTLS con la CA interna Un certificado público por servicio
Autenticación Verifica el JWT (RS256 contra el JWKS de Keycloak, 06-03) y rechaza en el borde lo inválido; opcionalmente introspección de tokens opacos Cada servicio lo hace, y lo inválido consume recursos internos
Enrutamiento /api/catalogo/*catalogo, /api/pedidos/*pedidos; por ruta, método, cabecera, versión Los clientes conocen seis hosts
Transformación Añadir cabeceras (X-Request-Id, identidad verificada), quitar cabeceras internas de las respuestas, adaptar formatos Repetido
Versionado de APIs /api/v1/pedidos y /api/v2/pedidos a servicios o rutas distintas; retirada gradual Rupturas para clientes antiguos
CORS Cabeceras Access-Control-* para que el navegador de km0.example pueda llamar a api.km0.example Configuración por servicio, a menudo * por descuido
Protección contra abusos Rate limiting, cuotas, tamaño máximo, timeouts, bloqueo por IP El servicio se satura antes de poder defenderse
Observabilidad Un punto por el que pasa todo el tráfico externo: métricas (07-01), trazas (07-02), auditoría Fragmentada
Caché de respuestas Para GET públicos del catálogo, complementa a Redis (04-05)

Lo que no debería hacer: lógica de negocio, agregación compleja de varios servicios (ese es el papel del BFF, apartado 3), ni autorización fina (la ABAC de "sus productos" necesita datos que solo tiene el servicio).

  1. Opciones: Kong, Envoy Gateway, NGINX, gateways gestionados; BFF

Opción Naturaleza Configuración Puntos fuertes A tener en cuenta
Kong Gateway Proxy sobre NGINX/OpenResty con plugins (Lua, Go, Python) Declarativa (YAML, modo sin base de datos) o API de administración Ecosistema de plugins (JWT, OIDC, rate limiting, auditoría), maduro, versión abierta completa Plugins avanzados en la edición comercial
Envoy Gateway / Envoy Proxy L7 de alto rendimiento (C++), base de Istio Kubernetes Gateway API o xDS Rendimiento, filtros (JWT, ext_authz para OPA, rate limit global), el mismo Envoy que la malla Configuración verbosa fuera de Kubernetes
NGINX / OpenResty Servidor web y proxy nginx.conf Ubicuo, rápido, simple para enrutar y terminar TLS JWT y rate limiting distribuido requieren módulos comerciales o Lua propio
Traefik, KrakenD, Tyk, APISIX Gateways de código abierto Diversa Traefik: descubrimiento automático de contenedores; KrakenD: agregación; APISIX: rendimiento
Gestionados (AWS API Gateway, Google Apigee/Cloud Endpoints, Azure API Management) Servicio de nube Consola/IaC Sin operar; integración con IAM, WAF, cuotas y facturación por uso Coste por petición, dependencia del proveedor (08-03)

BFF (Backend for Frontend): cuando cada tipo de cliente (web, app móvil, panel de productores) necesita agregaciones distintas (la pantalla de inicio de la app combina catálogo, pedidos en curso y posición del repartidor), se coloca un servicio por cliente que compone esas respuestas, detrás del gateway. El gateway sigue siendo transversal; el BFF es específico. Kilómetro Cero, en esta lección, usa Kong sin BFF; el BFF de la app de repartidores aparecerá en el proyecto final (08-05).

  1. Limitación de tasa: por qué y contra qué

La limitación de tasa (rate limiting) acota cuántas peticiones puede hacer un cliente en un intervalo. Protege contra:

  • Abuso deliberado: fuerza bruta contra /login (miles de contraseñas por minuto), scraping del catálogo completo cada minuto, denegación de servicio con peticiones válidas.
  • Errores de clientes: una app con un bucle de reintentos sin backoff (07-04) que, tras un fallo, dispara mil peticiones por segundo desde cada dispositivo.
  • Sobrecarga en picos legítimos: la Semana de la Vendimia llena la web; sin límite, pedidos se satura, todas las peticiones fallan, y nadie compra. Con límite, el 95 % compra y el 5 % recibe un "inténtalo en unos segundos".
  • Equidad: que un integrador que consume la API no monopolice catalogo a costa del resto.
  • Coste: si detrás hay llamadas a la pasarela de pago o a un proveedor de mapas facturados por uso.

Se aplica en el borde (por cliente, token o IP, antes de tocar un servicio) y a veces también dentro (un servicio que se protege de otro, lo que en 07-04 se llamará bulkhead). Y se distingue de la cuota: el límite de tasa es a corto plazo (100 por minuto: suavizar), la cuota a largo plazo (10 000 al día: contrato).

  1. Algoritmos: token bucket, leaky bucket, ventana fija, ventana deslizante

Algoritmo Cómo funciona Ráfagas Memoria por clave Precisión Uso típico
Ventana fija Un contador por clave y por intervalo (10:04 → 37); se resetea al cambiar el minuto Permite el doble del límite en el cambio de ventana (100 a las 10:04:59 + 100 a las 10:05:00) 1 entero Baja Cuotas diarias, donde el borde de ventana no importa
Ventana deslizante (log) Guarda la marca de tiempo de cada petición; cuenta las de los últimos 60 s Exacto, sin efecto borde Una entrada por petición (caro) Alta Límites bajos y estrictos (login)
Ventana deslizante (contador ponderado) Contador de la ventana actual + contador de la anterior ponderado por la fracción transcurrida Aproxima bien, sin el pico del borde 2 enteros Media-alta El más usado en gateways (Kong, Cloudflare)
Token bucket Un cubo de capacidad C que se rellena a r tokens/s; cada petición consume uno; sin tokens, rechazo Permite ráfagas de hasta C y luego el ritmo sostenido r 2 valores (tokens, última recarga) Alta APIs: tolera ráfagas naturales (cargar una página lanza 20 peticiones)
Leaky bucket Una cola de capacidad C que se vacía a r/s; la petición entra si hay hueco y se sirve a ritmo constante Suaviza: la salida es siempre r, las ráfagas esperan o se descartan Cola Alta Modelar tráfico hacia un servicio que no tolera picos (la pasarela de pago)

Token bucket y leaky bucket son duales: el primero limita la admisión permitiendo ráfagas, el segundo limita la salida eliminándolas. Para una API pública, token bucket es la elección habitual: Ana carga la página de inicio (20 peticiones en un segundo, que caben en el cubo) y luego navega a un ritmo bajo que el goteo de recarga cubre de sobra; un bot que hace 50 por segundo agota el cubo en un instante y queda limitado a r.

flowchart LR
    R[Recarga: r tokens/s] --> B[(Cubo<br/>capacidad C)]
    P[Petición] -->|¿hay token?| B
    B -->|sí: consume 1| OK[200 → servicio]
    B -->|no| KO[429 Too Many Requests<br/>Retry-After]

  1. Token bucket distribuido en Redis con Lua

El gateway tiene varias instancias; el estado del cubo debe ser compartido, y la operación "leer tokens, recargar según el tiempo transcurrido, decidir, escribir" debe ser atómica, o dos instancias concederían el mismo último token. Redis (04-05) resuelve ambas cosas: el estado vive allí, y un script Lua se ejecuta atómicamente en el servidor.

# km0/servicios/borde/limitador.py
import time, redis

# El script se ejecuta entero en Redis sin que ningún otro comando se intercale:
# leer, recargar, decidir y escribir son una sola operación.
_LUA_TOKEN_BUCKET = """
local clave      = KEYS[1]
local capacidad  = tonumber(ARGV[1])
local tasa       = tonumber(ARGV[2])   -- tokens por segundo
local ahora      = tonumber(ARGV[3])   -- segundos con decimales (lo pasa el cliente para que
                                       -- el script sea determinista y replicable)
local coste      = tonumber(ARGV[4])   -- tokens que consume esta petición (1 normalmente)

local estado = redis.call('HMGET', clave, 'tokens', 'ts')
local tokens = tonumber(estado[1])
local ts     = tonumber(estado[2])
if tokens == nil then tokens = capacidad; ts = ahora end   -- primera vez: cubo lleno

-- Recarga proporcional al tiempo transcurrido, sin superar la capacidad
tokens = math.min(capacidad, tokens + (ahora - ts) * tasa)

local permitido = 0
if tokens >= coste then
    tokens = tokens - coste
    permitido = 1
end

redis.call('HSET', clave, 'tokens', tokens, 'ts', ahora)
-- El cubo se rellena solo en capacidad/tasa segundos: después de eso, la clave sobra
redis.call('EXPIRE', clave, math.ceil(capacidad / tasa) + 1)

-- Segundos hasta que habrá 'coste' tokens (para Retry-After); 0 si permitido
local espera = 0
if permitido == 0 then espera = math.ceil((coste - tokens) / tasa) end
return {permitido, math.floor(tokens), espera}
"""

class LimitadorTasa:
    def __init__(self, r: redis.Redis, capacidad: int, tasa_por_s: float, prefijo="rl"):
        self._r = r
        self._script = r.register_script(_LUA_TOKEN_BUCKET)   # se carga una vez (EVALSHA después)
        self.capacidad, self.tasa, self.prefijo = capacidad, tasa_por_s, prefijo

    def permitir(self, identidad: str, coste: int = 1) -> tuple[bool, int, int]:
        """Devuelve (permitido, tokens_restantes, segundos_de_espera)."""
        permitido, restantes, espera = self._script(
            keys=[f"{self.prefijo}:{identidad}"],
            args=[self.capacidad, self.tasa, time.time(), coste])
        return bool(permitido), int(restantes), int(espera)


if __name__ == "__main__":
    r = redis.Redis(host="localhost", port=6379)
    lim = LimitadorTasa(r, capacidad=20, tasa_por_s=5)      # ráfaga de 20, luego 5/s sostenido
    ok = ko = 0
    for i in range(60):                                     # 60 peticiones lo más rápido posible
        permitido, restantes, espera = lim.permitir("u-ana")
        ok += permitido; ko += not permitido
    print(f"permitidas={ok} rechazadas={ko}")               # permitidas=20 rechazadas=40 (aprox.)
    time.sleep(2)
    print(lim.permitir("u-ana"))                            # (True, 9, 0): 2 s × 5/s = 10 tokens recargados

Detalles que importan:

  • time.time() lo pasa el cliente, no redis.call('TIME'): los scripts Lua deben ser deterministas para que la replicación de Redis los reproduzca; y un desfase de reloj entre instancias del gateway (01-05) solo introduce un error de milisegundos en la recarga, irrelevante aquí.
  • Una clave por identidad (rl:u-ana) con EXPIRE: los clientes inactivos no ocupan memoria.
  • coste permite que operaciones caras (una búsqueda de texto en el catálogo) consuman más tokens que un GET simple.
  • En Redis Cluster (04-05), cada clave se enruta a su nodo por hash slot; el script solo toca una clave, así que funciona sin cambios.
  • Coste: un viaje a Redis por petición en el borde (~0,3 ms en la misma red). Para picos extremos, se hace limitación local aproximada en cada instancia del gateway (un cubo en memoria con C/n) y se sincroniza con Redis de forma asíncrona; es lo que eligen las políticas (policy) local/redis/cluster del plugin de limitación de Kong.

Como middleware en un servicio Flask (para pedidos, que además de la protección del gateway quiere un límite propio más estricto en POST /pedidos):

# km0/servicios/pedidos/api.py (fragmento)
from flask import Flask, request, g, jsonify
from servicios.borde.limitador import LimitadorTasa
import redis

app = Flask(__name__)
lim_crear = LimitadorTasa(redis.Redis(host="redis"), capacidad=5, tasa_por_s=0.1, prefijo="rl:crear")

@app.before_request
def limitar_creacion():
    if request.method == "POST" and request.path == "/pedidos":
        sujeto = getattr(g, "claims", {}).get("sub") or request.remote_addr    # por usuario; si no, por IP
        permitido, restantes, espera = lim_crear.permitir(sujeto)
        if not permitido:
            resp = jsonify(error="demasiadas peticiones", 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_restantes = restantes

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

Cinco pedidos de golpe y luego uno cada diez segundos: nadie crea pedidos legítimos más rápido, y un script que intente cientos de compras con tarjetas robadas se detiene en el quinto.

  1. Cabeceras, límites por identidad y cuotas

  • 429 Too Many Requests es el código; Retry-After (segundos o fecha) le dice al cliente cuándo volver, y un cliente bien hecho lo respeta (07-04 tratará los reintentos con backoff del lado cliente).
  • RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset (borrador IETF, ya de uso común; Kong y otros usan variantes X-RateLimit-*) informan en cada respuesta, no solo en el 429, para que el cliente se autorregule antes de chocar.
  • Por qué identidad limitar, de mejor a peor: por sub del token (Ana, o svc-integrador-x) → por client_id de OAuth (toda la app de repartidores como conjunto, útil para proteger de un bug de la app) → por clave de API → por IP (lo único posible antes de autenticarse, en /login; imprecisa por NAT: una oficina entera comparte IP; y evitable con proxies). Combinar: /login por IP y por cuenta destino; el resto por sub.
  • Límites distintos por ruta y rol: GET /api/catalogo generoso (200/min) y cacheable; POST /api/pedidos estricto; operadores con límites mayores; integradores con cuotas contractuales (100 000/día) que se contabilizan con ventana fija diaria además del token bucket.
  • Qué devolver cuando Redis no responde: fail open (permitir todo, y alertar) suele ser lo correcto en el borde de un marketplace (mejor vender sin límite unos minutos que no vender); fail closed en /login y en operaciones de pago.

  1. Protección adicional en el borde

Protección Qué hace En Kilómetro Cero
Validación de esquema (OpenAPI) El gateway (o el servicio) rechaza cuerpos que no cumplen el contrato: tipos, campos obligatorios, rangos, longitudes contratos/pedidos.openapi.yaml; un pedido con cantidad: -5 o un producto de 10 000 caracteres muere en el borde
Tamaño máximo de cuerpo Rechaza peticiones mayores de N KB/MB antes de leerlas 64 KB para la API; las fotos van por URL prefirmada a MinIO (04-03), no por el gateway
Timeouts en el borde Tiempo máximo de conexión, de lectura y total hacia cada servicio 5 s a catalogo, 10 s a pedidos; evita que conexiones lentas ocupen el gateway (slowloris). Los timeouts internos, reintentos y circuit breakers son 07-04
Límite de conexiones concurrentes por cliente y por servicio Complementa al rate limit (peticiones lentas y muchas a la vez) 50 por IP
WAF (web application firewall: ModSecurity con el OWASP Core Rule Set, Cloudflare, AWS WAF) Reglas contra patrones de ataque: inyección SQL, XSS, rutas de escáner, agentes maliciosos Delante del gateway, gestionado por la CDN/nube; solo se nombra: su afinación es un oficio propio
Cabeceras de seguridad en las respuestas Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options Añadidas por el gateway para toda respuesta
Bloqueo por reputación / geografía Listas de IPs, países, ASN Bloquear rangos con abuso reiterado
Protección de bots Desafíos, huellas de navegador Servicio de la CDN; se nombra

La validación de entrada es la más importante y la que menos se hace: la mayoría de las vulnerabilidades de aplicación empiezan por una entrada que nadie validó. El esquema OpenAPI, versionado en contratos/ junto a los .proto de 02-03, es al HTTP público lo que protobuf es al interior: el contrato como fuente de verdad, y el gateway como quien lo hace cumplir.

  1. Auditoría: qué registrar y por qué no es un log más

Un log de auditoría responde, meses después y ante un auditor, un juez o un cliente, a la pregunta: ¿quién hizo qué, cuándo, desde dónde y con qué resultado? No es lo mismo que los logs operativos (07-02), aunque técnicamente ambos sean líneas con marca de tiempo:

Log operativo (07-02) Log de auditoría
Propósito Depurar, entender el comportamiento del sistema Rendir cuentas, investigar incidentes, cumplir obligaciones
Contenido Lo que el programador consideró útil: stack traces, tiempos, variables Un esquema fijo: sujeto, acción, recurso, resultado, contexto
Volumen Alto; se muestrea y se descarta Todo evento relevante, sin muestreo
Retención Días o semanas Años (según obligación: fiscal, RGPD, PCI DSS)
Mutabilidad Se rotan, se borran, se editan sin drama Inmutable: nadie, ni un administrador, puede alterar o borrar
Acceso Todo el equipo Restringido: seguridad, cumplimiento; acceso a su vez auditado
Ejemplo WARN pedidos: reintento 2/3 a inventario (deadline 1.8s) 2026-09-15T10:04:12Z sub=u-marc jti=7f4… accion=pedidos.leer recurso=P-2026-000123 resultado=DENEGADO ip=…

Qué registrar (cada evento, con esquema fijo):

  • Quién: sub del JWT y jti (para correlacionar con el token concreto), client_id, roles en el momento; para servicios, el SPIFFE ID (06-04).
  • Qué: acción (pedidos.crear, pedidos.leer, productos.editar, login.fallido, token.revocado, secreto.leido) y recurso (P-2026-000123, queso-curado, kv/km0/pedidos/keycloak).
  • Cuándo: marca de tiempo del gateway/servicio en UTC (01-05 explica por qué se registra también el origen del reloj).
  • Desde dónde: IP de origen, User-Agent, servicio intermedio, X-Request-Id.
  • Resultado: PERMITIDO/DENEGADO/ERROR, con el motivo de la denegación.
  • Nunca: contraseñas, tokens completos, datos personales que no sean imprescindibles (el pedido P-2026-000123 sí; el teléfono de Ana no).

Qué eventos: toda autenticación (éxito y fallo), toda denegación de autorización, todo acceso a datos personales (apartado 11), toda operación administrativa (cambios de rol, aprobación de productores, cambios de configuración del gateway), todo acceso a secretos (Vault ya lo hace), y las operaciones de negocio sensibles (cancelaciones, reembolsos, ajustes de stock).

  1. Logs de auditoría inmutables: hash encadenado y object lock

Un log de auditoría que el atacante (o un administrador) puede editar no vale nada: la primera acción de un intruso competente es borrar sus huellas. Tres capas de inmutabilidad:

  1. Append-only en origen: el servicio publica cada evento en el tópico Kafka auditoria.eventos (02-04) y no lo escribe en ningún sitio que pueda editar. Kafka es un log de solo anexión; con ACLs (06-04), los servicios solo tienen permiso de escritura en ese tópico, no de lectura ni de administración.
  2. Hash encadenado: cada registro incluye el hash del registro anterior. Modificar o eliminar uno rompe la cadena a partir de él, y un verificador lo detecta. Es la estructura de una blockchain sin nada de lo demás: un prev_hash por registro y, periódicamente, un ancla (el hash del último registro del día, firmado y publicado en un lugar externo: un tópico distinto, un notario de marcas de tiempo, un tercero) para que ni siquiera reescribir toda la cadena desde el principio pase inadvertido.
  3. Almacenamiento con bloqueo de objetos: un consumidor dedicado escribe los eventos en lotes (una hora, un día) en el bucket km0-auditoria de MinIO con object lock en modo compliance (04-03): ni el propietario del bucket ni el administrador pueden borrar o sobrescribir el objeto hasta que expire la retención (5 años, por ejemplo). Junto con SSE-KMS (06-02) y un lease de acceso de solo lectura para el equipo de seguridad.
flowchart LR
    S[Servicio / gateway] -->|evento firmado, prev_hash| K[Kafka: auditoria.eventos<br/>ACL: solo escritura]
    K --> C[Consumidor auditoria<br/>verifica cadena]
    C -->|lotes por hora| M[(MinIO km0-auditoria<br/>object lock 5 años, SSE-KMS)]
    C -->|ancla diaria firmada| A[Ancla externa]
    C --> Q[(Índice de consulta<br/>solo lectura, acceso auditado)]

  1. Auditoría de acceso a datos personales y detección de anomalías

El RGPD exige poder demostrar quién accedió a los datos de una persona. "¿Quién consultó el pedido de Ana?" debe responderse con una consulta al índice de auditoría:

sub          accion         recurso          resultado   cuando                 desde
u-ana        pedidos.leer   P-2026-000123    PERMITIDO   2026-09-14T18:02:11Z   web-km0 / 83.x.x.x
mpuig        pedidos.leer   P-2026-000123    PERMITIDO   2026-09-15T09:41:03Z   panel-operadores / oficina
u-marc       pedidos.leer   P-2026-000123    DENEGADO    2026-09-15T10:04:12Z   web-km0 / 90.x.x.x
svc-reparto  pedidos.leer   P-2026-000123    PERMITIDO   2026-09-15T11:20:45Z   spiffe://km0.internal/reparto

Marta (operadora) leyó el pedido a las 9:41: legítimo si había un ticket de soporte abierto (y el registro debería llevar el ticket_id como contexto); Marc intentó leerlo y fue denegado (el ABAC de 06-01 funcionando); reparto lo leyó para asignar repartidor. La correlación con la identidad se hace por sub y, cuando importa distinguir sesiones, por jti: si el token de Ana fue robado (ejercicio 1 de 06-01), todos los accesos con ese jti desde una IP distinta son la huella del atacante.

Sobre ese registro se construye la detección de anomalías básica, sin aprendizaje automático: reglas sobre ventanas de tiempo que producen alertas (07-01 las llevará al sistema de alertas general):

Regla Señal de
> 10 login.fallido para la misma cuenta en 5 min, o > 100 desde la misma IP Fuerza bruta, relleno de credenciales
Un sub con rol operador lee > 200 pedidos distintos en una hora Exfiltración, curiosidad indebida
Un jti usado desde dos países en 10 minutos Token robado
DENEGADO repetido del mismo sub sobre recursos de otros usuarios Enumeración de ids (P-2026-000123, 124, 125...)
Acceso a Vault fuera del horario de despliegue, o desde una identidad nueva Compromiso de un servicio
Un productor ajusta stock de > 50 productos en un minuto Cuenta de productor comprometida

  1. Seguridad como proceso: STRIDE, vulnerabilidades y dependencias

Ninguna de las piezas de este módulo es definitiva: se descubren vulnerabilidades, cambian los servicios, se añaden rutas. La seguridad es un proceso, con tres prácticas mínimas:

Modelado de amenazas ligero. Antes de diseñar o cambiar un servicio, sentarse media hora con su diagrama y preguntarse, por cada flujo y componente, qué podría salir mal. STRIDE es la lista de comprobación clásica. Aplicada a pedidos:

Amenaza Significado Ejemplo en pedidos Contramedida Lección
Spoofing Suplantar una identidad Un proceso se hace pasar por pagos para marcar P-2026-000125 como pagado mTLS + política servicio-a-servicio; JWT verificado para usuarios 06-01, 06-04
Tampering Alterar datos Cambiar el importe de un pedido en tránsito; modificar un evento en pedidos.eventos TLS; HMAC de eventos; validación de esquema; el importe se recalcula en el servidor 06-02, 06-05
Repudiation Negar haber hecho algo Ana dice que nunca hizo el pedido; un operador niega haber cancelado Auditoría inmutable con sub/jti; firma de acciones críticas 06-05
Information disclosure Revelar información Marc lee el pedido de Ana; el teléfono aparece en un log; un error muestra un stack trace con la cadena de conexión ABAC; cifrado de campo; seudonimización; mensajes de error genéricos; nunca secretos en logs 06-01, 06-02, 06-04
Denial of service Impedir el servicio 40 000 pedidos por segundo falsos en la Semana de la Vendimia; cuerpos de 100 MB Rate limiting; tamaño máximo; timeouts; cuotas; (07-04 para la resiliencia interna) 06-05
Elevation of privilege Obtener más permisos Un cliente se añade operador al token; una inyección en un parámetro de búsqueda ejecuta SQL Firma RS256 y verificación estricta; consultas parametrizadas; validación de entrada; mínimo privilegio en la BD (credenciales dinámicas con solo los GRANT necesarios) 06-01, 06-04, 06-05

Gestión de vulnerabilidades y dependencias. El código de Kilómetro Cero es una fracción del que ejecuta: grpcio, PyJWT, cryptography, redis, Kong, Keycloak, Vault, Kafka, PostgreSQL, imágenes base de Docker. Cada uno publica vulnerabilidades (CVE) con regularidad. Proceso mínimo: fijar versiones (requirements.txt con hashes, imágenes por digest), un escáner de dependencias en CI (pip-audit, Dependabot/Renovate para las actualizaciones, Trivy o Grype para las imágenes), una ventana de parcheo definida por severidad (crítico: 48 h), y un inventario de qué corre dónde (SBOM). Y revisión: de código para cambios en autenticación y autorización, de configuración del gateway y de Vault, y pruebas de penetración periódicas por alguien externo al equipo.

  1. Kilómetro Cero: Kong en docker-compose.yml, limitador.py y auditoria.py

Kong como gateway, en modo declarativo

Kong en modo DB-less lee toda su configuración de un fichero YAML: rutas, servicios, plugins y consumidores. Es versionable y revisable, como el realm de Keycloak.

# km0/docker-compose.yml (fragmento)
  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      # certificado público (ACME en producción)
      KONG_SSL_CERT_KEY: /certs/api.km0.example.key
      KONG_ADMIN_LISTEN: "127.0.0.1:8001"            # la API de administración NUNCA expuesta
      KONG_LUA_SSL_TRUSTED_CERTIFICATE: /certs/km0-ca.crt   # para verificar los servicios (TLS interno)
      KONG_CLIENT_SSL: "on"                          # mTLS hacia los servicios (06-04)
      KONG_CLIENT_SSL_CERT: /certs/kong.crt
      KONG_CLIENT_SSL_CERT_KEY: /certs/kong.key
    volumes:
      - ./borde/kong.yaml:/kong/kong.yaml:ro
      - ./certs:/certs:ro
    ports: [ "8443:8443" ]
    depends_on: [ redis, catalogo, pedidos ]
# km0/borde/kong.yaml
_format_version: "3.0"

services:
  - name: catalogo
    url: https://catalogo:8000
    connect_timeout: 2000
    read_timeout: 5000
    routes:
      - name: catalogo-v1
        paths: [ "/api/v1/catalogo" ]
        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: pedidos
    url: https://pedidos:8000
    connect_timeout: 2000
    read_timeout: 10000
    routes:
      - name: pedidos-v1
        paths: [ "/api/v1/pedidos" ]
        strip_path: true
    plugins:
      - name: jwt                               # rechaza en el borde lo que no firme Keycloak
        config:
          key_claim_name: iss                   # busca el consumidor por el claim 'iss'
          claims_to_verify: [ exp ]
          maximum_expiration: 900               # tokens de más de 15 min: rechazados
      - name: rate-limiting
        config: { minute: 60, policy: redis, redis_host: redis, limit_by: consumer,
                  fault_tolerant: false }        # fail closed: pedidos es 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' de los tokens
        algorithm: RS256
        rsa_public_key: |                          # clave pública del realm (o el plugin openid-connect con JWKS)
          -----BEGIN PUBLIC KEY-----
          MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
          -----END PUBLIC KEY-----

plugins:                                          # globales
  - 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ón → consumidor de auditoría (apartado siguiente)
    config: { http_endpoint: "http://auditoria:9000/borde", timeout: 1000, queue_size: 100 }

Con esto, https://api.km0.example/api/v1/catalogo/productos/queso-curado llega a catalogo sin token (catálogo público) pero limitado a 200 por minuto; https://api.km0.example/api/v1/pedidos/P-2026-000123 sin Authorization recibe 401 del propio Kong sin tocar pedidos, y con un token válido llega a pedidos con X-Request-Id, que pedidos propaga a inventario en los metadatos gRPC (07-02 lo usará para las trazas). El plugin jwt de la edición abierta verifica firma y exp; la verificación de aud y de roles la sigue haciendo requiere_rol en pedidos (06-01), y para descubrir claves por JWKS y validar aud en el borde se usaría el plugin openid-connect o el filtro JWT de Envoy, que sí lo hacen. El principio no cambia: el borde filtra lo evidente; el servicio decide.

servicios/borde/auditoria.py

El registro de auditoría con hash encadenado, usado tanto por los servicios (una función registrar) como por el consumidor que archiva en MinIO:

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

TOPICO = "auditoria.eventos"

class RegistroAuditoria:
    """Emite eventos de auditoría con hash encadenado a Kafka. Un emisor por proceso;
    la cadena es por emisor (campo 'origen'), y el consumidor verifica cada cadena."""
    def __init__(self, origen: str, productor: KafkaProducer):
        self.origen, self._p = origen, productor
        self._prev = "0" * 64                          # génesis de esta cadena
        self._lock = threading.Lock()

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

    def registrar(self, sub: str, accion: str, recurso: str, resultado: str,
                  jti: str | None = None, desde: str | None = None, contexto: dict | None = None):
        ev = {
            "id": str(uuid.uuid4()),
            "cuando": datetime.now(timezone.utc).isoformat(timespec="milliseconds"),
            "origen": self.origen,                     # 'pedidos', 'kong', 'spiffe://.../inventario'
            "sub": sub, "jti": jti, "accion": accion, "recurso": recurso,
            "resultado": resultado, "desde": desde,
            "contexto": contexto or {},                # ticket_id, request_id... NUNCA datos personales ni tokens
        }
        with self._lock:                               # la cadena exige orden: un evento a la vez
            ev["prev_hash"] = self._prev
            ev["hash"] = self._hash(ev)
            self._prev = ev["hash"]
            # Clave = origen: mismo origen → misma partición → orden preservado (02-04)
            self._p.send(TOPICO, key=self.origen.encode(), value=json.dumps(ev).encode())
        return ev["id"]


def verificar_cadena(eventos: list[dict]) -> tuple[bool, int | None]:
    """Recorre eventos de un mismo origen en orden; devuelve (ok, índice del primer fallo)."""
    prev = "0" * 64
    for i, ev in enumerate(eventos):
        if ev["prev_hash"] != prev or RegistroAuditoria._hash(ev) != ev["hash"]:
            return False, i
        prev = ev["hash"]
    return True, None


class ArchivadorAuditoria:
    """Consumidor: acumula eventos por origen y hora, verifica la cadena y escribe en
    km0-auditoria con object lock (retención 5 años, modo COMPLIANCE)."""
    def __init__(self, s3, bucket="km0-auditoria"):
        self._s3, self._bucket = s3, bucket
        self._lotes: dict[tuple[str, str], list[dict]] = {}

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

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

Uso en pedidos, en la ruta que 06-01 protegió con ABAC:

# km0/servicios/pedidos/api.py (fragmento)
auditoria = RegistroAuditoria("pedidos", KafkaProducer(bootstrap_servers="kafka:19092", acks="all"))

@app.get("/pedidos/<pedido_id>")
@requiere_rol("cliente", "operador", audiencia="pedidos")
def ver_pedido(pedido_id):
    pedido = repositorio.cargar(pedido_id)
    permitido = pedido is not None and ("operador" in g.claims["roles"] or pedido.cliente_id == g.claims["sub"])
    auditoria.registrar(sub=g.claims["sub"], jti=g.claims.get("jti"), accion="pedidos.leer",
                        recurso=pedido_id, resultado="PERMITIDO" if permitido else "DENEGADO",
                        desde=request.headers.get("X-Forwarded-For", request.remote_addr),
                        contexto={"request_id": request.headers.get("X-Request-Id"),
                                  "ticket_id": request.headers.get("X-Ticket-Id")})
    if not permitido:
        abort(403 if pedido is not None else 404)
    return pedido.a_json()

Y el docker-compose.yml gana el tópico y el bucket, creados con object lock desde el principio (no se puede activar después):

docker compose exec kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 \
    --create --topic auditoria.eventos --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

El intento de Marc de leer P-2026-000123 produce ahora un evento DENEGADO en auditoria.eventos, encadenado al anterior de pedidos, archivado en km0-auditoria/pedidos/2026-09-15/10.jsonl con retención de cinco años, y visible en el índice para la consulta del apartado 11. Si alguien altera el fichero (no puede, por el object lock) o intenta reescribir el tópico, verificar_cadena lo detecta.

Errores Comunes y Consejos

  • Exponer la API de administración del gateway. KONG_ADMIN_LISTEN en 0.0.0.0 es entregar la configuración del borde a Internet. Solo en localhost o en una red de gestión con mTLS.
  • Confiar en que "el gateway ya valida". Las llamadas internas no pasan por él. Cada servicio verifica el token y la identidad de servicio.
  • Rate limiting solo por IP. Un NAT corporativo bloquea a cien clientes legítimos y un atacante con mil proxies no se entera. Por identidad cuando la hay; por IP solo antes de autenticar.
  • Sin Retry-After ni cabeceras RateLimit-*. Los clientes no pueden autorregularse y reintentan en bucle, agravando el problema que se quería evitar.
  • Contador en memoria en un gateway con varias instancias. Cada instancia deja pasar el límite completo. Estado en Redis con script atómico.
  • Auditoría en el mismo log que lo operativo, con la misma retención. A los 14 días se ha borrado la prueba. Esquema propio, canal propio, retención propia.
  • Auditoría que puede editar quien la escribe. Solo escritura en Kafka (ACL), hash encadenado, object lock. Si el administrador puede borrarla, no es auditoría.
  • Datos personales o tokens en los eventos de auditoría. El registro de auditoría es a su vez un tesoro para un atacante; identificadores, no contenidos; jti, no el token.
  • Validar entrada solo en el navegador. El navegador es del usuario; el atacante no lo usa. El esquema se hace cumplir en el borde y en el servicio.
  • Dependencias sin fijar ni escanear. La vulnerabilidad más probable en Kilómetro Cero no está en su código, sino en una librería con una versión de hace dos años.
  • Consejo: guarda kong.yaml, el realm de Keycloak, las políticas de Vault y el esquema OpenAPI en el repositorio y revísalos como código: un cambio de minute: 60 a minute: 600000 debe pasar por revisión.
  • Consejo: ensaya la consulta "¿quién accedió a los datos de X?" antes de que la haga un auditor; si tarda más de unos minutos en responderse, el índice de auditoría no está bien diseñado.
  • Advertencia de cumplimiento. La retención de logs de auditoría, su contenido (que incluye identificadores de personas y direcciones IP, datos personales a su vez), el acceso a ellos y la respuesta a solicitudes de acceso o supresión están regulados por el RGPD; PCI DSS impone requisitos concretos de auditoría en cualquier componente que toque pagos. Todo lo descrito debe revisarse con un profesional de seguridad y cumplimiento.

Ejercicios

Ejercicio 1: La primera hora de la Semana de la Vendimia

A las 10:00 abre la campaña. En los primeros 60 segundos llegan al gateway 2 400 000 peticiones: 1 900 000 GET /api/v1/catalogo/* de 60 000 clientes distintos, 300 000 POST /api/v1/pedidos de 40 000 clientes, y 200 000 peticiones de 50 IPs que hacen GET /api/v1/catalogo/productos/vino-crianza en bucle. Con la configuración de kong.yaml y el lim_crear de pedidos (capacidad 5, 0,1/s), calcula aproximadamente cuántas peticiones de cada grupo llegan a los servicios y cuántas reciben 429, y qué ve un cliente legítimo que carga la página de inicio (18 peticiones al catálogo). ¿Qué cambiarías para el tercer grupo, y por qué el límite por consumidor no los frena?

Ejercicio 2: Un operador curioso

Se sospecha que un operador ha estado consultando pedidos de clientes sin motivo. Describe (a) qué consulta harías sobre el índice de auditoría y qué campos usarías, (b) cómo demostrarías que los registros no han sido alterados por el propio operador (que tiene acceso a la consola de Kafka y a MinIO como administrador), (c) qué regla de anomalía habría alertado antes, y (d) qué dos cosas del diseño de registrar hacen que la consulta no exponga a su vez los datos de los clientes.

Ejercicio 3: STRIDE para reparto

Aplica la tabla STRIDE al servicio reparto (recibe posiciones de 140 repartidores por la app, publica en reparto.posiciones, sirve el panel en tiempo real desde reparto.panel a los operadores, y muestra a Ana la posición aproximada de su repartidor). Da un ejemplo concreto por letra y la contramedida con su lección. Señala la amenaza que consideras más grave y por qué.

Soluciones

Ejercicio 1.

Grupo 1 (catálogo, 60 000 clientes, ~32 peticiones cada uno en el minuto): el límite es 200/min por consumidor, así que todas pasan (~1,9 M llegan a catalogo, que las sirve desde la caché de Redis de 04-05); un cliente que carga la página de inicio (18 peticiones) ve RateLimit-Remaining: 182 y ninguna espera. Grupo 2 (pedidos, 40 000 clientes, ~7,5 POST cada uno): Kong deja pasar 60/min por consumidor (ninguno lo alcanza), pero lim_crear en pedidos concede 5 de golpe y luego 1 cada 10 s: en un minuto, como mucho 5 + 6 = 11 por cliente; los que hicieron 7-8 pasan casi todos (5 inmediatos + 1 o 2 recargados), y una fracción recibe 429 con Retry-After: 10: unas 40 000 × ~1,5 ≈ 60 000 rechazadas y ~240 000 llegan a pedidos. Ese es el comportamiento deseado: nadie legítimo hace 8 pedidos en un minuto salvo un doble clic, que el 429 con reintento suave resuelve. Grupo 3 (50 IPs, 4 000 peticiones cada una al catálogo): limit_by: consumer no aplica porque el catálogo no exige token; Kong cae al límite por IP (200/min): pasan 50 × 200 = 10 000 y se rechazan 190 000; pero 10 000 peticiones idénticas siguen llegando y, sobre todo, esos 50 hosts pueden ser 5 000 mañana. Cambios: caché de respuesta en el gateway para GET públicos (proxy-cache: las 10 000 ni siquiera tocan catalogo), un límite más bajo por IP para rutas públicas sin token (60/min es más que suficiente para un humano), bloqueo por reputación de los rangos reincidentes y, si persiste, protección de bots en la CDN. El límite por consumidor no los frena porque no son consumidores: no se autentican, y contra tráfico anónimo solo quedan IP, huella y caché.

Ejercicio 2.

(a) SELECT recurso, cuando, contexto->>'ticket_id' FROM auditoria WHERE sub = 'mpuig' AND accion = 'pedidos.leer' AND cuando BETWEEN ... ORDER BY cuando, y después unir con los tickets de soporte: cada lectura sin ticket_id (o con un ticket que no menciona ese pedido) es sospechosa; además, contar pedidos distintos por hora y compararlo con la media de otros operadores. (b) La cadena: cada evento de origen pedidos lleva prev_hash; verificar_cadena sobre los ficheros horarios de km0-auditoria/pedidos/ detecta cualquier borrado o modificación; los ficheros están con object lock en modo compliance, así que ni como administrador de MinIO pudo alterarlos ni borrarlos; en Kafka, la ACL de auditoria.eventos solo concede escritura a los servicios y lectura al archivador, y aunque como administrador pudiera borrar el tópico, los ficheros archivados y el ancla diaria firmada (el último hash del día publicado externamente) demostrarían la manipulación; por último, su propio acceso a la consola de Kafka y a MinIO está auditado en los logs de esos sistemas. (c) "Un sub con rol operador lee > 200 pedidos distintos en una hora", o su variante más fina "lecturas sin ticket_id asociado". (d) registrar guarda el identificador del pedido (P-2026-000123) y no su contenido, y el contexto es solo request_id/ticket_id: la consulta revela qué pedidos consultó, no el teléfono ni la dirección de nadie; y el índice de auditoría tiene acceso restringido y a su vez auditado, así que la investigación deja su propio rastro.

Ejercicio 3.

Spoofing: una app modificada publica posiciones como furgoneta-3-017 sin ser ese repartidor: el token con repartidor_id (06-01, ejercicio 2) y el servicio ignora el id del cuerpo y usa el del token. Tampering: un proceso interno inyecta posiciones falsas en reparto.posiciones para que el panel muestre entregas donde no las hay: autenticación del productor en Kafka con certificado y ACL (06-04) más HMAC de la envoltura (06-02). Repudiation: un repartidor niega haber marcado como entregado el pedido P-2026-000124: auditoría de reparto.marcar_entregado con sub, jti, posición en ese momento y hash encadenado (06-05). Information disclosure: Ana ve la posición exacta de su repartidor, y con ella la casa del cliente anterior; o reparto.panel con las rutas de 140 personas es legible por cualquier operador y se filtra: mostrar a Ana solo posición aproximada y solo mientras su pedido esté "en reparto" (minimización, 06-02), retención de horas en reparto.posiciones, panel con rol operador y auditoría de acceso; las posiciones de los repartidores son datos personales de empleados con obligaciones específicas (evaluación de impacto). Denial of service: 140 apps en bucle por un bug (o una app maliciosa) publican 1 000 posiciones por segundo cada una: rate limiting por sub en el borde (una posición cada 5 s es suficiente), tamaño máximo, y reparto que descarta silenciosamente lo que exceda. Elevation of privilege: un repartidor llama a ListarEntregas de otro, o encuentra que MarcarEntregado no comprueba la asignación: ABAC en el servicio (06-01) y una prueba automática por cada regla. La más grave es la de información: es la única con daño irreversible y directo sobre personas (la dirección de Ana, la ruta diaria de un empleado); las demás se detectan y se revierten. Es también la que menos se ve en un diagrama, porque no hay un "atacante": solo un diseño que muestra más de lo necesario.

Conclusión

El borde es donde la plataforma se encuentra con el mundo, y por eso concentra las responsabilidades transversales que ningún servicio debería repetir: un API gateway (Kong, Envoy, NGINX, o uno gestionado) termina TLS, valida los tokens de Keycloak, enruta y versiona, aplica CORS y cabeceras de seguridad, hace cumplir el contrato OpenAPI, limita tamaños y tiempos, y frena los abusos con limitación de tasa. De los algoritmos, token bucket es el que mejor encaja con una API (tolera ráfagas humanas, corta a las máquinas), y en Redis con un script Lua atómico funciona para todas las instancias del gateway; 429, Retry-After y RateLimit-* le dicen al cliente cómo comportarse, y limitar por identidad (no solo por IP) hace que el límite recaiga sobre quien corresponde. La auditoría es una categoría distinta de los logs operativos: esquema fijo (quién, qué, cuándo, desde dónde, resultado), sin datos personales innecesarios, con sub y jti para correlacionar, inmutable por construcción (solo escritura en Kafka, hash encadenado, object lock en MinIO), con acceso restringido, retención larga, y reglas de anomalía que convierten el registro en alerta. Y todo ello como proceso: STRIDE antes de cada cambio, dependencias fijadas y escaneadas, revisión de configuración de seguridad como código.

Con esto se cierra el Módulo 6. Kilómetro Cero tiene ahora una respuesta explícita a "quién puede qué":

Sujeto Se autentica con Puede No puede
Ana, Marc, Lucía (cliente) Keycloak (contraseña Argon2 o Google) + JWT RS256 de 15 min; MFA opcional Ver el catálogo; crear pedidos (5 de golpe, 1 cada 10 s); ver sus pedidos y la posición aproximada de su repartidor Ver pedidos ajenos (ABAC); editar productos; más de 60 llamadas/min a pedidos
Huerta La Vega, Quesería Montblanc, Bodega Roble Alto (productor) Keycloak + MFA obligatoria; grupo con productor_id Editar sus productos y ajustar su stock; ver pedidos con sus productos; subir fotos por URL prefirmada Tocar productos de otro productor; ver clientes; reservar stock
Repartidores de furgoneta-3 (repartidor) app-repartidores (OIDC + PKCE) con repartidor_id Publicar su posición; ver sus entregas; marcar entregados sus pedidos Suplantar a otro repartidor; ver el panel completo
Jordi, Marta (operador) OpenLDAP → Keycloak, MFA obligatoria Ver cualquier pedido (auditado, con ticket); cancelar; aprobar productores; ver el panel de reparto Publicar posiciones; leer teléfonos sin que quede registro; alterar la auditoría
pedidos (spiffe://km0.internal/pedidos) Certificado mTLS de 24 h emitido por Vault PKI; token client credentials para compensaciones ReservarStock, LiberarReserva, ConsultarStock; Cobrar, Reembolsar; escribir pedidos.eventos y auditoria.eventos AjustarStock; leer reparto.posiciones; leer secretos de otros servicios en Vault
catalogo Certificado mTLS ConsultarStock; leer inventario.alertas; firmar URLs de km0-fotos Reservar, cobrar, publicar pedidos
reparto Certificado mTLS Escribir reparto.posiciones; leer pedidos.eventos; leer y descifrar teléfonos de pedidos en reparto Cancelar pedidos; leer facturas
analitica (Airflow, Spark, Flink) Certificado mTLS; AppRole en Vault; credenciales dinámicas de km0_analitica con TTL de 2 h Leer pedidos.eventos y reparto.posiciones; leer el lago seudonimizado; escribir ventas_diarias y reparto.panel Leer km0_pedidos; ver emails o teléfonos; escribir en pedidos.eventos; conservar una contraseña
Kong (borde) Certificado público ACME hacia fuera; mTLS hacia dentro Terminar TLS, verificar JWT, enrutar, limitar, registrar Decidir autorización fina; ver la administración desde Internet
Cualquiera en la red interna sin identidad Nada Nada: sin certificado no hay handshake, sin token no hay llamada

Y los controles implantados, lección a lección: contraseñas con Argon2id, JWT RS256 con claims estrictos, refresh con rotación y revocación por jti, RBAC y ABAC con mínimo privilegio (06-01); AES-GCM, HMAC de eventos, TLS en gRPC y en todos los almacenes, cifrado de campo con envelope encryption, seudonimización del lago, crypto-shredding, nada de tarjetas (06-02); Keycloak como IdP con OIDC + PKCE, client credentials, JWKS, OpenLDAP para empleados, MFA por rol, ciclo de vida centralizado (06-03); zero trust con mTLS, PKI interna, autorización servicio-a-servicio, Vault con credenciales dinámicas y auditoría de secretos (06-04); gateway, rate limiting, validación de esquemas, auditoría inmutable y STRIDE (06-05). Ninguno de ellos es infalible; juntos, en capas, hacen que comprometer una pieza no sea comprometer la plataforma.

La plataforma es segura. Pero ¿sabemos si funciona? Un 429 de más en la Semana de la Vendimia, una cadena de auditoría que se rompe, un lease de Vault que no se renueva, un certificado que caduca un domingo: nada de lo construido en seis módulos avisa por sí solo cuando va mal. Sin instrumentos, un sistema distribuido falla en silencio hasta que lo nota un cliente. El Módulo 7 trata el monitoreo y el mantenimiento de la plataforma, y empieza por el monitoreo con métricas: qué medir en cada servicio, cómo recogerlo con Prometheus, cómo definir los objetivos de nivel de servicio de Kilómetro Cero y cómo alertar antes de que Ana se entere.

Curso de Arquitecturas Distribuidas

Módulo 1: Introducción a los Sistemas Distribuidos

Módulo 2: Comunicación en Sistemas Distribuidos

Módulo 3: Consistencia y Replicación

Módulo 4: Almacenamiento Distribuido

Módulo 5: Computación Distribuida

Módulo 6: Seguridad en Sistemas Distribuidos

Módulo 7: Monitoreo y Mantenimiento

Módulo 8: Casos de Estudio y Aplicaciones

© Copyright 2026. Todos los derechos reservados