La lección anterior dejó a inventario exigiendo un token firmado antes de mover una unidad de stock. Pero ese token, el teléfono de Ana en el pedido P-2026-000123, el evento pedido.creado que viaja por pedidos.eventos y las copias de seguridad de km0_inventario en MinIO siguen circulando y descansando en claro. Cualquiera con acceso a un segmento de red, a un disco reciclado, a un volumen de Docker o a un bucket mal configurado los lee o, peor, los modifica sin que nadie lo note. Saber quién es quién no sirve si la conversación se puede escuchar y alterar.
Esta lección trata de la criptografía aplicada a una plataforma distribuida: no cómo se diseñan los algoritmos, sino qué garantiza cada uno, cuándo usar cuál y cómo integrarlo sin cometer los errores que convierten un cifrado correcto en uno inútil. Veremos cifrado simétrico y asimétrico, hashes, HMAC y firmas; TLS para los datos en tránsito, con gRPC entre pedidos e inventario como caso práctico; cifrado en reposo en disco, base de datos y columna, con envelope encryption para el teléfono de Ana; cifrado del lado del servidor en MinIO y en los backups; y la protección de datos personales: minimización, seudonimización para el lago de datos y cifrado como base del derecho al olvido. Quedan para 06-04 la autenticación mutua con certificados (mTLS) y la custodia operativa de las claves (Vault).
Contenido
- Tres propiedades: confidencialidad, integridad, autenticidad
- Cifrado simétrico: AES-GCM, claves, nonces y etiquetas
- Cifrado asimétrico y cifrado híbrido
- Hashes, HMAC y firmas digitales
- Tabla resumen: qué primitiva para qué problema
- Datos en tránsito: TLS
- TLS en gRPC entre
pedidoseinventario - TLS en PostgreSQL, Kafka, Redis y MinIO
- Datos en reposo: disco, base de datos, columna
- Envelope encryption: el teléfono de Ana
- Gestión de claves: rotación, entornos y dónde no van
- Datos personales: minimización, seudonimización, anonimización y derecho al olvido
- Mapa de protección de Kilómetro Cero
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Tres propiedades: confidencialidad, integridad, autenticidad
Cuando decimos "proteger un dato" mezclamos tres garantías distintas que se consiguen con herramientas distintas:
| Propiedad | Pregunta | Amenaza | Herramienta |
|---|---|---|---|
| Confidencialidad | ¿Solo lo lee quien debe? | Escucha en la red, disco robado, bucket público | Cifrado (simétrico, asimétrico, TLS) |
| Integridad | ¿Ha cambiado por el camino? | Alteración de un evento, de un fichero, de un precio en tránsito | Hash, MAC, firma; el tag de AES-GCM |
| Autenticidad | ¿Lo produjo quien dice? | Suplantación de pedidos en Kafka, servidor inventario falso |
MAC (con clave compartida), firma digital, certificado TLS |
Cifrar no da integridad por sí solo: con algunos modos de cifrado clásicos (AES-CBC sin MAC) un atacante puede voltear bits del texto cifrado y producir cambios controlados en el texto en claro sin conocer la clave. Por eso la práctica moderna es el cifrado autenticado (AEAD), que combina ambas en una operación. Y a las tres se suele añadir una cuarta, el no repudio (que el autor no pueda negar la autoría), que solo la firma digital proporciona, porque el MAC lo pueden generar todos los que comparten la clave.
- Cifrado simétrico: AES-GCM, claves, nonces y etiquetas
En el cifrado simétrico la misma clave cifra y descifra. Es rápido (AES tiene instrucciones dedicadas en las CPU: varios GB/s por núcleo) y es lo que se usa para todo el volumen de datos: discos, ficheros, columnas, y el cuerpo de una conexión TLS. Su problema es la distribución de la clave: los dos extremos tienen que tenerla, y hacérsela llegar de forma segura es precisamente el problema que resuelve el cifrado asimétrico.
El estándar actual es AES-256-GCM (o ChaCha20-Poly1305 donde no hay aceleración AES). Tiene cinco piezas:
- Clave: 32 bytes aleatorios. De su secreto depende todo.
- Nonce (number used once): 12 bytes que no se repiten nunca con la misma clave. No es secreto: se guarda junto al texto cifrado. Su función es que cifrar dos veces el mismo mensaje dé resultados distintos.
- Texto cifrado: del mismo tamaño que el original.
- Etiqueta de autenticación (tag): 16 bytes que garantizan la integridad. Al descifrar, si un solo bit del cifrado (o de los datos asociados) cambió, la verificación falla y no se devuelve nada.
- Datos asociados (AAD): información en claro que no se cifra pero sí se autentica: por ejemplo, el
pedido_idal que pertenece un teléfono cifrado, para que nadie pueda copiar el teléfono cifrado de Ana al pedido de Marc.
# km0/servicios/comun/cripto.py
# pip install cryptography
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def generar_clave() -> bytes:
return AESGCM.generate_key(bit_length=256) # 32 bytes de os.urandom
def cifrar(clave: bytes, texto: bytes, aad: bytes = b"") -> bytes:
nonce = os.urandom(12) # NUEVO en cada cifrado
cifrado_y_tag = AESGCM(clave).encrypt(nonce, texto, aad)
return nonce + cifrado_y_tag # el nonce viaja delante, en claro
def descifrar(clave: bytes, blob: bytes, aad: bytes = b"") -> bytes:
nonce, cifrado_y_tag = blob[:12], blob[12:]
return AESGCM(clave).decrypt(nonce, cifrado_y_tag, aad) # InvalidTag si algo cambió
if __name__ == "__main__":
k = generar_clave()
blob = cifrar(k, b"+34 600 123 456", aad=b"P-2026-000123")
print(len(blob)) # 12 + 15 + 16 = 43 bytes
print(descifrar(k, blob, aad=b"P-2026-000123")) # b'+34 600 123 456'
try:
descifrar(k, blob, aad=b"P-2026-000124") # otro pedido: el AAD no cuadra
except Exception as e:
print(type(e).__name__) # InvalidTagPor qué nunca reutilizar un nonce. GCM genera internamente un flujo de bits (keystream) a partir de clave y nonce, y hace XOR con el mensaje. Si dos mensajes se cifran con el mismo par clave-nonce, el XOR de los dos textos cifrados es igual al XOR de los dos textos en claro: el atacante elimina el cifrado sin conocer la clave, y además puede recuperar la clave de autenticación y falsificar etiquetas. Con nonces aleatorios de 96 bits, la probabilidad de colisión se vuelve preocupante hacia los 2³² mensajes con la misma clave (unos 4 000 millones): más que suficiente para una clave por servicio y por año, pero es la razón por la que las claves se rotan y por la que un contador (nonce secuencial) es preferible cuando hay un único escritor que puede garantizar que no se repite.
- Cifrado asimétrico y cifrado híbrido
En el cifrado asimétrico hay un par de claves: lo que se cifra con la pública solo se descifra con la privada, y lo que se firma con la privada lo verifica cualquiera con la pública. Resuelve la distribución: inventario publica su clave pública y cualquiera puede enviarle un secreto que solo él abre. Dos familias:
| Familia | Ejemplos | Tamaño de clave para ~128 bits de seguridad | Uso |
|---|---|---|---|
| RSA | RSA-2048, RSA-3072 | 2048-3072 bits | Firmas (RS256 de 06-01), certificados; cifrado de claves pequeñas |
| Curvas elípticas | ECDSA P-256, Ed25519 (firma), X25519 (intercambio de claves) | 256 bits | Firmas (ES256, EdDSA), intercambio de claves en TLS 1.3; más rápido y compacto |
Lo que no se hace es cifrar datos voluminosos con RSA: es cientos de veces más lento que AES y solo puede cifrar mensajes más cortos que la clave. La solución universal es el cifrado híbrido: se genera una clave simétrica aleatoria para el mensaje, se cifra el mensaje con AES-GCM, y se cifra esa clave (32 bytes) con la clave pública del destinatario. TLS hace exactamente esto en cada conexión (apartado 6), y el envelope encryption del apartado 10 es el mismo patrón aplicado al almacenamiento.
- Hashes, HMAC y firmas digitales
Un hash criptográfico (SHA-256, SHA-3, BLAKE2) resume cualquier entrada en un valor fijo de forma que es inviable encontrar la entrada a partir del hash, o dos entradas con el mismo hash. Da integridad solo si el hash llega por un canal fiable: un atacante que puede cambiar el fichero puede cambiar el hash que lo acompaña. Por eso hay dos construcciones con clave:
- HMAC (hash-based message authentication code):
HMAC(clave, mensaje). Solo quien tiene la clave puede generarlo o verificarlo. Da integridad y autenticidad entre partes que comparten un secreto; no da no repudio (cualquiera de las partes pudo generarlo). Es lo que firma las URLs prefirmadas de MinIO (04-03) y los JWT HS256. - Firma digital (RSA-PSS, ECDSA, Ed25519): hash del mensaje cifrado con la clave privada. Cualquiera verifica con la pública; solo el firmante pudo producirla. Integridad, autenticidad y no repudio. Es RS256 en 06-01 y la base de los certificados TLS.
Un caso práctico de HMAC en Kilómetro Cero: la envoltura de eventos de 02-04. Hoy cualquier proceso con acceso a Kafka puede publicar un pedido.creado falso en pedidos.eventos, o modificar un evento reproducido desde el lago. Añadir un HMAC de la envoltura, con una clave que solo tienen los productores legítimos y los consumidores, permite a inventario y analitica descartar eventos manipulados:
# km0/servicios/comun/eventos_firmados.py
import hmac, hashlib, json
def _canonico(evento: dict) -> bytes:
"""Serialización determinista: mismas claves, mismo orden, sin espacios.
Sin esto, dos JSON equivalentes darían HMAC distintos."""
sin_firma = {k: v for k, v in evento.items() if k != "firma"}
return json.dumps(sin_firma, sort_keys=True, separators=(",", ":"),
ensure_ascii=False).encode()
def firmar_evento(evento: dict, clave: bytes) -> dict:
evento["firma"] = hmac.new(clave, _canonico(evento), hashlib.sha256).hexdigest()
return evento
def verificar_evento(evento: dict, clave: bytes) -> bool:
esperada = hmac.new(clave, _canonico(evento), hashlib.sha256).hexdigest()
# compare_digest: tiempo constante, para no filtrar cuántos bytes coinciden
return hmac.compare_digest(esperada, evento.get("firma", ""))En el productor de pedidos (02-04), antes de producer.send, se llama a firmar_evento(evento, CLAVE_EVENTOS); en cada consumidor, if not verificar_evento(evento, CLAVE_EVENTOS): descartar_y_alertar(). La clave se distribuye con el gestor de secretos de 06-04. Cuando los consumidores no deban poder producir (por ejemplo, un tercero que lee reparto.panel), la envoltura se firma con Ed25519 en lugar de HMAC: mismo código con cryptography.hazmat.primitives.asymmetric.ed25519.
- Tabla resumen: qué primitiva para qué problema
| Necesidad | Primitiva | Clave | Ejemplo en Kilómetro Cero |
|---|---|---|---|
| Cifrar volumen de datos | AES-256-GCM (AEAD) | Simétrica | Teléfono de Ana en km0_pedidos; volúmenes; backups |
| Enviar una clave a alguien / acordarla | RSA-OAEP, X25519 (ECDH) | Asimétrica | Handshake TLS; envelope encryption con KMS |
| Detectar alteración con secreto compartido | HMAC-SHA256 | Simétrica | Envoltura de pedidos.eventos; URLs prefirmadas |
| Detectar alteración sin secreto compartido, con autoría | Firma Ed25519 / ECDSA / RSA-PSS | Asimétrica | JWT RS256 (06-01); certificados; eventos hacia terceros |
| Huella de un contenido | SHA-256 | Ninguna | Deduplicar fotos en km0-fotos; ETag; hash encadenado de auditoría (06-05) |
| Almacenar contraseñas | Argon2id / bcrypt | Ninguna (sal) | identidad (06-01) |
| Derivar claves de una maestra | HKDF | Simétrica | Una clave por servicio y propósito a partir de una raíz |
- Datos en tránsito: TLS
TLS (Transport Layer Security) es el protocolo que cifra y autentica una conexión TCP: HTTPS es HTTP sobre TLS, y gRPC, PostgreSQL, Kafka y Redis lo soportan de forma nativa. Combina todo lo anterior: cifrado asimétrico para acordar una clave, simétrico para el tráfico, firmas para autenticar al servidor, hashes para la integridad.
El handshake de TLS 1.3, resumido:
sequenceDiagram
participant C as Cliente (pedidos)
participant S as Servidor (inventario)
C->>S: ClientHello: versiones, suites, clave pública efímera (X25519)
S->>C: ServerHello: suite elegida, clave pública efímera
Note over C,S: Ambos derivan la misma clave simétrica (ECDH).<br/>A partir de aquí todo va cifrado.
S->>C: Certificate (cadena) + CertificateVerify (firma con la privada) + Finished
C->>C: Verifica cadena hasta una CA de confianza,<br/>que el nombre coincide (SAN), fechas, revocación
C->>S: Finished
C->>S: Datos de aplicación (gRPC) cifrados con AES-GCM / ChaCha20
Conceptos que conviene fijar:
- Certificado: la clave pública del servidor más su identidad (nombre DNS en el campo Subject Alternative Name, SAN), firmadas por una autoridad de certificación (CA). El cliente confía en la CA (tiene su certificado raíz en su almacén de confianza) y, por transitividad, en el servidor.
- Cadena de confianza: raíz → intermedia → servidor. La raíz se guarda fuera de línea; las intermedias firman los certificados de los servidores. En Internet, las raíces las distribuyen los sistemas operativos y navegadores; dentro de Kilómetro Cero, la CA será propia (esta lección la crea con OpenSSL; 06-04 la convierte en una PKI con emisión automática y certificados para clientes).
- Secreto perfecto hacia delante (forward secrecy): en TLS 1.3 la clave de sesión se acuerda con claves efímeras que se destruyen; robar la clave privada del servidor mañana no permite descifrar el tráfico capturado hoy.
- TLS 1.3 (2018) eliminó las suites débiles, redujo el handshake a un viaje de ida y vuelta y cifra el certificado del servidor. TLS 1.0 y 1.1 están obsoletos; 1.2 se acepta si está bien configurado. Configura siempre
mínimo 1.2, preferido 1.3. - Terminación: la conexión TLS puede acabar en el propio servicio o en un componente delante de él (el gateway de 06-05 terminará el TLS de Internet). Entre el terminador y el servicio, si hay red, hace falta TLS otra vez.
Lo que TLS no hace por defecto: autenticar al cliente. inventario sabrá que habla con cifrado, y pedidos sabrá que habla con el verdadero inventario, pero inventario no sabe si el cliente es pedidos o un proceso cualquiera. Eso es mTLS, y es la lección 06-04.
- TLS en gRPC entre
pedidos e inventario
pedidos e inventarioVamos a sustituir el add_insecure_port que 02-03 dejó "sin TLS por ahora". Primero, una CA interna y un certificado de servidor con OpenSSL:
# km0/certs/generar.sh (desarrollo; en producción lo hará la PKI de 06-04)
set -e
mkdir -p km0/certs && cd km0/certs
# 1. CA raíz de Kilómetro Cero (clave EC P-256, válida 10 años, solo en desarrollo)
openssl ecparam -name prime256v1 -genkey -noout -out km0-ca.key
openssl req -x509 -new -key km0-ca.key -sha256 -days 3650 \
-subj "/CN=Kilometro Cero CA interna" -out km0-ca.crt
# 2. Clave y CSR para inventario, con SAN (nombre DNS en Compose y localhost para pruebas)
openssl ecparam -name prime256v1 -genkey -noout -out inventario.key
openssl req -new -key inventario.key -subj "/CN=inventario" -out inventario.csr
cat > inventario.ext <<EOF
subjectAltName = DNS:inventario, DNS:inventario.km0.internal, DNS:localhost
extendedKeyUsage = serverAuth
EOF
# 3. La CA firma el certificado de inventario (90 días: los certificados cortos se rotan, no se vigilan)
openssl x509 -req -in inventario.csr -CA km0-ca.crt -CAkey km0-ca.key -CAcreateserial \
-days 90 -sha256 -extfile inventario.ext -out inventario.crt
openssl x509 -in inventario.crt -noout -subject -dates -ext subjectAltName
rm inventario.csr inventario.extEl detalle que más fallos produce es el SAN: el cliente compara el nombre al que se conectó (inventario, el nombre del servicio en Compose) con los del certificado; si no coincide, rechaza la conexión aunque la firma sea válida. El CN ya no cuenta para esa comprobación.
Servidor (servicios/inventario/servidor.py), solo cambia el arranque:
import grpc
from concurrent import futures
def _leer(ruta):
with open(ruta, "rb") as f:
return f.read()
credenciales = grpc.ssl_server_credentials(
private_key_certificate_chain_pairs=[(_leer("certs/inventario.key"),
_leer("certs/inventario.crt"))],
# root_certificates y require_client_auth llegan en 06-04 (mTLS)
)
servidor = grpc.server(futures.ThreadPoolExecutor(max_workers=10),
interceptors=[AuthInterceptor()]) # el de 06-01 sigue ahí
inventario_pb2_grpc.add_InventarioServicer_to_server(InventarioServicer(), servidor)
servidor.add_secure_port("0.0.0.0:50051", credenciales) # antes: add_insecure_portCliente en pedidos:
# km0/servicios/pedidos/cliente_inventario.py (fragmento)
credenciales = grpc.ssl_channel_credentials(root_certificates=_leer("certs/km0-ca.crt"))
canal = grpc.secure_channel("inventario:50051", credenciales) # el nombre debe estar en el SAN
stub = inventario_pb2_grpc.InventarioStub(canal)
# El JWT sigue viajando en metadatos, ahora cifrado por TLS:
stub.ReservarStock(req, metadata=(("authorization", f"Bearer {token}"),), timeout=2)Solo se le da al cliente la CA, no el certificado de inventario: así, cuando el certificado de inventario se renueve dentro de 90 días, pedidos seguirá confiando sin cambiar nada. Si pedidos se conecta por localhost:50051 desde el host, funciona porque localhost está en el SAN; si se conectara por IP, fallaría (Ssl handshake failed ... Hostname mismatch).
En docker-compose.yml, inventario y pedidos montan ./certs en solo lectura; el fichero km0-ca.key no se monta en ningún servicio (solo se necesita para firmar):
inventario:
build: ./servicios/inventario
volumes:
- ./certs/inventario.crt:/app/certs/inventario.crt:ro
- ./certs/inventario.key:/app/certs/inventario.key:ro
pedidos:
build: ./servicios/pedidos
volumes:
- ./certs/km0-ca.crt:/app/certs/km0-ca.crt:roPuedes comprobar que el canal va cifrado capturando el tráfico: docker compose exec pedidos tcpdump -A port 50051 mostraba antes queso-curado y el JWT en claro; ahora solo bytes ilegibles tras el ClientHello.
- TLS en PostgreSQL, Kafka, Redis y MinIO
Cada almacén de los Módulos 2 y 4 tiene su forma de activar TLS. No la desarrollamos aquí, pero sí la dejamos localizada, porque todos los clientes de Kilómetro Cero tendrán que cambiar su cadena de conexión:
| Sistema | Lado servidor | Lado cliente (Python) | Nota |
|---|---|---|---|
PostgreSQL (km0_analitica, km0_inventario) |
ssl = on, ssl_cert_file, ssl_key_file en postgresql.conf; hostssl en pg_hba.conf para obligar |
psycopg.connect(..., sslmode="verify-full", sslrootcert="certs/km0-ca.crt") |
sslmode=require cifra pero no verifica el servidor; verify-full sí |
Kafka (pedidos.eventos, reparto.posiciones...) |
Listener SSL://:9093, ssl.keystore.location, ssl.truststore.location; SASL_SSL si además hay autenticación |
KafkaProducer(security_protocol="SSL", ssl_cafile="certs/km0-ca.crt") |
Kafka usa keystores JKS/PKCS12; conviértelos con keytool/openssl pkcs12 |
| Redis (caché, sesiones, revocados) | tls-port 6379, port 0, tls-cert-file, tls-key-file, tls-ca-cert-file |
redis.Redis(ssl=True, ssl_ca_certs="certs/km0-ca.crt") o rediss:// |
Redis ≥ 6; los clientes de Redis Cluster de 04-05 también lo soportan |
Cassandra (km0_pedidos) |
client_encryption_options: enabled: true en cassandra.yaml |
Cluster(ssl_context=ctx) con un ssl.SSLContext cargando la CA |
La comunicación entre nodos se cifra aparte (server_encryption_options) |
MinIO (km0-fotos...) |
Certificados en ~/.minio/certs/public.crt y private.key |
boto3.client("s3", endpoint_url="https://minio:9000", verify="certs/km0-ca.crt") |
Las URLs prefirmadas de 04-03 pasan a https:// sin más cambios |
HDFS (/km0/eventos/...) |
dfs.http.policy=HTTPS_ONLY, dfs.encrypt.data.transfer=true |
WebHDFS por https:// |
Kerberos para autenticación es un mundo aparte, fuera del alcance del curso |
La regla común: verificar siempre el certificado del servidor contra la CA interna. Un cliente que cifra sin verificar (sslmode=require, verify=False) protege contra escuchas pasivas pero no contra un servidor impostor, que es el ataque de "hombre en el medio" que TLS existe para impedir.
- Datos en reposo: disco, base de datos, columna
Los datos pasan mucho más tiempo en reposo que en tránsito, y allí las amenazas son otras: un disco retirado sin borrar, un volumen de nube clonado, una copia de seguridad en un bucket accesible, un administrador de base de datos curioso, o un SELECT * de un servicio comprometido. Hay tres niveles, que se acumulan:
| Nivel | Qué protege | Contra qué | Contra qué no | Coste |
|---|---|---|---|---|
| Disco / volumen (LUKS, dm-crypt, cifrado de volúmenes EBS/PD) | Todo lo que está en el disco | Disco robado o reciclado, volumen clonado, acceso físico | Cualquiera con acceso al sistema en marcha: la base de datos ve los datos en claro | Casi nulo (AES-NI); transparente |
| Base de datos (TDE: transparent data encryption, cifrado de ficheros de datos y WAL) | Los ficheros de la BD y sus backups | Ficheros copiados, backups filtrados | Administradores de la BD, cualquier SELECT autorizado |
Bajo; requiere gestionar la clave fuera de la BD |
Columna / campo (la aplicación cifra antes de INSERT) |
Campos concretos: teléfono, dirección, IBAN | DBA, dumps, un servicio comprometido que lee la tabla, filtraciones de replicas y analítica | Un compromiso del servicio que posee la clave | Alto: no se puede indexar ni buscar por el campo; hay que gestionar claves por servicio |
Kilómetro Cero aplica los tres: volúmenes cifrados en toda la infraestructura (una línea de configuración en la nube o en la instalación), TDE donde el motor lo ofrezca (Cassandra y PostgreSQL con extensiones o cifrado de ficheros), y cifrado de campo para los datos personales sensibles que solo un servicio necesita en claro. El teléfono de Ana lo necesita reparto para que el repartidor llame al llegar; analitica jamás debería verlo. Con cifrado de campo y clave propia de reparto, aunque analitica lea la tabla, ve bytes.
- Envelope encryption: el teléfono de Ana
Si cada servicio cifra sus campos con una clave, ¿dónde vive esa clave? Guardarla junto a los datos anula el cifrado; guardarla en cada proceso hace la rotación imposible (habría que recifrar toda la tabla). El patrón estándar es el cifrado por sobres (envelope encryption):
- Existe una clave maestra (KEK, key encryption key) que nunca sale de un KMS (key management service): AWS KMS, Google Cloud KMS, Azure Key Vault, o el motor
transitde HashiCorp Vault. El KMS ofrece dos operaciones:cifrar(bytes)ydescifrar(bytes), con control de acceso y auditoría. - Para cada dato (o cada lote, o cada cliente) se genera una clave de datos (DEK) aleatoria, se cifra el dato con AES-GCM y la DEK, y se cifra la DEK con la KEK en el KMS.
- Se guardan juntos el dato cifrado y la DEK cifrada. La DEK en claro se descarta.
- Para leer: se pide al KMS que descifre la DEK, y con ella se descifra el dato.
flowchart LR
T[Teléfono de Ana<br/>+34 600 123 456] -->|AES-GCM con DEK| TC[Teléfono cifrado]
DEK[DEK aleatoria<br/>32 bytes] -->|KMS.cifrar con KEK| DEKC[DEK cifrada]
TC --> BD[(km0_pedidos<br/>telefono_cif, dek_cif, kek_id)]
DEKC --> BD
KMS[KMS / Vault transit<br/>KEK 'km0-pedidos-2026' nunca sale]
DEK -.generada localmente.-> DEK
La ventaja decisiva: rotar la KEK no obliga a recifrar los datos, solo a recifrar las DEK (32 bytes cada una, y puede hacerse perezosamente), y revocar el acceso de un servicio a la KEK deja todos sus datos ilegibles de golpe, aunque tenga la tabla entera. En Kilómetro Cero no tenemos un KMS de nube; en 06-04 Vault ocupará ese papel con su motor transit. Para esta lección, simulamos el KMS con una clase que expone solo cifrar_dek/descifrar_dek y guarda la KEK fuera del alcance del código de negocio:
# km0/servicios/pedidos/cifrado_campos.py
import os, base64, json
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
class KMSLocal:
"""Sustituto de desarrollo de un KMS: la KEK vive aquí y nunca se expone.
En 06-04 esta clase se reemplaza por el motor 'transit' de Vault."""
def __init__(self, kek_por_id: dict[str, bytes], kek_activa: str):
self._keks = kek_por_id
self.kek_activa = kek_activa
def cifrar_dek(self, dek: bytes) -> tuple[str, bytes]:
nonce = os.urandom(12)
return self.kek_activa, nonce + AESGCM(self._keks[self.kek_activa]).encrypt(nonce, dek, b"dek")
def descifrar_dek(self, kek_id: str, dek_cifrada: bytes) -> bytes:
kek = self._keks[kek_id] # KeyError si la KEK fue retirada: crypto-shredding
return AESGCM(kek).decrypt(dek_cifrada[:12], dek_cifrada[12:], b"dek")
class CifradorCampos:
"""Cifra campos de un registro con envelope encryption y AAD = id del registro."""
def __init__(self, kms: KMSLocal):
self._kms = kms
def cifrar(self, valor: str, registro_id: str) -> str:
dek = AESGCM.generate_key(bit_length=256)
nonce = os.urandom(12)
cifrado = AESGCM(dek).encrypt(nonce, valor.encode(), registro_id.encode())
kek_id, dek_cifrada = self._kms.cifrar_dek(dek)
sobre = {"v": 1, "kek": kek_id,
"dek": base64.b64encode(dek_cifrada).decode(),
"n": base64.b64encode(nonce).decode(),
"c": base64.b64encode(cifrado).decode()}
return json.dumps(sobre, separators=(",", ":")) # se guarda como TEXT/BLOB
def descifrar(self, sobre_json: str, registro_id: str) -> str:
s = json.loads(sobre_json)
dek = self._kms.descifrar_dek(s["kek"], base64.b64decode(s["dek"]))
return AESGCM(dek).decrypt(base64.b64decode(s["n"]), base64.b64decode(s["c"]),
registro_id.encode()).decode()
if __name__ == "__main__":
# En desarrollo, la KEK viene de una variable de entorno cargada desde el gestor de secretos (06-04)
kms = KMSLocal({"km0-pedidos-2026": base64.b64decode(os.environ["KM0_KEK_PEDIDOS"])},
kek_activa="km0-pedidos-2026")
cif = CifradorCampos(kms)
sobre = cif.cifrar("+34 600 123 456", "P-2026-000123")
print(sobre[:80], "...")
print(cif.descifrar(sobre, "P-2026-000123")) # +34 600 123 456
# Copiar el sobre al pedido de Marc no sirve: el AAD lo delata
try:
cif.descifrar(sobre, "P-2026-000124")
except Exception as e:
print("Rechazado:", type(e).__name__) # InvalidTagEn Cassandra (km0_pedidos), la columna telefono pasa a ser telefono_cif text, y en servicios/pedidos solo el código que atiende a reparto llama a descifrar. Los requisitos de consulta cambian: no se puede hacer WHERE telefono = ... sobre un campo cifrado con nonce aleatorio. Si hace falta buscar por teléfono (por ejemplo, para que soporte localice un pedido), se añade una columna telefono_hmac con HMAC(clave_indice, telefono normalizado): determinista, buscable, y no reversible. Es el mismo truco de la seudonimización del apartado 12.
Para campos de pagos, la regla es no tenerlos: el número de tarjeta (PAN) nunca llega a Kilómetro Cero; el navegador lo entrega directamente a la pasarela de pago, que devuelve un token de pago que pagos almacena. Cifrar un PAN propio es asumir PCI DSS completo; delegar es la única opción sensata para un marketplace.
- Gestión de claves: rotación, entornos y dónde no van
El cifrado desplaza el problema: de proteger datos a proteger claves, que son pocas y pequeñas. Principios:
- Nunca en el código ni en el repositorio. Ni en un
.py, ni en undocker-compose.ymlversionado, ni en una imagen de contenedor. Un secreto en Git lo es para siempre en el historial. La variable de entorno del ejemplo anterior es un paso intermedio que 06-04 sustituye por inyección desde Vault. - Una clave por entorno: desarrollo, pruebas y producción no comparten ninguna clave. Un volcado de la base de pruebas jamás debe ser descifrable con la clave de producción.
- Una clave por servicio y por propósito:
pedidosno puede descifrar los campos dereparto, y la clave de HMAC de eventos no es la de cifrado de campos. Con HKDF se derivan varias de una raíz si es necesario, pero las KEK de producción viven en el KMS. - Rotación programada: la KEK anualmente (o antes si hay sospecha), las DEK por registro (no hace falta rotarlas: se regeneran al reescribir), los certificados TLS cada 90 días o menos, la clave de firma de JWT cada pocos meses con
kid. Todo cifrado guarda el identificador de la clave con que se hizo, para poder descifrar durante la transición. - Separación de funciones: quien administra la base de datos no administra el KMS; quien despliega no ve las claves de producción.
- Copia de seguridad de las claves, cifradas y en otro lugar que los datos: unos datos cifrados cuya clave se ha perdido son datos borrados.
Lo que aquí es principio, en 06-04 será operación: Vault, credenciales dinámicas, leases y auditoría de accesos a cada clave.
- Datos personales: minimización, seudonimización, anonimización y derecho al olvido
Ana, Marc y Lucía son personas, y sus nombres, direcciones, teléfonos, correos, historial de compras y (en reparto.posiciones) la posición de los repartidores son datos personales sujetos al RGPD. La criptografía es una de las herramientas, no la única, y las decisiones de diseño importan más que los algoritmos:
- Minimización: no recoger ni conservar lo que no se necesita. El lago de datos de 04-02 guardaba en
/km0/eventos/<día>/pedidos.jsonlel evento completo, con email y dirección.analiticanecesita saber que un mismo cliente compró tres veces, no quién es. Yventas_diariasno necesita ni eso. - Seudonimización: sustituir los identificadores directos por un seudónimo estable que solo puede revertirse con información guardada aparte.
cliente_iden lugar de email es el mínimo; mejor aún, un HMAC delcliente_idcon una clave queanaliticano tiene, de modo que ni siquiera unJOINaccidental conkm0_pedidosreidentifique. Los datos seudonimizados siguen siendo personales, pero el riesgo de una fuga del lago baja radicalmente. - Anonimización: transformar los datos de forma que la reidentificación sea razonablemente imposible: agregar (ventas por producto y día, no por cliente), generalizar (ciudad en lugar de dirección; franja horaria en lugar de instante), suprimir valores raros (un único pedido de
vino-crianzaen Lleida un martes identifica a alguien aunque no haya nombre). Los agregados dekm0_analiticason anónimos; los eventos seudonimizados no. - Tokenización: reemplazar un valor por un token sin relación matemática con él, con una bóveda que guarda la correspondencia; es lo que hace la pasarela de pago con la tarjeta.
- Derecho de supresión ("al olvido"): cuando Lucía pide que se borren sus datos, hay que borrarlos de
km0_pedidos, de las réplicas, de los backups, del lago y de los tópicos de Kafka con retención larga. Borrar de un backup inmutable o de un log de Kafka es impracticable. El crypto-shredding lo resuelve: si los datos personales de Lucía se cifraron con una DEK propia de Lucía (una por cliente, guardada cifrada por la KEK), destruir esa DEK convierte todos sus datos, en todos los sitios, en ruido irrecuperable, sin tocar los backups. Es la razón de peso para elegir "una DEK por cliente" en lugar de "una por registro" en el apartado 10.
La seudonimización de eventos para el lago, en código:
# km0/servicios/analitica/seudonimizar.py
# Se ejecuta en el consumidor que escribe /km0/eventos/<día>/pedidos.jsonl (04-02),
# de modo que el lago nunca reciba identificadores directos.
import hmac, hashlib
CAMPOS_DIRECTOS = {"email", "nombre", "telefono", "direccion"} # se eliminan
CAMPOS_SEUDONIMO = {"cliente_id"} # se sustituyen
def seudonimizar(evento: dict, clave_seudonimos: bytes) -> dict:
datos = dict(evento["datos"])
for campo in CAMPOS_DIRECTOS:
datos.pop(campo, None)
for campo in CAMPOS_SEUDONIMO:
if campo in datos:
datos[campo] = hmac.new(clave_seudonimos, datos[campo].encode(),
hashlib.sha256).hexdigest()[:24]
if "direccion_entrega" in datos: # generalizar: solo el mercado
datos["mercado"] = datos.pop("direccion_entrega").get("mercado") # 'girona'
return {**evento, "datos": datos}
# Entrada: {"tipo": "pedido.creado", "datos": {"pedido_id": "P-2026-000125", "cliente_id": "u-lucia",
# "email": "[email protected]", "telefono": "+34 6...", "lineas": [...],
# "direccion_entrega": {"calle": "...", "mercado": "lleida"}}}
# Salida: {"tipo": "pedido.creado", "datos": {"pedido_id": "P-2026-000125",
# "cliente_id": "9f1c2a...b7", "lineas": [...], "mercado": "lleida"}}clave_seudonimos la custodia identidad (o el KMS), no analitica: así el equipo de datos puede contar clientes recurrentes y calcular recomendaciones (05-03) sin poder saber quién es 9f1c2a...b7. Si un día se necesita contactar con ese cliente (por ejemplo, para una retirada de producto), la petición pasa por identidad, que sí puede recalcular el HMAC de cada cliente y encontrar la correspondencia, con auditoría de quién lo pidió (06-05).
Advertencia de cumplimiento. El RGPD (y la LOPDGDD en España) exige mucho más que cifrado: base jurídica, información al interesado, registro de actividades de tratamiento, evaluación de impacto cuando hay seguimiento de posiciones, contratos con encargados (la pasarela de pago, el proveedor de nube), y notificación de brechas en 72 horas. PCI DSS regula cualquier contacto con datos de tarjeta. Lo aquí descrito es el diseño técnico; la política de retención, el alcance de la seudonimización y el procedimiento de supresión deben definirse y revisarse con un profesional de protección de datos y seguridad.
- Mapa de protección de Kilómetro Cero
Con todo lo anterior, cada dato de la plataforma queda localizado y protegido:
| Dato | Dónde vive | En tránsito | En reposo | Personal |
|---|---|---|---|---|
| Contraseñas | identidad (PostgreSQL) |
TLS | Argon2id (irreversible) | Sí |
| JWT de acceso | Memoria de la app; metadatos gRPC / Authorization |
TLS (gRPC y HTTP) | No se persiste | Contiene sub |
| Catálogo, precios, fotos | catalogo (PostgreSQL), Redis, km0-fotos |
TLS; URLs prefirmadas por HTTPS | Volumen cifrado; SSE en MinIO | No |
| Pedidos (líneas, importes) | km0_pedidos (Cassandra) |
TLS cliente-nodo y entre nodos | Volumen cifrado + TDE | Sí (vinculados a cliente_id) |
| Teléfono y dirección de entrega | km0_pedidos |
TLS | Cifrado de campo (envelope, DEK por cliente, KEK en KMS) | Sí, sensible |
| Datos de tarjeta | No se almacenan: token de la pasarela en pagos |
TLS navegador → pasarela | Token opaco | PCI DSS delegado |
| Stock | km0_inventario (inv-bcn, inv-vlc) |
TLS | Volumen cifrado | No |
Eventos (pedidos.eventos, inventario.alertas) |
Kafka | TLS + HMAC de la envoltura | Volumen cifrado; retención limitada | Sí hasta seudonimizar |
Posiciones (reparto.posiciones, reparto.panel) |
Kafka, Flink | TLS + HMAC | Retención corta (horas) | Sí (repartidores): minimizar y agregar |
| Lago de eventos | HDFS /km0/eventos/... |
HTTPS / transferencia cifrada | Volumen cifrado; seudonimizado al escribir | Seudónimos |
| Agregados | km0_analitica (PostgreSQL) |
TLS | Volumen cifrado | Anónimos |
| Facturas PDF | km0-facturas (MinIO) |
HTTPS prefirmado | SSE-S3 con clave de MinIO + object lock | Sí |
| Backups | km0-backups (MinIO) |
HTTPS | Cifrados por el proceso de backup (AES-GCM, clave del KMS) antes de subir, además de SSE | Sí |
| Configuración de etcd | etcd | TLS entre pares y clientes | Volumen cifrado; sin secretos dentro (06-04) | No |
Sobre el cifrado del lado del servidor en MinIO/S3 (SSE): el almacén cifra cada objeto al escribirlo y lo descifra al servirlo. SSE-S3 (clave gestionada por el almacén) protege contra discos y volúmenes; SSE-KMS (clave en el KMS, con auditoría por acceso) protege además contra administradores del almacén; SSE-C (el cliente envía la clave en cada petición) deja la custodia al cliente. Para km0-fotos basta SSE-S3; para km0-facturas y km0-backups, SSE-KMS y, en el caso de los backups, cifrado adicional en el cliente para que un compromiso de MinIO no los exponga.
Errores Comunes y Consejos
- Cifrar sin autenticar. AES-CBC o AES-CTR "a pelo" permiten manipular el texto cifrado. Usa siempre un modo AEAD (GCM, ChaCha20-Poly1305) o cifrado + HMAC (encrypt-then-MAC).
- Reutilizar nonces. Un nonce fijo, o derivado de algo repetible (el id del pedido), destruye AES-GCM.
os.urandom(12)por cifrado, o un contador que se persiste. - Inventar criptografía. Ni algoritmos propios ni combinaciones creativas de primitivas.
cryptographycon su API de alto nivel (AESGCM,Fernet) y protocolos estándar (TLS) son el límite de lo que un equipo de producto debe tocar. verify=Falseen desarrollo que llega a producción. Un cliente TLS que no verifica el certificado es una escucha pasiva de más. Usa la CA interna también en desarrollo.- Certificado sin SAN correcto. "Hostname mismatch" es el error de TLS más frecuente; el SAN debe contener todos los nombres por los que se accede al servicio.
- Guardar la clave junto a los datos (en la misma base de datos, en el mismo bucket, en el
docker-compose.yml). Envelope encryption con la KEK fuera, en el KMS. - Cifrar un campo y luego buscar por él. No es posible con nonce aleatorio; planifica una columna HMAC determinista si necesitas buscar.
- Comparar HMAC con
==. La comparación normal se detiene en el primer byte distinto y filtra información por tiempo;hmac.compare_digestsiempre. - Confundir seudonimizar con anonimizar. Sustituir el email por un hash sigue siendo dato personal; solo la agregación o generalización suficiente anonimiza.
- Backups en claro "porque el bucket es privado". Los buckets se hacen públicos por error con una frecuencia asombrosa. Cifra antes de subir.
- Consejo: guarda con cada dato cifrado el identificador de la clave y la versión del formato (
{"v":1,"kek":"km0-pedidos-2026",...}). Sin eso, la primera rotación de clave es una migración a ciegas. - Consejo: prueba la recuperación: un backup cifrado que nunca se ha restaurado con la clave de otro entorno es un backup que no existe.
Ejercicios
Ejercicio 1: Un evento manipulado
Un proceso desconocido dentro de la red de Kilómetro Cero publica en pedidos.eventos un evento pedido.creado para P-2026-000126 con una línea de 200 unidades de vino-crianza a 0,01 €. Explica qué ocurre en inventario y analitica (a) con el diseño de 02-04 sin cambios, (b) con TLS en Kafka pero sin HMAC, (c) con el HMAC del apartado 4. Después, indica qué protege el HMAC que TLS no protege, y viceversa, y por qué el HMAC no impide que el proceso siga publicando eventos basura en el tópico (qué lección resuelve eso).
Ejercicio 2: Lucía ejerce su derecho de supresión
Lucía pide que se borren sus datos. Enumera dónde están (usa la tabla del apartado 13) y, para cada lugar, si se puede borrar directamente, si hace falta crypto-shredding, o si el dato ya no es personal. Explica qué habría que haber decidido en el apartado 10 para que el crypto-shredding funcione, y qué queda en km0_analitica después.
Ejercicio 3: Rotar la KEK
El equipo de seguridad decide rotar km0-pedidos-2026 a km0-pedidos-2027. Describe paso a paso, usando las clases KMSLocal y CifradorCampos, qué hay que cambiar para que (1) los datos nuevos se cifren con la nueva KEK, (2) los antiguos sigan leyéndose, (3) los antiguos se migren a la nueva sin recifrar los teléfonos, y (4) la antigua se retire. ¿Qué campo del sobre hace posible el paso 2? ¿Cuántos bytes hay que recifrar por registro en el paso 3?
Soluciones
Ejercicio 1.
(a) El evento llega, inventario lo consume y reserva 200 unidades de vino-crianza del stock de la Bodega Roble Alto (o agota el stock y dispara inventario.alertas), analitica lo escribe en el lago y ventas_diarias contabiliza 2 € de ventas de vino; nadie detecta nada porque el evento es sintácticamente correcto. (b) TLS cifra el canal entre cada cliente y el broker y autentica al broker, pero no al productor: el proceso desconocido abre su propia conexión TLS válida y publica igual; solo protege contra alguien que escuche o modifique el tráfico de otro cliente. (c) Ambos consumidores calculan HMAC(clave, envoltura canónica), no coincide con el campo firma (el atacante no tiene la clave) y descartan el evento con una alerta; el stock no se toca y el lago no lo recibe. El HMAC protege la integridad y autenticidad del mensaje de extremo a extremo, incluso frente al broker, a un reproductor desde el lago o a un productor no autorizado; TLS protege la conexión (confidencialidad y que el broker es el auténtico), cosa que el HMAC no hace (el evento sigue siendo legible en el tópico). El HMAC no impide publicar porque Kafka no exige que el productor se identifique: eso requiere autenticación del cliente en el broker (SASL o certificados de cliente, es decir, mTLS y ACLs), que es la identidad de carga de trabajo de 06-04.
Ejercicio 2.
identidad (cuenta, hash de contraseña): borrado directo. km0_pedidos: líneas e importes vinculados a cliente_id: pueden borrarse o disociarse (sustituir cliente_id por un marcador), según lo que exija la obligación fiscal de conservar facturas; teléfono y dirección cifrados: crypto-shredding destruyendo la DEK de Lucía, lo que también invalida las copias en réplicas y en los backups de km0-backups, donde el borrado directo es impracticable. km0-facturas: las facturas tienen retención legal de años con object lock; no se borran, se justifica la conservación por obligación legal (y sus datos de contacto pueden estar cifrados con la misma DEK). Kafka pedidos.eventos: los eventos con datos directos dentro de la retención no se pueden editar; si la retención es corta expiran solos; si es larga, es un motivo más para que los eventos lleven los campos personales cifrados con la DEK del cliente o seudonimizados. Lago HDFS: solo contiene el seudónimo HMAC; sigue siendo dato personal mientras identidad conserve la clave y el cliente_id; al borrar la cuenta en identidad, el seudónimo deja de ser vinculable y puede argumentarse que queda anonimizado (a revisar con el profesional de protección de datos). km0_analitica: agregados anónimos, no se toca. La decisión necesaria en el apartado 10 es una DEK por cliente (no por registro), guardada cifrada por la KEK en un lugar central: destruir esa única DEK es el borrado.
Ejercicio 3.
(1) Añadir "km0-pedidos-2027": nueva_kek al diccionario de KMSLocal y poner kek_activa = "km0-pedidos-2027": cifrar_dek usará la nueva para todo sobre nuevo. (2) Nada más: descifrar_dek recibe s["kek"] del sobre y busca esa KEK en el diccionario, que sigue conteniendo la 2026. El campo "kek" del sobre es lo que lo hace posible. (3) Un proceso de migración que, para cada registro, hace dek = kms.descifrar_dek(s["kek"], s["dek"]) y s["kek"], s["dek"] = kms.cifrar_dek(dek), y guarda el sobre; el teléfono cifrado ("c") y su nonce no se tocan. Se recifran 32 bytes (la DEK) más los 12 del nonce y 16 del tag del sobre de la DEK: unos 60 bytes por registro, frente a recifrar el campo entero y, sobre todo, sin necesidad de que el proceso de migración vea los teléfonos en claro. (4) Cuando ningún sobre en la base de datos ni en los backups que se quieran mantener legibles tenga "kek": "km0-pedidos-2026", eliminar la entrada del diccionario (en Vault: deshabilitar la versión). Cualquier sobre olvidado dará KeyError, que es exactamente el comportamiento de crypto-shredding: por eso se verifica con una consulta antes de retirar.
Conclusión
Proteger un dato es garantizar tres cosas distintas, confidencialidad, integridad y autenticidad, y cada una tiene su herramienta: AES-GCM para cifrar volumen con nonce único y etiqueta de autenticación; RSA y curvas elípticas para acordar claves y firmar, siempre en modo híbrido; hashes para huellas, HMAC para integridad con secreto compartido, firmas para autoría verificable por todos. TLS reúne todas ellas para el tránsito, con certificados, cadena de confianza, SAN y TLS 1.3, y Kilómetro Cero lo tiene ahora entre pedidos e inventario con una CA interna, y localizado en la configuración de PostgreSQL, Kafka, Redis, Cassandra, MinIO y HDFS. En reposo, el cifrado se acumula por capas (volumen, base de datos, campo) y el envelope encryption con una KEK en un KMS hace posible rotar claves, revocar accesos y borrar por destrucción de clave. Los datos personales de Ana, Marc y Lucía se minimizan, se seudonimizan antes de entrar en el lago, se agregan para analítica, y se cifran con una DEK por cliente que convierte el derecho de supresión en una operación de 32 bytes. Los datos de tarjeta, sencillamente, no entran. Y todo ello bajo la revisión de un profesional de cumplimiento, porque el RGPD y PCI DSS son bastante más que criptografía.
Queda una pieza que esta lección ha dado por resuelta con un fichero .pub copiado a mano y un formulario de login propio: de dónde salen las identidades. Los clientes se registran en la web, los empleados tienen cuentas corporativas, los productores quieren entrar con su cuenta de Google, la app de repartidores necesita un flujo sin contraseña visible, y pedidos necesita un token de máquina para hablar con inventario. Gestionar todo eso servicio por servicio es inviable. La siguiente lección centraliza la identidad en un proveedor (Keycloak), conecta a los empleados con LDAP, y explica los protocolos que hacen que un token emitido allí valga en toda la plataforma: SAML, OAuth 2.0 y OpenID Connect.
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
