O modelo de segurança “confia na rede interna” morreu. Em 2026, qualquer serviço que aceite ligações sem provar quem está do outro lado é um alvo. O mTLS em Node.js resolve isto ao exigir que cliente e servidor apresentem certificados válidos antes de trocar um único byte de dados aplicacionais. Não é um cadeado decorativo, é autenticação criptográfica nas duas direções.

Este tutorial leva-te do zero a um sistema mTLS completo e funcional, com TLS 1.3, em 12 passos. Vais criar a tua própria Autoridade Certificadora, emitir certificados de servidor e de cliente, construir um servidor Node.js que recusa qualquer cliente não autenticado, escrever um cliente que apresenta o seu certificado, e mapear a identidade do certificado para permissões reais da aplicação. No fim tens um projeto que podes adaptar para APIs internas, comunicação entre microsserviços ou acesso a sistemas sensíveis.

Tempo estimado: 45 a 60 minutos. Todo o código usa apenas módulos nativos do Node.js (node:tls, node:https, node:crypto) e a ferramenta openssl. Sem dependências externas pesadas, sem frameworks criptográficos opacos.

mTLS em Node.js: porque é o padrão zero-trust em 2026

O TLS normal (o que protege o HTTPS que usas todos os dias) autentica apenas o servidor. O teu navegador verifica que banco.pt é mesmo o banco, mas o banco não tem como saber, ao nível do TLS, quem és tu. A autenticação do utilizador acontece depois, com palavra-passe ou token, já dentro da sessão cifrada.

O TLS mútuo, ou mTLS, fecha esse buraco. Ambos os lados apresentam um certificado e ambos verificam o certificado do outro contra uma Autoridade Certificadora (CA) de confiança. Se o cliente não tiver um certificado válido emitido pela CA esperada, a ligação cai durante o handshake. Nenhum pedido chega sequer ao código da aplicação. Isto torna o mTLS a base prática da arquitetura zero-trust, onde nada é confiável só por estar “dentro” da rede.

Em 2026, três forças empurram o mTLS para a corrente principal. Primeiro, a velocidade dos ataques: quando um intruso entra na rede, mover-se lateralmente entre serviços que confiam cegamente uns nos outros é trivial, e o mTLS bloqueia esse movimento. Segundo, as malhas de serviço (service meshes) como o Istio e o Linkerd usam mTLS por omissão, normalizando o padrão. Terceiro, regulação como a NIS2 pressiona organizações em Portugal e na UE a demonstrar controlo de acesso forte entre sistemas. O mTLS dá uma resposta auditável.

O Node.js é uma escolha natural para implementar mTLS porque o suporte está embutido. O módulo node:tls expõe diretamente as opções que precisas: requestCert, rejectUnauthorized, ca, minVersion. Não precisas de um proxy reverso a fazer o trabalho, embora possas combinar os dois. Segundo a documentação oficial do módulo TLS, estas primitivas dependem do OpenSSL incluído na build do Node, por isso o comportamento exato dos algoritmos segue as capacidades do OpenSSL da tua plataforma.

O que vais construir: arquitetura do projeto mTLS

Antes de escrever código, convém ver o mapa. O projeto tem quatro peças que trabalham juntas. A CA raiz é a âncora de confiança: assina os outros certificados e ambos os lados confiam nela. O certificado do servidor identifica o servidor. O certificado do cliente identifica cada cliente autorizado. O servidor e o cliente Node.js trocam e verificam tudo isto durante o handshake TLS 1.3.

ComponenteFicheiroFunçãoQuem confia nele
CA raizca-cert.pemAssina e valida todos os certificadosServidor e cliente
Chave da CAca-key.pemAssina novos certificados (segredo máximo)Apenas o operador
Certificado do servidorserver-cert.pemProva a identidade do servidorCliente
Certificado do clienteclient-cert.pemProva a identidade do clienteServidor
Servidor Nodeserver.jsExige e valida o certificado do cliente(serviço)
Cliente Nodeclient.jsApresenta o seu certificado ao servidor(serviço)

O fluxo é direto. O cliente liga-se ao servidor e envia o seu certificado. O servidor verifica esse certificado contra a CA raiz. Em paralelo, o cliente verifica o certificado do servidor contra a mesma CA. Se ambas as verificações passarem, o túnel TLS 1.3 abre e a aplicação corre. Se qualquer uma falhar, a ligação termina antes de qualquer pedido HTTP. É exatamente este comportamento “falha fechada” que torna o mTLS em Node.js tão robusto.

Pré-requisitos e versões necessárias

Este tutorial assume uma base de runtime atual e suportada. As linhas LTS do Node.js receberam correções de segurança relevantes para TLS na ronda de janeiro de 2026, incluindo melhorias no parsing de certificados, por isso usa uma versão LTS atualizada e não uma build antiga esquecida no portátil. Consulta o calendário oficial de versões do Node.js para confirmar qual a LTS ativa.

FerramentaVersão recomendadaComo verificarNotas
Node.js24 LTS (ou 22 LTS)node --versionUsa sempre uma linha LTS suportada
npmIncluído no Nodenpm --versionSó para inicializar o projeto
OpenSSL3.xopenssl versionGera CA e certificados
curlRecente com TLS 1.3curl --versionOpcional, para testes
Sistema operativoLinux, macOS ou WSL2(qualquer)Os comandos assumem shell POSIX

Confirma tudo antes de avançar. Abre o terminal e corre as três verificações principais:

node --version
# Esperado: v24.x.x ou v22.x.x

openssl version
# Esperado: OpenSSL 3.x.x

npm --version
# Qualquer versao recente incluida no Node serve

Se o node --version mostrar uma versão antiga (18 ou inferior), atualiza antes de continuar. As opções de TLS 1.3 e o comportamento de validação de certificados são mais previsíveis nas linhas recentes. Conhecimentos úteis mas não obrigatórios: saber o que é uma chave pública e o conceito de cadeia de confiança. Se precisares de uma revisão, o nosso artigo sobre HTTPS e TLS explica o cadeado e o handshake em linguagem simples.

Passo 1 e 2: inicializar o projeto e criar a CA raiz

Começa por criar a estrutura. Vamos manter os certificados numa pasta certs separada do código, um hábito que evita commits acidentais de chaves privadas para o Git.

mkdir mtls-node && cd mtls-node
npm init -y
mkdir certs
echo "certs/" > .gitignore
echo "node_modules/" >> .gitignore

Agora cria a Autoridade Certificadora raiz. A CA é o coração da confiança: a sua chave privada assina todos os outros certificados, e o seu certificado público é o que servidor e cliente usam para verificar tudo. Gera primeiro a chave privada da CA com uma curva elíptica moderna.

# Chave privada da CA (curva eliptica P-256, moderna e rapida)
openssl ecparam -name prime256v1 -genkey -noout -out certs/ca-key.pem

# Certificado raiz autoassinado, valido 10 anos
openssl req -new -x509 -sha256 -days 3650 \
  -key certs/ca-key.pem \
  -out certs/ca-cert.pem \
  -subj "/C=PT/O=Exemplo Lda/CN=mTLS Demo Root CA"

Repara em duas escolhas. Usamos prime256v1 (também conhecida por P-256) em vez de RSA porque as chaves elípticas são mais pequenas e rápidas para o mesmo nível de segurança. A documentação do Node recomenda curvas de pelo menos 224 bits para ECDSA, e a P-256 fica confortavelmente acima disso. Usamos -sha256 porque o SHA-1 e o MD5 já não são aceitáveis para assinaturas, um ponto que tanto a documentação do Node como a história da colisão SHAttered do SHA-1 deixam claro.

A chave ca-key.pem é o segredo mais valioso de todo o sistema. Quem a tiver pode emitir certificados que o teu servidor vai aceitar. Em produção, esta chave fica num módulo de hardware (HSM) ou num cofre de segredos, nunca no disco de um servidor de aplicação.

Passo 3 e 4: emitir os certificados de servidor e de cliente

Com a CA pronta, emitimos dois certificados assinados por ela. Cada um precisa de três passos: gerar a chave privada, criar um pedido de assinatura (CSR) e assiná-lo com a CA. Começa pelo servidor.

# 1. Chave privada do servidor
openssl ecparam -name prime256v1 -genkey -noout -out certs/server-key.pem

# 2. Pedido de assinatura (CSR), CN = localhost para testes
openssl req -new -sha256 \
  -key certs/server-key.pem \
  -out certs/server.csr \
  -subj "/C=PT/O=Exemplo Lda/CN=localhost"

# 3. Assinar com a CA, com SAN para localhost (obrigatorio em clientes modernos)
openssl x509 -req -sha256 -days 365 \
  -in certs/server.csr \
  -CA certs/ca-cert.pem -CAkey certs/ca-key.pem -CAcreateserial \
  -out certs/server-cert.pem \
  -extfile <(printf "subjectAltName=DNS:localhost,IP:127.0.0.1")

O Subject Alternative Name (SAN) é crítico. Clientes TLS modernos, incluindo o Node.js, ignoram o campo CN e validam o nome do host contra o SAN. Se esqueceres o SAN, o cliente recusa o certificado com um erro de hostname, mesmo que tudo o resto esteja correto. Esta é uma das armadilhas mais comuns e voltamos a ela mais à frente.

Agora o certificado do cliente. O processo é igual, mas o nome comum (CN) identifica o cliente, não um host. Vamos usar esse CN mais tarde para decidir permissões. Pensa no CN como o “nome de utilizador” criptográfico do cliente.

# 1. Chave privada do cliente
openssl ecparam -name prime256v1 -genkey -noout -out certs/client-key.pem

# 2. CSR, CN identifica o cliente (vamos usa-lo para autorizacao)
openssl req -new -sha256 \
  -key certs/client-key.pem \
  -out certs/client.csr \
  -subj "/C=PT/O=Exemplo Lda/CN=servico-faturacao"

# 3. Assinar com a CA
openssl x509 -req -sha256 -days 365 \
  -in certs/client.csr \
  -CA certs/ca-cert.pem -CAkey certs/ca-key.pem -CAcreateserial \
  -out certs/client-cert.pem

Confirma que tudo foi assinado corretamente inspecionando um certificado. O comando seguinte mostra o emissor (deve ser a tua CA), o sujeito e o período de validade. Consulta a página de manual do openssl-req para todas as opções disponíveis.

openssl x509 -in certs/client-cert.pem -noout -subject -issuer -dates

# Saida esperada:
# subject=C=PT, O=Exemplo Lda, CN=servico-faturacao
# issuer=C=PT, O=Exemplo Lda, CN=mTLS Demo Root CA
# notBefore=Mar  3 10:00:00 2026 GMT
# notAfter=Mar  3 10:00:00 2027 GMT

Passo 5: o servidor TLS 1.3 em Node.js

Chegou o código. Cria um ficheiro server.js. Começamos com um servidor HTTPS que carrega a sua chave e certificado, mas ainda sem exigir o certificado do cliente. Vamos adicionar a exigência no passo seguinte, para veres a diferença.

// server.js
import https from 'node:https';
import fs from 'node:fs';

const options = {
  key: fs.readFileSync('certs/server-key.pem'),
  cert: fs.readFileSync('certs/server-cert.pem'),
  ca: fs.readFileSync('certs/ca-cert.pem'),

  // Forcar TLS 1.3, sem fallback para versoes antigas
  minVersion: 'TLSv1.3',
  maxVersion: 'TLSv1.3',
};

const server = https.createServer(options, (req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ mensagem: 'Ligacao TLS 1.3 estabelecida' }));
});

server.listen(8443, () => {
  console.log('Servidor mTLS a escutar em https://localhost:8443');
});

As linhas minVersion e maxVersion a ‘TLSv1.3’ são a tua primeira linha de defesa. Elas dizem ao Node para recusar qualquer ligação que não fale TLS 1.3. Isto elimina de uma vez toda uma classe de ataques contra versões antigas do protocolo (TLS 1.0, 1.1 e os pontos fracos do 1.2 mal configurado). O TLS 1.3, definido no RFC 8446, removeu cipher suites inseguras e simplificou o handshake.

Repara também que o ficheiro usa import em vez de require. O ecossistema Node de 2026 é cada vez mais ESM-first. Para isto funcionar, adiciona "type": "module" ao teu package.json. Arranca o servidor com node server.js e confirma que vês a mensagem de escuta.

Passo 6: exigir e validar o certificado do cliente

Agora transformamos um servidor HTTPS normal num servidor mTLS. Duas opções fazem a magia: requestCert pede o certificado ao cliente, e rejectUnauthorized recusa a ligação se esse certificado não for válido contra a CA. Atualiza o objeto options.

const options = {
  key: fs.readFileSync('certs/server-key.pem'),
  cert: fs.readFileSync('certs/server-cert.pem'),
  ca: fs.readFileSync('certs/ca-cert.pem'),

  minVersion: 'TLSv1.3',
  maxVersion: 'TLSv1.3',

  // O coracao do mTLS:
  requestCert: true,        // pede o certificado ao cliente
  rejectUnauthorized: true, // recusa se o certificado nao for valido
};

Com rejectUnauthorized: true, qualquer cliente sem um certificado assinado pela tua CA é rejeitado durante o handshake. O callback do servidor nunca chega a correr para esses clientes. Esta é a propriedade “falha fechada” do mTLS bem feito: a segurança não depende de o teu código de aplicação lembrar-se de verificar nada.

Inspecionar o certificado do cliente autenticado

Quando um cliente válido se liga, queres saber quem é. O Node expõe o certificado do par através de req.socket.getPeerCertificate() e o estado de validação através de req.socket.authorized. Vamos usar isto para registar a identidade de cada pedido.

const server = https.createServer(options, (req, res) => {
  const cert = req.socket.getPeerCertificate();
  const autorizado = req.socket.authorized;

  // Com rejectUnauthorized:true so chegamos aqui se autorizado for true,
  // mas verificamos na mesma por defesa em profundidade.
  if (!autorizado || !cert || !cert.subject) {
    res.writeHead(401);
    res.end(JSON.stringify({ erro: 'Certificado de cliente em falta ou invalido' }));
    return;
  }

  const cliente = cert.subject.CN;
  console.log(`Pedido autenticado de: ${cliente}`);

  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ mensagem: `Ola, ${cliente}`, autenticado: true }));
});

O campo cert.subject.CN contém o “servico-faturacao” que definimos quando emitimos o certificado do cliente. A partir daqui, a identidade do cliente é um facto criptográfico, não uma afirmação que ele faz num cabeçalho HTTP que poderia falsificar.

Passo 7: mapear identidade e autorização ao nível da aplicação

Aqui está o erro conceptual mais perigoso do mTLS: pensar que autenticação é o mesmo que autorização. Não é. O mTLS prova quem é o cliente. Mas decidir o que esse cliente pode fazer é trabalho da tua aplicação. Um certificado válido não deve dar acesso a tudo automaticamente.

A documentação do Node reforça este ponto: o certificado identifica o cliente, mas o servidor ainda deve mapear essa identidade para permissões, escopos ou políticas internas. Vamos construir uma tabela simples de autorização que mapeia o CN do certificado para um papel.

// Mapa de autorizacao: CN do certificado -> permissoes
const POLITICAS = {
  'servico-faturacao': { papel: 'escrita', rotas: ['/faturas'] },
  'servico-relatorios': { papel: 'leitura', rotas: ['/faturas', '/relatorios'] },
};

function autorizar(cn, rota) {
  const politica = POLITICAS[cn];
  if (!politica) return false;
  return politica.rotas.includes(rota);
}

// Dentro do handler:
const cliente = cert.subject.CN;
const rota = new URL(req.url, 'https://localhost').pathname;

if (!autorizar(cliente, rota)) {
  res.writeHead(403);
  res.end(JSON.stringify({ erro: `Cliente ${cliente} sem acesso a ${rota}` }));
  return;
}

Este padrão separa de forma limpa as duas responsabilidades. O TLS trata da autenticação (“este é mesmo o servico-faturacao”). A função autorizar trata do controlo de acesso (“o servico-faturacao pode escrever em /faturas, mas não pode ver relatórios”). É a mesma filosofia de princípio do menor privilégio que aplicamos em autenticação SSH com chaves Ed25519: a identidade forte é o início, não o fim, do controlo de acesso.

Passo 8: o cliente mTLS em Node.js

Um servidor que exige certificados de cliente é inútil sem um cliente que os apresente. Cria client.js. O cliente carrega a sua própria chave e certificado e, crucialmente, a CA, para poder verificar o certificado do servidor.

// client.js
import https from 'node:https';
import fs from 'node:fs';

const options = {
  hostname: 'localhost',
  port: 8443,
  path: '/faturas',
  method: 'GET',

  // O cliente apresenta o SEU certificado
  key: fs.readFileSync('certs/client-key.pem'),
  cert: fs.readFileSync('certs/client-cert.pem'),

  // E verifica o certificado do servidor contra a CA
  ca: fs.readFileSync('certs/ca-cert.pem'),

  minVersion: 'TLSv1.3',
};

const req = https.request(options, (res) => {
  let dados = '';
  res.on('data', (c) => (dados += c));
  res.on('end', () => {
    console.log('Estado:', res.statusCode);
    console.log('Resposta:', dados);
  });
});

req.on('error', (e) => console.error('Erro de ligacao:', e.message));
req.end();

Corre o servidor numa janela (node server.js) e o cliente noutra (node client.js). Se tudo estiver bem, vês uma resposta 200 com a mensagem de saudação. Para provar que o mTLS funciona, tenta ligar-te sem certificado, por exemplo com curl https://localhost:8443/faturas -k. A ligação é recusada porque o curl, sem certificado de cliente, falha o handshake. Esse é o sistema a fazer o seu trabalho.

Passo 9: forçar TLS 1.3 e cipher suites seguras

Já forçámos TLS 1.3 com minVersion. Vale a pena perceber o que isso te dá. O TLS 1.3 só permite cinco cipher suites, todas com sigilo perfeito futuro (forward secrecy) e cifra autenticada. Não há como negociar uma combinação fraca por engano, ao contrário do TLS 1.2 onde a configuração errada abre buracos.

Cipher suite TLS 1.3CifraHashRecomendação
TLS_AES_256_GCM_SHA384AES-256-GCMSHA-384Preferida para dados sensíveis
TLS_CHACHA20_POLY1305_SHA256ChaCha20-Poly1305SHA-256Ótima em hardware sem AES-NI
TLS_AES_128_GCM_SHA256AES-128-GCMSHA-256Sólida e rápida

Se quiseres restringir explicitamente as cipher suites de TLS 1.3, podes passar a opção ciphers. Na maioria dos casos, deixar o Node usar a ordem por omissão do OpenSSL é a escolha certa, porque essa ordem já prioriza as opções fortes. Só restrinjas se tiveres um requisito de conformidade específico.

// Restringir cipher suites de TLS 1.3 (opcional)
const options = {
  // ... resto da configuracao ...
  minVersion: 'TLSv1.3',
  ciphers: [
    'TLS_AES_256_GCM_SHA384',
    'TLS_CHACHA20_POLY1305_SHA256',
  ].join(':'),
};

A OWASP mantém recomendações atualizadas sobre configuração de TLS. Se vais para produção, vale a pena passar pela folha de dicas de TLS da OWASP antes de fixar a tua configuração final.

Passo 10 e 11: rotação, expiração e revogação de certificados

Certificados expiram, e é bom que assim seja. Um certificado de cliente válido 365 dias limita o dano se uma chave for comprometida sem ser detetada. Mas isto cria uma obrigação operacional: tens de rodar os certificados antes de expirarem, ou os teus serviços param de comunicar à meia-noite de uma data qualquer.

A rotação é simplesmente repetir o passo de emissão com uma nova chave e novo certificado, depois recarregar no serviço. Automatiza a verificação de expiração para nunca seres apanhado de surpresa.

# Verificar se um certificado expira nos proximos 30 dias
openssl x509 -in certs/client-cert.pem -noout -checkend 2592000
# Codigo de saida 0 = ainda valido por mais de 30 dias
# Codigo de saida 1 = expira dentro de 30 dias, RODAR JA

# Ver a data exata de expiracao
openssl x509 -in certs/client-cert.pem -noout -enddate

A revogação resolve o problema oposto: e se um certificado ainda válido for comprometido e precisares de o invalidar já? Para sistemas pequenos, a abordagem mais simples é manter uma lista de allow no código (como o nosso mapa POLITICAS): remove o CN comprometido e ele perde acesso imediatamente, mesmo com certificado tecnicamente válido. Para sistemas maiores, usa uma Lista de Revogação de Certificados (CRL) ou OCSP, que o Node consegue verificar com configuração adicional.

Passo 12: testar com OpenSSL, curl e automação

Confiar sem testar é negligência. A ferramenta openssl s_client deixa-te simular um cliente mTLS e ver o handshake completo, incluindo a versão de protocolo negociada e a cipher suite escolhida.

# Ligar com certificado de cliente e inspecionar o handshake
openssl s_client -connect localhost:8443 \
  -cert certs/client-cert.pem \
  -key certs/client-key.pem \
  -CAfile certs/ca-cert.pem \
  -tls1_3

# Procura na saida:
# Protocol  : TLSv1.3
# Cipher    : TLS_AES_256_GCM_SHA384
# Verify return code: 0 (ok)

O curl faz um teste mais próximo do mundo real. Com os certificados certos, deve devolver 200. Sem eles, deve falhar.

# Pedido autenticado correto
curl --cert certs/client-cert.pem \
     --key certs/client-key.pem \
     --cacert certs/ca-cert.pem \
     https://localhost:8443/faturas
# Esperado: {"mensagem":"Ola, servico-faturacao", ...}

# Sem certificado de cliente (deve FALHAR)
curl --cacert certs/ca-cert.pem https://localhost:8443/faturas
# Esperado: erro de handshake TLS, ligacao recusada

Esta dualidade (sucesso com certificado, falha sem ele) é o teste mais importante de todos. Se o segundo comando devolver dados em vez de um erro, a tua configuração mTLS está partida e qualquer um pode aceder. Inclui ambos os testes num script de CI para apanhar regressões antes de chegarem a produção.

Erros comuns e armadilhas no mTLS em Node.js

O mTLS falha quase sempre pelas mesmas razões. Conhecer estas armadilhas poupa-te horas de depuração frustrante. A tabela resume as mais frequentes.

ArmadilhaSintomaSolução
Falta de SAN no certificado do servidorErro de hostname no clienteAdicionar subjectAltName com o nome ou IP
rejectUnauthorized: false esquecidoClientes inválidos são aceitesGarantir true em produção
CA em falta no clienteCliente recusa o servidor (self-signed)Carregar ca-cert.pem no cliente
Chave privada com permissões abertasRisco de fuga de chavechmod 600 nas chaves .pem
Certificado expiradoLigações param de repenteMonitorizar com -checkend
Confundir autenticação com autorizaçãoQualquer cert válido acede a tudoMapear CN para permissões

A pior de todas, e a mais comum em tutoriais apressados pela internet, é rejectUnauthorized: false. As pessoas adicionam essa linha para “fazer funcionar” durante o desenvolvimento e esquecem-se de a remover. O resultado é um servidor que pede certificados mas aceita qualquer um, incluindo certificados autoassinados por um atacante. É segurança teatral: parece mTLS, mas não protege nada. Trata qualquer rejectUnauthorized: false num code review como um bug crítico.

Resolução de problemas: 8 erros e como os corrigir

Quando o handshake falha, o Node devolve códigos de erro específicos. Esta tabela mapeia os mais comuns para a sua causa e correção.

Código de erroCausa provávelCorreção
UNABLE_TO_VERIFY_LEAF_SIGNATURECliente não tem a CA carregadaPassar ca nas opções do cliente
ERR_TLS_CERT_ALTNAME_INVALIDSAN não corresponde ao hostnameReemitir cert com SAN correto
CERT_HAS_EXPIREDCertificado fora de validadeRodar o certificado
DEPTH_ZERO_SELF_SIGNED_CERTCert não assinado pela CA esperadaReemitir assinado pela CA correta
ECONNRESET no handshakeCliente sem certificado e servidor exigeAdicionar cert e key ao cliente
ERR_SSL_WRONG_VERSION_NUMBERCliente tenta HTTP em porta TLSUsar https://, não http://
ERR_TLS_HANDSHAKE_TIMEOUTVersões de TLS incompatíveisAlinhar minVersion nos dois lados
EACCES ao ler chavePermissões de ficheiro erradasCorrigir dono e chmod da chave

Quando estiveres perdido, ativa o registo detalhado do Node com a variável de ambiente NODE_DEBUG=tls node server.js. Isto despeja cada passo do handshake, incluindo que certificados foram pedidos e porque a validação falhou. É verboso, mas mostra exatamente onde o processo quebra. Combina com openssl s_client do lado do cliente para ver os dois ângulos do mesmo handshake.

Um detalhe sobre o UNABLE_TO_VERIFY_LEAF_SIGNATURE: ele aparece quando o lado que valida não tem como construir a cadeia até uma CA de confiança. Em mTLS, isto acontece tanto no cliente (sem a CA do servidor) como no servidor (sem a CA do cliente). Verifica sempre que a opção ca está presente em ambos os lados e aponta para o mesmo ca-cert.pem.

Dicas avançadas para mTLS em produção

Sair do localhost para produção muda algumas coisas. Estas práticas separam um protótipo de um sistema que aguenta auditoria.

  • Nunca metas chaves no código nem no repositório. Carrega-as de um cofre de segredos (Vault, AWS Secrets Manager) ou de ficheiros com permissões restritas montados em runtime. A linha certs/ no .gitignore é o mínimo absoluto.
  • Usa uma CA intermédia. Em produção, a CA raiz fica offline e assina apenas uma CA intermédia, que por sua vez emite os certificados do dia a dia. Se a intermédia for comprometida, revogas só essa, sem tocar na raiz.
  • Recarrega certificados sem reiniciar. O https.Server aceita um SNICallback e o método setSecureContext() permite trocar certificados em runtime, útil para rotação sem downtime.
  • Termina o mTLS o mais perto possível da aplicação. Se um proxy (Nginx, Envoy) terminar o TLS, garante que ele passa a identidade do certificado validado para a app por um cabeçalho de confiança, e que esse cabeçalho não pode vir do exterior.
  • Monitoriza expirações de forma centralizada. Um certificado expirado em produção é um incidente de disponibilidade. Alerta com 30 dias de antecedência, no mínimo.
  • Regista a identidade em cada pedido. O CN do certificado deve aparecer nos teus logs de acesso. Isto dá-te uma trilha de auditoria criptograficamente forte, valiosa em resposta a incidentes.

Para serviços de alto volume, considera a reutilização de sessões TLS (session resumption), que o TLS 1.3 suporta com tickets de sessão e reduz o custo do handshake em ligações repetidas. O Node ativa isto por omissão, mas em clusters com vários processos precisas de partilhar o segredo dos tickets entre eles para a reutilização funcionar entre instâncias.

O projeto completo: estrutura final

Juntando tudo, a árvore do projeto fica assim. Os ficheiros de certificado vivem em certs/ (e nunca no Git), o código fica na raiz.

mtls-node/
|- certs/
|  |- ca-cert.pem        # CA publica (confiada por ambos)
|  |- ca-key.pem         # CA privada (SEGREDO MAXIMO)
|  |- server-cert.pem    # certificado do servidor
|  |- server-key.pem     # chave do servidor
|  |- client-cert.pem    # certificado do cliente
|  |- client-key.pem     # chave do cliente
|- server.js              # servidor mTLS com TLS 1.3
|- client.js              # cliente mTLS
|- package.json           # com "type": "module"
|- .gitignore             # ignora certs/ e node_modules/

Aqui está o server.js final, com autenticação, autorização e TLS 1.3 forçado, pronto para adaptares. Este é o projeto completo e funcional que prometemos no início.

// server.js (versao completa)
import https from 'node:https';
import fs from 'node:fs';

const options = {
  key: fs.readFileSync('certs/server-key.pem'),
  cert: fs.readFileSync('certs/server-cert.pem'),
  ca: fs.readFileSync('certs/ca-cert.pem'),
  minVersion: 'TLSv1.3',
  maxVersion: 'TLSv1.3',
  requestCert: true,
  rejectUnauthorized: true,
};

const POLITICAS = {
  'servico-faturacao': { rotas: ['/faturas'] },
  'servico-relatorios': { rotas: ['/faturas', '/relatorios'] },
};

function autorizar(cn, rota) {
  const p = POLITICAS[cn];
  return p ? p.rotas.includes(rota) : false;
}

const server = https.createServer(options, (req, res) => {
  const cert = req.socket.getPeerCertificate();
  if (!req.socket.authorized || !cert?.subject) {
    res.writeHead(401); res.end('{"erro":"nao autenticado"}'); return;
  }
  const cn = cert.subject.CN;
  const rota = new URL(req.url, 'https://localhost').pathname;
  if (!autorizar(cn, rota)) {
    res.writeHead(403); res.end(`{"erro":"${cn} sem acesso"}`); return;
  }
  console.log(`OK: ${cn} -> ${rota}`);
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ mensagem: `Ola, ${cn}`, rota }));
});

server.listen(8443, () => console.log('mTLS em https://localhost:8443'));

Com este projeto tens uma base sólida de mTLS em Node.js que podes estender. Adiciona mais certificados de cliente para mais serviços, integra com um cofre de segredos, ou coloca-o atrás de uma malha de serviço. Os princípios (CA de confiança, certificados nos dois lados, autorização separada da autenticação) mantêm-se em qualquer escala.

Perguntas frequentes sobre mTLS em Node.js

Qual a diferença entre TLS e mTLS?

O TLS normal autentica só o servidor: o cliente verifica que está a falar com o servidor certo, mas o servidor não sabe quem é o cliente ao nível do protocolo. O mTLS adiciona a autenticação do cliente, exigindo que ambos os lados apresentem e validem certificados. O resultado é confiança bidirecional estabelecida antes de qualquer dado aplicacional ser trocado.

Preciso de comprar certificados de uma CA pública para mTLS?

Não, e na maioria dos casos não deves. Para comunicação interna entre serviços, a tua própria CA privada (como criámos no passo 2) é a escolha certa: dá-te controlo total sobre quem recebe certificados. CAs públicas como Let’s Encrypt servem para certificados de servidor que navegadores públicos precisam de confiar, não para certificados de cliente de uma API interna.

O mTLS substitui tokens JWT ou chaves de API?

Não exatamente, resolvem camadas diferentes. O mTLS autentica a máquina ou serviço ao nível da ligação. JWT e chaves de API costumam autenticar um utilizador ou um pedido específico dentro da ligação. Muitos sistemas robustos combinam os dois: o mTLS prova que o serviço cliente é legítimo, e um token dentro do pedido identifica o utilizador final. Vê o nosso guia de autenticação de dois fatores em Node.js para as camadas de utilizador.

Porque devo usar curvas elípticas (P-256) em vez de RSA?

As chaves de curva elíptica oferecem o mesmo nível de segurança que chaves RSA muito maiores, com menos custo computacional e handshakes mais rápidos. Uma chave P-256 é comparável em segurança a uma RSA de 3072 bits, mas muito mais leve. O Node recomenda curvas de pelo menos 224 bits para ECDSA, e a P-256 cumpre isso. Para um mergulho no tema, vê as nossas assinaturas digitais explicadas.

Como revogo um certificado de cliente comprometido?

Para sistemas pequenos, a forma mais rápida é remover o CN do cliente da tua lista de autorização da aplicação (o mapa POLITICAS): o acesso desaparece de imediato, mesmo com o certificado tecnicamente válido. Para sistemas maiores, usa uma CRL (Lista de Revogação de Certificados) ou OCSP, mecanismos padrão que o Node consegue verificar com configuração adicional.

O mTLS protege contra um certificado roubado?

Se um atacante roubar tanto o certificado como a chave privada do cliente, pode personificar esse cliente até o certificado ser revogado ou expirar. Por isso a proteção da chave privada (permissões restritas, cofres de segredos, HSMs) e a validade curta dos certificados são tão importantes. O mTLS é tão forte quanto a tua gestão das chaves privadas.

Posso usar mTLS com um proxy como Nginx à frente?

Sim, e é comum. O proxy pode terminar o mTLS e passar a identidade validada do cliente para a app Node por um cabeçalho de confiança. O ponto crítico de segurança: garante que esse cabeçalho só pode ser definido pelo proxy interno e nunca aceite vindo do exterior, ou um atacante pode forjar a identidade. Para máxima segurança, termina o mTLS na própria app Node, como fizemos neste tutorial.

Que versão de TLS devo usar em 2026?

TLS 1.3, sem exceções para tráfego novo. É mais rápido, removeu cipher suites inseguras e simplificou o handshake face ao TLS 1.2. Força-o com minVersion: 'TLSv1.3' nos dois lados. Só desce para TLS 1.2 se tiveres de comunicar com um sistema legado que não suporta 1.3, e nesse caso documenta a exceção e planeia a migração.

O mTLS em Node.js deixou de ser um luxo de grandes empresas para se tornar uma prática base de qualquer arquitetura zero-trust em 2026. Com a tua própria CA, certificados nos dois lados, TLS 1.3 forçado e autorização separada da autenticação, tens um padrão que escala de dois serviços a duzentos. O código deste tutorial é o teu ponto de partida: clona-o, adapta-o e leva a confiança bidirecional para os teus sistemas.