Configurar mTLS entre Nginx e uma aplicação Node.js usando HashiCorp Vault para emissão automática de certificados
Redes & Segurança · Por Kevin Marcondes · Publicado em · Atualizado em · 11 min de leitura
Este tutorial ensina a configurar mTLS entre Nginx e uma aplicação Node.js usando HashiCorp Vault para emissão automática de certificados. Você vai montar uma PKI interna, criar AppRoles, usar o Vault Agent para materializar chaves e certificados em disco e exigir verificação mútua entre cliente e servidor no upstream HTTPS do Nginx.
Arquitetura e objetivo do tutorial
Vamos configurar mTLS entre Nginx (cliente) e Node.js (servidor) no link interno de proxy. O Vault cuidará da emissão e renovação via Vault Agent. Assim, o Nginx verifica o certificado do Node e apresenta seu próprio certificado de cliente, que o Node valida.
Este é um tutorial prático para “tutorial: configurar mTLS entre Nginx e uma aplicação Node.js usando HashiCorp Vault para emissão automática de certificados”.
Se quiser mTLS entre microserviços via Docker, veja também o passo a passo com PKI interna em Docker Compose em mTLS no Docker Compose (PKI).
| Componente | Certificados/arquivos | Finalidade |
|---|---|---|
| Vault (PKI) | Autoridade raiz + intermediária | Assinar e renovar certificados |
| Nginx (cliente) | /etc/nginx/certs/nginx.crt e .key | Apresentar cert de cliente ao Node.js |
| Node.js (servidor) | /etc/node/certs/server.crt e .key | Servir HTTPS e exigir client cert |
| CA compartilhada | /etc/*/certs/ca.pem | Confiar na cadeia de certificação |
Pré‑requisitos e avisos
Você precisa de um servidor Linux com Nginx, Node.js (>= 18), Vault, o CLI do Vault e OpenSSL instalados. Use hostnames internos (ex.: nginx.local e node.local) resolvendo para seus IPs.
Configure portas: Vault (8200), Node (8443) e Nginx (80/443). Não exponha o Vault sem autenticação e TLS. Para fortalecer o servidor, consulte o checklist de segurança de VPS e ajuste o firewall UFW.
Para proxy reverso alternativo, veja o comparativo Traefik vs Nginx.
- Acesso sudo/root
- Editor de texto (nano/vi)
- Relógio sincronizado (NTP) para evitar erros de validade de certificados
- Espaço em disco para chaves/certs com permissões restritas
1) Subir o Vault e configurar a PKI (raiz + intermediária + role)
Para fins de laboratório, usaremos o Vault em modo dev para acelerar a configuração. Jamais use o modo dev em produção. Em produção, inicialize e desembrulhe (unseal) o Vault corretamente e aplique TLS no acesso ao servidor do Vault.
Vamos criar uma CA raiz, uma intermediária e a role que permitirá emitir certificados de cliente e servidor.
# 1) Inicie o Vault (laboratório)
export VAULT_ADDR="http://127.0.0.1:8200"
nohup vault server -dev -dev-root-token-id=root >/tmp/vault-dev.log 2>&1 &
export VAULT_TOKEN="root"
# 2) Habilite PKI raiz e gere a CA
vault secrets enable pki
vault secrets tune -max-lease-ttl=87600h pki
vault write pki/root/generate/internal common_name="Demo Root CA" ttl=87600h
# 3) Habilite PKI intermediária e gere CSR
vault secrets enable -path=pki_int pki
vault secrets tune -max-lease-ttl=43800h pki_int
vault write -field=csr pki_int/intermediate/generate/internal \
common_name="Demo Intermediate CA" > /tmp/pki_intermediate.csr
# 4) Assine a intermediária com a raiz e instale
vault write -field=certificate pki/root/sign-intermediate \
csr=@/tmp/pki_intermediate.csr format=pem_bundle ttl=43800h \
> /tmp/intermediate.cert.pem
vault write pki_int/intermediate/set-signed \
certificate=@/tmp/intermediate.cert.pem
# 5) Crie role para emitir certificados de cliente e servidor
vault write pki_int/roles/svc \
allow_any_name=true \
key_type=ec key_bits=256 \
server_flag=true client_flag=true \
max_ttl=720h require_cn=false
# 6) Opcional: exporte cadeia da CA intermediária
vault read -field=certificate pki_int/ca_chain > /tmp/ca_chain.pem
2) Policies e AppRole para emissão automática
Usaremos AppRole para o Vault Agent autenticar e emitir certificados sem intervenção humana. Cada componente (Nginx e Node) terá um AppRole distinto, ambos com a mesma policy mínima para emitir pela role "svc".
Mantenha role_id e secret_id fora de VCS e com permissões 0600. Se vazar o secret_id, revogue-o imediatamente.
# Policy mínima para emissão pela role "svc"
sudo install -d -m 0750 -o root -g root /etc/vault
cat <<'HCL' | sudo tee /etc/vault/pki-svc.hcl >/dev/null
path "pki_int/issue/svc" {
capabilities = ["update"]
}
path "pki_int/ca_chain" {
capabilities = ["read"]
}
HCL
# Crie a policy e habilite AppRole
authEnable=$(vault auth list -format=table | grep approle || true)
[ -z "$authEnable" ] && vault auth enable approle || true
vault policy write pki-svc /etc/vault/pki-svc.hcl
# Crie AppRoles (nginx e node) com a policy
vault write auth/approle/role/nginx token_policies="pki-svc" \
secret_id_ttl=0 token_ttl=24h token_max_ttl=720h token_num_uses=0
vault write auth/approle/role/node token_policies="pki-svc" \
secret_id_ttl=0 token_ttl=24h token_max_ttl=720h token_num_uses=0
# Capture role_id e secret_id em arquivos locais protegidos
vault read -field=role_id auth/approle/role/nginx/role-id | sudo tee /etc/vault/nginx-role-id >/dev/null
vault write -field=secret_id -f auth/approle/role/nginx/secret-id | sudo tee /etc/vault/nginx-secret-id >/dev/null
vault read -field=role_id auth/approle/role/node/role-id | sudo tee /etc/vault/node-role-id >/dev/null
vault write -field=secret_id -f auth/approle/role/node/secret-id | sudo tee /etc/vault/node-secret-id >/dev/null
sudo chmod 600 /etc/vault/*-role-id /etc/vault/*-secret-id
3) Vault Agent para Nginx e para a aplicação Node.js
O Vault Agent fará auto_auth por AppRole e renderizará chaves e certificados em disco, renovando-os antes do vencimento. Defina destinos, permissões e um comando de reload quando os arquivos mudarem.
Garanta que apenas os processos do Nginx e do Node consigam ler suas chaves privadas (0600). Nunca deixe .key legível por outros usuários.
# Diretórios de certificados
sudo install -d -m 0750 -o root -g www-data /etc/nginx/certs
sudo install -d -m 0750 -o root -g node /etc/node/certs || true
# Config do Vault Agent para o Nginx
cat <<'HCL' | sudo tee /etc/vault/agent-nginx.hcl >/dev/null
vault {
address = "http://127.0.0.1:8200"
}
auto_auth {
method "approle" {
mount_path = "auth/approle"
config = {
role_id_file_path = "/etc/vault/nginx-role-id"
secret_id_file_path = "/etc/vault/nginx-secret-id"
}
}
sink "file" { config = { path = "/etc/vault/nginx-token" } }
}
cache { use_auto_auth_token = true }
template {
destination = "/etc/nginx/certs/nginx.key"
perms = 0600
contents = <<EOT
{{- with secret "pki_int/issue/svc" "common_name=nginx.local" "ttl=720h" -}}
{{ .Data.private_key }}
{{- end -}}
EOT
}
template {
destination = "/etc/nginx/certs/nginx.crt"
perms = 0644
contents = <<EOT
{{- with secret "pki_int/issue/svc" "common_name=nginx.local" "ttl=720h" -}}
{{ .Data.certificate }}
{{ range .Data.ca_chain }}{{ . }}
{{ end }}
{{- end -}}
EOT
}
template {
destination = "/etc/nginx/certs/ca.pem"
perms = 0644
contents = <<EOT
{{- with secret "pki_int/ca_chain" -}}
{{ .Data.certificate }}
{{- end -}}
EOT
}
HCL
# Config do Vault Agent para o Node.js
cat <<'HCL' | sudo tee /etc/vault/agent-node.hcl >/dev/null
vault {
address = "http://127.0.0.1:8200"
}
auto_auth {
method "approle" {
mount_path = "auth/approle"
config = {
role_id_file_path = "/etc/vault/node-role-id"
secret_id_file_path = "/etc/vault/node-secret-id"
}
}
sink "file" { config = { path = "/etc/vault/node-token" } }
}
cache { use_auto_auth_token = true }
template {
destination = "/etc/node/certs/server.key"
perms = 0600
contents = <<EOT
{{- with secret "pki_int/issue/svc" "common_name=node.local" "ttl=720h" -}}
{{ .Data.private_key }}
{{- end -}}
EOT
}
template {
destination = "/etc/node/certs/server.crt"
perms = 0644
contents = <<EOT
{{- with secret "pki_int/issue/svc" "common_name=node.local" "ttl=720h" -}}
{{ .Data.certificate }}
{{ range .Data.ca_chain }}{{ . }}
{{ end }}
{{- end -}}
EOT
}
template {
destination = "/etc/node/certs/ca.pem"
perms = 0644
contents = <<EOT
{{- with secret "pki_int/ca_chain" -}}
{{ .Data.certificate }}
{{- end -}}
EOT
}
HCL
# Inicie os agentes (em segundo plano)
nohup vault agent -config=/etc/vault/agent-nginx.hcl >/tmp/agent-nginx.log 2>&1 &
nohup vault agent -config=/etc/vault/agent-node.hcl >/tmp/agent-node.log 2>&1 &
4) Código Node.js exigindo mTLS do Nginx
O servidor Node.js expõe HTTPS na porta 8443 e exige certificado de cliente. Ele confia na CA da sua PKI intermediária. Ajuste caminhos se usou diretórios diferentes.
Em produção, rode seu app como serviço e recarregue no rotate de chaves. Alguns runtimes suportam reload sem derrubar conexões.
// server.js (Node 18+)
const fs = require('fs');
const https = require('https');
const options = {
key: fs.readFileSync('/etc/node/certs/server.key'),
cert: fs.readFileSync('/etc/node/certs/server.crt'),
ca: fs.readFileSync('/etc/node/certs/ca.pem'),
requestCert: true,
rejectUnauthorized: true
};
https.createServer(options, (req, res) => {
if (!req.client.authorized) {
res.writeHead(401);
return res.end('Client cert inválido');
}
const peer = req.socket.getPeerCertificate();
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(`Olá, mTLS. CN do cliente: ${peer.subject.CN}`);
}).listen(8443, () => {
console.log('Node.js mTLS escutando em https://127.0.0.1:8443');
});
5) Nginx como reverse proxy com mTLS para o upstream
O Nginx atuará como cliente TLS para o upstream Node.js. Ele apresentará seu certificado de cliente e verificará o do servidor. Use SNI para casar com o CN/SAN do Node (node.local).
Para HTTPS público no edge, integre com ACME/Let’s Encrypt depois. Veja o guia de SSL grátis com Let’s Encrypt.
# /etc/nginx/conf.d/node-mtls.conf
upstream node_upstream {
server 127.0.0.1:8443;
}
server {
listen 80;
server_name _;
location / {
proxy_pass https://node_upstream;
# mTLS no upstream
proxy_ssl_name node.local; # SNI para casar com o cert do Node
proxy_ssl_server_name on;
proxy_ssl_verify on;
proxy_ssl_verify_depth 2;
proxy_ssl_trusted_certificate /etc/nginx/certs/ca.pem; # CA que assinou o cert do Node
# Certificado de cliente apresentado pelo Nginx ao Node
proxy_ssl_certificate /etc/nginx/certs/nginx.crt;
proxy_ssl_certificate_key /etc/nginx/certs/nginx.key;
# Cabeçalhos usuais
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
6) Como confirmar que funcionou e solução de problemas
Após concluir o “tutorial: configurar mTLS entre Nginx e uma aplicação Node.js usando HashiCorp Vault para emissão automática de certificados”, valide a presença dos arquivos. Suba o Node e recarregue o Nginx. Depois, teste a conexão direta com openssl e via Nginx com curl. Verifique também datas de expiração para confirmar a renovação automática.
Problemas comuns e correções:
- x509: unknown authority: verifique proxy_ssl_trusted_certificate e ca no Node.
- mismatch SNI/CN: alinhe proxy_ssl_name com o CN/SAN emitido para o Node.
- EACCES nas chaves: ajuste donos/grupos e permissões 0600/0644.
- AppRole inválido: regenere secret_id e confira role_id/secret_id nos agentes.
- Skew de relógio: sincronize NTP para evitar NotBefore/NotAfter incorretos.
# 1) Confira os arquivos foram emitidos
ls -l /etc/nginx/certs /etc/node/certs
# 2) Suba o servidor Node.js
node /path/para/server.js &> /tmp/node.log &
# 3) Teste mTLS direto contra o Node (apresentando o cert do Nginx)
openssl s_client -connect 127.0.0.1:8443 \
-cert /etc/nginx/certs/nginx.crt \
-key /etc/nginx/certs/nginx.key \
-CAfile /etc/nginx/certs/ca.pem -quiet <<< "GET / HTTP/1.1\nHost: node.local\n\n"
# 4) Valide a config do Nginx e recarregue
sudo nginx -t && sudo systemctl reload nginx
# 5) Teste via Nginx (HTTP -> Nginx -> mTLS -> Node)
curl -i http://127.0.0.1/
# 6) Cheque a expiração do cert do Node (renovação automática)
openssl x509 -in /etc/node/certs/server.crt -noout -enddate
Perguntas frequentes
Posso usar o modo dev do Vault em produção?
Não. O modo dev é apenas para laboratório: ele roda em memória, vem desembrulhado e usa um token estático. Em produção, inicialize o Vault com armazenamento persistente, habilite TLS no acesso ao Vault e faça unseal seguro com várias chaves.
Como combinar mTLS interno com HTTPS público para clientes?
Use mTLS apenas no link interno Nginx→Node e configure HTTPS público no Nginx com certificados ACME/Let’s Encrypt para o tráfego externo. O tutorial de SSL grátis com Let’s Encrypt mostra os passos para o edge.
Como restringir quais domínios podem ser emitidos pela role do Vault?
No exemplo usamos allow_any_name para simplificar. Em ambientes reais, defina allowed_domains e allow_subdomains, e desabilite allow_any_name. Assim você evita a emissão acidental de certificados com nomes fora do seu domínio interno.
Como rotacionar certificados sem downtime?
Com Vault Agent escrevendo novos arquivos, faça reload do Nginx (nginx -s reload) para carregar o material renovado. Para Node.js, rode como serviço com suporte a reload gracioso ou reinício controlado atrás do Nginx para evitar interrupções.
Posso trocar AppRole por outro método de auto_auth?
Sim. Você pode usar métodos como Kubernetes, AWS IAM ou JWT/OIDC, conforme seu ambiente. O importante é conceder apenas as capacidades mínimas (issue na role específica e leitura da cadeia da CA) para o princípio do menor privilégio.
Precisa adaptar este mTLS ao seu ambiente?
Se quiser revisar sua PKI, políticas do Vault e automação de renovação em produção, descreva seu cenário e podemos avaliar juntos os próximos passos.
Falar sobre mTLS e VaultVeja 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