SSD Nodes Learn Hosting plans →
Guias Matt ConnorPor Matt Connor

Certificado autoassinado no Ubuntu 24.04 com Chrome

Gere um certificado TLS autoassinado aceito pelo Chrome no Ubuntu 24.04, com SAN, nginx ou Apache e confiança correta, sem usar curl -k.

O que você vai criar

Um certificado TLS autoassinado que os navegadores e clientes modernos aceitam de facto, com subjectAltName correto, permissões de chave adequadas e configuração integrada no nginx ou Apache. Inclui também a parte que quase todos os guias omitem: fazer com que os seus clientes confiem nele corretamente, em vez de ignorar avisos e fixar curl -k nos scripts para sempre. No final, verá uma CA privada com cinco comandos, útil quando um serviço interno passa a ser seis.

Primeiro, a decisão, porque um certificado autoassinado é a ferramenta certa com muito menos frequência do que é utilizado. Se o serviço estiver acessível a partir da internet pública com um nome DNS real, pare de ler e obtenha um certificado Let's Encrypt gratuito com certbot no nginx ou o equivalente para Apache. Não tem custos, renova-se automaticamente e os principais navegadores e sistemas operativos já confiam nele. Um certificado autoassinado num site público habitua os utilizadores a ignorar avisos de segurança. Esse é um hábito pior do que usar HTTP simples.

O certificado autoassinado é a ferramenta certa quando a internet pública não está envolvida: um painel de administração associado a um endereço de túnel WireGuard no seu VPS, um servidor de staging numa rede privada, tráfego entre serviços internos, um dispositivo de home lab ou a substituição do certificado predefinido que o Webmin gera para si próprio na porta 10000. O Let's Encrypt também não pode emitir certificados para 10.8.0.1 ou git.internal.lan. Nenhuma CA pública incluirá um endereço IP privado ou um TLD inventado num certificado. Para esses nomes, a CA é sua.

Tudo o que é apresentado abaixo é executado num sistema Ubuntu 24.04 recém-instalado, que inclui o OpenSSL 3.0.x (openssl version para confirmar). Nada disto requer acesso à internet. Tudo funciona num ambiente isolado.

Por que o comando antigo de uma linha gera certificados que o Chrome rejeita

O comando que todos os tutoriais anteriores a 2017 apresentam é semelhante a este:

# Do not run this: shown so you recognise it in old guides
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout selfsigned.key -out selfsigned.crt

Ele faz uma série de perguntas interativas, coloca o nome do host no campo Common Name e gera um certificado sem a extensão subjectAltName. Esse certificado falha imediatamente. O Chrome deixou de consultar o Common Name na versão 58, em abril de 2017. A RFC 2818 já tinha descontinuado a correspondência pelo CN em 2000. O Firefox e o Safari comportam-se da mesma forma. O curl e o módulo ssl do Python ainda usam o Common Name como alternativa quando um certificado não tem nenhum SAN. É por isso que um certificado sem SAN pode passar num teste rápido de curl e ainda assim falhar em todos os browsers. Para um browser, um certificado identifica o servidor através da extensão SAN ou não o identifica. O browser informa isso exatamente com estas palavras:

NET::ERR_CERT_COMMON_NAME_INVALID

This server could not prove that it is git.internal.lan; its security
certificate does not specify Subject Alternative Names.

Nenhuma alteração ao trust store corrige esse erro, porque o certificado realmente não identifica nenhum nome. Se está a ver NET::ERR_CERT_COMMON_NAME_INVALID agora, o seu certificado não tem SAN ou tem um SAN incorreto, e precisa de gerar um novo. Felizmente, a correção consiste num único comando.

Criar um certificado aceite pelos browsers: um comando

O OpenSSL passou a suportar a flag -addext na versão 1.1.1. Já não é necessário fazer as alterações complexas aos ficheiros de configuração usadas pelos tutoriais antigos para injetar um SAN. No Ubuntu 24.04:

sudo openssl req -x509 -newkey rsa:4096 -sha256 -days 730 -noenc \
  -keyout /etc/ssl/private/git.internal.key \
  -out /etc/ssl/certs/git.internal.crt \
  -subj "/CN=git.internal.lan" \
  -addext "subjectAltName=DNS:git.internal.lan,IP:10.8.0.1"

O que cada flag faz:

  • -x509 gera diretamente um certificado autoassinado, em vez de um pedido de assinatura.
  • -newkey rsa:4096 gera uma chave nova no mesmo passo. RSA 4096 é compatível com clientes antigos; se todos os clientes forem modernos, -newkey ec -pkeyopt ec_paramgen_curve:P-256 é menor e mais rápido.
  • -noenc é a sintaxe do OpenSSL 3.x para o antigo -nodes: a chave não tem frase-passe. Ambas as formas funcionam. Uma chave com frase-passe faz com que o nginx fique bloqueado à espera de entrada em cada arranque. Para uma chave de servidor, é isto que deve usar.
  • -days 730, dois anos; consulte a secção sobre expiração para obter mais informações sobre este número.
  • -subj responde às perguntas interativas na própria linha de comando. O CN é agora apenas informativo, mas defina-o como o nome principal. Algumas ferramentas apresentam-no.
  • -addext "subjectAltName=..." é a flag essencial. Liste todos os nomes e todos os endereços IP que os clientes vão introduzir: entradas DNS: para nomes de host (são permitidos wildcards como DNS:*.internal.lan) e entradas IP: para endereços. Se alguém aceder a https://10.8.0.1, a entrada IP:10.8.0.1 tem de estar presente. Um SAN apenas com DNS volta a produzir NET::ERR_CERT_COMMON_NAME_INVALID para esses clientes.

Confirme que o SAN foi realmente incluído antes de configurar qualquer serviço:

openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -ext subjectAltName

Saída correta:

X509v3 Subject Alternative Name:
    DNS:git.internal.lan, IP Address:10.8.0.1

Se, em vez disso, for apresentada a saída No extensions in certificate, o certificado não tem SAN e os browsers vão rejeitá-lo. Gere-o novamente em vez de prosseguir.

Restringir o acesso à chave

Uma chave privada legível por todos os utilizadores do sistema não é uma chave privada. Numa instalação padrão do Ubuntu 24.04, /etc/ssl/private já tem 700 root:root, por isso apenas root consegue sequer listá-la; ela só passa a ter 710 root:ssl-cert se algum componente tiver instalado o pacote ssl-cert (apache2 recomenda-o). Em qualquer caso, defina explicitamente as permissões do próprio ficheiro:

sudo chown root:root /etc/ssl/private/git.internal.key
sudo chmod 600 /etc/ssl/private/git.internal.key

nginx e Apache leem os certificados como root antes de reduzir os privilégios, por isso o modo root:root 600 funciona com ambos. Se a chave for usada por um serviço que corre com o seu próprio utilizador e carrega a chave diretamente, como uma aplicação Node, Gitea ou um daemon Python, chown atribua-a a esse utilizador do serviço, mantendo o modo 600. O que nunca deve fazer: usar o modo 644, guardar uma cópia num repositório git ou guardar uma cópia em /tmp.

Ligue-o ao nginx

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name git.internal.lan;

    ssl_certificate     /etc/ssl/certs/git.internal.crt;
    ssl_certificate_key /etc/ssl/private/git.internal.key;

    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}
sudo nginx -t && sudo systemctl reload nginx

nginx -t deve apresentar syntax is ok e test is successful antes de o reload produzir qualquer efeito. Se apresentar SSL_CTX_use_PrivateKey_file() failed ... key values mismatch, o certificado e a chave pertencem a duas execuções de geração diferentes; consulte a secção sobre modos de falha.

Integrar no Apache

sudo a2enmod ssl proxy proxy_http

ssl não é suficiente neste caso: o virtual host abaixo usa ProxyPass e, sem mod_proxy e mod_proxy_http, o teste de configuração falha com Invalid command 'ProxyPass', perhaps misspelled or defined by a module not included in the server configuration. Guarde o virtual host como /etc/apache2/sites-available/git-internal.conf:

<VirtualHost *:443>
    ServerName git.internal.lan
    SSLEngine on
    SSLCertificateFile      /etc/ssl/certs/git.internal.crt
    SSLCertificateKeyFile   /etc/ssl/private/git.internal.key

    ProxyPass        / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/
</VirtualHost>
sudo a2ensite git-internal
sudo apache2ctl configtest && sudo systemctl reload apache2

configtest deve responder em Syntax OK. Agora teste a partir de uma máquina cliente:

curl -v https://git.internal.lan/

e verá um erro:

curl: (60) SSL certificate problem: self-signed certificate

Isso não é um erro do sistema. É o TLS a funcionar: o curl nunca ouviu falar do seu certificado e recusa comunicar com um servidor que não consegue autenticar. A secção seguinte apresenta a correção real, que não é o procedimento usado neste momento por metade da Internet.

Fazer os clientes confiarem nele e os anti-padrões a recusar

Comece pelas correções erradas, identificadas pelo que realmente fazem. curl -k (ou --insecure) incluído num script, verify=False em Python requests e NODE_TLS_REJECT_UNAUTHORIZED=0 em Node não fazem o seu certificado ser confiável. Essas opções desativam a verificação do certificado. Com isso, o cliente comunica sem problemas com qualquer servidor que apresente qualquer certificado, incluindo um certificado colocado no caminho por um atacante. Mantém-se o custo do TLS e perde-se a autenticação que justificava o seu uso. Pior ainda, estas opções propagam-se: são copiadas para uma tarefa cron, depois para um script de deploy e, por fim, para código de produção, até ninguém se lembrar de quais ligações deveriam ser temporárias. Se um verify=False continuar ativo depois da sessão de debugging que o originou, o desenho está errado.

A correção correta é ensinar ao sistema operativo de cada cliente que este certificado é uma raiz confiável. Nos clientes Ubuntu e Debian:

sudo cp git.internal.crt /usr/local/share/ca-certificates/git.internal.crt
sudo update-ca-certificates

A linha importante na saída (seguida de um bloco Running hooks in /etc/ca-certificates/update.d...) é:

Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.

Estas linhas escondem duas armadilhas. O ficheiro tem de terminar em .crt. Uma extensão .pem é ignorada silenciosamente, e obtém-se 0 added sem qualquer mensagem de erro. Além disso, o conteúdo tem de estar no formato PEM; o ficheiro começa com -----BEGIN CERTIFICATE-----. Converta primeiro um ficheiro binário DER com openssl x509 -inform der -in file.der -out file.crt. Adicionar o próprio certificado autoassinado como raiz funciona porque um certificado autoassinado é a sua própria raiz.

Depois disso, curl, wget, git, apt e qualquer outro componente que use OpenSSL com o bundle do sistema confiam no servidor sem quaisquer opções adicionais. Alguns clientes mantêm os seus próprios repositórios de certificados confiáveis e precisam de configuração individual:

  • Chrome/Chromium no Linux lê uma base de dados NSS, não o repositório do sistema: sudo apt install libnss3-tools e, depois, certutil -d sql:$HOME/.local/share/pki/nssdb -A -t "C,," -n "git.internal" -i git.internal.crt por utilizador. Esse caminho é o predefinido do Chrome desde a versão 146. Se o utilizador ainda tiver um ~/.pki/nssdb legado de uma instalação antiga, o Chrome continua a ler esse ficheiro. Nesses computadores, aponte -d para sql:$HOME/.pki/nssdb.
  • O Firefox tem o seu próprio repositório: Settings → Privacy & Security → Certificates → Import. No Windows e no macOS, pode, em alternativa, alterar security.enterprise_roots.enabled para true em about:config, para que leia o repositório do sistema operativo. A Mozilla documenta essa preferência para Windows e macOS, mas não para Linux. No Ubuntu, use a importação.
  • Python requests inclui o seu próprio bundle de CAs (certifi) e ignora o repositório do sistema: passe verify="/usr/local/share/ca-certificates/git.internal.crt" ou exporte REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt.
  • O Node.js usa por predefinição as suas próprias raízes incluídas: exporte NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/git.internal.crt ou faça-o ler o repositório do sistema que acabou de atualizar com a opção --use-system-ca (Node 23.9 e posteriores no Linux) ou NODE_USE_SYSTEM_CA=1 (Node 24.6 e 22.19 e posteriores).

Nos clientes Windows, faça duplo clique no .crt e instale-o em Trusted Root Certification Authorities. No macOS, adicione-o ao porta-chaves System através do Keychain Access e marque-o como Always Trust.

Uma CA privada para vários serviços

A confiança configurada por certificado deixa de ser escalável imediatamente: seis serviços vezes quatro máquinas cliente correspondem a vinte e quatro instalações de confiança, e cada novo serviço acrescenta mais uma. A solução é uma CA privada. Os clientes confiam numa única raiz, e cada certificado de serviço é assinado com essa raiz.

A opção mais simples é mkcert, disponível nos repositórios do Ubuntu 24.04. A ferramenta também gere os repositórios NSS (Firefox e Chrome, com uma ressalva abaixo), que update-ca-certificates não gere:

sudo apt install -y mkcert libnss3-tools
mkcert -install
mkcert git.internal.lan "*.internal.lan" 10.8.0.1

mkcert -install cria uma raiz e regista-a no repositório do sistema, nos repositórios NSS dos browsers que consegue localizar e no repositório do Java quando JAVA_HOME está definido. O Node não lê o repositório do sistema sem instruções explícitas. Por isso, inicie as aplicações Node com --use-system-ca (ou NODE_USE_SYSTEM_CA=1), como acima, ou exporte NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem". O terceiro comando gera git.internal.lan+2.pem e git.internal.lan+2-key.pem, prontos para serem incluídos nos blocos de configuração do nginx ou do Apache acima. A ferramenta pressupõe uma máquina de desenvolvimento. A chave da raiz fica na máquina onde -install foi executado. Por isso, é adequada para um portátil de desenvolvimento, mas não para uma frota de servidores.

A ressalva, em outubro de 2026, é que o mkcert só procura a base de dados NSS do Chrome no caminho legado ~/.pki/nssdb, e não em ~/.local/share/pki/nssdb, que o Chrome usa por predefinição desde a versão 146. Numa máquina sem a base de dados legada, adicione manualmente a raiz ao Chrome com o comando certutil da secção anterior, usando -i "$(mkcert -CAROOT)/rootCA.pem".

Para servidores, o OpenSSL sem ferramentas adicionais executa todo o processo da CA em cinco comandos:

openssl genrsa -out lab-ca.key 4096
openssl req -x509 -new -key lab-ca.key -sha256 -days 3650 \
  -out lab-ca.crt -subj "/CN=Lab Internal CA" \
  -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
  -addext "keyUsage=critical,keyCertSign,cRLSign"
openssl genrsa -out git.key 2048
openssl req -new -key git.key -out git.csr -subj "/CN=git.internal.lan" \
  -addext "subjectAltName=DNS:git.internal.lan,IP:10.8.0.1"
openssl x509 -req -in git.csr -CA lab-ca.crt -CAkey lab-ca.key \
  -CAcreateserial -days 730 -sha256 -copy_extensions copy -out git.crt

O problema está no último comando: por predefinição, openssl x509 -req remove todas as extensões do CSR, incluindo o SAN que adicionou cuidadosamente. -copy_extensions copy (uma opção do OpenSSL 3.x, que funciona no 24.04) preserva essas extensões. Se a omitir, o certificado assinado fica sem SAN, e o Chrome apresenta novamente NET::ERR_CERT_COMMON_NAME_INVALID. Confirme com a mesma verificação openssl x509 -noout -ext subjectAltName usada anteriormente.

Distribua lab-ca.crt pelos clientes seguindo os passos acima para o repositório de confiança, uma vez por máquina. Proteja lab-ca.key como protegeria uma chave privada de CA: use o modo 600 e, idealmente, mantenha-a numa máquina que não seja um dos servidores para os quais assina certificados. Quem possuir essa chave pode emitir um certificado para qualquer nome em que os seus clientes confiem.

Expiração e rotação

Os períodos de validade dos certificados de CAs públicas estão a diminuir. O CA/Browser Forum limitou os certificados recém-emitidos e publicamente confiáveis a 200 dias em março de 2026, abaixo dos 398 dias anteriores. O limite passa para 100 dias em 2027 e para 47 dias em março de 2029. No entanto, essas regras aplicam-se às CAs publicamente confiáveis. A sua CA privada não está sujeita a elas, e os browsers não impõem esses limites a raízes instaladas manualmente. Existe uma limitação prática: as plataformas Apple rejeitam qualquer certificado de servidor TLS válido por mais de 825 dias, independentemente de quem o tenha emitido. Por isso, se iPhones ou Macs se vão ligar, mantenha os certificados leaf com uma validade máxima de dois anos. -days 730 cumpre esse limite em qualquer plataforma. Uma raiz com validade de dez anos e certificados leaf com validade de dois anos é uma estrutura interna confortável.

Os certificados de longa duração falham de uma única forma: silenciosamente, todos ao mesmo tempo, numa data que ninguém se lembra de ter escolhido. Verifique o que tem:

openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -enddate

Inclua a renovação num calendário real ou configure o cron para o alertar 30 dias antes. openssl x509 -checkend 2592000 -in cert.crt termina com um código diferente de zero quando a expiração estiver dentro desse número de segundos. Se já usa Uptime Kuma para monitorização de estado, os monitores HTTPS assinalam gratuitamente a aproximação da expiração dos certificados.

A rotação com uma CA privada é simples: execute novamente os comandos de CSR e assinatura, substitua os ficheiros e recarregue o servidor web. A raiz não mudou, por isso nenhum cliente nota a alteração.

Modos de falha e mensagens apresentadas

NET::ERR_CERT_AUTHORITY_INVALID, o estado esperado antes de instalar a confiança. Não é um defeito do certificado. Se persistir depois de instalar a raiz: no Linux, o Chrome lê a base NSS em vez do armazenamento do sistema (consulte o passo certutil). Desde o Chrome 146, essa base está em ~/.local/share/pki/nssdb, exceto quando existe um ~/.pki/nssdb legado. Por isso, uma raiz adicionada ao outro caminho nunca é detetada. Também pode acontecer que o ficheiro copiado não termine em .crt e que update-ca-certificates tenha indicado 0 added. Outra possibilidade é o servidor apresentar um certificado diferente daquele em que confiou. Compare as impressões digitais com openssl s_client -connect git.internal.lan:443 </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256.

NET::ERR_CERT_COMMON_NAME_INVALID, o certificado não tem SAN ou o SAN não inclui o nome apresentado na barra de endereços. O caso clássico ocorre quando o SAN lista DNS:git.internal.lan, mas o utilizador acedeu a https://10.8.0.1. Alterações ao armazenamento de confiança não corrigem este problema. Emita novamente o certificado com a entrada em falta.

curl: (60) SSL certificate problem: self-signed certificate, o curl não confia no certificado. A variante self-signed certificate in certificate chain significa o mesmo para um certificado assinado pela sua CA privada. Correção pontual: curl --cacert lab-ca.crt https://.... Correção permanente: o armazenamento de confiança. Não use -k.

unable to load certificate ... Expecting: TRUSTED CERTIFICATE (ou Expecting: CERTIFICATE REQUEST, ou no start line), confusão entre formatos PEM. Foi fornecido ao OpenSSL um ficheiro do tipo errado: uma chave ou CSR quando era esperado um certificado, ou um binário DER quando era esperado PEM. head -1 filename indica o que o ficheiro contém. Um certificado começa por -----BEGIN CERTIFICATE-----. Para DER, converta com openssl x509 -inform der -in file.der -out file.crt.

nginx: [emerg] SSL_CTX_use_PrivateKey_file(...) failed (SSL: error ... key values mismatch), o certificado e a chave não pertencem ao mesmo par. Isto costuma acontecer quando o comando de geração é executado duas vezes e os ficheiros são misturados. Confirme com openssl x509 -in git.internal.crt -noout -pubkey | sha256sum e openssl pkey -in git.internal.key -pubout | sha256sum. Hashes iguais indicam um par correspondente. Se forem diferentes, gere novamente ambos em conjunto.

FAQ

Por que o Chrome continua a indicar "Não seguro" depois de eu criar um certificado autoassinado?

Se o erro for NET::ERR_CERT_AUTHORITY_INVALID, o certificado está correto; o Chrome simplesmente ainda não tem motivos para confiar nele. Instale-o (ou a raiz da sua CA privada) no trust store do cliente. No Linux, o Chrome usa a base de dados NSS através de certutil (em ~/.local/share/pki/nssdb desde o Chrome 146, ou uma ~/.pki/nssdb legada, se existir), e não o trust store do sistema. Se o erro for NET::ERR_CERT_COMMON_NAME_INVALID, falta ao certificado um Subject Alternative Name correspondente ao URL. Nesse caso, é necessário reemiti-lo com -addext "subjectAltName=...".

Como faço o curl confiar num certificado autoassinado sem usar -k?

Copie o certificado (em formato PEM, com a extensão .crt) para /usr/local/share/ca-certificates/ e execute sudo update-ca-certificates. O resultado deve indicar 1 added. A partir desse momento, o curl valida-o como qualquer certificado público. Para um pedido pontual sem alterar o sistema, curl --cacert /path/to/cert.crt valida apenas com base nesse ficheiro. -k desativa totalmente a validação e não deve ser usado nos scripts de ninguém.

Durante quanto tempo pode um certificado autoassinado ser válido?

Tecnicamente, durante o período que quiser. Os limites do CA/Browser Forum (200 dias atualmente, 47 até 2029) aplicam-se a CAs publicamente confiáveis, não à confiança privada. Na prática, limite os certificados de servidor a 825 dias, porque os dispositivos Apple rejeitam certificados mais longos, independentemente do emissor. Uma raiz privada com validade de dez anos e certificados leaf de dois anos (-days 730) é um valor predefinido razoável. Registe a renovação no calendário, porque um certificado interno expirado pode interromper tudo silenciosamente numa data de que ninguém se lembra.

Devo usar um certificado autoassinado ou Let's Encrypt?

Se o serviço tiver um nome DNS público e estiver acessível a partir da Internet, use sempre Let's Encrypt: é gratuito, automatizado e já é confiável pela maioria dos browsers e sistemas operativos. Consulte a lista de compatibilidade da Let's Encrypt se for necessário ligar dispositivos muito antigos. Um certificado autoassinado (ou uma CA privada) destina-se aos casos em que a Let's Encrypt não pode emitir certificados: IPs privados, nomes de host internos como .lan, redes isoladas e serviços deliberadamente ocultos atrás de uma VPN. A decisão depende da acessibilidade e da nomenclatura, não da força da segurança. A criptografia é idêntica.

Por que motivo o meu certificado é rejeitado mesmo depois de o adicionar a /usr/local/share/ca-certificates?

Verifique três pontos. O ficheiro deve terminar em .crt. Uma extensão .pem é ignorada silenciosamente, e update-ca-certificates apresenta 0 added. O conteúdo deve ser texto PEM iniciado por -----BEGIN CERTIFICATE-----, e não um ficheiro binário DER. Além disso, a aplicação tem de usar efetivamente o trust store do sistema. O Chrome no Linux, o Firefox, o Python requests, o Node.js (exceto quando iniciado com --use-system-ca) e o Java mantêm, cada um, um trust store privado. O certificado tem de ser adicionado separadamente a cada um deles.