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
- El borde de la plataforma: gateway frente a balanceador
- Qué hace un API gateway
- Opciones: Kong, Envoy Gateway, NGINX, gateways gestionados; BFF
- Limitación de tasa: por qué y contra qué
- Algoritmos: token bucket, leaky bucket, ventana fija, ventana deslizante
- Token bucket distribuido en Redis con Lua
- Cabeceras, límites por identidad y cuotas
- Protección adicional en el borde
- Auditoría: qué registrar y por qué no es un log más
- Logs de auditoría inmutables: hash encadenado y object lock
- Auditoría de acceso a datos personales y detección de anomalías
- Seguridad como proceso: STRIDE, vulnerabilidades y dependencias
- Kilómetro Cero: Kong en
docker-compose.yml,limitador.pyyauditoria.py - Errores Comunes y Consejos
- Ejercicios
- Conclusión
- 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.
- 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).
- 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).
- 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,
pedidosse 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
catalogoa 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).
- 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]
- 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 recargadosDetalles que importan:
time.time()lo pasa el cliente, noredis.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) conEXPIRE: los clientes inactivos no ocupan memoria. costepermite 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/clusterdel 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 respCinco 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.
- Cabeceras, límites por identidad y cuotas
429 Too Many Requestses 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 variantesX-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
subdel token (Ana, osvc-integrador-x) → porclient_idde 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:/loginpor IP y por cuenta destino; el resto porsub. - Límites distintos por ruta y rol:
GET /api/catalogogeneroso (200/min) y cacheable;POST /api/pedidosestricto; 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
/loginy en operaciones de pago.
- 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.
- 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:
subdel JWT yjti(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-000123sí; 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).
- 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:
- 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. - 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_hashpor 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. - Almacenamiento con bloqueo de objetos: un consumidor dedicado escribe los eventos en lotes (una hora, un día) en el bucket
km0-auditoriade 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)]
- 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 |
- 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.
- Kilómetro Cero: Kong en
docker-compose.yml, limitador.py y auditoria.py
docker-compose.yml, limitador.py y auditoria.pyKong 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-auditoriaEl 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_LISTENen0.0.0.0es entregar la configuración del borde a Internet. Solo enlocalhosto 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-Afterni cabecerasRateLimit-*. 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 deminute: 60aminute: 600000debe 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
- Conceptos Básicos de Sistemas Distribuidos
- Modelos de Sistemas Distribuidos
- Ventajas y Desafíos de los Sistemas Distribuidos
- Las Falacias de la Computación Distribuida
- Tiempo, Relojes y Ordenación de Eventos
- Del Monolito a la Plataforma Distribuida: el Caso Kilómetro Cero
Módulo 2: Comunicación en Sistemas Distribuidos
- Protocolos de Comunicación
- RPC y RMI
- gRPC y Serialización de Datos
- Mensajería y Colas de Mensajes
- Patrones de Comunicación Asíncrona
Módulo 3: Consistencia y Replicación
- Modelos de Consistencia
- El Teorema CAP y PACELC
- Algoritmos de Consenso
- Replicación de Datos
- Transacciones Distribuidas y Sagas
Módulo 4: Almacenamiento Distribuido
- Particionado de Datos y Hashing Consistente
- Sistemas de Archivos Distribuidos
- Almacenamiento de Objetos
- Bases de Datos Distribuidas
- Cachés Distribuidos
Módulo 5: Computación Distribuida
- Modelos de Computación Distribuida
- MapReduce y Hadoop
- Spark y Computación en Memoria
- Procesamiento de Flujos de Datos
- Planificación de Trabajos y Pipelines de Datos
Módulo 6: Seguridad en Sistemas Distribuidos
- Autenticación y Autorización
- Cifrado y Protección de Datos
- Gestión de Identidades
- Seguridad entre Servicios: mTLS y Gestión de Secretos
- Puertas de Enlace, Limitación de Tasa y Auditoría
Módulo 7: Monitoreo y Mantenimiento
- Monitoreo de Sistemas Distribuidos
- Logs Centralizados y Trazabilidad Distribuida
- Gestión de Fallos y Recuperación
- Patrones de Resiliencia: Timeouts, Reintentos y Circuit Breaker
- Automatización y Orquestación
- Pruebas en Sistemas Distribuidos e Ingeniería del Caos
