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 browsers e clientes modernos aceitam de facto, com subjectAltName correto, permissões de chave adequadas e integração com nginx ou Apache. Inclui também a parte que quase todos os guias ignoram: configurar corretamente os clientes para confiarem nele, em vez de ignorar avisos e manter curl -k codificado nos scripts para sempre. No fim, há uma CA privada em cinco comandos para quando um serviço interno se transformar em seis.
Primeiro, a decisão, porque um certificado autoassinado é a escolha certa com muito menos frequência do que é utilizado. Se o serviço estiver acessível a partir da internet pública através de 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 todos os browsers do mundo já confiam nele. Um certificado autoassinado num site público habitua os utilizadores a ignorar avisos de segurança, o que é um hábito pior do que usar HTTP simples.
Um certificado autoassinado é a escolha certa quando não há internet pública 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 equipamento de um laboratório doméstico 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 se segue é executado numa instalação nova do Ubuntu 24.04, que inclui OpenSSL 3.0.x (openssl version para confirmar). Nada disto requer acesso à internet; funciona integralmente 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.crtEle 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 ler o Common Name na versão 58, em abril de 2017. A RFC 2818 já tinha descontinuado a correspondência pelo CN em 2000. Firefox, Safari, curl e Python comportam-se da mesma forma. Um certificado identifica o servidor através da extensão SAN ou não o identifica de todo. O navegador informa isso exatamente nestas 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 no trust store corrige esse erro, porque o certificado realmente não identifica nenhum nome. Se NET::ERR_CERT_COMMON_NAME_INVALID aparece agora, o seu certificado não tem SAN, ou tem um SAN incorreto, e é necessário gerar um novo certificado. Felizmente, a correção consiste em um 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:
-x509gera diretamente um certificado autoassinado, em vez de um pedido de assinatura.-newkey rsa:4096gera 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.-subjresponde à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: entradasDNS:para nomes de host (são permitidos wildcards comoDNS:*.internal.lan) e entradasIP:para endereços. Se alguém aceder ahttps://10.8.0.1, a entradaIP:10.8.0.1tem de estar presente. Um SAN apenas com DNS volta a produzirNET::ERR_CERT_COMMON_NAME_INVALIDpara 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 subjectAltNameSaída correta:
X509v3 Subject Alternative Name:
DNS:git.internal.lan, IP Address:10.8.0.1Se, 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.
Bloqueie o acesso à chave
Uma chave privada legível por todos os utilizadores do sistema não é uma chave privada. No Ubuntu, /etc/ssl/private já tem a permissão 710 root:ssl-cert, que impede o acesso casual, mas 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.keyO Nginx e o Apache leem os certificados como root antes de abandonarem os privilégios, portanto o modo root:root 600 funciona com ambos. Se a chave for usada por um serviço que é executado com o seu próprio utilizador e carrega a chave diretamente, como uma aplicação Node, Gitea ou um daemon Python, chown transfira-a para esse utilizador do serviço e mantenha o modo 600. Nunca faça o seguinte: use o modo 644, mantenha uma cópia num repositório git ou guarde 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 nginxnginx -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_httpssl 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 apache2configtest 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 certificateIsso 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.
Faça os clientes confiarem nele e conheça os anti-padrões que deve 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 no Node não tornam o seu certificado confiável. Essas opções desativam a verificação do certificado. O cliente passa a comunicar com qualquer servidor que apresente qualquer certificado, incluindo um certificado colocado no caminho por um atacante. Mantém a sobrecarga do TLS e perde 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 a existir depois da sessão de depuração 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-certificatesA linha relevante da saída (seguida por um bloco Running hooks in /etc/ca-certificates/update.d...) é:
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.Essas linhas escondem dois problemas comuns. O ficheiro tem de terminar em .crt. Uma extensão .pem é ignorada silenciosamente e obtém 0 added sem qualquer mensagem de erro. O conteúdo também tem de estar em PEM. O ficheiro começa por -----BEGIN CERTIFICATE-----. Converta primeiro um 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 utilize OpenSSL com o pacote de certificados do sistema confiarão no servidor sem quaisquer opções. Alguns clientes mantêm os seus próprios repositórios de certificados 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-toolse, depois,certutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n "git.internal" -i git.internal.crtpor utilizador. - Firefox tem o seu próprio repositório: Definições → Privacidade e segurança → Certificados → Importar. Em alternativa, altere
security.enterprise_roots.enabledparatrueemabout:configpara que leia o repositório do sistema. - Python requests inclui o seu próprio pacote de autoridades de certificação (certifi) e ignora o repositório do sistema: passe
verify="/usr/local/share/ca-certificates/git.internal.crt"ou exporteREQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt. - Node.js: exporte
NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/git.internal.crt.
Nos clientes Windows, faça duplo clique em .crt e instale-o em Autoridades de certificação de raiz fidedignas. No macOS, adicione-o ao porta-chaves do sistema através do Acesso às Chaves e marque-o como Confiar sempre.
Uma raiz para vários serviços: uma pequena CA privada
A confiança por certificado deixa de escalar imediatamente: seis serviços vezes quatro máquinas cliente significam vinte e quatro instalações de confiança, e cada serviço novo acrescenta mais uma. A solução é uma CA privada. Os clientes confiam numa única raiz, e cada certificado de serviço é assinado com ela.
A opção mais simples é mkcert, disponível nos repositórios do Ubuntu 24.04 e capaz de gerir os armazenamentos NSS (Chrome, Firefox) que update-ca-certificates não consegue gerir:
sudo apt install -y mkcert libnss3-tools
mkcert -install
mkcert git.internal.lan "*.internal.lan" 10.8.0.1mkcert -install cria uma raiz e regista-a em todos os armazenamentos de confiança dessa máquina; 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 Apache acima. Parte do princípio de que está a ser usada numa máquina de desenvolvimento. A chave raiz fica na máquina onde -install foi executado. Por isso, é perfeita para um portátil de desenvolvimento, mas não é adequada para uma frota de servidores.
Para servidores, o OpenSSL simples executa toda a 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.crtO 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, por isso funciona no 24.04) preserva essas extensões. Se a omitir, o certificado assinado fica sem SAN, e o Chrome volta a apresentar NET::ERR_CERT_COMMON_NAME_INVALID. Verifique com a mesma validação openssl x509 -noout -ext subjectAltName usada anteriormente.
Distribua lab-ca.crt pelos clientes seguindo os passos de armazenamento de confiança acima, uma vez por máquina. Proteja lab-ca.key como uma credencial crítica: use o modo 600 e, idealmente, mantenha-a numa máquina que não seja um dos servidores para os quais assina certificados. Quem tiver essa chave poderá 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 -enddateInclua 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 as mensagens que verá
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ê o NSS em vez do armazenamento do sistema (consulte o passo certutil); ou o ficheiro copiado não terminou em .crt e update-ca-certificates indicou 0 added; ou o servidor está a 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ço. O caso clássico: 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), há uma confusão entre formatos PEM. Foi fornecido ao OpenSSL o tipo de ficheiro errado: uma chave ou CSR quando era esperado um certificado, ou um binário DER quando era esperado PEM. head -1 filename mostra o que tem efetivamente; 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 correspondem, normalmente porque o comando de geração foi executado duas vezes e os ficheiros foram misturados. Confirme com openssl x509 -in git.internal.crt -noout -pubkey | sha256sum e openssl pkey -in git.internal.key -pubout | sha256sum; hashes iguais significam que o par corresponde. 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 motivo para confiar nele. Instale-o, ou instale 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, e não o trust store do sistema. Se o erro for NET::ERR_CERT_COMMON_NAME_INVALID, o certificado não tem um Subject Alternative Name correspondente ao URL e tem de ser emitido novamente com -addext "subjectAltName=...".
Como faço o curl confiar num certificado autoassinado sem usar -k?
Copie o certificado, em formato PEM e com a extensão .crt, para /usr/local/share/ca-certificates/ e execute sudo update-ca-certificates. A saída tem de 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 completamente a validação e não deve ser usado nos scripts.
Durante quanto tempo pode um certificado autoassinado ser válido?
Tecnicamente, durante o tempo que quiser. Os limites do CA/Browser Forum, atualmente 200 dias e 47 até 2029, aplicam-se às CAs confiáveis publicamente, não à confiança privada. Na prática, limite os certificados dos servidores a 825 dias, porque os dispositivos Apple rejeitam certificados mais longos, independentemente do emissor. Uma raiz privada válida por dez anos, com certificados leaf válidos por dois anos (-days 730), é uma configuração razoável. Agende a renovação, porque um certificado interno expirado pode interromper tudo silenciosamente numa data de que ninguém se lembra.
Devo usar um certificado autoassinado ou o Let's Encrypt?
Se o serviço tiver um nome DNS público e estiver acessível a partir da internet, use sempre o Let's Encrypt: é gratuito, automatizado e já é confiável para todos os clientes. Um certificado autoassinado, ou uma CA privada, serve para os casos em que o Let's Encrypt não pode emitir certificados: IPs privados, nomes de host apenas 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 robustez da segurança; a criptografia é idêntica.
Por que o meu certificado é rejeitado mesmo depois de o adicionar a /usr/local/share/ca-certificates?
Verifique três pontos. O ficheiro tem de terminar em .crt. Uma extensão .pem é ignorada silenciosamente, e update-ca-certificates indica 0 added. O conteúdo tem de ser texto PEM iniciado por -----BEGIN CERTIFICATE-----, e não um 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 e o Java mantêm, cada um, um trust store privado e precisam que o certificado seja adicionado separadamente.