Tutorial prático: implementar mTLS entre Nginx e microserviços usando smallstep CA em Docker Compose
Redes & Segurança · Por Kevin Marcondes · Publicado em · 9 min de leitura
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 projetoVeja também
- tutorial: implementar Canary releases com Traefik em Docker Compose usando headers e weight routing para deploys graduais de aplicações PHP
- Tutorial: configurar HAProxy com Let's Encrypt e healthchecks para balanceamento de tráfego entre containers Docker em VPS (passo a passo)
- Tutorial: configurar cgroup v2 e systemd ResourceControl para limitar CPU, memória e I/O de containers Docker no Debian
- Tutorial: automatizar snapshots LVM e uploads incrementais para S3 usando systemd timers e Restic no Debian