Paso a paso: configurar mTLS entre microservicios en Docker Compose usando PKI interna

Redes & Segurança · Por · Publicado el · Actualizado el · 9 min de lectura

Monitor mostrando un IDE con código fuente y panel de control de versiones (Git) visible
Pantalla de desarrollo con código e historial Git, apropiada para un tutorial técnico Foto: Daniil Komov / Pexels

Esta guía muestra cómo configurar mTLS entre microservicios en Docker Compose usando una PKI interna. Vas a generar una CA local y certificados con OpenSSL, configurar un servicio con Nginx para exigir certificado de cliente y validar la comunicación segura entre contenedores, sin exponer puertos públicos.

Pre requisitos y objetivos del laboratorio

Necesitas Docker y Docker Compose instalados y OpenSSL disponible en el host. Crearemos una CA local, emitiremos certificados para dos servicios y exigiremos autenticación mutua entre ellos.

Si aún no tienes Docker/Compose en el servidor, consulta la guía de instalación antes de continuar.

El objetivo es que service-a invoque a service-b vía HTTPS interno, con verificación obligatoria del certificado de cliente.

  • Docker y Docker Compose funcionando en el host
  • OpenSSL instalado para generar claves y certificados
  • Editor de texto para crear archivos de configuración
  • Conocimientos básicos de redes Docker y volúmenes
Recurso Uso en el tutorial
CA interna Firmar los certificados de servidor y cliente
Nginx Terminar TLS y exigir certificado de cliente
curl Probar peticiones con mTLS
Docker Compose Orquestar servicios y volúmenes de certificados
#!/usr/bin/env bash
set -euo pipefail

mkdir -p pki/{ca,csr,certs,private,ext}

# 1) CA raiz local
openssl genrsa -out pki/ca/ca.key.pem 4096
openssl req -x509 -new -key pki/ca/ca.key.pem -days 3650 -sha256 \
  -subj "/CN=Dev Local Root CA" -out pki/ca/ca.cert.pem

# 2) Arquivos de extensão (SAN + usos)
cat > pki/ext/service-b.ext <<'EOF'
basicConstraints=CA:FALSE
keyUsage=critical, digitalSignature, keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=@alt_names
[alt_names]
DNS.1=service-b
EOF

cat > pki/ext/service-a.ext <<'EOF'
basicConstraints=CA:FALSE
keyUsage=critical, digitalSignature, keyEncipherment
extendedKeyUsage=clientAuth
subjectAltName=@alt_names
[alt_names]
DNS.1=service-a
EOF

# 3) Certificado de servidor (service-b)
openssl req -new -newkey rsa:2048 -nodes \
  -keyout pki/private/service-b.key.pem \
  -subj "/CN=service-b" -out pki/csr/service-b.csr.pem
openssl x509 -req -in pki/csr/service-b.csr.pem \
  -CA pki/ca/ca.cert.pem -CAkey pki/ca/ca.key.pem -CAcreateserial \
  -out pki/certs/service-b.cert.pem -days 825 -sha256 \
  -extfile pki/ext/service-b.ext

# 4) Certificado de cliente (service-a)
openssl req -new -newkey rsa:2048 -nodes \
  -keyout pki/private/service-a.key.pem \
  -subj "/CN=service-a" -out pki/csr/service-a.csr.pem
openssl x509 -req -in pki/csr/service-a.csr.pem \
  -CA pki/ca/ca.cert.pem -CAkey pki/ca/ca.key.pem -CAcreateserial \
  -out pki/certs/service-a.cert.pem -days 825 -sha256 \
  -extfile pki/ext/service-a.ext

chmod 600 pki/private/*.key.pem

echo "PKI pronta em ./pki"

Arquitectura: mTLS entre contenedores en la misma network

En Docker Compose, cada servicio se resuelve por su propio nombre DNS en la red predeterminada. Exploraremos esto emitiendo certificados con SAN DNS igual al nombre del servicio.

service-b ejecutará Nginx en el puerto 8443, con un certificado de servidor firmado por nuestra CA. Hará obligatoria la presentación de un certificado de cliente confiable para aceptar conexiones.

service-a tendrá un certificado de cliente firmado por la misma CA y usará curl para autenticarse contra service-b.

Si planeas usar un proxy reverso en producción, revisa un comparativo práctico de opciones.

  • SAN del servidor: DNS:service-b
  • SAN del cliente: DNS:service-a
  • Red: default de Compose, sin puertos públicos
  • Autenticación mutua: ambos presentan certificados confiables

1) Crear la PKI interna (CA, extensiones y claves)

Crearemos la estructura de directorios, una CA raíz local y los archivos de extensión para emitir certificados de servidor y cliente con SAN apropiado. No versiones claves privadas en Git; mantenlas fuera del control de versiones y con permisos restrictivos.

Archivo/dir Finalidad
pki/ca/ca.key.pem Clave privada de la CA (secreta)
pki/ca/ca.cert.pem Certificado público de la CA (compartible)
pki/private/*.key.pem Claves privadas de los servicios
pki/certs/*.cert.pem Certificados firmados para servicios
pki/ext/*.ext Extensiones: SAN y usos de clave
#!/usr/bin/env bash
set -euo pipefail

mkdir -p pki/{ca,csr,certs,private,ext}

# 1) CA raiz local
openssl genrsa -out pki/ca/ca.key.pem 4096
openssl req -x509 -new -key pki/ca/ca.key.pem -days 3650 -sha256 \
  -subj "/CN=Dev Local Root CA" -out pki/ca/ca.cert.pem

# 2) Arquivos de extensão (SAN + usos)
cat > pki/ext/service-b.ext <<'EOF'
basicConstraints=CA:FALSE
keyUsage=critical, digitalSignature, keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=@alt_names
[alt_names]
DNS.1=service-b
EOF

cat > pki/ext/service-a.ext <<'EOF'
basicConstraints=CA:FALSE
keyUsage=critical, digitalSignature, keyEncipherment
extendedKeyUsage=clientAuth
subjectAltName=@alt_names
[alt_names]
DNS.1=service-a
EOF

# 3) Certificado de servidor (service-b)
openssl req -new -newkey rsa:2048 -nodes \
  -keyout pki/private/service-b.key.pem \
  -subj "/CN=service-b" -out pki/csr/service-b.csr.pem
openssl x509 -req -in pki/csr/service-b.csr.pem \
  -CA pki/ca/ca.cert.pem -CAkey pki/ca/ca.key.pem -CAcreateserial \
  -out pki/certs/service-b.cert.pem -days 825 -sha256 \
  -extfile pki/ext/service-b.ext

# 4) Certificado de cliente (service-a)
openssl req -new -newkey rsa:2048 -nodes \
  -keyout pki/private/service-a.key.pem \
  -subj "/CN=service-a" -out pki/csr/service-a.csr.pem
openssl x509 -req -in pki/csr/service-a.csr.pem \
  -CA pki/ca/ca.cert.pem -CAkey pki/ca/ca.key.pem -CAcreateserial \
  -out pki/certs/service-a.cert.pem -days 825 -sha256 \
  -extfile pki/ext/service-a.ext

chmod 600 pki/private/*.key.pem

echo "PKI pronta em ./pki"

2) Configurar el Nginx de service-b para exigir mTLS

Nginx actuará como servidor HTTPS en service-b, validando el certificado de cliente con la CA local. Para el laboratorio serviremos una respuesta simple. En producción, apuntarías a un upstream interno.

Si prefieres proxies preparados para contenedores, compara opciones de enrutamiento HTTPS.

events {}
http {
  server {
    listen 8443 ssl;

    ssl_certificate     /etc/nginx/certs/service-b.cert.pem;
    ssl_certificate_key /etc/nginx/certs/service-b.key.pem;

    ssl_client_certificate /etc/nginx/certs/ca.cert.pem;
    ssl_verify_client on;  # exige certificado de cliente

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    location / {
      default_type text/plain;
      return 200 "Hello from service-b. Client DN: $ssl_client_s_dn\n";
    }
  }
}

3) Declarar los servicios en Docker Compose y montar los certificados

Ahora crearemos el docker-compose.yml con dos servicios. service-b ejecuta Nginx con los certificados montados en volumen sólo lectura. service-a mantiene un contenedor con curl y sus artefactos de cliente. No se publicará ninguna puerta externamente.

Mantén las claves privadas como volúmenes con permisos restrictivos.

version: "3.8"
services:
  service-b:
    image: nginx:alpine
    volumes:
      - ./nginx/service-b.conf:/etc/nginx/nginx.conf:ro
      - ./pki/certs/service-b.cert.pem:/etc/nginx/certs/service-b.cert.pem:ro
      - ./pki/private/service-b.key.pem:/etc/nginx/certs/service-b.key.pem:ro
      - ./pki/ca/ca.cert.pem:/etc/nginx/certs/ca.cert.pem:ro
    expose:
      - "8443"

  service-a:
    image: curlimages/curl:8.9.0
    command: ["sleep", "infinity"]
    volumes:
      - ./pki/certs/service-a.cert.pem:/certs/service-a.cert.pem:ro
      - ./pki/private/service-a.key.pem:/certs/service-a.key.pem:ro
      - ./pki/ca/ca.cert.pem:/certs/ca.cert.pem:ro

4) Levantar el entorno y validar la configuración básica

Crea el directorio de configuración de Nginx, guarda el archivo y sube los contenedores. A continuación, confirma si service-b está activo y accesible por la red interna de Compose.

Si necesitas un entorno de monitorización después, puedes integrar métricas fácilmente con Prometheus y Grafana.

# Salve o arquivo acima como nginx/service-b.conf
mkdir -p nginx

# Suba os serviços
docker compose up -d

# Verifique status
docker compose ps

# Opcional: inspecione logs do Nginx
docker compose logs -f service-b

5) Pruebas de mTLS: éxito y fallos esperados

Vamos a probar llamadas desde service-a. Primero, sin certificado de cliente: la conexión debe fallar. Luego, con los artefactos correctos: la respuesta debe incluir el DN del cliente. Asegúrate de usar el SAN correcto en el nombre de host: service-b.

# 1) Sem certificado de cliente: debe fallar con 400/495
docker compose exec service-a sh -lc 'curl -sv --cacert /certs/ca.cert.pem https://service-b:8443/'

# 2) Com mTLS: debe retornar 200 y DN del cliente
docker compose exec service-a sh -lc \
  'curl -sS --cert /certs/service-a.cert.pem --key /certs/service-a.key.pem \
  --cacert /certs/ca.cert.pem https://service-b:8443/'

Buenas prácticas para producción y rotación de claves

Evita usar la misma CA para desarrollo y producción. Prefiere una cadena con raíz offline y una intermedia activa.

Implementa rotación periódica de certificados con solapamiento de validez y despliegue gradual.

Almacena claves en cofres de secretos. En Compose, usa volúmenes con permisos mínimos. Nunca expongas puertos TLS internos sin necesidad.

Si tu malla crece, considera gateways o sidecars dedicados a TLS.

  • Cadena: raíz offline + intermedia online
  • Certificados de corta duración con rotación automatizada
  • Montajes de volumen sólo lectura
  • Audita quién monta cada clave en el contenedor

Solución de problemas comunes

Autoridad desconocida: el cliente no está usando la CA correcta. Monta ca.cert.pem y pásalo con --cacert.

Nombre inválido en el certificado: el SAN no coincide con el host service-b. Reemite con DNS:service-b.

Fecha inválida: hora del host o del contenedor fuera de sincronía. Ajusta NTP.

Permiso denegado para la clave: verifica chmod 600 en el host y la montura ro en el contenedor.

Conexión cae sin logs: confirma la network de Compose y el nombre del servicio.

  • x509: certificate signed by unknown authority
  • SSL: no required SSL certificate sent
  • certificate verify failed: IP address mismatch
  • permission denied: /etc/nginx/certs/*.key.pem

Preguntas frecuentes

¿Qué es mTLS y cuándo debo usarlo entre microservicios?

mTLS autentica ambos extremos de la conexión TLS: servidor y cliente. Úsalo cuando los servicios internos necesiten garantizar identidad fuerte y reducir riesgos de tráfico lateral malicioso. Es útil en entornos multi‑tenant, datos sensibles y redes compartidas.

¿Cómo elegir el SAN correcto de los certificados en Docker Compose?

Usa el nombre DNS que los contenedores resuelven en la red de Compose, normalmente el nombre del servicio, por ejemplo service-b. Si usas aliases, incluye todos los DNS relevantes en el SAN para evitar fallos de verificación.

¿Puedo usar Traefik o Caddy en lugar de Nginx?

Sí. Traefik y Caddy tienen buenas integraciones con Docker y soporte TLS. También permiten exigir certificado de cliente. Evalúa cuál se adapta mejor a tu flujo y requisitos. Revisa comparativos prácticos para decidir.

¿mTLS interno sustituye certificados públicos como Let's Encrypt?

No. Let's Encrypt es ideal para exponer servicios en Internet con confianza pública. mTLS con PKI interna cubre la comunicación privada entre servicios. Puedes usar ambos: público en la frontera y PKI interna para tráfico este‑oeste.

¿Cómo rotar certificados sin detener servicios?

Genera nuevos certificados antes de que expiren, móntalos en rutas fijas y recarga el proceso (p. ej. reload en Nginx). Mantén la CA estable. Si cambias la CA, implementa un periodo de coexistencia confiando tanto la CA antigua como la nueva simultáneamente.

¿Quieres adaptar este mTLS a tu entorno?

Si necesitas llevar esta solución a producción, con rotación automatizada y observabilidad, podemos conversar sobre tu contexto y prioridades.

Hablar sobre mTLS en producción

Ver también