Configurar mTLS entre Nginx e uma aplicação Node.js usando HashiCorp Vault para emissão automática de certificados

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

teia de aranha detalhada entre ramos com uma pequena aranha no centro
Metáfora visual para redes e conexões em arquiteturas mTLS Foto: kareni / Pixabay

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 Vault

Veja também