Tutorial práctico: implementar mTLS entre Nginx y microservicios usando smallstep CA en Docker Compose

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

Dos bloques rectangulares negros sobre fondo gris claro en composición minimalista
Aspecto neutro y limpio, adecuado para tutoriales técnicos Foto: Google DeepMind / Pexels

En esta guía aprenderás a implementar mTLS entre Nginx y microservicios usando smallstep CA en Docker Compose. Montaremos una CA interna funcional con step‑ca, emitiremos certificados con step‑cli y configuraremos Nginx para autenticación mutua con el servicio upstream. Todo compacto, automatizable y listo para evolucionar.

Requisitos y escenario del tutorial

Necesitas un host con Docker y Docker Compose instalados. El objetivo es que Nginx, actuando como proxy reverso, se conecte al microservicio vía HTTPS presentando un certificado de cliente. El microservicio aceptará únicamente clientes emitidos por nuestra CA smallstep.

Si aún no tienes el entorno de contenedores listo, sigue este tutorial: instalar Docker y Docker Compose en Linux. No almacenes claves privadas en repositorios Git.

Para comparar enfoques, consulta mTLS con PKI manual: mTLS en Docker Compose (PKI). Y, si prefieres emisión dinámica basada en secrets, revisa el tutorial con Vault: mTLS con Nginx y Vault.

  • Docker y Docker Compose instalados
  • Acceso por shell al host
  • Editor de texto
  • Puertos libres: 8080 (Nginx), 9000 (CA), 8443 (app)
Servicio Función Puerto Certificados usados
step-ca Autoridad Certificadora smallstep 9000 root_ca.crt (raíz)
step-cli Emisión/gestión de certificados - usa root_ca.crt para bootstrap
nginx Reverse proxy hacia la app vía mTLS 8080 (externo) nginx-client.crt/key + root_ca.crt
app Microservicio HTTPS que exige certificado de cliente 8443 app.crt/key + root_ca.crt

Arquitectura y flujo del mTLS

El tráfico externo llega a Nginx por HTTP simple, para centrarnos en el mTLS interno. Entre Nginx y la app usamos TLS mutuo: ambos validan certificados firmados por nuestra CA. Para enrutado dinámico en producción, compara alternativas en Traefik vs Nginx como reverse proxy.

  • El cliente externo llama a Nginx por HTTP.
  • Nginx reenvía a la app vía HTTPS presentando un certificado de cliente.
  • La app valida el certificado de Nginx y presenta el suyo; Nginx valida la cadena del app.

Paso 1 — Compose base y volúmenes de trabajo

Crea una carpeta de proyecto y subcarpetas para la CA y para los certificados emitidos. Definiremos el Docker Compose con cuatro servicios: step‑ca, step‑cli (utilitario), app y nginx. Los archivos en ./step contienen la CA: protégelos y haz backup seguro.

version: '3.8'
services:
  step-ca:
    image: smallstep/step-ca:latest
    container_name: step-ca
    ports:
      - "9000:9000"
    volumes:
      - ./step:/home/step
    command: ["step-ca","/home/step/config/ca.json","--password-file","/home/step/password.txt"]

  step-cli:
    image: smallstep/step-cli:latest
    container_name: step-cli
    environment:
      - STEPPATH=/home/step
    volumes:
      - ./step:/home/step
      - ./pki:/pki
    entrypoint: ["sleep","infinity"]

  app:
    build: ./app
    container_name: app
    volumes:
      - ./pki/app:/certs:ro
    depends_on:
      - step-ca

  nginx:
    image: nginx:alpine
    container_name: nginx
    ports:
      - "8080:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./pki/nginx:/certs:ro
    depends_on:
      - app

Paso 2 — Inicializar la smallstep CA (step-ca)

Inicializaremos la CA en el volumen ./step usando el step‑cli. Esto crea la raíz, el intermediario y el ca.json. Después, arrancamos el step‑ca. Guarda la contraseña de forma segura. Para este laboratorio puedes usar la misma contraseña para la CA y el provisioner JWK, solo a modo de demostración.

# 1) Estrutura de diretórios
mkdir -p step pki/app pki/nginx

# 2) Crie um arquivo de senha forte (use um gerenciador)
# AVISO: arquivo em texto puro. Restrinja permissões no host.
echo "mude-esta-senha-forte" > step/password.txt
chmod 600 step/password.txt

# 3) Inicialize a CA dentro do volume ./step
docker compose run --rm \
  -e STEPPATH=/home/step step-cli \
  step ca init \
    --deployment-type=standalone \
    --name "Dev CA" \
    --provisioner "[email protected]" \
    --dns "step-ca" \
    --address ":9000" \
    --password-file /home/step/password.txt \
    --provisioner-password-file /home/step/password.txt

# 4) Inicie o step-ca
docker compose up -d step-ca

docker compose ps

Paso 3 — Bootstrap de confianza y emisión de certificados

Con la CA en ejecución, haz el bootstrap de confianza en step‑cli y emite: un certificado de servidor para la app y un certificado de cliente para Nginx. Incluimos SANs con los hostnames Docker para la verificación de servidor. También copiamos la raíz de la CA a las carpetas del Nginx y de la app.

# 1) Obtenha o fingerprint da raiz da CA
FINGERPRINT=$(docker compose exec -T step-cli \
  step certificate fingerprint /home/step/certs/root_ca.crt)
echo $FINGERPRINT

# 2) Bootstrap de confiança
docker compose exec -T step-cli \
  step ca bootstrap --ca-url https://step-ca:9000 --fingerprint $FINGERPRINT

# 3) Emita certificados (app: servidor; nginx: cliente)
docker compose exec -T step-cli bash -lc '
  mkdir -p /pki/app /pki/nginx && \
  step ca certificate "app" /pki/app/app.crt /pki/app/app.key \
    --san "app" --san "app.local" && \
  step ca certificate "nginx" /pki/nginx/nginx-client.crt /pki/nginx/nginx-client.key && \
  cp /home/step/certs/root_ca.crt /pki/app/root_ca.crt && \
  cp /home/step/certs/root_ca.crt /pki/nginx/root_ca.crt'

# Verifique os arquivos gerados
ls -lah pki/app pki/nginx

Paso 4 — Microservicio de ejemplo (Node.js HTTPS que exige client cert)

La app expone HTTPS en el puerto 8443, exige certificado de cliente y valida contra la raíz de nuestra CA. Para fines didácticos, el servidor devuelve un JSON con el estado de la autenticación mutua. En producción, mantiene las claves fuera de la imagen y móntalas como secrets o volúmenes.

# app/app.js
const fs = require('fs');
const https = require('https');
const options = {
  key: fs.readFileSync('/certs/app.key'),
  cert: fs.readFileSync('/certs/app.crt'),
  ca: fs.readFileSync('/certs/root_ca.crt'),
  requestCert: true,
  rejectUnauthorized: true,
};
const server = https.createServer(options, (req, res) => {
  const peer = req.socket.getPeerCertificate();
  const authorized = req.socket.authorized;
  res.writeHead(200, {'Content-Type':'application/json'});
  res.end(JSON.stringify({ ok: true, authorized, subject: peer.subject }));
});
server.listen(8443, () => console.log('app listening on 8443'));

# app/Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY app.js .
EXPOSE 8443
CMD ["node", "app.js"]

Paso 5 — Nginx como reverse proxy con mTLS hacia el upstream

Nginx hará proxy hacia la app en HTTPS con verificación y presentará un certificado de cliente. Habilitamos SNI, validación del servidor y protocolos modernos. Asegura las rutas correctas de los archivos montados.

# nginx.conf (montado em /etc/nginx/conf.d/default.conf)
upstream app_tls {
  server app:8443;
}

server {
  listen 80;

  location / {
    proxy_pass https://app_tls;

    # Verificação do servidor (app) e SNI
    proxy_ssl_server_name on;
    proxy_ssl_verify on;
    proxy_ssl_trusted_certificate /certs/root_ca.crt;
    proxy_ssl_verify_depth 2;
    proxy_ssl_protocols TLSv1.2 TLSv1.3;

    # Certificado de cliente apresentado ao app
    proxy_ssl_certificate /certs/nginx-client.crt;
    proxy_ssl_certificate_key /certs/nginx-client.key;

    # Cabeçalhos padrão
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}

Paso 6 — Levantar el entorno y probar mTLS de punta a punta

Construiremos la imagen del app, arrancaremos todo y probaremos vía Nginx. Si el mTLS interno está correcto, la app responderá con authorized=true. Si falla, revisa logs y la sección de resolución de problemas. Este es el test final tras implementar mTLS entre Nginx y microservicios usando smallstep CA en Docker Compose.

# 1) Construa o app e suba serviços necessários
docker compose up -d --build app nginx

# 2) Teste via Nginx (HTTP externo simples)
curl -i http://localhost:8080/

# 3) Verifique logs do app (deve mostrar conexões autorizadas)
docker compose logs --tail=100 app

# 4) Opcional: inspecione o certificado do app visto pelo Nginx
# (dentro do container nginx)
docker compose exec -T nginx sh -lc 'apk add --no-cache openssl >/dev/null && \
  echo | openssl s_client -connect app:8443 -servername app -CAfile /certs/root_ca.crt 2>/dev/null | openssl x509 -noout -subject -issuer -dates'

Resolución de problemas y recomendaciones para producción

Si aparece x509: certificate signed by unknown authority, verifica que Nginx apunte a la raíz correcta. Para mismatch de hostname, asegúrate de incluir el SAN con el hostname que usa Nginx. Usa certificados de corta duración y automatiza su renovación. Recarga Nginx y la app tras renovar. Sincroniza el reloj del host con NTP.

  • Activa logs detallados en Nginx (error_log debug) para depurar handshakes.
  • Ajusta verify_depth si existe cadena intermedia.
  • Guarda las claves fuera del repositorio; usa volúmenes o secrets del orquestador.
  • Automatiza la rotación con step‑cli + timers y healthchecks.
  • Estandariza nombres/SANs entre ambientes para evitar roturas.

Preguntas frecuentes

¿Qué es smallstep CA (step‑ca) y por qué usarla aquí?

Es una Autoridad Certificadora ligera, con servidor y CLI, pensada para la automatización de certificados internos. Aquí facilita emitir y renovar certificados mTLS entre servicios, reduciendo el esfuerzo frente a una PKI manual.

¿Puedo añadir más microservicios que exijan mTLS?

Sí. Emite un certificado de servidor para cada servicio con los SAN correctos. Para cada cliente que llame al servicio, emite un certificado de cliente y configura el cliente para presentarlo durante el handshake TLS.

¿Se puede usar HTTPS público en Nginx y mTLS interno al mismo tiempo?

Sí. Mantén TLS público en el frontal con un certificado reconocido y, en el plano interno, usa mTLS entre el proxy y los servicios. Las capas son independientes y complementarias.

¿Cómo renuevo los certificados automáticamente?

Usa step‑cli con un programador (cron o systemd timer) para solicitar nuevos certificados antes del vencimiento. Tras renovar, recarga Nginx y los servicios dependientes. Monitoriza fechas de expiración y configura alertas.

¿Y si cambia el hostname del contenedor?

El SAN del certificado debe coincidir con el hostname que el cliente verifica. Estandariza nombres en Compose o Kubernetes y genera certificados con SANs coherentes. Si cambia, reemite el certificado con los nuevos SANs.

¿Puedo ejecutar la CA sin exponerla externamente?

Sí. Limita la red al Compose interno y usa firewall. La CA debe ser accesible solo para emisión y renovación por clientes autorizados. Expón puertos externamente solo cuando sea necesario.

¿Llevar mTLS con smallstep a tu stack?

Este tutorial crea la base: CA interna, emisión y validación en Nginx. ¿Necesitas adaptarlo a varios servicios, automatizar renovaciones en producción o integrarlo con tu CI/CD? Hablemos.

Hablar sobre mTLS en mi proyecto

Ver también