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

  1. Amenazas en tránsito y superficie de un sistema distribuido
  2. TLS en el borde: certificados, cadena de confianza y versiones
  3. TLS en el Ingress con cert-manager y Let's Encrypt
  4. HSTS, redirección y cómo comprobar el TLS del borde
  5. TLS interno y mTLS entre servicios: por qué y con qué opciones
  6. mTLS por service mesh: PeerAuthentication y AuthorizationPolicy
  7. La decisión de TechCorp para la fase 1
  8. Seguridad de RabbitMQ y de las bases de datos
  9. Integridad de mensajes y webhooks: HMAC, timestamp y nonce
  10. Protección de las cabeceras internas y gRPC con TLS
  11. Tabla resumen: canal, amenaza, medida y dónde se configura

  1. 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: 1cantidad: 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.

  1. 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.3 en 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

  1. TLS en el Ingress con cert-manager y Let's Encrypt

cert-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 60

Lo 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.

  1. HSTS, redirección y cómo comprobar el TLS del borde

  • Redirección HTTP→HTTPS: ssl-redirect: "true" devuelve 308 a cualquier http://. 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 con helmet (07-03). Cuidado con preload: 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 Redirect

Herramientas externas como SSL Labs (Qualys) puntúan la configuración completa; TechCorp exige "A" antes de cada auditoría.

  1. 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.

  1. mTLS por service mesh: PeerAuthentication y AuthorizationPolicy

Con 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.

  1. 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 Barato, imprescindible, un solo sitio
Red del clúster privada (nodos sin IP pública, API server restringido) Base de la nube/proveedor
NetworkPolicies deny-all + reglas explícitas (07-04) Limita quién habla con quién sin certificados
Tokens client credentials en las llamadas internas (07-01) Identidad de servicio a nivel de aplicación, ya implementada
Doble verificación del JWT en cada servicio (07-01) Las cabeceras X-Usuario-* no se aceptan como única fuente
TLS a RabbitMQ y a las bases de datos (apartado 8) 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.

  1. Seguridad de RabbitMQ y de las bases de datos

RabbitMQ. Tres reglas:

  1. 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). El RABBITMQ_URL de 04-03 pasa a amqps://pedidos:…@rabbitmq:5671/techcorp y el esquema de zod acepta amqps. Con amqplib, connect(url, { ca: [fs.readFileSync('/etc/tls/ca.crt')] }).
  2. Un usuario por servicio con permisos mínimos, nunca guest (que además solo puede conectar desde localhost). Los permisos de RabbitMQ son tres expresiones regulares por vhost: configure (declarar), write (publicar / enlazar), read (consumir). Para pedidos, 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.

  1. 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 usuario administrator.

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 (require cifra pero no verifica el certificado; verify-full también evita la suplantación del servidor); en el servidor, ssl = on y hostssl en pg_hba.conf para rechazar conexiones en claro. En MongoDB, tls=true&tlsCAFile=… en la URL y net.tls.mode: requireTLS.
  • Usuarios svc_* con privilegios mínimos por esquema, exactamente como en 02-04: svc_pedidos solo tiene USAGE y SELECT/INSERT/UPDATE/DELETE sobre el esquema pedidos, ningún CREATE (las migraciones del Job de 05-02 usan un usuario aparte, mig_pedidos, con más privilegios y solo durante el despliegue), y svc_pagos no puede leer pedidos.* 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.

  1. 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.

  1. 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 con X-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 con grpc.credentials.createSsl(caCert, key, cert); con un mesh, el sidecar lo hace y el código usa createInsecure() hacia localhost. Solo mención: TechCorp no expone gRPC en la fase 1.

  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=require pensando que verifica el certificado. Solo cifra; verify-full es el que impide la suplantación del servidor de base de datos.
  • guest/guest en RabbitMQ o un único usuario techcorp con .* 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 con express.raw en esa ruta.
  • Comparar firmas con ===. Filtra información por tiempo; timingSafeEqual siempre.
  • PeerAuthentication STRICT sin sidecar en todos los pods (o con Prometheus fuera del mesh): todo deja de hablar (05-05). PERMISSIVE primero.
  • Consejo: guarda en el runbook plataforma/certificados los comandos de los apartados 3 y 4; y en cada Secret TLS, 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=false200 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 min400 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

Módulo 2: Diseño de Microservicios

Módulo 3: Comunicación entre Microservicios

Módulo 4: Implementación de Microservicios

Módulo 5: Despliegue y Orquestación

Módulo 6: Monitoreo y Mantenimiento

Módulo 7: Seguridad en Microservicios

Módulo 8: Casos de Estudio y Ejemplos Prácticos

© Copyright 2026. Todos los derechos reservados