SSD Nodes Learn 🎉 VPS desde $5.50/mês
Guias Matt ConnorPor Matt Connor

mTLS no nginx: certificados de cliente com CA privada

Proteja um painel com mTLS no nginx: crie uma CA privada com openssl, emita certificados de cliente e recuse conexoes sem certificado valido.

O que o mTLS faz

O TLS mútuo, normalmente escrito como mTLS, faz com que o nginx peça um certificado a cada cliente e recuse o pedido quando esse certificado está ausente ou não foi emitido por uma autoridade de certificação (CA) que controla. A verificação ocorre durante o handshake do TLS (segurança da camada de transporte). Assim, um cliente sem um certificado válido nunca chega à aplicação. Essa é a principal vantagem: um painel de administração ou um endpoint de métricas pode ficar na internet pública sem uma página de login e sem nada para um bot tentar adivinhar.

A configuração é pequena. Inclui uma CA privada criada com openssl, um certificado por pessoa e três diretivas no bloco de servidor do nginx. O que determina se esta configuração continuará a funcionar durante um ano é operacional. Por isso, a maior parte deste guia aborda períodos de validade, revogação, certificados individuais e o que fazer quando um cliente é recusado e ninguém consegue perceber o motivo.

Duas cadeias, não uma

Existem duas cadeias de certificados numa configuração mTLS, e não têm qualquer relação entre si. Misturá-las é o primeiro erro que quase toda a gente comete.

A primeira cadeia é a do servidor. O seu VPS apresenta um certificado para admin.example.com emitido por uma CA pública, como a Let's Encrypt, e o browser valida-o comparando-o com o repositório de raízes incluído no sistema operativo. O mTLS não altera esta parte. Se o certbot emitir hoje esse certificado por si, mantenha-o exatamente como está: consulte emitir um certificado Let's Encrypt para o nginx com o certbot.

A segunda cadeia é a do cliente. Cria uma CA própria, assina um certificado para cada pessoa que precisa de acesso e configura o nginx para confiar nessa CA, e apenas nessa CA, ao validar os clientes. Nenhum repositório público de raízes conhece a sua CA, e nenhum precisa de a conhecer. A única entidade que tem de confiar nela é o nginx, através do ficheiro ssl_client_certificate.

Por isso, ssl_client_certificate nunca afeta o certificado que o nginx apresenta, e a cadeia da Let's Encrypt nunca determina quais os clientes autorizados a entrar. Apontar ssl_client_certificate para fullchain.pem não faz o que parece: essa diretiva identifica os emissores de onde pode vir um certificado de cliente, que é a outra extremidade da ligação. Fazer o próprio servidor confiar na sua CA para ligações de saída é uma tarefa separada, descrita em adicionar a sua própria CA ao repositório de confiança do Ubuntu, e o repositório de confiança do sistema não é o que o nginx lê ao validar um cliente.

Crie a sua própria CA de clientes com openssl

Crie a CA num local diferente do servidor web. O nginx precisa apenas do certificado público da CA. A chave privada da CA assina novos certificados de cliente. Por isso, deixá-la num servidor exposto à Internet significa que uma intrusão permite ao atacante criar clientes válidos para si próprio quando quiser.

mkdir -p ~/client-ca/certs ~/client-ca/newcerts ~/client-ca/private ~/client-ca/csr
cd ~/client-ca
chmod 700 private
touch index.txt
echo 1000 > serial
echo 1000 > crlnumber

index.txt, serial e crlnumber são a base de dados da CA. openssl ca recusa executar sem estes ficheiros. Eles também permitem a revogação posterior, porque uma lista de revogação identifica números de série. Por isso, a CA tem de registar qual foi o número de série atribuído a cada cliente.

Crie ~/client-ca/openssl.cnf. Defina dir com o caminho real desse diretório, porque openssl ca não expande ~.

[ ca ]
default_ca = client_ca

[ client_ca ]
dir               = /home/you/client-ca
database          = $dir/index.txt
new_certs_dir     = $dir/newcerts
certificate       = $dir/ca.crt
private_key       = $dir/private/ca.key
serial            = $dir/serial
crlnumber         = $dir/crlnumber
default_md        = sha256
default_days      = 365
default_crl_days  = 30
policy            = policy_loose
rand_serial       = no
unique_subject    = no
email_in_dn       = no

[ policy_loose ]
commonName              = supplied
countryName             = optional
stateOrProvinceName     = optional
organizationName        = optional
organizationalUnitName  = optional
emailAddress            = optional

[ client_ext ]
basicConstraints        = CA:FALSE
keyUsage                = critical, digitalSignature, keyEncipherment
extendedKeyUsage        = clientAuth
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid,issuer

Agora, crie a chave da CA e o respetivo certificado autoassinado:

openssl genrsa -aes256 -out private/ca.key 4096
chmod 600 private/ca.key
openssl req -x509 -new -key private/ca.key -sha256 -days 3650 \
  -subj "/O=Example Ops/CN=Example Ops Client CA" \
  -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
  -addext "keyUsage=critical,keyCertSign,cRLSign" \
  -out ca.crt

-aes256 protege a chave da CA com uma frase-passe, pelo que cada operação de assinatura a solicita. Esse é o objetivo. Verifique o que criou:

openssl x509 -in ca.crt -noout -subject -dates -ext basicConstraints

O subject deve identificar a sua CA e a validade deve ser de dez anos. A linha de extensão deve ser CA:TRUE, pathlen:0. pathlen:0 significa que esta CA pode assinar certificados finais, mas não pode assinar outra CA. Isto mantém a cadeia exatamente com um nível e permite deixar ssl_verify_depth inalterado.

Emitir um certificado de cliente por pessoa

Um certificado por pessoa. Nunca use um certificado partilhado por uma equipa, porque não é possível revogar um certificado partilhado sem bloquear toda a equipa, e ele não informa quem fez a chamada.

openssl genrsa -out private/alice.key 2048
openssl req -new -key private/alice.key -out csr/alice.csr \
  -subj "/O=Example Ops/CN=alice"
openssl ca -config openssl.cnf -extensions client_ext \
  -days 365 -notext -in csr/alice.csr -out certs/alice.crt

openssl ca imprime o certificado que está prestes a assinar, pede a passphrase da CA, pede duas confirmações e acrescenta uma linha a index.txt. Adicione -batch quando o executar num script. A secção client_ext é importante por causa de uma linha: extendedKeyUsage = clientAuth. Um certificado que contenha uma lista de usos de chave estendidos apenas com serverAuth é rejeitado por não ser adequado para autenticação de cliente. Indique a finalidade em vez de esperar que ela seja inferida.

Verifique o par em relação à CA antes de o entregar:

openssl verify -CAfile ca.crt certs/alice.crt

Isto imprime certs/alice.crt: OK. Qualquer outra saída significa que o certificado e a CA não correspondem. Nenhuma configuração do nginx poderá corrigir isso.

Agrupe a chave e o certificado num único ficheiro que um navegador possa importar:

openssl pkcs12 -export -inkey private/alice.key -in certs/alice.crt \
  -name "alice at example ops" -out alice.p12

A exportação pede uma palavra-passe que protege o ficheiro durante o transporte. Envie o ficheiro e a palavra-passe por canais diferentes e entregue às pessoas o .p12, não um .key isolado. Pode adicionar -certfile ca.crt para incluir a CA no pacote, mas o nginx não precisa disso: o nginx já contém ca.crt, portanto um certificado assinado diretamente por essa CA é validado por si só.

O OpenSSL 3, incluído no Ubuntu 24.04, grava ficheiros PKCS#12 com encriptação atual, e os navegadores e sistemas operativos em uso em agosto de 2026 conseguem lê-los. Se um importador antigo recusar o ficheiro, exporte-o novamente com -legacy, que recorre aos algoritmos antigos esperados por esse importador. Leia a mensagem apresentada pelo importador antes de usar essa opção.

Configurar o nginx com ssl_client_certificate e ssl_verify_client

Copie o certificado da CA, e apenas o certificado da CA, para o servidor.

scp ca.crt user@admin.example.com:/tmp/client-ca.crt
ssh user@admin.example.com \
  'sudo install -o root -g root -m 644 /tmp/client-ca.crt /etc/nginx/client-ca.crt'

O modo 644 está correto neste caso. Um certificado da CA é informação pública. A chave da CA permanece na sua estação de trabalho.

Em seguida, adicione três diretivas ao bloco do servidor que já termina o TLS:

server {
    listen 443 ssl;
    server_name admin.example.com;

    ssl_certificate     /etc/letsencrypt/live/admin.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/admin.example.com/privkey.pem;

    ssl_client_certificate /etc/nginx/client-ca.crt;
    ssl_verify_client      on;
    ssl_verify_depth       1;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

ssl_verify_depth 1 é o valor predefinido do nginx e indica que o certificado do cliente deve ser assinado diretamente pela CA nesse ficheiro. Aumente-o apenas se adicionar uma CA intermédia. Durante o handshake, o nginx também envia ao cliente os nomes dos sujeitos de ssl_client_certificate. É assim que um navegador sabe quais dos seus certificados deve oferecer. Por esse motivo, use ssl_client_certificate em vez de ssl_trusted_certificate. Ambos verificam o certificado da mesma forma, mas ssl_trusted_certificate não envia nenhuma lista.

O Ubuntu 24.04 inclui o nginx 1.24, no qual o HTTP/2 é configurado na linha listen como listen 443 ssl http2;. No nginx 1.25.1 e posterior, essa forma está obsoleta e o HTTP/2 usa a sua própria diretiva, http2 on;. Nenhuma das opções altera a verificação do certificado.

Recarregue a configuração e leia o resultado:

sudo nginx -t && sudo systemctl reload nginx
curl -i https://admin.example.com/

nginx -t imprime syntax is ok e test is successful. A chamada curl não envia nenhum certificado, por isso deve retornar 400 Bad Request com o corpo No required SSL certificate was sent. Isso significa que o nginx recusou a ligação no seu próprio controlo de acesso, portanto a configuração está ativa e a aplicação nem sequer foi consultada. Agora tente corretamente:

curl --cert certs/alice.crt --key private/alice.key https://admin.example.com/

Isso deve retornar o conteúdo servido pela sua aplicação.

Por que o controlo deve ficar no bloco server

O certificado é trocado durante o handshake TLS, antes de o nginx ler uma linha de pedido. Nesse momento, o nginx ainda não sabe em que location o pedido vai entrar. Colocar ssl_verify_client on; dentro de um location pede ao cliente para renegociar a ligação. O TLS 1.3 removeu a renegociação e o HTTP/2 proíbe-a. Numa stack atual, esse padrão falha em vez de apresentar um pedido.

Faça o controlo de escopo diretamente. Peça um certificado ao nível do servidor e depois decida por localização:

ssl_verify_client optional;

location /metrics {
    if ($ssl_client_verify != SUCCESS) { return 403; }
    proxy_pass http://127.0.0.1:9090;
}

location /healthz {
    proxy_pass http://127.0.0.1:8080;
}

$ssl_client_verify contém SUCCESS, ou NONE quando o cliente não enviou nada, ou FAILED: seguido de um motivo. Com optional, o nginx pede um certificado e só o valida se receber um. É isso que permite que o caminho público /healthz acima funcione, enquanto /metrics permanece fechado. Um certificado enviado que falhe a validação continua a ser recusado pelo nginx nesse ponto. Se preferir inspecionar pessoalmente um certificado que falhou, isso corresponde a optional_no_ca. Nesse caso, o seu próprio teste tem de tratar qualquer valor diferente de SUCCESS como uma recusa.

O nginx tem códigos de estado não padrão para este caso. O error_page pode capturá-los, para que um visitante recusado receba uma explicação em vez de um 400 vazio:

error_page 495 496 = @needcert;

location @needcert {
    default_type text/plain;
    return 200 "This host requires a client certificate. Ask ops for one.\n";
}

495 significa que o certificado do cliente falhou na validação. 496 significa que o cliente não apresentou um certificado. Mantenha essa página em texto simples, porque a pessoa que a lê não tem sessão nem conta.

Como instalo o certificado do cliente num navegador?

O Firefox mantém o seu próprio armazenamento de certificados: Definições, depois Privacidade e segurança, depois Ver certificados, depois o separador Os seus certificados, depois Importar. Selecione o .p12 e introduza a palavra-passe.

O Chrome e o Edge usam o armazenamento do sistema operativo no Windows e no macOS. Por isso, abrir o ficheiro .p12 inicia o assistente de importação do sistema. No Linux, o Chrome lê uma base de dados NSS (serviços de segurança de rede) separada no seu diretório pessoal. A ferramenta de linha de comandos é a opção mais fiável:

sudo apt install -y libnss3-tools
pk12util -d sql:$HOME/.pki/nssdb -i alice.p12

Carregue o site depois. O navegador pergunta qual certificado deve enviar. O Chrome memoriza essa escolha durante o resto da sessão do navegador. Reinicie o navegador quando quiser que a pergunta seja apresentada novamente. O certificado fica disponível apenas num perfil de navegador num computador. Por isso, um certificado importado no Firefox não fica disponível no Chrome, e nenhum dos dois fica disponível no telemóvel.

Teste com curl --cert

Faça a depuração com curl, porque ele informa o que executou.

curl -v --cert certs/alice.crt --key private/alice.key https://admin.example.com/

Pode concatenar o certificado e a chave num único ficheiro PEM e passá-lo como --cert alice.pem. Se a chave tiver uma frase-passe, o curl pede-a. Também aceita --cert alice.pem:passphrase, mas a frase-passe fica no histórico da shell, por isso prefira o pedido interativo.

Vale a pena executar duas verificações antes de culpar o nginx. Primeiro, o certificado e a chave têm de formar um par:

openssl x509 -noout -pubkey -in certs/alice.crt | openssl sha256
openssl pkey -pubout -in private/alice.key | openssl sha256

Dois hashes idênticos significam que os ficheiros pertencem ao mesmo par. Dois hashes diferentes significam que misturou os ficheiros de duas pessoas, e nenhum cliente indicará essa causa por si.

Segundo, o servidor deve pedir o seu CA:

openssl s_client -connect admin.example.com:443 -servername admin.example.com </dev/null

Procure o bloco Acceptable client certificate CA names na saída e o subject do seu CA dentro dele. Se esse bloco estiver totalmente ausente, o nginx não está a pedir um certificado no server block que respondeu. Nesse caso, as suas diretivas foram aplicadas noutro server block, muitas vezes no servidor predefinido.

Passando o CN do cliente para a aplicação

O certificado identifica quem fez a chamada, mas a aplicação atrás do proxy não consegue ver a camada TLS. Por isso, o nginx tem de transmitir esse nome.

map $ssl_client_s_dn $client_cn {
    default              "";
    "~,?CN=(?<cn>[^,]+)" $cn;
}

$ssl_client_s_dn contém o nome distinto do sujeito no formato RFC 2253, que se parece com CN=alice,O=Example Ops. O map extrai o campo CN para $client_cn. Mantenha o CN como um nome de utilizador simples, porque uma vírgula dentro de um CN é escapada nesse formato e a expressão regular curta acima não trata esse escape.

location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host                 $host;
    proxy_set_header X-Forwarded-For      $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto    $scheme;
    proxy_set_header X-Client-Cert-CN     $client_cn;
    proxy_set_header X-Client-Cert-Serial $ssl_client_serial;
}

proxy_set_header substitui qualquer cabeçalho com esse nome enviado pelo cliente, para que ninguém possa falsificar X-Client-Cert-CN através desta localização. Duas condições garantem isso. O nginx herda proxy_set_header do nível externo apenas quando o nível interno não define nenhum valor próprio. Assim, uma segunda localização com uma linha proxy_set_header elimina silenciosamente todos os cabeçalhos definidos acima dela, incluindo este. Além disso, a aplicação tem de estar inacessível, exceto através do nginx. Isso significa associá-la a 127.0.0.1 em vez de 0.0.0.0, porque uma aplicação numa porta pública lerá o cabeçalho falsificado diretamente da Internet. O lado do proxy é explicado em uma configuração de reverse proxy nginx explicada linha a linha. Se a aplicação precisar do certificado completo em vez de apenas um nome, $ssl_client_escaped_cert transporta-o codificado em URL e seguro dentro de um cabeçalho.

Como revogar um certificado de cliente?

Quando alguém sai da organização ou um portátil desaparece, revogue apenas esse certificado. Todos os outros continuam a funcionar. Essa é precisamente a razão para emitir um certificado por pessoa.

cd ~/client-ca
openssl ca -config openssl.cnf -revoke certs/alice.crt
openssl ca -config openssl.cnf -gencrl -out crl.pem

O primeiro comando altera a linha correspondente a esse número de série em index.txt, passando-a de V para R. O segundo gera uma lista de revogação de certificados (CRL), um ficheiro assinado que identifica os números de série revogados. Transfira esse ficheiro e indique-o ao nginx com ssl_crl /etc/nginx/client-ca.crl;, junto das restantes diretivas.

scp crl.pem user@admin.example.com:/tmp/client-ca.crl
ssh user@admin.example.com 'sudo install -m 644 /tmp/client-ca.crl /etc/nginx/client-ca.crl && sudo nginx -t && sudo systemctl reload nginx'

Aqui existe uma armadilha que bloqueia todos os clientes. Uma CRL contém uma data nextUpdate, definida por default_crl_days, que é 30 na configuração acima. Depois dessa data, o OpenSSL considera a lista obsoleta e a verificação falha para todos os certificados de cliente com CRL has expired, não apenas para o certificado revogado. O nginx lê o ficheiro quando carrega a configuração. Por isso, atualizar a CRL no disco não produz efeito até fazer reload. Gere a CRL novamente e faça reload segundo um calendário que fique confortavelmente dentro desse prazo: semanalmente para um prazo de 30 dias. Confirme as datas antes de copiar o ficheiro:

openssl crl -in crl.pem -noout -lastupdate -nextupdate

Para poucos utilizadores, existe uma opção mais simples. A CA é sua, portanto o nginx pode recusar diretamente um número de série e dispensar a infraestrutura da CRL:

map $ssl_client_serial $revoked {
    default 0;
    "1002"  1;
}

Combine essa diretiva com if ($revoked) { return 403; } na location. Ela não tem uma data de expiração que possa ser esquecida. Também não é distribuída, portanto qualquer outro sistema que confie na sua CA não tem conhecimento dessa revogação. Para um único nginx à frente de uma única aplicação, esta é a opção simples e adequada. Passe a usar uma CRL quando existir mais de um ponto de entrada.

Qual deve ser a validade dos certificados de cliente?

Defina a validade dos certificados de cliente para 1 ano, ou menos se conseguir lidar com o trabalho de reemissão. A expiração é a falha silenciosa neste caso, porque nada avisa o titular antecipadamente. Numa manhã, a pessoa abre o painel, o nginx recusa a ligação e o navegador descreve a recusa com as suas próprias palavras, que raramente incluem a palavra expirado. Mantenha a CA com validade de 10 anos e registe a sua data de expiração num local que irá realmente consultar, porque, quando o certificado da CA expirar, todos os certificados emitidos por ela deixarão de ser validados no mesmo dia.

Dois comandos ajudam a antecipar esse problema:

openssl x509 -in certs/alice.crt -noout -subject -serial -enddate
awk -F'\t' '{print $1, $2, $4}' ~/client-ca/index.txt

A primeira coluna de index.txt indica o estado: V para válido, R para revogado e E para expirado. A segunda coluna contém a data de expiração no formato YYMMDDHHMMSSZ e a quarta contém o número de série. Esse ficheiro é o seu único registo de quem possui cada certificado. Faça uma cópia de segurança juntamente com a chave da CA e trate ambos como segredos.

A renovação cria um certificado novo. Não é uma extensão do certificado existente. Gere uma chave e uma CSR (certificate signing request) novas, assine-a, entregue o novo certificado e revogue o antigo assim que a pessoa confirmar que o novo funciona.

Contra o que o mTLS protege e contra o que não protege

O mTLS elimina o acesso não autenticado. Um scanner que encontre o seu hostname é recusado durante o handshake. Por isso, nunca envia um pedido HTTP, nunca vê um formulário de início de sessão e nunca consegue tentar uma palavra-passe roubada nesse formulário. O credential stuffing deixa de ter credenciais para testar. Uma vulnerabilidade no fluxo de início de sessão da aplicação fica inacessível a qualquer pessoa sem um certificado. O mTLS também elimina o segredo partilhado que as pessoas colam no chat, porque uma chave privada é um ficheiro difícil de copiar por acidente.

O mTLS não faz nada contra um cliente comprometido. O malware num portátil tem o ficheiro da chave e obtém a passphrase no momento em que o proprietário a introduz. Para o servidor, esse atacante parece exatamente um utilizador legítimo, porque um certificado prova a posse de um ficheiro, não a presença de uma pessoa. A palavra-passe .p12 e a encriptação integral do disco continuam a ser importantes.

O mTLS também não é autorização. Todos os certificados válidos têm acesso a tudo o que o server block disponibiliza, a menos que verifique $client_cn e atue com base nesse valor. Por predefinição, dois titulares de certificados têm acesso idêntico.

Além disso, o mTLS protege apenas o caminho através do nginx. Se a aplicação também escutar numa porta pública, o mTLS à sua frente é apenas decorativo. Faça a aplicação escutar em 127.0.0.1 e mantenha a firewall fechada nessa porta. A outra porta de entrada no mesmo sistema é o SSH, que merece a mesma atenção, abordada em proteger o acesso SSH ao seu VPS.

Existe ainda uma última limitação, que se manifesta no dia em que ativa esta configuração. Tudo o que não conseguir apresentar um certificado deixa de funcionar: um monitor de disponibilidade, um webhook de um fornecedor de pagamentos, um leitor RSS ou uma aplicação móvel sem um armazenamento de certificados acessível. Decida o que fazer com esses clientes antes de configurar ssl_verify_client on, porque a falha é total e, do lado deles, silenciosa.

Quando um cliente é recusado, leia o que o cliente comunica

A mensagem apresentada por um cliente recusado depende do navegador, da versão do curl e da biblioteca TLS subjacente. Por isso, leia o que o seu próprio cliente apresenta, em vez de comparar com uma mensagem registada noutro local. O detalhe útil está no servidor.

sudo tail -n 50 /var/log/nginx/error.log

Um certificado rejeitado deixa uma linha que contém client SSL certificate verify error, seguida do motivo indicado pelo OpenSSL. Esse motivo é o facto que deve orientar a ação. Normalmente, trata-se de uma destas situações. O certificado foi emitido por uma CA diferente da indicada no ficheiro nomeado em ssl_client_certificate. O certificado está fora do período de validade. A CRL no servidor ultrapassou o seu nextUpdate e, por isso, passou a falhar para todos os clientes, e não apenas para um.

Quando o navegador nunca apresenta um certificado, o problema ocorre antes da verificação. Durante o handshake, o nginx envia os nomes dos emissores aceitáveis. O navegador não encontrou no seu repositório nenhum certificado correspondente e, por isso, não teve nada para apresentar. Importe novamente o .p12 para o perfil com que está realmente a navegar.

Há ainda um caso que vale a pena mencionar. Se testou com um certificado de cliente autoassinado isolado, em vez de um certificado assinado pela sua CA, a verificação não pode ser concluída. O nginx valida a assinatura com base no ficheiro da CA, e um certificado autoassinado não está incluído nesse ficheiro. O processo de criação do certificado é igual ao descrito em gerar um certificado autoassinado no Ubuntu. O mTLS precisa apenas do passo adicional em que a sua CA o assina.

FAQ

Ainda preciso de um certificado Let's Encrypt se usar mTLS?

Sim. Os dois certificados não têm relação entre si. O servidor apresenta o seu próprio certificado para que o navegador confie no nome do host, e esse certificado ainda tem de ser emitido por uma CA que o navegador já conheça. A CA de cliente é uma cadeia privada separada, usada apenas para verificar quem está a ligar-se. Definir ssl_client_certificate não altera o certificado que o nginx apresenta, e essa diretiva não deve apontar para a sua cadeia Let's Encrypt.

Por que motivo o meu navegador nunca me pede para escolher um certificado?

O nginx envia, durante o handshake, uma lista das entidades emissoras aceites, criada a partir do ficheiro em ssl_client_certificate. Um navegador só disponibiliza certificados cuja entidade emissora aparece nessa lista. Portanto, a ausência do pedido significa que o navegador não tem nenhum certificado da sua CA: a importação foi feita noutro perfil do navegador ou o certificado foi assinado por uma CA diferente da instalada no servidor. Execute openssl s_client -connect admin.example.com:443 e procure no resultado os nomes das CAs de certificados de cliente aceites para ver qual CA o servidor está efetivamente a solicitar.

Posso exigir um certificado de cliente apenas num URL?

Não com ssl_verify_client on dentro de um location. O certificado é trocado durante o handshake, antes de o nginx conhecer o caminho do pedido, e a renegociação que permitiria contornar isto foi removida do TLS 1.3 e é proibida no HTTP/2. Defina ssl_verify_client optional; no bloco do servidor. Depois, em cada location protegida, teste $ssl_client_verify e devolva 403 quando o valor não for SUCCESS.

Como revogo o acesso de uma pessoa?

Revogue esse certificado com openssl ca -revoke, volte a gerar a lista com openssl ca -gencrl, copie-a para o servidor e recarregue o nginx para que leia o novo ficheiro. As restantes pessoas não são afetadas. Isto só funciona se cada pessoa tiver o seu próprio certificado, em vez de partilhar um certificado. Monitorize a data nextUpdate da CRL, porque uma CRL expirada faz a verificação falhar para todos os clientes, e não apenas para os revogados.

O mTLS substitui uma página de login?

Para controlar o acesso, sim: sem um certificado, nada chega à aplicação. Assim, não há nenhum formulário para atacar nem nenhuma palavra-passe para adivinhar. Para a identidade dentro da aplicação, não. Um certificado prova que o cliente possui um ficheiro de chave, portanto um portátil roubado continua a ser um cliente válido. Encaminhe o CN para o backend, mantenha as contas e permissões que a aplicação já utiliza e trate o certificado como a barreira de acesso à frente delas.

#tls#mtls#nginx#openssl#access-control