Paso a paso: configurar mTLS entre microservicios en Docker Compose usando PKI interna
Redes & Segurança · Por Kevin Marcondes · Publicado el · Actualizado el · 9 min de lectura
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ónVer también
- Tutorial: implementar Canary releases con Traefik en Docker Compose usando headers y weighted routing para despliegues graduales de aplicaciones PHP
- Tutorial: configurar HAProxy con Let's Encrypt y healthchecks para balanceo de tráfico entre contenedores Docker en VPS (paso a paso)
- Tutorial práctico: implementar mTLS entre Nginx y microservicios usando smallstep CA en Docker Compose
- Tutorial: configurar cgroup v2 y systemd ResourceControl para limitar CPU, memoria e I/O de contenedores Docker en Debian