En 07-01 dejamos una suposición sin garantía: que el token de Ana, las cabeceras X-Usuario-* que pone el gateway y las llamadas de servicio-pedidos a Catálogo y Clientes viajan por una red en la que nadie escucha ni suplanta. Un JWT bien verificado no sirve de nada si alguien puede leerlo en tránsito y reutilizarlo, y una cabecera X-Usuario-Roles: admin es fiable solo si nadie más que el gateway puede llegar al servicio. Esta lección asegura el canal: qué amenazas hay en tránsito, cómo se cifra el borde con TLS y certificados de Let's Encrypt gestionados por cert-manager, qué opciones hay para cifrar y autenticar la comunicación entre servicios (mTLS por aplicación o por service mesh, cerrando lo que 05-05 dejó nombrado), cómo se protegen RabbitMQ y las bases de datos, y cómo se garantiza la integridad de los webhooks de la pasarela de pago. Termina con la decisión de TechCorp para su primera fase y una tabla que resume, canal a canal, amenaza, medida y dónde se configura. La verificación de tokens es de 07-01, la validación de entrada de 07-03 y los secretos, RBAC y NetworkPolicies en detalle de 07-04.
Aviso. Los certificados, algoritmos y configuraciones de esta lección son didácticos y cambian con el tiempo. La configuración TLS real (versiones, cipher suites, HSTS, mTLS) debe revisarse con un profesional de seguridad y, en el ámbito de pagos, con quien responda del cumplimiento (PCI DSS).
Contenido
- Amenazas en tránsito y superficie de un sistema distribuido
- TLS en el borde: certificados, cadena de confianza y versiones
- TLS en el
Ingresscon cert-manager y Let's Encrypt - HSTS, redirección y cómo comprobar el TLS del borde
- TLS interno y mTLS entre servicios: por qué y con qué opciones
- mTLS por service mesh:
PeerAuthenticationyAuthorizationPolicy - La decisión de TechCorp para la fase 1
- Seguridad de RabbitMQ y de las bases de datos
- Integridad de mensajes y webhooks: HMAC, timestamp y nonce
- Protección de las cabeceras internas y gRPC con TLS
- Tabla resumen: canal, amenaza, medida y dónde se configura
- Amenazas en tránsito y superficie de un sistema distribuido
Todo lo que viaja por un cable o una red virtual puede sufrir cuatro cosas:
| Amenaza | Qué hace el atacante | Ejemplo en TechCorp | Contramedida |
|---|---|---|---|
| Escucha (eavesdropping) | Lee el tráfico | Captura el Authorization: Bearer de Ana en una wifi pública, o PEDIDOS_DB_URL en el clúster |
Cifrado (TLS) |
| Manipulación (tampering) | Cambia el contenido en ruta | Modifica cantidad: 1 → cantidad: 100 o el importe de un webhook de pago |
Integridad (TLS, firma HMAC) |
| Suplantación (spoofing) | Se hace pasar por una de las partes | Un pod malicioso responde como servicio-catalogo o llama a Inventario "como si fuera Pedidos" |
Autenticación del canal (certificados, mTLS) |
| Replay | Reenvía un mensaje legítimo capturado | Repite el webhook pago.confirmado de ped-88213 para confirmar otro pedido |
Timestamp + nonce, idempotencia |
En el monolito había un solo canal externo (navegador → servidor) y una llamada a la base de datos. En el sistema distribuido, la superficie se multiplica: navegador → Ingress → gateway → seis servicios → PostgreSQL/MongoDB/RabbitMQ/Keycloak → pasarela de pago externa. La tentación es dividir el mundo en perímetro (hostil, se cifra) y red interna (de confianza, texto plano). El principio zero trust dice lo contrario: ninguna red es de confianza por estar "dentro"; cada llamada se autentica y se cifra como si viniera de Internet. TechCorp lo adopta como dirección, y en el apartado 7 decide hasta dónde llega en la primera fase.
- TLS en el borde: certificados, cadena de confianza y versiones
TLS resuelve las tres primeras amenazas en un canal: el cliente verifica la identidad del servidor con su certificado, ambos acuerdan claves de sesión y a partir de ahí todo va cifrado y con integridad. Los conceptos mínimos:
- Un certificado X.509 liga un nombre (
api.techcorp.example) a una clave pública, y lo firma una autoridad de certificación (CA). El navegador confía en el certificado porque confía en la CA (o en la CA intermedia firmada por una raíz que ya trae): esa es la cadena de confianza. Un certificado autofirmado no tiene cadena: sirve en local, nunca en el borde. - Versiones: TLS 1.3 es la actual (más rápida, sin cipher suites débiles); TLS 1.2 se mantiene por compatibilidad. TLS 1.0/1.1 y SSL están prohibidos. En Kubernetes lo fija el ingress controller (
ssl-protocols: TLSv1.2 TLSv1.3en el ConfigMap de ingress-nginx). - Terminación TLS: en TechCorp el TLS del exterior termina en el Ingress (05-02); del Ingress al gateway y del gateway a los servicios el tráfico va por la red del clúster. Es la práctica habitual: certificados públicos en un único sitio; el interior se protege con las medidas de los apartados 5-7.
flowchart LR
N[Navegador / app] -- "HTTPS (TLS 1.3, Let's Encrypt)" --> I[Ingress nginx<br/>termina TLS]
I -- "HTTP, red del clúster" --> G[gateway 8080]
G -- "HTTP + JWT + X-Usuario-*" --> P[servicio-pedidos 3002]
P -- "HTTP + token de servicio" --> C[servicio-catalogo 3001]
P -- "amqps:// (5671)" --> R[(RabbitMQ)]
P -- "sslmode=verify-full" --> D[(PostgreSQL)]
PS[pasarela de pago] -- "HTTPS + firma HMAC" --> I
subgraph red["Red del clúster: privada + NetworkPolicy (fase 1); mTLS con mesh (fase 2)"]
I
G
P
C
R
D
end
- TLS en el
Ingress con cert-manager y Let's Encrypt
Ingress con cert-manager y Let's Encryptcert-manager es un operador de Kubernetes que pide certificados a una CA, los guarda en un Secret y los renueva solos antes de caducar. Con Let's Encrypt (gratuita, certificados de 90 días, validación por reto HTTP-01) es la solución estándar. Instalación y emisor:
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.3/cert-manager.yaml# techcorp/plataforma/k8s/cert-manager/clusterissuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer # visible desde todos los namespaces (Issuer sería solo del suyo)
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory # en pruebas: acme-staging-v02… (certificados no confiables, sin límites de cuota)
email: [email protected] # avisos de caducidad
privateKeySecretRef: { name: letsencrypt-prod-cuenta } # clave de la cuenta ACME, la crea cert-manager
solvers:
- http01:
ingress: { ingressClassName: nginx } # reto: Let's Encrypt pide http://api.techcorp.example/.well-known/acme-challenge/…Y el Ingress del gateway de 05-02, con la línea tls: que dejamos comentada y la anotación que activa cert-manager:
# k8s/gateway/base/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gateway
namespace: techcorp
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod # cert-manager crea un Certificate y rellena el Secret
nginx.ingress.kubernetes.io/ssl-redirect: "true" # 308 de http:// a https:// (apartado 4)
nginx.ingress.kubernetes.io/proxy-body-size: 2m
spec:
ingressClassName: nginx
tls:
- hosts: [api.techcorp.example]
secretName: api-techcorp-tls # Secret tipo kubernetes.io/tls con tls.crt y tls.key
rules:
- host: api.techcorp.example
http:
paths:
- path: /
pathType: Prefix
backend: { service: { name: gateway, port: { number: 8080 } } }kubectl get certificate -n techcorp # api-techcorp-tls READY True (tarda ~1 min la primera vez)
kubectl describe certificate api-techcorp-tls -n techcorp | grep -A3 "Not After" # caduca en 90 días; se renueva a los 60Lo que ocurre por debajo: cert-manager ve la anotación, crea un recurso Certificate, genera una clave privada, resuelve el reto HTTP-01 publicando un Ingress temporal, recibe el certificado y lo escribe en api-techcorp-tls; ingress-nginx lo recarga. Renovación: 30 días antes de caducar repite el proceso sin intervención. Y como toda automatización falla alguna vez (cuota de Let's Encrypt agotada, DNS roto), la alerta CertificadoCaduca de 06-05 (probe_ssl_earliest_cert_expiry con Blackbox Exporter, umbral 14 días) es la red: si salta, la renovación lleva 16 días fallando y el runbook plataforma/certificados dice qué mirar (kubectl describe challenge -n techcorp).
Los mismos recursos sirven para auth.techcorp.example (Keycloak, 07-01) y shop.techcorp.example; con Traefik como ingress controller (03-04) la anotación cambia de nombre pero cert-manager es el mismo.
- HSTS, redirección y cómo comprobar el TLS del borde
- Redirección HTTP→HTTPS:
ssl-redirect: "true"devuelve308a cualquierhttp://. Es necesaria pero insuficiente: la primera petición en claro ya puede ser interceptada. - HSTS (
Strict-Transport-Security): la cabecera que dice al navegador "durante N segundos, ni intentes HTTP con este dominio". Se activa en el ConfigMap de ingress-nginx (hsts: "true",hsts-max-age: "31536000",hsts-include-subdomains: "true") o desde el gateway conhelmet(07-03). Cuidado conpreload: es difícil de revertir. - Comprobar:
openssl s_client -connect api.techcorp.example:443 -servername api.techcorp.example </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
# subject=CN = api.techcorp.example
# issuer=C = US, O = Let's Encrypt, CN = R11
# notBefore=Aug 15 09:12:00 2026 GMT / notAfter=Nov 13 09:11:59 2026 GMT
curl -sv https://api.techcorp.example/api/v1/productos?ids=p-501 -o /dev/null 2>&1 | grep -E "SSL connection|subject|expire|strict-transport"
# * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
# < strict-transport-security: max-age=31536000; includeSubDomains
curl -sI http://api.techcorp.example/ | head -1 # HTTP/1.1 308 Permanent RedirectHerramientas externas como SSL Labs (Qualys) puntúan la configuración completa; TechCorp exige "A" antes de cada auditoría.
- TLS interno y mTLS entre servicios: por qué y con qué opciones
Dentro del clúster hoy todo va en claro: gateway → servicio-pedidos:3002, Pedidos → servicio-catalogo:3001, con el JWT de Ana y las cabeceras X-Usuario-* a la vista. ¿Es un problema? Si un atacante consigue ejecutar un pod en el clúster (una imagen comprometida, 07-04) o acceso a un nodo, puede escuchar ese tráfico, suplantar a un servicio y falsificar X-Usuario-* para llamar directamente a un servicio saltándose el gateway. La red interna es una barrera, no una garantía.
mTLS (TLS mutuo) añade a TLS la autenticación del cliente: ambos extremos presentan certificado y cada uno verifica el del otro contra una CA interna. El resultado es cifrado + identidad de servicio en cada conexión: servicio-inventario sabe criptográficamente que quien llama es servicio-pedidos, sin fiarse de cabeceras. Las opciones:
| Opción | Cómo | Coste | Cuándo |
|---|---|---|---|
| mTLS en la aplicación | Cada servicio arranca con https.createServer y requestCert: true; certificados de una CA interna montados en el pod |
Emitir, montar, rotar y revocar certificados en seis servicios; código en cada uno | Pocos servicios y sin mesh; para entenderlo |
| mTLS por service mesh | El sidecar (Istio/Linkerd) cifra y autentica; certificados emitidos y rotados por el plano de control | Adoptar el mesh (05-05) | Cuando el mesh ya está o hay requisito de mTLS obligatorio |
| NetworkPolicy (complemento, no sustituto) | Reglas de red: quién puede abrir conexión con quién | Bajo | Siempre; detalle en 07-04. Limita, pero no cifra ni autentica |
Para entender qué hace el mesh por nosotros, la versión "a mano" en Node.js. Con una CA interna (ca.crt) y un certificado por servicio (tls.crt/tls.key, emitidos por cert-manager con un Issuer de tipo ca, montados desde un Secret en /etc/tls):
// servicio-inventario/src/servidor.js (fragmento didáctico: mTLS por aplicación)
const https = require('node:https');
const fs = require('node:fs');
const opciones = {
key: fs.readFileSync('/etc/tls/tls.key'),
cert: fs.readFileSync('/etc/tls/tls.crt'),
ca: fs.readFileSync('/etc/tls/ca.crt'), // solo se aceptan clientes con certificado firmado por la CA interna
requestCert: true, // exigir certificado al cliente
rejectUnauthorized: true, // y rechazar la conexión si no es válido
minVersion: 'TLSv1.2'
};
https.createServer(opciones, app).listen(3006);
// Y en el servicio, un middleware que lee la identidad del certificado del cliente:
app.use((req, res, next) => {
const cert = req.socket.getPeerCertificate();
req.servicioLlamante = cert?.subject?.CN; // "servicio-pedidos.techcorp.svc"
if (req.servicioLlamante !== 'servicio-pedidos.techcorp.svc') return res.status(403).end(); // solo Pedidos reserva stock
next();
});Y el cliente (crearClienteHttp de 06-03) necesitaría un Agent de https con su key, cert y ca. Funciona, y muestra el precio: cada servicio gestiona ficheros de certificado, hay que rotarlos (cert-manager ayuda) y reiniciar o recargar al rotar, la CA interna es un secreto crítico, y el CN pasa a ser un contrato más. Con seis servicios es asumible; con veinte, no.
- mTLS por service mesh:
PeerAuthentication y AuthorizationPolicy
PeerAuthentication y AuthorizationPolicyCon Istio (05-05) el mismo resultado son dos recursos, sin tocar código ni certificados: istiod emite un certificado SPIFFE por ServiceAccount (spiffe://cluster.local/ns/techcorp/sa/servicio-pedidos), lo rota cada 24 h y los sidecars negocian mTLS entre sí.
# techcorp/plataforma/k8s/mesh/peer-authentication.yaml
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: techcorp
spec:
mtls: { mode: STRICT } # PERMISSIVE durante la migración (acepta claro y mTLS); STRICT cuando todos tienen sidecar
---
# techcorp/plataforma/k8s/mesh/authz-inventario.yaml
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: inventario-solo-desde-pedidos
namespace: techcorp
spec:
selector: { matchLabels: { app: servicio-inventario } }
action: ALLOW # con al menos una regla ALLOW, todo lo no listado queda denegado
rules:
- from:
- source: { principals: ["cluster.local/ns/techcorp/sa/servicio-pedidos"] } # identidad del certificado, no IP
to:
- operation: { methods: ["POST", "DELETE"], paths: ["/v1/reservas", "/v1/reservas/*"] }
- from:
- source: { principals: ["cluster.local/ns/monitoring/sa/prometheus"] }
to:
- operation: { methods: ["GET"], paths: ["/metrics"] }Esto cierra lo que 05-05 dejó nombrado: la identidad viene del certificado emitido al ServiceAccount (07-04 crea uno por servicio precisamente para esto), y la política se lee como negocio: "a Inventario solo le habla Pedidos, y Prometheus solo lee métricas". Un pod intruso sin sidecar ni identidad recibe RBAC: access denied del proxy de Inventario antes de que la petición llegue a Node. Linkerd tiene su equivalente (Server + AuthorizationPolicy/MeshTLSAuthentication), con mTLS activado por defecto.
- La decisión de TechCorp para la fase 1
Marta y el equipo de Plataforma aplican el criterio de 05-05 (sin mesh en la primera fase) y deciden, para la comunicación interna:
| Medida | Estado en fase 1 | Motivo |
|---|---|---|
| TLS en el borde con cert-manager/Let's Encrypt, HSTS, redirección | Sí | Barato, imprescindible, un solo sitio |
| Red del clúster privada (nodos sin IP pública, API server restringido) | Sí | Base de la nube/proveedor |
| NetworkPolicies deny-all + reglas explícitas (07-04) | Sí | Limita quién habla con quién sin certificados |
| Tokens client credentials en las llamadas internas (07-01) | Sí | Identidad de servicio a nivel de aplicación, ya implementada |
| Doble verificación del JWT en cada servicio (07-01) | Sí | Las cabeceras X-Usuario-* no se aceptan como única fuente |
| TLS a RabbitMQ y a las bases de datos (apartado 8) | Sí | Son los canales con credenciales y datos personales |
| mTLS entre servicios | Pospuesto | Coste de certificados por aplicación; llegará con el mesh |
Disparador para reevaluar: la auditoría del área de Pagos prevista para la fase 2. Si exige mTLS obligatorio entre servicios (probable en el perímetro de servicio-pagos), la vía será un mesh —Linkerd como primera opción por su mTLS automático, coherente con 05-05— y no certificados por aplicación. Mientras, la lección del apartado 5 sirve para saber qué se está comprando.
- Seguridad de RabbitMQ y de las bases de datos
RabbitMQ. Tres reglas:
- Canal cifrado:
amqps://(puerto 5671) con certificado del servidor (emitido por cert-manager con la CA interna, o el de Let's Encrypt si el broker tiene nombre público). ElRABBITMQ_URLde 04-03 pasa aamqps://pedidos:…@rabbitmq:5671/techcorpy el esquema de zod aceptaamqps. Conamqplib,connect(url, { ca: [fs.readFileSync('/etc/tls/ca.crt')] }). - Un usuario por servicio con permisos mínimos, nunca
guest(que además solo puede conectar desdelocalhost). Los permisos de RabbitMQ son tres expresiones regulares por vhost:configure(declarar),write(publicar / enlazar),read(consumir). Parapedidos, según el contrato de 03-02:
# Ejecutado por Plataforma al provisionar; contraseñas generadas y guardadas en el Secret pedidos-rabbitmq (07-04)
rabbitmqctl add_vhost techcorp
rabbitmqctl delete_user guest
rabbitmqctl add_user pedidos "$RABBITMQ_PEDIDOS_PASSWORD"
rabbitmqctl set_permissions -p techcorp pedidos \
"^(pedidos\.saga|pedidos\.clientes)(\.reintento|\.dlq)?$" \
"^(techcorp\.eventos|(pedidos\.saga|pedidos\.clientes)(\.reintento|\.dlq)?)$" \
"^(techcorp\.eventos|techcorp\.eventos\.dlx|(pedidos\.saga|pedidos\.clientes)(\.reintento|\.dlq)?)$"Es decir: pedidos configura solo sus colas (y sus .reintento/.dlq de 06-03); escribe en el exchange techcorp.eventos (publicar) y en sus colas (enlazarlas y reencolar en .reintento); y lee de sus colas y de los dos exchanges (RabbitMQ exige read sobre el exchange para enlazar una cola a él). No puede consumir de inventario.pedidos ni publicar directamente en la cola de otro. Un pedidos comprometido no puede leer eventos de Pagos.
- Contraseñas en Secrets (
pedidos-rabbitmq), rotadas (07-04), y la interfaz de gestión (15672) sin exponer fuera del clúster, con su propio usuarioadministrator.
Bases de datos. La misma lógica en PostgreSQL y MongoDB:
- TLS obligatorio:
PEDIDOS_DB_URL=postgres://svc_pedidos:…@postgres:5432/techcorp?sslmode=verify-full&sslrootcert=/etc/tls/ca.crt(requirecifra pero no verifica el certificado;verify-fulltambién evita la suplantación del servidor); en el servidor,ssl = onyhostsslenpg_hba.confpara rechazar conexiones en claro. En MongoDB,tls=true&tlsCAFile=…en la URL ynet.tls.mode: requireTLS. - Usuarios
svc_*con privilegios mínimos por esquema, exactamente como en 02-04:svc_pedidossolo tieneUSAGEySELECT/INSERT/UPDATE/DELETEsobre el esquemapedidos, ningúnCREATE(las migraciones delJobde 05-02 usan un usuario aparte,mig_pedidos, con más privilegios y solo durante el despliegue), ysvc_pagosno puede leerpedidos.*aunque compartan instancia. - Bases de datos no expuestas fuera del clúster o de la VPC; en servicios gestionados, sin IP pública y con la red del clúster en la lista de permitidos.
- Integridad de mensajes y webhooks: HMAC, timestamp y nonce
servicio-pagos recibe webhooks de la pasarela de pago externa (POST /v1/webhooks/pasarela) que le dicen "el cobro ch_7f3a… de 79,70 € se ha confirmado". Esa ruta debe estar expuesta a Internet (a través del gateway o de un Ingress propio), y TLS solo garantiza el canal, no quién envía: cualquiera que conozca la URL podría enviar un pago.confirmado falso. Las pasarelas resuelven esto con una firma HMAC: un secreto compartido (PASARELA_WEBHOOK_SECRET, en el Secret pagos-pasarela), y en cada webhook una cabecera con t=<timestamp>,v1=<HMAC-SHA256(t + "." + cuerpo)>.
// servicio-pagos/src/rutas/webhooks.js
const { createHmac, timingSafeEqual } = require('node:crypto');
const TOLERANCIA_MS = 5 * 60_000; // 5 minutos: más allá, replay o reloj roto
// Nota: esta ruta necesita el cuerpo CRUDO (Buffer): express.raw({ type: 'application/json' }) en vez de express.json()
enrutador.post('/v1/webhooks/pasarela', express.raw({ type: 'application/json', limit: '100kb' }), async (req, res, next) => {
try {
const firma = Object.fromEntries((req.get('Pasarela-Signature') ?? '').split(',').map((p) => p.split('='))); // { t, v1 }
const t = Number(firma.t);
if (!t || Math.abs(Date.now() - t * 1000) > TOLERANCIA_MS) throw new ErrorNegocio('WEBHOOK_INVALIDO', 'timestamp fuera de ventana', 400);
const esperado = createHmac('sha256', config.PASARELA_WEBHOOK_SECRET).update(`${t}.`).update(req.body).digest('hex');
const recibido = Buffer.from(firma.v1 ?? '', 'hex');
if (recibido.length !== 32 || !timingSafeEqual(Buffer.from(esperado, 'hex'), recibido)) { // comparación en tiempo constante
req.log.warn({ ip: req.ip }, 'webhook con firma inválida');
throw new ErrorNegocio('WEBHOOK_INVALIDO', 'firma no válida', 401);
}
const evento = JSON.parse(req.body); // solo ahora se confía en el cuerpo
const nuevo = await repositorio.registrarWebhook(evento.id); // INSERT … ON CONFLICT DO NOTHING sobre webhooks_recibidos(evento_id)
if (!nuevo) return res.status(200).end(); // replay o reintento de la pasarela: idempotente, 200 sin repetir efectos
await procesarPago(evento); // publica pago.confirmado / pago.rechazado (saga de 02-05)
res.status(200).end();
} catch (err) { next(err); }
});Tres capas: la firma prueba que lo envió quien tiene el secreto y que el cuerpo no cambió; el timestamp acota la ventana de replay; y el identificador del evento como nonce persistido (webhooks_recibidos) hace inocuo cualquier reenvío, igual que procesarUnaVez en los consumidores de 04-04. La misma técnica sirve para los webhooks que TechCorp envía (a un socio, a un ERP): firma con secreto por destinatario, y el receptor verifica.
Para los eventos internos por RabbitMQ no se firma cada mensaje: el canal ya es amqps con usuario autenticado y permisos por cola (apartado 8), y la idempotencia por eventoId (02-05) neutraliza duplicados y replays. Firmar mensajes tendría sentido si el broker fuera compartido con terceros.
- Protección de las cabeceras internas y gRPC con TLS
Dos cierres breves:
- Cabeceras internas. En 07-01 el gateway ya elimina cualquier
X-Usuario-*entrante antes de poner las suyas. Hay que hacer lo mismo conX-Forwarded-For/X-Real-IP(el Ingress las fija; el gateway no debe aceptar las del cliente para el rate limit de 03-04:app.set('trust proxy', 1)confía solo en el salto del Ingress) y con cualquier cabecera que un servicio use como "de confianza". Regla: una cabecera interna es tan fiable como el conjunto de quienes pueden alcanzar el puerto —por eso las NetworkPolicies de 07-04 y, en su día, mTLS. - gRPC (03-03): usa HTTP/2 y se cifra igual. Servidor con
grpc.ServerCredentials.createSsl(caCert, [{ private_key, cert_chain }], /* checkClientCertificate */ true)y cliente congrpc.credentials.createSsl(caCert, key, cert); con un mesh, el sidecar lo hace y el código usacreateInsecure()hacialocalhost. Solo mención: TechCorp no expone gRPC en la fase 1.
- Tabla resumen: canal, amenaza, medida y dónde se configura
| Canal | Amenaza principal | Medida | Dónde |
|---|---|---|---|
| Navegador/app → Ingress | Escucha, suplantación del sitio | TLS 1.2/1.3, Let's Encrypt, HSTS, redirección | Ingress (tls:, anotaciones), ClusterIssuer, ConfigMap ingress-nginx |
| Ingress → gateway → servicios | Escucha interna, cabeceras falsificadas | Red privada + NetworkPolicy + doble verificación JWT; mTLS con mesh en fase 2 | 07-01, 07-04, PeerAuthentication/AuthorizationPolicy |
| Servicio → servicio (síncrono) | Suplantación del llamante | Client credentials (07-01); AuthorizationPolicy por identidad con mesh |
crearProveedorToken, mesh |
| Servicio → RabbitMQ | Escucha de credenciales/eventos, publicar/consumir sin permiso | amqps://, usuario por servicio, permisos por vhost/exchange/cola, sin guest |
rabbitmqctl set_permissions, Secret pedidos-rabbitmq |
| Servicio → PostgreSQL/MongoDB | Escucha, suplantación del servidor, acceso a datos ajenos | sslmode=verify-full, requireTLS, usuarios svc_* por esquema |
URL de conexión, pg_hba.conf, roles de 02-04 |
Pasarela → servicio-pagos (webhook) |
Suplantación, manipulación, replay | HMAC-SHA256 con timingSafeEqual, ventana de 5 min, webhooks_recibidos |
rutas/webhooks.js, Secret pagos-pasarela |
Cabeceras internas (X-Usuario-*, X-Forwarded-*) |
Inyección desde el exterior | Limpieza en el gateway, trust proxy acotado |
gateway/servidor.js |
| gRPC | Igual que HTTP | createSsl o sidecar |
Código o mesh |
Errores Comunes y Consejos
- Terminar TLS en el Ingress y creer que "todo está cifrado". El interior va en claro; hay que decidirlo conscientemente (apartado 7) y compensarlo con red y autenticación.
- Certificados renovados a mano. Alguien se olvida, el sitio cae un domingo. cert-manager + la alerta
CertificadoCaduca; y probar la renovación con el emisor de staging de Let's Encrypt antes de agotar la cuota del de producción. sslmode=requirepensando que verifica el certificado. Solo cifra;verify-fulles el que impide la suplantación del servidor de base de datos.guest/guesten RabbitMQ o un único usuariotechcorpcon.*en los tres permisos. Un servicio comprometido lo lee todo. Un usuario por servicio con expresiones regulares acotadas.- Verificar la firma HMAC sobre el cuerpo ya parseado y reserializado:
JSON.stringify(req.body)rara vez reproduce los bytes originales y la firma falla (o peor, se relaja). Cuerpo crudo conexpress.rawen esa ruta. - Comparar firmas con
===. Filtra información por tiempo;timingSafeEqualsiempre. PeerAuthentication STRICTsin sidecar en todos los pods (o con Prometheus fuera del mesh): todo deja de hablar (05-05).PERMISSIVEprimero.- Consejo: guarda en el runbook
plataforma/certificadoslos comandos de los apartados 3 y 4; y en cadaSecretTLS, quién lo emite y cuándo caduca (kubectl get certificate -A).
Ejercicios
Ejercicio 1. Un desarrollador de Notificaciones propone que, para no depender de cert-manager en local, todos los entornos usen sslmode=disable con PostgreSQL "porque la base de datos está dentro del clúster". Explica qué amenazas quedan abiertas y da una configuración por entorno coherente con la tabla de 04-03.
Ejercicio 2. Escribe los tres permisos de RabbitMQ (configure, write, read) para el usuario inventario, sabiendo que Inventario consume de inventario.pedidos (eventos pedido.creado y pedido.cancelado), publica stock.reservado/stock.rechazado/stock.liberado en techcorp.eventos, y tiene sus colas .reintento y .dlq. Explica qué no puede hacer con ellos.
Ejercicio 3. La pasarela reenvía el mismo webhook tres veces (reintentos por timeout) y, además, un atacante captura uno y lo reenvía dos horas después. Recorre el código del apartado 9 y di qué ocurre en cada uno de los cuatro casos y qué efecto tiene en la saga.
Soluciones
Solución 1. Sin TLS, cualquier proceso con acceso a la red del clúster (pod comprometido, nodo, sniffer en un switch virtual mal configurado) lee las credenciales svc_notificaciones en el handshake y todos los datos (correos y nombres de clientes: datos personales) en cada consulta; y nada impide que un servidor falso responda como postgres. Configuración coherente con la columna de entornos de 04-03: local sslmode=disable (PostgreSQL en Docker Compose, sin datos reales), staging y producción sslmode=verify-full&sslrootcert=/etc/tls/ca.crt, con hostssl en pg_hba.conf para que el servidor rechace conexiones en claro aunque un servicio se equivoque. El valor va en el Secret notificaciones-db, así que el código no cambia entre entornos; solo la URL.
Solución 2.
rabbitmqctl set_permissions -p techcorp inventario \
"^inventario\.pedidos(\.reintento|\.dlq)?$" \
"^(techcorp\.eventos|inventario\.pedidos(\.reintento|\.dlq)?)$" \
"^(techcorp\.eventos|techcorp\.eventos\.dlx|inventario\.pedidos(\.reintento|\.dlq)?)$"configure: solo declara su cola y las auxiliares. write: publica en techcorp.eventos (los stock.*) y en sus colas (enlazarlas, reencolar en .reintento). read: consume de sus colas y puede enlazarlas a los dos exchanges. No puede: consumir de pedidos.saga, pagos.pedidos o cualquier cola de otro (ni siquiera puede declararlas para enlazarlas); declarar colas fuera de su prefijo; ni borrar o redeclarar el exchange (configure no lo incluye). Un inventario comprometido puede publicar eventos falsos stock.reservado (por eso Pedidos valida la carga y la correlaciona con su reserva pendiente), pero no lee pagos ni pedidos.
Solución 3. (1) Primer webhook: firma válida, t dentro de ventana, evento.id nuevo → se procesa, se publica pago.confirmado, la saga avanza. (2) Reintentos 2 y 3 de la pasarela (segundos después): firma y t válidos, pero registrarWebhook devuelve nuevo=false → 200 sin efectos: la pasarela deja de reintentar y la saga no ve duplicados (aunque los viera, procesarUnaVez los descartaría por eventoId). (3) Reenvío del atacante dos horas después: la firma es válida (es el mensaje original) pero |Date.now() - t| > 5 min → 400 WEBHOOK_INVALIDO antes de mirar nada; y si el atacante intentara "refrescar" t, la firma dejaría de coincidir porque t forma parte de lo firmado. (4) Si el atacante además hubiera podido esperar menos de 5 minutos: el evento.id ya está registrado → 200 sin efectos. En ningún caso se confirma dos veces ni se confirma un pedido distinto (el evento.id y el pedidoId van dentro del cuerpo firmado).
Conclusión
La comunicación de TechCorp ya no descansa en la fe en la red interna. El borde va cifrado con TLS 1.2/1.3 y certificados de Let's Encrypt que cert-manager (ClusterIssuer letsencrypt-prod, anotación cert-manager.io/cluster-issuer, Secret api-techcorp-tls) emite y renueva, con HSTS, redirección 308 y la alerta CertificadoCaduca como red; hemos visto qué es mTLS y cuánto cuesta hacerlo por aplicación (https.createServer con requestCert), y cómo un mesh lo regala con PeerAuthentication STRICT y una AuthorizationPolicy que solo deja a servicio-pedidos hablar con Inventario —cerrando lo que 05-05 dejó nombrado—; la decisión de fase 1 es TLS en el borde + red privada + NetworkPolicies + client credentials + doble verificación del JWT, con mTLS pospuesto hasta la auditoría de Pagos y Linkerd como primer candidato; RabbitMQ va por amqps:// con un usuario por servicio y permisos por cola (pedidos solo publica en techcorp.eventos y consume pedidos.saga/pedidos.clientes), las bases de datos con sslmode=verify-full y usuarios svc_*; los webhooks de la pasarela llegan firmados con HMAC-SHA256, con ventana de 5 minutos y webhooks_recibidos contra el replay; y el gateway limpia las cabeceras internas antes de propagar las suyas. Con la identidad (07-01) y el canal resueltos, la siguiente pregunta es qué hace cada servicio con lo que recibe: cómo valida la entrada, cómo evita inyecciones, cómo trata los datos personales y cómo se audita. Ese es el tema de la siguiente lección: prácticas de seguridad.
Curso de Microservicios
Módulo 1: Introducción a los Microservicios
- Conceptos Básicos de Microservicios
- Ventajas y Desventajas de los Microservicios
- Comparación con la Arquitectura Monolítica
- Cuándo Adoptar Microservicios: Criterios de Decisión
- El Caso Práctico del Curso: la Tienda Online de TechCorp
Módulo 2: Diseño de Microservicios
- Principios de Diseño de Microservicios
- Descomposición de Aplicaciones Monolíticas
- Definición de Bounded Contexts
- Gestión de Datos: una Base de Datos por Servicio
- Consistencia Distribuida: Sagas, CQRS y Event Sourcing
Módulo 3: Comunicación entre Microservicios
- APIs RESTful
- Mensajería Asíncrona
- Protocolos de Comunicación: gRPC, GraphQL
- API Gateway y Backend for Frontend
- Descubrimiento de Servicios y Balanceo de Carga
- Contratos y Versionado de APIs
Módulo 4: Implementación de Microservicios
- Elección de Tecnologías y Herramientas
- Desarrollo de un Microservicio Simple
- Gestión de Configuración
- Integración Práctica: Consumir APIs y Publicar Eventos
- Pruebas en Microservicios: Unitarias, de Integración y de Contrato
Módulo 5: Despliegue y Orquestación
- Contenedores y Docker
- Orquestación con Kubernetes
- CI/CD para Microservicios
- Estrategias de Despliegue: Rolling, Blue-Green y Canary
- Service Mesh: Istio y Linkerd
Módulo 6: Monitoreo y Mantenimiento
- Monitoreo y Logging
- Trazabilidad Distribuida con OpenTelemetry
- Gestión de Errores y Recuperación
- Escalabilidad y Rendimiento
- SLOs, Alertas y Gestión de Incidentes
Módulo 7: Seguridad en Microservicios
- Autenticación y Autorización
- Seguridad en la Comunicación
- Prácticas de Seguridad
- Seguridad en Contenedores y Kubernetes
