Passo a passo: configurar mTLS entre microserviços em Docker Compose usando PKI interna
Redes & Segurança · Por Kevin Marcondes · Publicado em · Atualizado em · 8 min de leitura
Este passo a passo mostra como configurar mTLS entre microserviços em Docker Compose usando uma PKI interna. Você vai gerar uma CA local e certificados com OpenSSL, configurar um serviço com Nginx para exigir certificado de cliente e validar a comunicação segura entre containers, sem expor portas públicas.
Pré-requisitos e objetivos do laboratório
Você precisa de Docker e Docker Compose instalados e OpenSSL disponível no host. Vamos criar uma CA local, emitir certificados para dois serviços e exigir autenticação mútua entre eles.
Se ainda não tem Docker/Compose no servidor, veja o guia de instalação antes de seguir.
O objetivo é ter service-a chamando service-b via HTTPS interno, com verificação de certificado de cliente obrigatória.
- Docker e Docker Compose funcionando no host
- OpenSSL instalado para gerar chaves e certificados
- Editor de texto para criar arquivos de configuração
- Conhecimento básico de redes Docker e volumes
| Recurso | Uso no tutorial |
|---|---|
| CA interna | Assinar os certificados de servidor e cliente |
| Nginx | Encerrar TLS e exigir certificado de cliente |
| curl | Testar requisições com mTLS |
| Docker Compose | Orquestrar serviços e volumes de certificados |
Arquitetura: mTLS entre containers no mesmo network
No Docker Compose, cada serviço é resolvido pelo próprio nome DNS no network padrão. Vamos explorar isso emitindo certificados com DNS SAN igual ao nome do serviço.
O service-b rodará Nginx em 8443, com certificado de servidor assinado pela nossa CA. Ele exigirá um certificado de cliente confiável para aceitar conexões.
O service-a terá um certificado de cliente assinado pela mesma CA e usará curl para se autenticar no service-b.
Se você planeja usar proxy reverso em produção, veja um comparativo prático de opções.
- SAN do servidor: DNS:service-b
- SAN do cliente: DNS:service-a
- Rede: default do Compose, sem portas públicas
- Autenticação mútua: ambos apresentam certificados confiáveis
1) Criar a PKI interna (CA, extensões e chaves)
Vamos criar a estrutura de diretórios, uma CA raiz local e os arquivos de extensão para emitir certificados de servidor e cliente com SAN apropriado. Não versione chaves privadas no Git; mantenha-as fora do controle de versão e com permissões restritas.
| Arquivo/dir | Finalidade |
|---|---|
| pki/ca/ca.key.pem | Chave privada da CA (secreta) |
| pki/ca/ca.cert.pem | Certificado público da CA (compartilhável) |
| pki/private/*.key.pem | Chaves privadas dos serviços |
| pki/certs/*.cert.pem | Certificados assinados para serviços |
| pki/ext/*.ext | Extensões: SAN e usos de chave |
#!/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 o Nginx do service-b para exigir mTLS
O Nginx atuará como servidor HTTPS no service-b, validando o certificado de cliente com a CA local. Para o laboratório, serviremos uma resposta simples. Em produção, você apontaria para um upstream interno.
Se prefere proxies prontos para containers, compare opções de roteamento 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 os serviços no Docker Compose e montar os certificados
Agora criaremos o docker-compose.yml com dois serviços. O service-b roda Nginx com os certificados montados em volume somente leitura. O service-a mantém um container com curl e os seus artefatos de cliente. Nenhuma porta será publicada externamente.
Mantenha chaves privadas como volumes com permissões restritas.
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) Subir o ambiente e validar a configuração básica
Crie o diretório de configuração do Nginx, salve o arquivo e suba os containers. Em seguida, confirme se o service-b está de pé e acessível pela rede interna do Compose.
Se precisar de um ambiente de monitoramento depois, você pode integrar métricas facilmente com Prometheus e 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) Testes de mTLS: sucesso e falhas esperadas
Vamos testar chamadas a partir do service-a. Primeiro, sem certificado de cliente: a conexão deve falhar. Depois, com os artefatos corretos: a resposta deve incluir o DN do cliente. Certifique-se de usar o SAN correto no nome de host: service-b.
# 1) Sem certificado de cliente: deve falhar com 400/495
docker compose exec service-a sh -lc 'curl -sv --cacert /certs/ca.cert.pem https://service-b:8443/'
# 2) Com mTLS: deve retornar 200 e DN do 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/'
Boas práticas para produção e rotação de chaves
Evite usar a mesma CA para desenvolvimento e produção. Prefira uma cadeia com raiz offline e intermediária ativa.
Implemente rotação periódica de certificados com sobreposição de validade e implantação gradual.
Armazene chaves em cofres de segredo. Em Compose, use volumes com permissões mínimas. Nunca exponha portas TLS internas sem necessidade.
Se sua malha crescer, considere gateways ou sidecars dedicados a TLS.
- Cadeia: raiz offline + intermediária online
- Certificados curtos com rotação automatizada
- Mapeamentos de volume somente leitura
- Audite quem monta cada chave no container
Solução de problemas comuns
Erro de autoridade desconhecida: o cliente não está usando a CA correta. Monte o ca.cert.pem e passe via --cacert.
Nome inválido no certificado: o SAN não bate com o host service-b. Reemita com DNS:service-b.
Data inválida: hora do host ou container fora de sincronia. Ajuste NTP.
Permissão negada para chave: verifique chmod 600 no host e montagem ro no container.
Conexão cai sem logs: confirme o network do Compose e nome do serviço.
- 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
Perguntas frequentes
O que é mTLS e quando devo usar entre microserviços?
mTLS autentica os dois lados da conexão TLS: servidor e cliente. Use quando serviços internos precisam garantir identidade forte e reduzir riscos de tráfego lateral malicioso. É útil em ambientes multi-tenant, dados sensíveis e redes compartilhadas.
Como escolher o SAN correto dos certificados no Docker Compose?
Use o nome DNS que os containers resolvem no network do Compose, normalmente o nome do serviço, como service-b. Se usar aliases, inclua todos os DNS relevantes no SAN para evitar falhas de verificação.
Posso usar Traefik ou Caddy no lugar do Nginx?
Sim. Traefik e Caddy têm boas integrações com Docker e suporte a TLS. A exigência de certificado de cliente também é possível. Avalie qual se adapta ao seu fluxo e recursos. Veja comparativos práticos para decidir.
mTLS interno substitui certificados públicos como Let’s Encrypt?
Não. Let’s Encrypt é ideal para expor serviços na internet com confiança pública. mTLS com PKI interna atende comunicação privada entre serviços. Você pode usar ambos: público para borda e PKI interna para tráfego leste-oeste.
Como rotacionar certificados sem parar serviços?
Gere novos certificados antes de expirar, monte-os em paths fixos e recarregue o processo (ex.: reload no Nginx). Mantenha a CA estável. Se trocar a CA, implemente período de coexistência confiando antiga e nova simultaneamente.
Quer adaptar este mTLS para seu ambiente?
Se você precisa levar essa abordagem para produção, com rotação automatizada e observabilidade, podemos conversar sobre o seu contexto e prioridades.
Falar sobre mTLS em produçãoVeja 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 prático: implementar mTLS entre Nginx e microserviços usando smallstep CA em Docker Compose
- Tutorial: configurar cgroup v2 e systemd ResourceControl para limitar CPU, memória e I/O de containers Docker no Debian