La lección anterior terminó con dos fragilidades. pedidos-servicio se identifica ante Keycloak con un client_secret que vive en un fichero de configuración, e inventario acepta su token sin poder saber si la conexión TLS que le llega sale del contenedor de pedidos o de un proceso cualquiera que haya copiado ese secreto. Y la contraseña de km0_analitica sigue donde la dejó 05-05: en la variable de entorno AIRFLOW_CONN_KM0_ANALITICA, visible para cualquiera que pueda hacer docker inspect o leer el docker-compose.yml. Las dos son la misma enfermedad: la red interna se sigue tratando como un lugar de confianza en el que basta con "estar dentro" para que te crean, y los secretos que permiten estar dentro están repartidos por ficheros, imágenes y entornos.
Esta lección abandona el perímetro. Primero, el principio: zero trust, cada llamada se autentica, se autoriza y se cifra, sin importar de dónde venga. Después, la herramienta para identificar servicios: certificados por carga de trabajo y mTLS, con una PKI interna que los emite y rota automáticamente, y la malla de servicios como forma de obtenerlo sin tocar el código. A continuación, la autorización servicio-a-servicio: qué puede llamar pedidos y qué solo puede leer analitica. Y por último, la gestión de secretos: qué es un secreto, por qué no puede estar en el código, la imagen ni el entorno, y cómo HashiCorp Vault lo custodia, lo entrega y, en el caso de las bases de datos, lo genera bajo demanda con caducidad. El código deja a pedidos e inventario hablando con certificados de ambos lados, a inventario autorizando por identidad de servicio, y al DAG km0_ventas_diarias entrando en km0_analitica con credenciales que Vault crea y destruye por él. El gateway del borde, la limitación de tasa y la auditoría son la lección siguiente.
Contenido
- Del perímetro a zero trust
- Identidad de carga de trabajo: certificados por servicio
- mTLS: autenticación en ambos extremos
- PKI interna: CA raíz, intermedia, emisión y rotación
- Malla de servicios: mTLS sin tocar el código
- Autorización servicio-a-servicio
- Amenazas internas y contramedidas
- Gestión de secretos: qué es un secreto y dónde no va
- HashiCorp Vault: motores, autenticación, leases y auditoría
- Inyección de secretos en los servicios
- Kilómetro Cero: PKI con OpenSSL y gRPC con mTLS
- Kilómetro Cero: Vault y credenciales dinámicas para
km0_analitica - Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Del perímetro a zero trust
El modelo clásico de seguridad es el castillo con foso: un cortafuegos separa "fuera" (hostil) de "dentro" (de confianza), y una vez dentro, los servicios se hablan libremente. La falacia 4 de 01-04, "la red es segura", es exactamente la creencia en que el foso basta. Y no basta, por razones que ya hemos visto acumularse:
- El "dentro" es enorme y heterogéneo: decenas de servicios, cientos de trabajos de Spark y Airflow, contenedores que se crean y destruyen, proveedores de nube, un portátil de desarrollador con VPN.
- Un solo componente comprometido (un
catalogocon una dependencia vulnerable, una imagen con una puerta trasera) da acceso lateral a todo:hdfs dfs -rmdesde cualquier contenedor,SELECT *enkm0_pedidos, publicar enpedidos.eventos. - Los atacantes con más impacto suelen ser internos, o externos que ya han cruzado el foso: credenciales filtradas, phishing a un operador, un proveedor con acceso.
Zero trust (confianza cero) sustituye "confío porque estás dentro" por "no confío en nadie por su posición en la red": cada petición, también entre servicios internos, debe demostrar quién la hace (autenticación), ser evaluada contra una política (autorización), y viajar cifrada. Los tres pilares ya los tenemos para usuarios (06-01, 06-03) y para el cifrado (06-02); lo que falta es que los servicios tengan identidad propia y verificable, y que nada de ello dependa de secretos esparcidos.
| Perímetro | Zero trust |
|---|---|
| Autenticación en el borde; dentro, ninguna | Autenticación en cada salto, incluidos servicio → servicio y servicio → base de datos |
| Autorización por posición en la red (IP, subred, VLAN) | Autorización por identidad (certificado, token) y política explícita |
| Cifrado hasta el borde; dentro, en claro | Cifrado en todos los enlaces (06-02) |
| Un compromiso interno se propaga lateralmente | El radio de daño es la lista de permisos de la identidad comprometida |
| Secretos compartidos y estáticos | Identidades por carga de trabajo, credenciales de corta vida, generadas bajo demanda |
- Identidad de carga de trabajo: certificados por servicio
Para que inventario sepa que le habla pedidos, pedidos necesita una identidad de carga de trabajo (workload identity): una credencial ligada al servicio, no a un humano, que no sea una contraseña copiada a mano. La forma más sólida es un certificado X.509 con la identidad del servicio en el SAN: pedidos presenta su certificado, inventario lo verifica contra la CA interna y lee el nombre.
SPIFFE (Secure Production Identity Framework for Everyone) estandariza esto: cada carga de trabajo tiene un SPIFFE ID en forma de URI, spiffe://km0.internal/pedidos, que va en el SAN del certificado (un SVID, SPIFFE Verifiable Identity Document), y SPIRE es la implementación de referencia que emite y rota esos certificados verificando en qué nodo y con qué atributos corre cada proceso (attestation). Istio y otras mallas usan SPIFFE internamente. En Kilómetro Cero usaremos un SAN DNS (pedidos.km0.internal) y un URI SPIFFE en los certificados, para que el código de verificación sirva con cualquiera de los dos.
Lo importante del certificado como identidad, frente a un client_secret:
- La clave privada se genera donde se usa y nunca viaja: la CA solo firma la clave pública. Robar la configuración no da la clave.
- Tiene caducidad corta incorporada (horas o días) y se rota automáticamente: un certificado robado caduca antes de que se pueda explotar mucho.
- La verificación no exige consultar a nadie: cadena de firmas hasta la CA, como en 06-02.
- Vincula la identidad a la conexión, no a un token que se pueda reenviar: con mTLS,
inventariosabe que el otro extremo de este socket espedidos.
- mTLS: autenticación en ambos extremos
En el TLS de 06-02, solo el servidor presentaba certificado. mTLS (mutual TLS) añade que el cliente también lo presente y que el servidor lo verifique. El handshake es el mismo con un paso más:
sequenceDiagram
participant P as pedidos (cliente)
participant I as inventario (servidor)
P->>I: ClientHello
I->>P: ServerHello + Certificate(inventario) + CertificateRequest (CAs aceptadas)
P->>P: Verifica cert de inventario: cadena → km0-ca, SAN = inventario, fechas
P->>I: Certificate(pedidos) + CertificateVerify (firma con la privada de pedidos) + Finished
I->>I: Verifica cert de pedidos: cadena → km0-ca, fechas.<br/>Extrae SAN: pedidos.km0.internal / spiffe://km0.internal/pedidos
I->>P: Finished
Note over P,I: Canal cifrado; AMBOS saben con quién hablan.<br/>La identidad del cliente queda disponible para autorizar cada RPC.
Qué comprueba cada extremo:
| Comprobación | Cliente sobre el servidor | Servidor sobre el cliente |
|---|---|---|
| Firma válida hasta una CA de confianza | Sí (km0-ca) |
Sí (km0-ca; a menudo una CA distinta de la de servidores) |
| Fechas de validez | Sí | Sí |
| Nombre | El SAN contiene el host al que se conectó (inventario) |
El SAN contiene una identidad conocida (pedidos.km0.internal); no basta con que sea válido |
| Uso de clave | extendedKeyUsage = serverAuth |
extendedKeyUsage = clientAuth |
| Revocación | CRL/OCSP, o certificados tan cortos que no haga falta | Igual |
La última fila es una decisión de diseño importante: la revocación de certificados en TLS (listas CRL, consultas OCSP) es lenta y frágil; la práctica moderna es emitir certificados de horas y rotarlos, de modo que revocar sea simplemente dejar de renovar.
mTLS convive con los tokens de 06-01, no los sustituye: el certificado dice qué servicio hace la llamada; el JWT en los metadatos dice en nombre de qué usuario. inventario verá ambos: "pedidos (certificado) reserva stock para u-ana (token)".
- PKI interna: CA raíz, intermedia, emisión y rotación
Una PKI (public key infrastructure) es el conjunto de CAs, procedimientos y herramientas que emiten, distribuyen y rotan certificados. La de 06-02 (un km0-ca.key en un directorio, firmas a mano con OpenSSL) sirve para aprender, pero no para producción, donde hacen falta:
flowchart TB
R[CA raíz km0-root<br/>fuera de línea, 10 años<br/>solo firma intermedias] --> I1[CA intermedia km0-servicios<br/>en Vault PKI o cert-manager, 1 año]
R --> I2[CA intermedia km0-clientes<br/>certificados de cliente, 1 año]
I1 --> S1[inventario.km0.internal<br/>24 h, renovado cada 16 h]
I1 --> S2[pedidos.km0.internal<br/>24 h]
I1 --> S3[reparto.km0.internal<br/>24 h]
I2 --> C1[operador-jsala<br/>certificado de cliente, 30 días]
- CA raíz fuera de línea: su clave privada solo se usa para firmar CAs intermedias, una vez al año, desde un equipo aislado; si una intermedia se compromete, se revoca esa intermedia y se emite otra sin tocar los almacenes de confianza (que contienen solo la raíz).
- CA intermedia en línea: la que firma certificados de servicios cada pocas horas. Tiene que estar automatizada y protegida (HSM en producción exigente, o Vault con su clave sellada).
- Emisión automática: el servicio (o un agente a su lado) genera un par de claves, envía un CSR con su identidad, la PKI verifica que tiene derecho a esa identidad (por su cuenta de Kubernetes, su rol AppRole, su nodo) y firma. Nadie toca
openssla mano. - Rotación automática: el agente renueva el certificado a los dos tercios de su vida, y el servicio lo recarga sin reiniciar (gRPC y Envoy lo soportan).
- Herramientas: Vault PKI (motor
pkide Vault: la CA intermedia vive en Vault y emite por API con políticas por rol), cert-manager (en Kubernetes: un recursoCertificatey el controlador lo emite y renueva, con Vault, una CA propia o Let's Encrypt como emisor), SPIRE, step-ca (smallstep). El apartado 11 hace la PKI a mano con OpenSSL para entender qué automatizan estas herramientas; el apartado 12 configura el motorpkide Vault para que sea él quien emita.
- Malla de servicios: mTLS sin tocar el código
Configurar mTLS en cada servicio, en cada lenguaje, con rotación, es trabajo repetido y propenso a error. Una malla de servicios (service mesh) lo saca del código: junto a cada servicio corre un sidecar (un proxy Envoy) que intercepta todo su tráfico entrante y saliente; el servicio habla en claro con su sidecar en localhost, y los sidecars hablan entre sí con mTLS, con certificados que el plano de control de la malla emite y rota (SPIFFE por debajo). La malla añade además políticas de autorización (apartado 6), métricas, trazas (07-02) y patrones de resiliencia (07-04) sin que pedidos sepa nada.
flowchart LR
subgraph Pod1["pedidos"]
A[pedidos.py] -->|claro, localhost| E1[Envoy sidecar]
end
subgraph Pod2["inventario"]
E2[Envoy sidecar] -->|claro, localhost| B[servidor.py]
end
E1 -->|mTLS<br/>spiffe://.../pedidos → spiffe://.../inventario| E2
CP[Plano de control<br/>Istio / Linkerd:<br/>CA, políticas, configuración] -.certificados, políticas.-> E1
CP -.-> E2
Istio (Envoy como sidecar o en modo ambient sin sidecar, plano de control istiod, políticas PeerAuthentication y AuthorizationPolicy) y Linkerd (proxy propio en Rust, más ligero, mTLS por defecto) son las dos mallas principales; Consul Connect es la de HashiCorp. Todas presuponen Kubernetes, cuyo despliegue es tema de 07-05: aquí basta con saber que la malla es la forma industrial de obtener lo que en el apartado 11 haremos a mano, y que el precio es un proxy por instancia (latencia de ~1 ms por salto, memoria) y un plano de control que operar.
Cuándo mTLS en el código y cuándo malla: con pocos servicios y un solo lenguaje, en el código (grpcio lo hace bien); a partir de una decena de servicios, o con varios lenguajes, o cuando se quieren políticas y observabilidad uniformes, la malla. Kilómetro Cero, con seis servicios en Python, lo hace en el código en esta lección y evaluará la malla al pasar a Kubernetes.
- Autorización servicio-a-servicio
Con mTLS, inventario sabe que le llama pedidos. Ahora decide si pedidos puede hacer eso. Es RBAC de 06-01 aplicado a identidades de servicio, y la política se expresa como una tabla que, en Kilómetro Cero, vive en el propio servicio (y en la malla, si la hubiera, como AuthorizationPolicy):
| Servicio llamante | inventario |
pedidos |
catalogo |
pagos |
Kafka |
|---|---|---|---|---|---|
pedidos |
ReservarStock, LiberarReserva, ConsultarStock |
— | ObtenerProducto |
Cobrar, Reembolsar |
Escribe pedidos.eventos |
catalogo |
ConsultarStock |
— | — | — | Lee inventario.alertas |
reparto |
— | ListarPorRepartidor, MarcarEntregado |
— | — | Escribe reparto.posiciones; lee pedidos.eventos |
analitica (Airflow, Flink, Spark) |
ConsultarStock (solo lectura) |
— | ObtenerProducto |
— | Lee pedidos.eventos, reparto.posiciones; escribe reparto.panel |
pagos |
— | ActualizarEstadoPago |
— | — | Escribe pagos.eventos |
Cada celda vacía es una llamada que el servidor rechaza con PERMISSION_DENIED aunque el certificado sea válido y el JWT del usuario también. Con esta tabla, un analitica comprometido puede leer stock y catálogo, y nada más: no reserva, no cobra, no publica pedidos. Es el mínimo privilegio para servicios, y su efecto se mide en el ejercicio 1.
Dos matices. Primero, la identidad de servicio y la de usuario se combinan: ReservarStock exige que el llamante sea pedidos y que el JWT tenga rol cliente u operador; pedidos no puede reservar sin un usuario detrás salvo con su token de máquina svc-pedidos (06-03), que solo vale para LiberarReserva en compensaciones. Segundo, la política debería vivir fuera del código en cuanto crezca (OPA de 06-01, o la malla), pero su semántica es siempre esta tabla.
- Amenazas internas y contramedidas
| Amenaza (dentro del perímetro) | Ejemplo en Kilómetro Cero | Contramedida | Lección |
|---|---|---|---|
| Escucha en la red interna | Un contenedor captura el JWT de Ana entre pedidos e inventario |
TLS en todos los enlaces | 06-02 |
| Suplantación de servicio | Un proceso se anuncia como inventario y recibe reservas |
Certificado de servidor verificado por el cliente | 06-02 |
| Suplantación de cliente | Un proceso con el client_secret robado llama a inventario como pedidos |
mTLS: identidad ligada a una clave privada que no viaja | 06-04 |
| Movimiento lateral | catalogo comprometido llama a pagos.Reembolsar |
Autorización servicio-a-servicio: catalogo no está en la lista |
06-04 |
| Secreto en repositorio o imagen | La contraseña de km0_analitica en docker-compose.yml |
Vault; escaneo de secretos en CI | 06-04 |
| Secreto de larga vida robado | Una contraseña de PostgreSQL válida durante años | Credenciales dinámicas con TTL | 06-04 |
| Publicación de eventos falsos | pedido.creado fabricado en pedidos.eventos |
HMAC de la envoltura + autenticación del productor en Kafka (mTLS/SASL) + ACLs | 06-02, 06-04 |
| Borrado masivo | hdfs dfs -rm -r /km0/eventos desde cualquier contenedor |
Identidad por carga de trabajo + ACLs en HDFS + object lock en backups | 06-04, 04-03 |
| Administrador con exceso de acceso | Un DBA lee teléfonos en km0_pedidos |
Cifrado de campo con clave fuera de la BD | 06-02 |
| Uso indebido no detectado | Alguien consulta 10 000 pedidos en una hora | Auditoría y detección de anomalías | 06-05 |
- Gestión de secretos: qué es un secreto y dónde no va
Un secreto es cualquier dato cuyo conocimiento da acceso: contraseñas de bases de datos, client_secret de Keycloak, claves de API de la pasarela de pago, claves de firma, claves privadas de certificados, KEK del cifrado de campos, tokens de acceso a MinIO, la clave HMAC de eventos. Kilómetro Cero acumula ya más de una docena. Y hay lugares donde no deben estar:
| Dónde suele acabar | Por qué es un problema |
|---|---|
| El código fuente / el repositorio | Queda en el historial de Git para siempre; cualquier clon lo tiene; los escáneres de GitHub encuentran miles cada día |
| La imagen del contenedor | docker history y cualquier registro al que se suba la imagen lo exponen; las imágenes se comparten entre entornos |
Variables de entorno en claro (docker-compose.yml, manifiestos) |
Visibles con docker inspect, en /proc/<pid>/environ, en volcados de error, heredadas por procesos hijos, y versionadas junto al código |
| Ficheros de configuración sin cifrar en el servidor | Copiados en backups, legibles por cualquier proceso con el mismo usuario |
| Logs | Una cadena de conexión impresa "para depurar" |
| Mensajes de chat, tickets, wikis | El clásico "te paso la contraseña por Slack" |
La contraseña analitica:analitica de AIRFLOW_CONN_KM0_ANALITICA está en tres de esas filas a la vez. Lo que se necesita es un sistema que custodie los secretos cifrados, controle quién puede leer cada uno, entregue cada secreto solo al proceso autorizado y solo mientras lo necesite, rote sin intervención y registre cada acceso. Eso es un gestor de secretos.
- HashiCorp Vault: motores, autenticación, leases y auditoría
Vault es el gestor de secretos de referencia fuera de las nubes públicas (y dentro de ellas, cuando se quiere neutralidad). Conceptos:
- Sellado: los datos de Vault están cifrados con una clave maestra que, al arrancar, hay que reconstruir con varias partes (Shamir) o con un KMS de nube (auto-unseal). Un Vault sellado no entrega nada.
- Motores de secretos (secrets engines), montados en rutas:
kv(versión 2): pares clave-valor estáticos con versionado:kv/km0/pedidos/keycloak→{client_secret: ...}.database: credenciales dinámicas. Vault tiene un usuario administrador en PostgreSQL y, cuando un cliente autorizado lo pide, ejecutaCREATE ROLE "v-airflow-abc123" WITH PASSWORD '...' VALID UNTIL '...'con losGRANTdefinidos, devuelve el usuario y la contraseña, y los destruye al expirar el lease. Cada trabajo tiene su propio usuario, efímero y auditable.pki: la CA intermedia del apartado 4:vault write pki_int/issue/km0-servicios common_name=pedidos.km0.internal ttl=24hdevuelve certificado y clave.transit: el KMS del envelope encryption de 06-02: Vault cifra y descifra DEKs con claves que nunca salen.- Otros: AWS/GCP (credenciales de nube temporales), SSH, RabbitMQ, Kubernetes.
- Métodos de autenticación: cómo un cliente demuestra a Vault quién es: AppRole (un
role_idfijo más unsecret_idde un solo uso y corta vida que se entrega en el arranque), Kubernetes (el token de la cuenta de servicio del pod, que Vault verifica contra el API server: sin secreto previo), TLS certificates, OIDC/JWT (un token de Keycloak:pedidos-serviciopodría autenticarse en Vault con él), AWS/GCP IAM, yuserpass/tokenpara humanos y desarrollo. - Políticas: HCL que dice qué rutas puede leer o escribir cada identidad:
path "kv/data/km0/pedidos/*" { capabilities = ["read"] }. Denegación por defecto. - Leases y renovación: todo secreto dinámico tiene un lease con TTL; el cliente lo renueva mientras lo necesita, o Vault lo revoca al expirar. Revocar un lease revoca la credencial en el sistema destino (el
DROP ROLEen PostgreSQL). - Rotación: para secretos estáticos,
kvguarda versiones y el servicio recarga; para la contraseña raíz de PostgreSQL que usa Vault,rotate-rootla cambia por una que solo Vault conoce. - Auditoría: un audit device escribe cada petición y respuesta (con los secretos hasheados) a fichero o syslog: quién pidió qué, cuándo, con qué política. Alimentará la auditoría de 06-05.
Alternativas: AWS Secrets Manager y Parameter Store, Google Secret Manager, Azure Key Vault (gestionados, integrados con IAM de cada nube, con rotación programada para RDS y similares); Kubernetes Secrets (objetos base64 en etcd: útiles como mecanismo de entrega al pod, pero no son un gestor: etcd debe cifrarse en reposo, no hay versionado ni credenciales dinámicas, y cualquiera con acceso al namespace los lee; el patrón habitual es que un operador, External Secrets o el Vault Agent Injector, los rellene desde Vault); SOPS (Mozilla: cifra ficheros YAML/JSON con claves de KMS o age para poder versionarlos; adecuado para configuración, no para credenciales dinámicas); Doppler, Infisical, 1Password Secrets Automation.
- Inyección de secretos en los servicios
Custodiar el secreto no basta; hay que hacérselo llegar al proceso de forma que no reaparezca en los lugares del apartado 8:
| Mecanismo | Cómo | Ventajas | Inconvenientes |
|---|---|---|---|
El servicio llama a Vault (hvac) |
El código se autentica (AppRole, Kubernetes) y lee lo que necesita al arrancar y al renovar | Control total; leases renovables; el secreto solo en memoria | El servicio depende de Vault en el arranque; código en cada servicio |
| Agente / sidecar (Vault Agent, Vault Agent Injector, External Secrets) | Un proceso auxiliar se autentica, obtiene los secretos, los escribe en un fichero temporal en memoria (tmpfs) y los renueva; el servicio solo lee el fichero |
Sin código; renovación transparente; lenguaje indiferente | Un proceso más; el servicio debe recargar cuando el fichero cambia |
| Variable de entorno inyectada al arrancar | El agente o el orquestador la rellena desde Vault justo antes de exec |
Compatible con software que solo lee env |
Todos los problemas del entorno (visible en /proc, heredada) excepto el de estar versionada; sin renovación |
| Fichero cifrado en el repositorio (SOPS) | La CI o el arranque lo descifra con una clave del KMS | Versionado y revisable | Sin dinámicos; la clave del KMS es el nuevo secreto |
La regla: preferir memoria o tmpfs a disco, ficheros a entorno, y leases renovables a valores fijos. Para Airflow, que no es código propio, el patrón es un backend de secretos: Airflow soporta VaultBackend (apache-airflow-providers-hashicorp), con el que PostgresHook(postgres_conn_id="km0_analitica") busca la conexión en Vault en lugar de en la variable de entorno; el apartado 12 lo configura y, además, usa hvac para pedir credenciales dinámicas.
- Kilómetro Cero: PKI con OpenSSL y gRPC con mTLS
Ampliamos la PKI de 06-02 con una CA de cliente conceptualmente separada (aquí, por sencillez, la misma raíz firma ambos usos, pero el certificado de cliente lleva clientAuth y un SPIFFE ID), y emitimos certificados para pedidos e inventario:
# km0/certs/emitir.sh: emite un certificado de servicio con SAN DNS + URI SPIFFE
# Uso: ./emitir.sh pedidos | ./emitir.sh inventario
set -e
S=$1
cd "$(dirname "$0")"
openssl ecparam -name prime256v1 -genkey -noout -out "$S.key" # la clave nace aquí y no sale
openssl req -new -key "$S.key" -subj "/CN=$S" -out "$S.csr"
cat > "$S.ext" <<EOF
subjectAltName = DNS:$S, DNS:$S.km0.internal, DNS:localhost, URI:spiffe://km0.internal/$S
extendedKeyUsage = serverAuth, clientAuth
keyUsage = digitalSignature
EOF
# 24 h de validez: en producción esto lo hace Vault PKI o cert-manager cada 16 h
openssl x509 -req -in "$S.csr" -CA km0-ca.crt -CAkey km0-ca.key -CAcreateserial \
-days 1 -sha256 -extfile "$S.ext" -out "$S.crt"
rm "$S.csr" "$S.ext"
openssl x509 -in "$S.crt" -noout -subject -enddate -ext subjectAltNameCada servicio recibe serverAuth y clientAuth porque casi todos son ambas cosas (pedidos es servidor para la web y cliente de inventario). El servidor inventario exige ahora certificado de cliente:
# km0/servicios/inventario/servidor.py (arranque con mTLS)
credenciales = grpc.ssl_server_credentials(
private_key_certificate_chain_pairs=[(_leer("certs/inventario.key"),
_leer("certs/inventario.crt"))],
root_certificates=_leer("certs/km0-ca.crt"), # CA con la que verificar a los CLIENTES
require_client_auth=True, # sin certificado de cliente válido, no hay handshake
)
servidor = grpc.server(futures.ThreadPoolExecutor(max_workers=10),
interceptors=[ServicioInterceptor(), AuthInterceptor()]) # primero ¿qué servicio?, luego ¿qué usuario?
servidor.add_secure_port("0.0.0.0:50051", credenciales)El cliente pedidos presenta el suyo:
# km0/servicios/pedidos/cliente_inventario.py (mTLS)
credenciales = grpc.ssl_channel_credentials(
root_certificates=_leer("certs/km0-ca.crt"), # para verificar a inventario
private_key=_leer("certs/pedidos.key"), # identidad de pedidos
certificate_chain=_leer("certs/pedidos.crt"),
)
canal = grpc.secure_channel("inventario:50051", credenciales)Y el interceptor que autoriza por identidad de servicio. gRPC expone el certificado del cliente en context.auth_context(); de él extraemos los SAN y los comparamos con la política del apartado 6:
# km0/servicios/inventario/servicio_interceptor.py
import grpc, contextvars
# Política servicio→método (la fila de 'inventario' de la tabla del apartado 6)
PERMISOS_SERVICIO = {
"spiffe://km0.internal/pedidos": {"ReservarStock", "LiberarReserva", "ConsultarStock"},
"spiffe://km0.internal/catalogo": {"ConsultarStock"},
"spiffe://km0.internal/analitica": {"ConsultarStock"},
"spiffe://km0.internal/operador": {"ConsultarStock", "AjustarStock", "ObservarCambios"},
}
_SERVICIO_ACTUAL = contextvars.ContextVar("servicio_km0")
def _abortar(codigo, detalle):
def handler(request, context):
context.abort(codigo, detalle)
return grpc.unary_unary_rpc_method_handler(handler)
def _spiffe_id_de(auth_context: dict) -> str | None:
"""auth_context es {clave: [bytes,...]}; los SAN llegan en 'x509_subject_alternative_name'."""
for san in auth_context.get("x509_subject_alternative_name", []):
s = san.decode()
if s.startswith("spiffe://"):
return s
return None
class ServicioInterceptor(grpc.ServerInterceptor):
def intercept_service(self, continuation, handler_call_details):
metodo = handler_call_details.method.rsplit("/", 1)[-1] # 'ReservarStock'
# El interceptor no tiene el contexto de la llamada; envolvemos el handler real
handler = continuation(handler_call_details)
if handler is None:
return None
def envuelto(request, context):
spiffe = _spiffe_id_de(context.auth_context())
if spiffe is None:
context.abort(grpc.StatusCode.UNAUTHENTICATED, "certificado sin SPIFFE ID")
if metodo not in PERMISOS_SERVICIO.get(spiffe, set()):
context.abort(grpc.StatusCode.PERMISSION_DENIED,
f"{spiffe} no puede invocar {metodo}")
_SERVICIO_ACTUAL.set(spiffe)
return handler.unary_unary(request, context)
return grpc.unary_unary_rpc_method_handler(
envuelto, request_deserializer=handler.request_deserializer,
response_serializer=handler.response_serializer)
def servicio_llamante() -> str:
return _SERVICIO_ACTUAL.get()(Para los métodos de streaming como ObservarCambios se envuelve handler.unary_stream de la misma forma; se omite por brevedad.) Ahora una reserva registra ambas identidades: en ReservarStock, servicio_llamante() devuelve spiffe://km0.internal/pedidos y claims_de(context)["sub"] (06-01) devuelve u-ana. Prueba el rechazo emitiendo un certificado para catalogo (./emitir.sh catalogo) y llamando a ReservarStock con él: PERMISSION_DENIED: spiffe://km0.internal/catalogo no puede invocar ReservarStock. Y sin certificado de cliente (ssl_channel_credentials solo con la CA): el handshake falla antes de llegar a ningún interceptor, con UNAVAILABLE: ... peer did not return a certificate.
En docker-compose.yml, cada servicio monta solo su clave y su certificado más la CA; km0-ca.key no se monta en ninguno. Para no reescribir certificados de 24 horas a mano, el apartado siguiente delega la emisión en Vault.
- Kilómetro Cero: Vault y credenciales dinámicas para
km0_analitica
km0_analiticaVault en docker-compose.yml
# km0/docker-compose.yml (fragmento)
vault:
image: hashicorp/vault:1.17
command: ["vault", "server", "-dev", "-dev-root-token-id=km0-dev-root",
"-dev-listen-address=0.0.0.0:8200"]
# ADVERTENCIA: el modo -dev arranca sin sellar, en memoria (todo se pierde al reiniciar),
# sin TLS y con un token raíz fijo. Sirve para aprender la API. En producción: almacenamiento
# Raft, TLS, auto-unseal con KMS, token raíz revocado tras la configuración inicial.
cap_add: [ IPC_LOCK ]
ports: [ "8200:8200" ]
postgres-analitica:
image: postgres:16
environment: { POSTGRES_USER: analitica_admin, POSTGRES_PASSWORD: cambiame, POSTGRES_DB: km0_analitica }
# analitica_admin solo lo usará Vault; después, 'rotate-root' hará que ni nosotros la sepamosConfiguración inicial, una vez (en producción esto es un script versionado, ejecutado con un token de administración que después se revoca):
export VAULT_ADDR=http://localhost:8200 VAULT_TOKEN=km0-dev-root
# 1. Secretos estáticos: el client_secret de Keycloak de pedidos (06-03) y la clave HMAC de eventos (06-02)
vault secrets enable -path=kv -version=2 kv
vault kv put kv/km0/pedidos/keycloak client_id=pedidos-servicio client_secret="$(openssl rand -base64 32)"
vault kv put kv/km0/comun/eventos hmac_key="$(openssl rand -base64 32)"
# 2. Credenciales dinámicas de PostgreSQL para km0_analitica
vault secrets enable database
vault write database/config/km0_analitica \
plugin_name=postgresql-database-plugin \
allowed_roles="airflow-escritura,spark-lectura" \
connection_url="postgresql://{{username}}:{{password}}@postgres-analitica:5432/km0_analitica?sslmode=disable" \
username="analitica_admin" password="cambiame"
vault write -f database/rotate-root/km0_analitica # Vault cambia 'cambiame' por algo que solo él sabe
vault write database/roles/airflow-escritura \
db_name=km0_analitica \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT SELECT, INSERT, DELETE ON ventas_diarias TO \"{{name}}\";" \
default_ttl="2h" max_ttl="8h"
vault write database/roles/spark-lectura \
db_name=km0_analitica \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" max_ttl="4h"
# 3. PKI: la CA intermedia que emite los certificados de servicio del apartado 11
vault secrets enable -path=pki_int pki
vault secrets tune -max-lease-ttl=8760h pki_int
vault write -format=json pki_int/intermediate/generate/internal common_name="km0 servicios CA" | jq -r .data.csr > int.csr
openssl x509 -req -in int.csr -CA certs/km0-ca.crt -CAkey certs/km0-ca.key -CAcreateserial -days 365 \
-sha256 -extensions v3_ca -out int.crt # la raíz (fuera de línea) firma la intermedia
vault write pki_int/intermediate/set-signed [email protected]
vault write pki_int/roles/km0-servicios \
allowed_domains="km0.internal" allow_subdomains=true allow_bare_domains=false \
allowed_uri_sans="spiffe://km0.internal/*" server_flag=true client_flag=true max_ttl="24h"
# 4. Políticas: qué puede leer cada identidad (denegación por defecto)
vault policy write airflow - <<EOF
path "database/creds/airflow-escritura" { capabilities = ["read"] }
path "kv/data/km0/comun/eventos" { capabilities = ["read"] }
EOF
vault policy write pedidos - <<EOF
path "kv/data/km0/pedidos/keycloak" { capabilities = ["read"] }
path "kv/data/km0/comun/eventos" { capabilities = ["read"] }
path "pki_int/issue/km0-servicios" { capabilities = ["update"] }
EOF
# 5. Autenticación AppRole: role_id fijo por servicio + secret_id de un solo uso entregado al arrancar
vault auth enable approle
vault write auth/approle/role/airflow token_policies="airflow" token_ttl=1h token_max_ttl=8h \
secret_id_ttl=10m secret_id_num_uses=1
vault write auth/approle/role/pedidos token_policies="pedidos" token_ttl=1h token_max_ttl=24h \
secret_id_ttl=10m secret_id_num_uses=1
# 6. Auditoría
vault audit enable file file_path=/vault/logs/audit.logFíjate en rotate-root: tras ese comando, la contraseña cambiame deja de valer y nadie la conoce salvo Vault. Y en secret_id_num_uses=1: el secret_id se entrega al contenedor en el arranque (por el orquestador, o por un operador la primera vez), se canjea por un token de Vault y muere. Lo que queda en la configuración del servicio es el role_id, que por sí solo no sirve de nada.
El flujo de credenciales dinámicas
sequenceDiagram
participant A as Airflow (tarea cargar_postgres)
participant V as Vault
participant PG as PostgreSQL km0_analitica
A->>V: POST /auth/approle/login (role_id, secret_id)
V-->>A: token de Vault (1 h, política airflow)
A->>V: GET /database/creds/airflow-escritura
V->>PG: CREATE ROLE "v-approle-airflow-esc-x7Kq..." PASSWORD '...' VALID UNTIL now()+2h; GRANT ...
V-->>A: {username, password, lease_id, lease_duration: 7200}
A->>PG: conexión con v-approle-airflow-esc-x7Kq / DELETE + INSERT ventas_diarias
A->>V: PUT /sys/leases/revoke (lease_id) — al terminar la tarea
V->>PG: DROP ROLE "v-approle-airflow-esc-x7Kq..."
Note over V,PG: Si la tarea muere sin revocar, Vault ejecuta el DROP al expirar el lease (2 h)
Cada ejecución de cargar_postgres entra en PostgreSQL con un usuario que no existía diez segundos antes y no existirá diez segundos después; pg_stat_activity y los logs de PostgreSQL muestran v-approle-airflow-escritura-..., distinto en cada run, con lo que "¿quién borró las filas del día 19?" tiene respuesta exacta en el log de auditoría de Vault.
Código: hvac en el DAG
# km0/dags/km0_secretos.py: acceso a Vault desde las tareas de Airflow
# pip install hvac psycopg
import os, hvac, psycopg
from contextlib import contextmanager
VAULT_ADDR = "http://vault:8200"
def cliente_vault() -> hvac.Client:
"""Se autentica por AppRole. El role_id es configuración; el secret_id llega en el
arranque del worker (fichero tmpfs escrito por el orquestador) y solo vale una vez."""
c = hvac.Client(url=VAULT_ADDR)
with open("/run/secrets/vault_secret_id") as f: # tmpfs, no variable de entorno
secret_id = f.read().strip()
c.auth.approle.login(role_id=os.environ["VAULT_ROLE_ID"], secret_id=secret_id)
assert c.is_authenticated()
return c
def leer_kv(c: hvac.Client, ruta: str) -> dict:
"""Secreto estático: p. ej. leer_kv(c, 'km0/comun/eventos')['hmac_key']"""
return c.secrets.kv.v2.read_secret_version(path=ruta, mount_point="kv")["data"]["data"]
@contextmanager
def conexion_analitica(c: hvac.Client, rol: str = "airflow-escritura"):
"""Credenciales dinámicas: usuario y contraseña nuevos, revocados al salir del bloque."""
creds = c.secrets.database.generate_credentials(name=rol)
usuario, contrasena, lease = creds["data"]["username"], creds["data"]["password"], creds["lease_id"]
print(f"[vault] credencial {usuario} (lease {creds['lease_duration']} s)")
try:
with psycopg.connect(host="postgres-analitica", dbname="km0_analitica",
user=usuario, password=contrasena, sslmode="require") as conn:
yield conn
finally:
c.sys.revoke_lease(lease_id=lease) # DROP ROLE inmediato: no esperamos al TTL
print(f"[vault] lease {lease} revocado")Y la tarea de dags/ventas_diarias.py (05-05) queda así, sin PostgresHook ni conexión configurada en el entorno:
# km0/dags/ventas_diarias.py (tarea cargar_postgres, versión con Vault)
from km0_secretos import cliente_vault, conexion_analitica
def cargar_postgres(ds, ti, **_):
filas = leer_parquet_del_dia(ds) # como en 05-05
vault = cliente_vault()
with conexion_analitica(vault, "airflow-escritura") as conn, conn.cursor() as cur:
cur.execute("DELETE FROM ventas_diarias WHERE dia = %s", (ds,))
cur.executemany("INSERT INTO ventas_diarias VALUES (%s, %s, %s, %s, %s)", filas)
total = sum(f[4] for f in filas)
conn.commit() # DELETE + INSERT en una transacción (idempotente)
ti.xcom_push(key="total", value=total)En el docker-compose.yml de Airflow desaparece la línea AIRFLOW_CONN_KM0_ANALITICA: postgresql://analitica:analitica@... y aparece VAULT_ROLE_ID (no secreto) y el montaje tmpfs de /run/secrets. Para las conexiones que Airflow gestiona por su cuenta (WebHDFS, Spark), se configura el backend airflow.providers.hashicorp.secrets.vault.VaultBackend en airflow.cfg, y PostgresHook(postgres_conn_id=...) las buscaría en kv/km0/airflow/connections/<id> de forma transparente.
Para pedidos, el mismo cliente_vault() lee kv/km0/pedidos/keycloak para el client_secret de 06-03 (que por fin no está en el código) y pide su certificado a pki_int/issue/km0-servicios con common_name=pedidos.km0.internal y uri_sans=spiffe://km0.internal/pedidos, renovándolo a las 16 horas: el emitir.sh del apartado 11 queda como referencia didáctica.
Errores Comunes y Consejos
- "Es interno, no necesita autenticación". Es la falacia 4 con otras palabras. Cada llamada entre servicios se autentica (certificado), se autoriza (política) y se cifra.
- mTLS con certificado válido pero sin comprobar el nombre. Que el cliente tenga un certificado de
km0-casolo dice que es alguien de la plataforma;analiticano debe poder reservar stock. Extrae el SAN y aplica la política. - Certificados de un año rotados a mano. Caducan un domingo a las 3:00 y nadie se acuerda. Certificados cortos con emisión y renovación automáticas (Vault PKI, cert-manager, SPIRE).
- Montar la clave de la CA en los servicios "para que se generen su certificado". La clave de la CA solo la tiene la PKI; los servicios envían un CSR.
- Vault en modo dev en producción, o con el token raíz en uso diario. El modo dev es un aula; el token raíz se revoca tras la configuración inicial.
- Un secreto en Vault y una copia "por si acaso" en el entorno. La copia es la que se filtra. Un solo lugar.
- Leases que nadie renueva ni revoca. Una tarea larga muere a mitad porque su credencial caducó; o miles de roles zombis en PostgreSQL porque nadie revocaba. Renovar en tareas largas, revocar al terminar, TTL máximo razonable.
- Kubernetes Secrets como gestor de secretos. Son un mecanismo de entrega en base64; sin cifrado de etcd, sin auditoría por acceso, sin dinámicos. Rellénalos desde Vault o usa el injector.
- Secretos en logs. Un
print(creds)para depurar acaba en Elasticsearch. Imprime el nombre de usuario y ellease_id, nunca la contraseña. - Consejo: activa un escáner de secretos en CI (gitleaks, trufflehog) y un pre-commit hook: el momento más barato para encontrar una clave en el código es antes del commit.
- Consejo: ensaya el "día malo": revoca todos los leases de un rol (
vault lease revoke -prefix database/creds/airflow-escritura) y comprueba que el DAG se recupera en el siguiente run pidiendo credenciales nuevas. Un sistema de secretos que nunca se ha rotado en producción no se sabe rotar. - Advertencia de cumplimiento. El acceso a bases de datos con datos personales (
km0_analiticacontiene agregados, perokm0_pedidosno) y su auditoría están sujetos al RGPD; los procedimientos de custodia de claves y las políticas de Vault deben revisarse con un profesional de seguridad.
Ejercicios
Ejercicio 1: catalogo comprometido
Una dependencia de catalogo incluye código malicioso que ejecuta comandos arbitrarios dentro del contenedor. Enumera qué puede hacer el atacante contra inventario, pedidos, pagos, Kafka, km0_pedidos y Vault (a) con la plataforma tal como estaba al final de 06-03 y (b) con lo implantado en esta lección. Para (b), indica exactamente qué pieza (certificado, política de inventario, política de Vault, ACL de Kafka) bloquea cada intento, y qué sí puede seguir haciendo.
Ejercicio 2: Rotar la CA intermedia
La clave de km0 servicios CA (la intermedia en Vault) se sospecha comprometida. Describe los pasos para sustituirla sin parar la plataforma, en orden, y qué ocurre con las conexiones mTLS existentes y con las nuevas en cada paso. ¿Por qué no hace falta tocar km0-ca.crt en ningún servicio? ¿Qué habría pasado si los servicios hubieran confiado directamente en el certificado de la intermedia en lugar de en la raíz?
Ejercicio 3: Flink y la clave HMAC
El consumidor de Flink de pedidos.eventos (05-04) necesita la clave HMAC de eventos (06-02) para verificar cada envoltura, y corre como un job de larga duración (semanas) en un clúster de Flink. Diseña cómo obtiene la clave de Vault (método de autenticación, política, mecanismo de inyección), cómo se entera de una rotación de la clave sin reiniciar el job, y cómo verifica eventos firmados con la clave anterior durante la transición. Compara con las credenciales dinámicas del DAG: ¿por qué aquí no sirve un lease de 2 horas?
Soluciones
Ejercicio 1.
(a) Al final de 06-03: catalogo está en la red interna y tiene la CA para verificar servidores, pero inventario no exige certificado de cliente; el atacante fabrica un token de usuario no (no tiene la clave privada de Keycloak), pero puede llamar a inventario con cualquier token que capture o, si catalogo tiene client_secret propio en el entorno, obtener un token svc-catalogo y probar ReservarStock (dependería solo de ROLES_POR_METODO); puede publicar en pedidos.eventos (Kafka sin autenticación de productor: eventos falsos, aunque el HMAC de 06-02 los haría rechazar si ya estaba implantado); puede conectarse a km0_pedidos si hay una cadena de conexión en el entorno de catalogo o si Cassandra no exige autenticación; puede leer docker-compose.yml o /proc/*/environ de su contenedor; Vault no existía. (b) Con esta lección: inventario exige mTLS, catalogo tiene certificado (spiffe://km0.internal/catalogo) y la política solo le permite ConsultarStock: ReservarStock, LiberarReserva y AjustarStock fallan con PERMISSION_DENIED (bloquea: PERMISOS_SERVICIO en ServicioInterceptor); pedidos y pagos aplican la misma tabla: catalogo no tiene ninguna celda, todo denegado; Kafka con autenticación por certificado y ACLs: catalogo solo puede leer inventario.alertas, no escribir en pedidos.eventos (bloquea: ACL del broker); km0_pedidos: catalogo no tiene credenciales (no hay cadena de conexión en su entorno; las de pedidos son dinámicas y viven en la memoria de pedidos); Vault: catalogo solo puede autenticarse con su role_id y un secret_id ya consumido; aunque obtuviera un token de Vault, la política catalogo solo le deja leer su propio kv/km0/catalogo/* y emitir certificados con su propio nombre (el rol km0-servicios debería restringirse por AppRole a allowed_domains concretos, o usar un rol por servicio; es una mejora que conviene aplicar). Lo que sí puede hacer: todo lo que catalogo puede: leer todo el catálogo y el stock (ConsultarStock), leer alertas de inventario, generar URLs prefirmadas de km0-fotos (con las credenciales de MinIO de catalogo, que deberían ser también dinámicas o de mínimo alcance), y servir contenido alterado a la web. El radio de daño ha pasado de "toda la plataforma" a "la lista de permisos de catalogo", que es la definición de zero trust.
Ejercicio 2.
(1) Generar una nueva CA intermedia en Vault (pki_int2/intermediate/generate/internal), firmarla con la raíz fuera de línea, instalarla con set-signed, y crear el rol km0-servicios en ella. (2) Cambiar la ruta de emisión que usan los servicios (o el agente) a pki_int2/issue/km0-servicios; a medida que cada servicio renueva (a las 16 h como máximo), obtiene un certificado de la nueva intermedia, cuya cadena incluye el nuevo certificado intermedio. Las conexiones existentes siguen funcionando (TLS no reverifica a mitad de sesión); las nuevas se verifican contra km0-ca.crt, que firma ambas intermedias, así que ambos tipos de certificado son válidos durante la transición. (3) Pasadas 24 horas (el max_ttl), ya no existe ningún certificado de la intermedia antigua; se revoca la intermedia antigua en la raíz (se publica en la CRL de la raíz, si se usa) y se elimina pki_int. (4) Si se quiere tolerancia cero, se puede además añadir la intermedia comprometida a una lista de rechazo en los servidores durante la ventana. No hace falta tocar km0-ca.crt porque los servicios confían en la raíz, y la raíz no se ha comprometido: cualquier intermedia firmada por ella es aceptable, y el certificado de servicio lleva la cadena completa en el handshake. Si hubieran confiado en la intermedia directamente, habría que actualizar el almacén de confianza de cada servicio antes de emitir con la nueva, y con dos intermedias simultáneas durante la transición: exactamente el despliegue coordinado que la jerarquía raíz-intermedia evita.
Ejercicio 3.
Autenticación: el job de Flink corre en un clúster (YARN, Kubernetes o standalone); si es Kubernetes, método kubernetes de Vault con la cuenta de servicio del job (sin secreto previo); si no, AppRole con secret_id entregado por el orquestador al lanzar el job. Política: path "kv/data/km0/comun/eventos" { capabilities = ["read"] } y nada más (Flink no necesita PostgreSQL ni PKI, salvo su propio certificado para Kafka, que sería pki_int/issue/km0-servicios). Inyección: Vault Agent como sidecar (o contenedor auxiliar) que renueva el token, escribe hmac_key en un fichero tmpfs y lo reescribe en cada cambio de versión del secreto en kv; el job lee el fichero en un RichFunction.open() y lo vigila (un hilo que comprueba el mtime cada minuto, o el propio agente que envía SIGHUP). Rotación: el kv v2 guarda versiones; la rotación consiste en escribir la nueva clave como versión N+1 sin borrar la N, y el fichero inyectado contiene ambas (hmac_key y hmac_key_anterior, o un pequeño JSON con kid → clave); los productores firman con la nueva e incluyen un kid en la envoltura; el consumidor verifica con la clave que indique kid, aceptando la anterior durante la retención del tópico (7 días, por ejemplo) y rechazándola después. Comparación: la credencial dinámica del DAG es una identidad temporal para una tarea de minutos; un lease de 2 horas obligaría al job de Flink a reautenticarse y recargar constantemente, y no tiene sentido como modelo: la clave HMAC es un secreto compartido de larga vida que cambia por rotación programada, no una credencial por ejecución. Lo que sí es de corta vida es el token de Vault con el que el agente lee el secreto, y ese sí se renueva cada hora.
Conclusión
Zero trust es la respuesta definitiva a la falacia 4: la red no es segura, así que ningún salto se fía de su posición en ella; cada llamada se autentica, se autoriza y se cifra. La identidad de un servicio es un certificado de corta vida cuya clave nunca sale del proceso, con un nombre (DNS o SPIFFE ID) en el SAN, y mTLS hace que ambos extremos de cada conexión sepan con quién hablan; una PKI con raíz fuera de línea, intermedias en línea y emisión automática (Vault PKI, cert-manager, SPIRE) convierte la rotación en algo que ocurre solo, y una malla de servicios (Istio, Linkerd) lo ofrece sin tocar el código cuando los servicios se multiplican. Sobre esa identidad, la autorización servicio-a-servicio reduce el radio de daño de cualquier compromiso a una fila de una tabla. Los secretos que quedan (contraseñas de bases de datos, claves de API, claves HMAC) salen del código, de las imágenes y de las variables de entorno para vivir en Vault: motores kv, database, pki y transit, autenticación AppRole o Kubernetes, políticas de denegación por defecto, leases con TTL y revocación, auditoría de cada acceso, e inyección en memoria o tmpfs. Kilómetro Cero tiene ahora pedidos e inventario con mTLS y ServicioInterceptor autorizando por SPIFFE ID, Vault en docker-compose.yml, y el DAG km0_ventas_diarias entrando en km0_analitica con un usuario que Vault crea para cada ejecución y destruye al terminar; la contraseña que 05-05 dejó en AIRFLOW_CONN_KM0_ANALITICA ya no existe, y la de analitica_admin solo la conoce Vault.
Todo esto protege el interior. Pero la plataforma tiene un exterior: Internet, desde donde llegan los navegadores de Ana, Marc y Lucía, la app de 140 repartidores, y también los bots, los robos de credenciales y las 40 000 peticiones por segundo de la primera hora de la Semana de la Vendimia. Hasta ahora cada servicio ha hecho su propia terminación TLS, su propia verificación de tokens y su propia defensa. La última lección del módulo construye el borde: una puerta de enlace única que termina TLS, valida tokens, enruta, limita la tasa por cliente con un token bucket en Redis, y deja registro inmutable de quién hizo qué, para cerrar el módulo con la tabla completa de quién puede qué en Kilómetro Cero.
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
