Tutorial prático: implementar mTLS entre Nginx e microserviços usando smallstep CA em Docker Compose

Redes & Segurança · Por · Publicado em · 9 min de leitura

Dois blocos retangulares pretos sobre fundo cinza claro em composição minimalista
Visual neutro e limpo, adequado para tutoriais técnicos Foto: Google DeepMind / Pexels

Neste guia, você vai aprender a implementar mTLS entre Nginx e microserviços usando smallstep CA em Docker Compose. Montaremos uma CA interna funcional com step-ca, emitiremos certificados com step-cli e configuraremos Nginx para autenticação mútua com o serviço upstream. Tudo enxuto, automatizável e pronto para evoluir.

Pré-requisitos e cenário do tutorial

Você precisa de um host com Docker e Docker Compose instalados. O objetivo é que o Nginx, atuando como proxy reverso, se conecte ao microserviço via HTTPS apresentando um certificado de cliente. O microserviço aceita apenas clientes emitidos pela nossa CA smallstep.

Se ainda não tem o ambiente container pronto, siga este guia: instalar Docker e Docker Compose no Linux. Não armazene chaves privadas em repositórios Git.

Para comparar abordagens, veja mTLS com PKI manual: mTLS no Docker Compose (PKI). E, para emissão dinâmica baseada em secrets, o tutorial com Vault: mTLS com Nginx e Vault.

  • Docker e Docker Compose instalados
  • Acesso de shell ao host
  • Editor de texto
  • Portas livres: 8080 (Nginx), 9000 (CA), 8443 (app)
Serviço Função Porta Certificados usados
step-ca Autoridade Certificadora smallstep 9000 root_ca.crt (raiz)
step-cli Emissão/gestão de certificados - usa root_ca.crt para bootstrap
nginx Reverse proxy para app via mTLS 8080 (externa) nginx-client.crt/key + root_ca.crt
app Microserviço HTTPS exigindo client cert 8443 app.crt/key + root_ca.crt

Arquitetura e fluxo do mTLS

O tráfego externo chega ao Nginx por HTTP simples, para focarmos no mTLS interno. Entre Nginx e app usamos TLS mútuo: ambos validam certificados assinados pela nossa CA. Para roteamento dinâmico em produção, compare alternativas em Traefik vs Nginx como reverse proxy.

  • O cliente externo chama o Nginx em HTTP.
  • O Nginx encaminha ao app via HTTPS, apresentando certificado de cliente.
  • O app valida o certificado do Nginx e apresenta o seu. O Nginx valida a cadeia do app.

Passo 1 — Compose base e volumes de trabalho

Crie uma pasta de projeto e subpastas para a CA e para os certificados emitidos. Vamos definir o Docker Compose com quatro serviços: step-ca, step-cli (utilitário), app e nginx. Os arquivos em ./step contêm a CA: proteja e faça 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

Passo 2 — Inicializar a smallstep CA (step-ca)

Vamos inicializar a CA no volume ./step usando o step-cli. Isso cria a raiz, o intermediário e o ca.json. Depois, iniciamos o step-ca. Guarde a senha com segurança. Você pode usar a mesma senha para a CA e o provisioner JWK apenas para este laboratório.

# 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

Passo 3 — Bootstrap de confiança e emissão dos certificados

Com a CA em execução, faça o bootstrap de confiança no step-cli e emita: um certificado de servidor para o app e um certificado de cliente para o Nginx. Incluímos SANs do hostname Docker para verificação de servidor. Também copiamos a raiz da CA para as pastas do Nginx e do 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

Passo 4 — Microserviço de exemplo (Node.js HTTPS exigindo client cert)

O app expõe HTTPS na porta 8443, exige client cert e valida contra a raiz da nossa CA. Para fins didáticos, o servidor retorna um JSON com o status da autenticação mútua. Em produção, mantenha chaves fora da imagem e monte-as como secrets ou volumes.

# 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"]

Passo 5 — Nginx como reverse proxy com mTLS para o upstream

O Nginx fará proxy para o app em HTTPS com verificação e apresentará um certificado de cliente. Habilitamos SNI, validação do servidor e limites de protocolo modernos. Garanta caminhos corretos dos arquivos 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;
  }
}

Passo 6 — Subir o ambiente e testar o mTLS ponta a ponta

Vamos construir a imagem do app, iniciar tudo e testar pelo Nginx. Se o mTLS interno estiver correto, o app responderá com authorized=true. Se falhar, veja logs e a seção de solução de problemas. Este é o teste final após implementar mTLS entre Nginx e microserviços usando smallstep CA em 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'

Solução de problemas e dicas de produção

Se aparecer x509: certificate signed by unknown authority, verifique se o Nginx aponta para a raiz correta. Para hostname mismatch, garanta SAN com o hostname usado pelo Nginx. Use certificados de curta duração e automatize a renovação. Recarregue Nginx e o app após renovar. Sincronize o relógio do host com NTP.

  • Ative logs detalhados no Nginx (error_log debug) para depurar handshake.
  • Ajuste verify_depth se houver cadeia intermediária.
  • Guarde chaves fora do repositório; use volumes ou secrets do orquestrador.
  • Automatize rotação com step-cli + timers e healthchecks.
  • Padronize nomes/SANs iguais entre ambientes para evitar quebras.

Perguntas frequentes

O que é smallstep CA (step-ca) e por que usá-lo aqui?

É uma Autoridade Certificadora leve, com CLI e servidor, pensada para automação de certificados internos. Aqui, facilita emitir e renovar certificados mTLS entre serviços, reduzindo esforço em comparação a uma PKI manual.

Posso adicionar mais microserviços exigindo mTLS?

Sim. Emita um certificado de servidor para cada serviço, com SANs corretos. Para cada cliente que chamar o serviço, emita um certificado de cliente e configure o cliente para apresentá-lo durante o handshake TLS.

Dá para usar HTTPS público no Nginx e mTLS interno ao mesmo tempo?

Sim. Mantenha TLS público no front com um certificado reconhecido e, internamente, use mTLS entre o proxy e os serviços. As camadas são independentes e complementares.

Como renovo os certificados automaticamente?

Use o step-cli com um agendador (cron ou systemd timer) para solicitar novos certificados antes do vencimento. Após renovar, recarregue Nginx e serviços dependentes. Monitore datas de expiração e configure alertas.

E se o hostname do container mudar?

O SAN do certificado deve corresponder ao hostname que o cliente verifica. Padronize nomes em Compose ou Kubernetes e gere certificados com SANs consistentes. Se mudar, reemita o certificado com os novos SANs.

Posso rodar a CA sem expô-la externamente?

Sim. Limite a rede ao Compose interno e use firewall. A CA precisa estar acessível apenas para emissão e renovação pelos clientes autorizados. Exponha portas externamente somente quando necessário.

Levar mTLS com smallstep ao seu stack?

Este tutorial cria a base: CA interna, emissão e validação no Nginx. Precisa adaptar para vários serviços, renovar em produção ou integrar ao seu CI/CD? Vamos conversar.

Falar sobre mTLS no seu projeto

Veja também