Como criar certificado autoassinado no Ubuntu
Aprenda a gerar certificados TLS com SAN no Ubuntu 24.04 para evitar erros no Chrome. Inclui configuração de nginx, Apache e confiança via CA privada.
O que você está construindo
Um certificado TLS autoassinado que navegadores e clientes modernos aceitam — com subjectAltName correto, permissões de chave adequadas, configurado no nginx ou Apache — além do ponto que quase todo guia ignora: fazer seus clientes confiarem nele corretamente, em vez de apenas ignorar avisos e deixar curl -k fixo em scripts para sempre. Ao final, uma CA privada de cinco comandos para quando um serviço interno se tornar seis.
Primeiro, a decisão, pois um certificado autoassinado é a ferramenta correta com muito menos frequência do que se costuma usar. Se o serviço é acessível pela internet pública sob um nome DNS real, pare de ler e obtenha um certificado gratuito Let's Encrypt com certbot no nginx ou o equivalente para Apache em vez disso. Não tem custo, renova-se sozinho e todos os navegadores do mundo já confiam nele. Um certificado autoassinado em um site público treina seus usuários a ignorar avisos de segurança, o que é um hábito pior do que o uso de HTTP simples.
O autoassinado é a ferramenta correta quando não há internet pública envolvida: um painel de administração vinculado a um endereço de túnel WireGuard no seu VPS, um servidor de staging em uma rede privada, tráfego service-to-service entre backends, um dispositivo de home-lab ou para substituir o certificado temporário que o Webmin gera para si mesmo na porta 10000. O Let's Encrypt não pode emitir para 10.8.0.1 ou git.internal.lan de qualquer forma — nenhuma CA pública colocará um IP privado ou um TLD inventado em um certificado. Para esses nomes, você é a CA.
Tudo abaixo roda em um servidor Ubuntu 24.04 recém-instalado, que utiliza OpenSSL 3.0.x (use openssl version para confirmar). Nada aqui requer acesso à internet; tudo funciona em modo air-gapped.
Por que o comando de uma linha antigo gera certificados rejeitados pelo Chrome
O comando presente em todos os tutoriais anteriores a 2017 é 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 solicita uma série de perguntas interativas, insere o hostname no campo Common Name e gera um certificado sem a extensão subjectAltName. Esse certificado é inválido. O Chrome parou de ler o Common Name na versão 58, em abril de 2017 — o RFC 2818 já havia depreciado a correspondência por CN no ano 2000 — e o Firefox, Safari, curl e Python funcionam da mesma forma. Um certificado identifica o servidor através da extensão SAN ou não identifica nada; o navegador informa isso 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 no trust-store resolve esse erro, pois o certificado não contém nenhum nome. Se você está vendo NET::ERR_CERT_COMMON_NAME_INVALID agora, seu certificado não possui SAN (ou possui o SAN incorreto) e você precisa gerar um novo. Felizmente, a correção consiste em um único comando.
Gere um certificado aceito por browsers: um comando
O OpenSSL adicionou a flag -addext na versão 1.1.1, o que significa que não é mais necessário usar configurações complexas de arquivos de configuração 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:
-x509emite um certificado self-signed diretamente em vez de um CSR.-newkey rsa:4096gera uma chave nova no mesmo passo. RSA 4096 é compatível com clientes legados; 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: sem passphrase na chave. Ambas as sintaxes funcionam. Uma chave com passphrase faz o nginx travar esperando input em cada boot; use esta opção para chaves de servidor.-days 730— dois anos; detalhes sobre este valor na seção de expiração.-subjresponde às perguntas interativas automaticamente. O CN é apenas cosmético agora, mas defina-o como o nome principal; algumas ferramentas o exibem.-addext "subjectAltName=..."é a flag principal. Liste todos os nomes e todos os IPs que os clientes usarão: entradasDNS:para hostnames (wildcards comoDNS:*.internal.lansão permitidos), entradasIP:para endereços. Se alguém acessarhttps://10.8.0.1, a entradaIP:10.8.0.1deve estar presente — um SAN apenas com DNS causará erro deNET::ERR_CERT_COMMON_NAME_INVALIDnovamente.
Confirme se o SAN foi aplicado antes de configurar o 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 o comando imprimir No extensions in certificate, o certificado não possui SAN e os browsers o rejeitarão — regenere o certificado em vez de continuar.
Proteja a chave
Uma chave privada que pode ser lida por qualquer usuário no sistema não é uma chave privada. No Ubuntu, /etc/ssl/private já é 710 root:ssl-cert, o que impede o acesso visual não autorizado, mas defina as permissões do arquivo explicitamente:
sudo chown root:root /etc/ssl/private/git.internal.key
sudo chmod 600 /etc/ssl/private/git.internal.keyO nginx e o Apache leem certificados como root antes de reduzir privilégios, portanto o modo 600 em root:root funciona para eles. Se a chave for para um serviço que roda com seu próprio usuário e carrega a chave por conta própria — como um app Node, Gitea ou um daemon Python — altere o proprietário para o usuário desse serviço com chown, mantendo o modo 600. O que você nunca deve fazer: usar o modo 644, manter uma cópia em um repositório git ou uma cópia em /tmp.
Configure no 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 nginxO comando nginx -t deve exibir syntax is ok e test is successful antes do reload surtir efeito. Se exibir SSL_CTX_use_PrivateKey_file() failed ... key values mismatch, o certificado e a chave pertencem a execuções de geração diferentes — veja a seção de modos de falha.
Configure no Apache
sudo a2enmod ssl proxy proxy_httpssl sozinho não é suficiente aqui: o vhost abaixo utiliza 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. Salve o vhost 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 Syntax OK. Agora teste a partir de uma máquina cliente:
curl -v https://git.internal.lan/e você receberá um erro:
curl: (60) SSL certificate problem: self-signed certificateIsso não é um bug. É o TLS funcionando: o curl não reconhece seu certificado e recusa a comunicação com um servidor que não pode autenticar. A próxima seção apresenta a solução real — e não é o que metade da internet faz neste exato momento.
Faça os clientes confiarem — e os anti-padrões para recusar
As soluções incorretas primeiro, nomeadas como são. curl -k (ou --insecure) inserido em um script, verify=False em requests do Python, NODE_TLS_REJECT_UNAUTHORIZED=0 no Node — nenhuma delas faz seu certificado ser confiável. Elas desativam a verificação do certificado, o que significa que o cliente aceitará se comunicar com qualquer servidor que apresente qualquer certificado, incluindo um inserido por um atacante no caminho. Você mantém o overhead do TLS e perde a autenticação que era o objetivo. Pior, essas flags se espalham: coladas em um cron job, depois em um script de deploy, depois no código de produção, até que ninguém mais lembre quais conexões deveriam ser temporárias. Se um verify=False sobreviver além da sessão de debugging que o gerou, o design está errado.
A solução correta é ensinar a cada SO cliente que este certificado é uma raiz confiável. Em clientes Ubuntu e Debian:
sudo cp git.internal.crt /usr/local/share/ca-certificates/git.internal.crt
sudo update-ca-certificatesA linha importante na saída (um bloco Running hooks in /etc/ca-certificates/update.d... a segue):
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.Dois problemas ocorrem nessas linhas. O arquivo deve terminar em .crt — uma extensão .pem é ignorada silenciosamente e você recebe 0 added sem mensagem de erro. E o conteúdo deve ser PEM — o arquivo começa com -----BEGIN CERTIFICATE-----; converta um binário DER primeiro com openssl x509 -inform der -in file.der -out file.crt. Adicionar o próprio certificado self-signed como uma raiz funciona porque um certificado self-signed é sua própria raiz.
Depois disso, curl, wget, git, apt, e qualquer outra coisa que use OpenSSL contra o bundle do sistema confia no servidor sem necessidade de flags. Alguns clientes possuem seus próprios trust stores e precisam de tratamento individual:
- Chrome/Chromium no Linux lê um banco de dados NSS, não o store do sistema:
sudo apt install libnss3-tools, depoiscertutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n "git.internal" -i git.internal.crtpor usuário. - Firefox possui seu próprio store: Settings → Privacy & Security → Certificates → Import, ou altere
security.enterprise_roots.enabledparatrueemabout:configpara que ele leia o store do sistema. - Python requests traz seu próprio CA bundle (certifi) e ignora o store 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.
Em clientes Windows, clique duas vezes no .crt e instale em Trusted Root Certification Authorities; no macOS, adicione ao System keychain no Keychain Access e marque como Always Trust.
Uma raiz para muitos serviços: uma pequena CA privada
A confiança por certificado impede a escalabilidade: seis serviços multiplicados por quatro máquinas clientes resultam em vinte e quatro instalações de confiança, e cada novo serviço aumenta esse número. A solução é uma CA privada — os clientes confiam em uma raiz, e você assina o certificado de cada serviço com ela.
A opção amigável é o mkcert, disponível nos repositórios do Ubuntu 24.04, que gerencia os stores NSS (Chrome, Firefox) que o update-ca-certificates não gerencia:
sudo apt install -y mkcert libnss3-tools
mkcert -install
mkcert git.internal.lan "*.internal.lan" 10.8.0.1O mkcert -install cria uma raiz e a registra em todos os trust stores da máquina; o terceiro comando gera git.internal.lan+2.pem e git.internal.lan+2-key.pem, prontos para serem usados nos snippets do nginx ou Apache acima. O design assume uma máquina de desenvolvimento — a chave raiz reside na máquina que executou o -install — portanto, é ideal para laptops de dev e inadequado para frotas de servidores.
Para servidores, o OpenSSL padrão realiza 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.crtA armadilha está no último comando: o openssl x509 -req remove todas as extensões do CSR por padrão, incluindo o SAN que você adicionou cuidadosamente. O -copy_extensions copy (uma opção do OpenSSL 3.x, compatível com o 24.04) as preserva; se omitido, o certificado assinado não terá SAN, e o Chrome exibirá o erro NET::ERR_CERT_COMMON_NAME_INVALID novamente. Verifique usando o mesmo check openssl x509 -noout -ext subjectAltName de antes.
Distribua o lab-ca.crt para os clientes via os passos de trust-store acima — uma única vez por máquina. Proteja o lab-ca.key como o item crítico que ele é: use mode 600 e, idealmente, mantenha-o em uma máquina que não seja um dos servidores que ele assina, pois quem possuir a chave pode emitir um certificado para qualquer nome que seus clientes aceitem.
Expiração e rotação
A validade dos certificados de CAs públicas está diminuindo — o CA/Browser Forum limitou novos certificados de confiança pública a 200 dias em março de 2026 (antes eram 398), passando para 100 dias em 2027 e 47 dias até março de 2029 — mas essas regras se aplicam apenas a CAs de confiança pública. Sua CA privada não é regida por elas, e navegadores não as aplicam contra raízes instaladas manualmente. Uma restrição real se aplica: plataformas Apple rejeitam qualquer certificado de servidor TLS com validade superior a 825 dias, independentemente de quem o emitiu; portanto, se iPhones ou Macs forem se conectar, mantenha os certificados leaf com dois anos ou menos. -days 730 atende a esse requisito em todos os lugares; uma root de dez anos com leaves de dois anos é uma estrutura interna segura.
Certificados de longa duração falham de uma única maneira: silenciosamente, todos de uma vez, em uma data que ninguém lembra de ter escolhido. Verifique o que você possui:
openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -enddateAgende a renovação em um calendário real ou configure um cron para alertar com 30 dias de antecedência — openssl x509 -checkend 2592000 -in cert.crt retorna um código de saída diferente de zero quando a expiração ocorre dentro desse intervalo de segundos. Se você já utiliza Uptime Kuma para monitoramento de status, seus monitores HTTPS sinalizam a expiração iminente do certificado gratuitamente.
A rotação com uma CA privada é simples: execute novamente os comandos de CSR-and-sign, substitua os arquivos e recarregue o web server. A root não foi alterada, portanto, nenhum cliente notará mudanças.
Modos de falha e as strings que você verá
NET::ERR_CERT_AUTHORITY_INVALID — o estado esperado antes da instalação da confiança, não é um defeito no certificado. Se persistir após a instalação da root: no Linux, o Chrome lê o NSS em vez do store do sistema (veja o passo certutil); ou o arquivo copiado não terminou em .crt e o update-ca-certificates retornou 0 added; ou o servidor está apresentando um certificado diferente do que você confiou — compare os fingerprints 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 possui SAN, ou o SAN não cobre o nome na barra de endereços. O caso clássico: o SAN lista DNS:git.internal.lan mas o usuário acessou https://10.8.0.1. Alterações no trust-store não corrigem isso; reemita o certificado com a entrada ausente.
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 por sua CA privada. Solução temporária: curl --cacert lab-ca.crt https://...; solução permanente: o trust store. Não use -k.
unable to load certificate ... Expecting: TRUSTED CERTIFICATE (ou Expecting: CERTIFICATE REQUEST, ou no start line) — confusão de formato PEM. Você forneceu o tipo de arquivo errado ao OpenSSL: uma key ou CSR onde era esperado um certificado, ou um binário DER onde era esperado um PEM. O comando head -1 filename informa o que você realmente tem — um certificado começa com -----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 key não pertencem ao mesmo par, geralmente porque o comando de geração foi executado duas vezes e os arquivos foram misturados. Confirme com openssl x509 -in git.internal.crt -noout -pubkey | sha256sum versus openssl pkey -in git.internal.key -pubout | sha256sum; hashes correspondentes indicam um par compatível. Se forem diferentes, regenere ambos juntos.
FAQ
Por que o Chrome ainda exibe "Não seguro" após eu criar um certificado self-signed?
Se o erro for NET::ERR_CERT_AUTHORITY_INVALID, o certificado está correto — o Chrome apenas não tem motivos para confiar nele ainda. Instale-o (ou sua CA root privada) no trust store do cliente. Lembre-se que, no Linux, o Chrome utiliza o banco de dados NSS via certutil, e não o store do sistema. Se o erro for NET::ERR_CERT_COMMON_NAME_INVALID, o certificado não possui um Subject Alternative Name correspondente à URL e deve ser reemitido com -addext "subjectAltName=...".
Como faço para o curl confiar em um certificado self-signed sem usar -k?
Copie o certificado (formato PEM, extensão .crt) para /usr/local/share/ca-certificates/ e execute sudo update-ca-certificates — a saída deve exibir 1 added. A partir disso, o curl verificará o certificado como qualquer certificado público. Para uma requisição única sem alterar o sistema, o curl --cacert /path/to/cert.crt verifica apenas contra esse arquivo; o -k desativa a verificação completamente e não deve ser usado em scripts.
Por quanto tempo um certificado self-signed pode ser válido?
Tecnicamente, pelo tempo que você desejar — os limites do CA/Browser Forum (atualmente 200 dias, 47 até 2029) aplicam-se a CAs públicas, não a confiança privada. Na prática, limite certificados de servidor a 825 dias, pois dispositivos Apple rejeitam qualquer valor superior, independentemente do emissor. Uma root privada de dez anos com certificados leaf de dois anos (-days 730) é um padrão seguro; apenas agende a renovação, pois um certificado interno expirado derruba todos os serviços silenciosamente em uma data que ninguém lembra.
Devo usar um certificado self-signed ou Let's Encrypt?
Se o serviço possui um nome DNS público e é acessível pela internet, use sempre Let's Encrypt — é gratuito, automatizado e já é confiável para todos os clientes. Self-signed (ou uma CA privada) serve para o que o Let's Encrypt não pode emitir: IPs privados, hostnames internos como .lan, redes air-gapped e serviços deliberadamente ocultos atrás de uma VPN. A decisão baseia-se em acessibilidade e nomenclatura, não em força criptográfica — a criptografia é idêntica.
Por que meu certificado é rejeitado mesmo após adicioná-lo em /usr/local/share/ca-certificates?
Verifique três pontos. O arquivo deve terminar em .crt — uma extensão .pem é ignorada silenciosamente e o update-ca-certificates reportará 0 added. O conteúdo deve ser texto PEM começando com -----BEGIN CERTIFICATE-----, não binário DER. Além disso, a aplicação deve realmente utilizar o store do sistema — Chrome no Linux, Firefox, Python requests, Node.js e Java possuem seus próprios trust stores privados e exigem que o certificado seja adicionado separadamente.