← Voltar para o Blog

O certificado venceu no feriado: renovação de SSL que não falha

O certificado tem 90 dias e a renovação é automática — até o dia em que não é. Estas são as quatro razões pelas quais o certbot para de renovar, e como descobrir antes do navegador avisar.

Equipe EasyOps Cloud · · 7 min de leitura

O certificado do Let's Encrypt vale 90 dias e se renova sozinho aos 60. Esse automatismo funciona tão bem que todo mundo esquece que ele existe — o que transforma a falha, quando acontece, em surpresa completa.

E a falha é silenciosa por natureza. O certbot tenta, erra, registra em um log que ninguém lê e tenta de novo doze horas depois. Se a causa persistir, ele repete por trinta dias até o certificado vencer. Só então o navegador estampa a página vermelha — e costuma ser num feriado.

Primeiro: você sabe quando vence?

Antes de configurar qualquer coisa, saiba o estado atual:

sudo certbot certificates

A saída lista cada certificado, os domínios cobertos e quantos dias restam. Para conferir o que o servidor realmente entrega — que pode ser diferente do que está no disco, se o nginx não recarregou:

echo | openssl s_client -connect meusite.com.br:443 -servername meusite.com.br \
  2>/dev/null | openssl x509 -noout -dates -subject

Essa distinção é o segundo erro mais comum desta lista inteira: o certbot renovou, o arquivo novo está lá, e o nginx continua servindo o antigo em memória.

O timer que parou

A renovação depende de um agendamento. Se ele não está ativo, nada acontece:

systemctl list-timers | grep -i certbot
systemctl status certbot.timer

Instalações por snap e por apt criam timers diferentes, e máquina que já teve os dois às vezes fica com o timer errado habilitado — ou com dois competindo. Confira também se sobrou um cron antigo duplicando o trabalho:

ls -la /etc/cron.d/ | grep -i certbot
sudo crontab -l | grep -i certbot

Ter cron e timer ao mesmo tempo não é apenas redundante: dobra as tentativas contra o limite de emissão do Let's Encrypt, e é assim que se chega ao bloqueio temporário descrito mais adiante.

Para testar sem gastar cota, existe o ensaio:

sudo certbot renew --dry-run

Este é o comando desta página. Ele executa o fluxo real contra o ambiente de teste, sem emitir certificado e sem consumir limite. Se ele passa, a renovação de verdade vai passar. Se falha, você acabou de descobrir com semanas de antecedência.

O hook que não recarrega o serviço

O certbot grava os arquivos novos e não sabe que o seu nginx precisa ser avisado. Sem o gancho de recarga, o certificado novo fica no disco enquanto o processo continua com o antigo — até alguém reiniciar por outro motivo, o que mascara o problema por meses.

Com o plugin do nginx, isso é automático. Em modo webroot ou standalone, declare explicitamente:

# /etc/letsencrypt/renewal/meusite.com.br.conf
[renewalparams]
authenticator = webroot
webroot_path = /var/www/html
deploy_hook = systemctl reload nginx

Ou de forma global, valendo para todos os certificados:

# /etc/letsencrypt/renewal-hooks/deploy/recarregar.sh
#!/usr/bin/env bash
set -euo pipefail
systemctl reload nginx
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/recarregar.sh

Prefira deploy_hook a post_hook: o primeiro roda apenas quando um certificado foi de fato renovado; o segundo roda em toda tentativa, inclusive nas que não fizeram nada. Se outros serviços usam o mesmo certificado — um postfix, um dovecot, um proxy próprio —, é neste script que eles entram.

A porta 80 que alguém fechou

O desafio HTTP-01 funciona assim: o Let's Encrypt acessa http://seudominio/.well-known/acme-challenge/... para confirmar que você controla o domínio. Em HTTP, na porta 80, sempre.

É por isso que fechar a porta 80 "porque o site é todo HTTPS" quebra a renovação sessenta dias depois — tempo suficiente para ninguém associar as duas coisas. A porta 80 precisa continuar aberta, mesmo que só redirecione:

server {
    listen 80;
    server_name meusite.com.br;

    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

A ordem dos blocos importa: o location do desafio precisa vir antes do redirect, senão o nginx manda o validador para HTTPS e o desafio falha.

Confirme que a porta está aberta nas duas camadas — na regra de firewall do painel e no ufw da VM. Um firewall de perímetro liberando só a 443 produz exatamente este sintoma, e o log do certbot só informa que o desafio não respondeu.

Vale testar de fora, com o desafio simulado:

echo ok | sudo tee /var/www/html/.well-known/acme-challenge/teste
curl -I http://meusite.com.br/.well-known/acme-challenge/teste

Precisa devolver 200. Se devolver 301, a ordem dos blocos está invertida.

Quando HTTP-01 não serve

Dois casos exigem o desafio por DNS:

  • Certificado curinga (*.meusite.com.br). O Let's Encrypt não emite curinga por HTTP-01, apenas por DNS-01.
  • Servidor sem porta 80 pública — ambiente interno, atrás de VPN, ou onde abrir a 80 não é aceitável.

O DNS-01 prova o controle criando um registro TXT. Com API do provedor de DNS, continua automático:

sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d meusite.com.br -d '*.meusite.com.br'
sudo chmod 600 /etc/letsencrypt/cloudflare.ini

A permissão 600 não é detalhe: o arquivo contém um token com poder de alterar o seu DNS. Se o provedor não tiver plugin, existe --manual, mas ele não renova sozinho — e um certificado que exige intervenção manual a cada 90 dias é um incidente agendado.

O limite que ninguém lê até bater nele

O Let's Encrypt impõe cotas. A que mais dói é 5 certificados idênticos por semana: passou disso, você fica sem emitir por sete dias — inclusive para consertar o que estava tentando consertar.

É fácil chegar lá durante uma depuração: cada certbot --force-renewal que você dispara "para testar" consome uma unidade. Por isso o --dry-run existe.

Se já bateu no limite e o certificado atual ainda é válido, não há emergência: espere. Se venceu, o caminho é um certificado com conjunto de domínios diferente (acrescente um subdomínio), que conta como outro certificado.

Mais de um servidor com o mesmo domínio

Tudo acima assume uma máquina. Com duas ou mais atrás de um balanceador, o HTTP-01 vira loteria: o validador do Let's Encrypt bate no domínio, o balanceador encaminha para qualquer uma das máquinas, e o desafio só existe na que iniciou a renovação. Falha em uma boa parte das tentativas, de forma intermitente — o pior tipo de falha para diagnosticar.

Há três saídas, em ordem de simplicidade:

  • Centralizar o TLS no balanceador. Só ele tem certificado; as VMs atrás falam HTTP puro na rede interna. É a arquitetura mais comum e elimina o problema pela raiz.
  • Rotear `/.well-known/acme-challenge/` sempre para a mesma máquina. Uma regra fixa no balanceador resolve, e as demais recebem o certificado por cópia.
  • Trocar para DNS-01. O desafio deixa de depender de qual servidor atende, já que a prova está no DNS. É a opção natural quando você já usa curinga.

Se optar por copiar o certificado entre máquinas, o deploy_hook é o lugar certo para isso — e lembre que a chave privada viaja junto:

# /etc/letsencrypt/renewal-hooks/deploy/distribuir.sh
#!/usr/bin/env bash
set -euo pipefail
D=/etc/letsencrypt/live/meusite.com.br
for h in 10.0.0.12 10.0.0.13; do
  rsync -a --chmod=600 "$D/" "root@$h:$D/"
  ssh "root@$h" systemctl reload nginx
done

Restrinja essa rota na regra de firewall à rede interna. Uma cópia de chave privada trafegando é aceitável dentro da sua rede e inaceitável saindo dela.

Descobrir antes do cliente

O último elo é a vigilância. Um teste semanal que verifica dias restantes e avisa com folga:

#!/usr/bin/env bash
# /usr/local/bin/check-ssl.sh
LIMITE=21
for d in meusite.com.br loja.meusite.com.br; do
  fim=$(echo | openssl s_client -connect "$d:443" -servername "$d" 2>/dev/null \
        | openssl x509 -noout -enddate | cut -d= -f2)
  dias=$(( ( $(date -d "$fim" +%s) - $(date +%s) ) / 86400 ))
  [ "$dias" -lt "$LIMITE" ] && echo "ALERTA: $d vence em $dias dias"
done

Repare que ele consulta a porta 443 de fora, e não o arquivo em disco. Isso pega os dois cenários de uma vez: a renovação que não aconteceu e a que aconteceu sem o serviço recarregar.

Agende com um systemd timer semanal e mande a saída para onde alguém realmente olhe. Se optar por cron, vale rever por que tarefas de cron não rodam — o script acima depende de openssl e date no PATH, que é justamente o que o cron não garante.

O resumo

Configure uma vez e verifique três coisas: certbot renew --dry-run passa, o deploy_hook recarrega o serviço, e a porta 80 continua aberta. Se as três respostas forem sim, a renovação vai acontecer sem você.

Para quem está montando o domínio agora, o guia de apontamento e SSL cobre o DNS e a emissão inicial — a renovação, que é a parte que falha meses depois, é o que este artigo resolve.

Suba um servidor em minutos

Preço em real, suporte em português e dados no Brasil.

Criar conta