Certificate management: renovação automática com Let's Encrypt

1. Fundamentos do Let's Encrypt e Certbot

O Let's Encrypt é uma autoridade certificadora gratuita que automatiza a emissão e renovação de certificados TLS/SSL através do protocolo ACME (Automatic Certificate Management Environment). O Certbot é o cliente oficial desenvolvido pela Electronic Frontier Foundation (EFF) que implementa esse protocolo em sistemas Unix-like.

Instalação do Certbot

A instalação moderna recomendada utiliza o snap:

sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

Para distribuições baseadas em Debian/Ubuntu via apt:

sudo apt update
sudo apt install certbot python3-certbot-nginx

Para RHEL/CentOS via yum:

sudo yum install epel-release
sudo yum install certbot python3-certbot-nginx

Modos de Validação ACME

O protocolo ACME oferece três métodos principais de validação de domínio:

  • HTTP-01: Verifica a criação de um arquivo em /.well-known/acme-challenge/ no servidor web
  • DNS-01: Requer registro TXT no DNS do domínio (ideal para wildcards)
  • TLS-ALPN-01: Utiliza uma conexão TLS na porta 443 com um certificado de desafio

Para scripts de automação, o modo HTTP-01 é o mais simples, desde que o servidor web esteja rodando na porta 80.

2. Estrutura de Diretórios e Arquivos de Configuração

Após a primeira emissão, o Certbot organiza os certificados em:

/etc/letsencrypt/live/seudominio.com/
├── cert.pem        # Certificado do domínio
├── chain.pem       # Cadeia de certificados intermediários
├── fullchain.pem   # cert.pem + chain.pem combinados
└── privkey.pem     # Chave privada (protegida)

Os logs são armazenados em:

/var/log/letsencrypt/
└── letsencrypt.log

Para configurações persistentes, crie o arquivo /etc/letsencrypt/cli.ini:

# Configuração global do Certbot
server = https://acme-v02.api.letsencrypt.org/directory
email = admin@seudominio.com
agree-tos = true
non-interactive = true

3. Script de Renovação Automática com Bash

O script central renew_certs.sh gerencia todo o processo de renovação:

#!/bin/bash
# renew_certs.sh - Script de renovação automática de certificados Let's Encrypt

# Configurações
LOGFILE="/var/log/letsencrypt/renew.log"
LOCKFILE="/var/run/certbot-renew.lock"
SERVICES=("nginx" "postfix" "dovecot")
ADMIN_EMAIL="admin@seudominio.com"

# Função para logging estruturado
log() {
    local level="$1"
    local message="$2"
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[${timestamp}] [${level}] ${message}" >> "${LOGFILE}"
}

# Prevenção de execução simultânea
exec 200>"${LOCKFILE}"
flock -n 200 || {
    log "ERROR" "Outra instância do script está em execução"
    exit 1
}

# Execução da renovação
log "INFO" "Iniciando renovação dos certificados"

RENEW_OUTPUT=$(certbot renew --quiet --non-interactive 2>&1)
RENEW_EXIT_CODE=$?

if [ ${RENEW_EXIT_CODE} -eq 0 ]; then
    log "INFO" "Renovação concluída com sucesso"

    # Verificar se houve renovação real
    if echo "${RENEW_OUTPUT}" | grep -q "No renewals were attempted"; then
        log "INFO" "Nenhum certificado precisou ser renovado"
    else
        log "INFO" "Certificados foram renovados"
        # Recarregar serviços
        for service in "${SERVICES[@]}"; do
            if systemctl is-active --quiet "${service}"; then
                systemctl reload "${service}"
                log "INFO" "Serviço ${service} recarregado"
            fi
        done
    fi
else
    log "ERROR" "Falha na renovação. Código de saída: ${RENEW_EXIT_CODE}"
    log "ERROR" "Saída: ${RENEW_OUTPUT}"
    exit 1
fi

4. Notificações e Alertas no Script

Adicione funções de notificação para monitoramento proativo:

# Função para enviar e-mail de alerta
send_email_alert() {
    local subject="$1"
    local body="$2"
    echo "${body}" | mail -s "${subject}" "${ADMIN_EMAIL}"
}

# Função para notificação via webhook Slack
send_slack_notification() {
    local message="$1"
    local webhook_url="https://hooks.slack.com/services/SEU/WEBHOOK/URL"
    curl -s -X POST -H 'Content-type: application/json' \
        --data "{\"text\":\"${message}\"}" \
        "${webhook_url}" > /dev/null 2>&1
}

# Função para notificação via Telegram
send_telegram_notification() {
    local message="$1"
    local bot_token="SEU_BOT_TOKEN"
    local chat_id="SEU_CHAT_ID"
    curl -s -X POST \
        "https://api.telegram.org/bot${bot_token}/sendMessage" \
        -d "chat_id=${chat_id}&text=${message}" > /dev/null 2>&1
}

# Exemplo de uso no script principal
if [ ${RENEW_EXIT_CODE} -ne 0 ]; then
    send_email_alert "Falha na renovação SSL" "Detalhes: ${RENEW_OUTPUT}"
    send_slack_notification ":x: Falha na renovação SSL em $(hostname)"
fi

5. Pós-renovação: Recarga de Serviços

A validação da integridade do certificado renovado é crucial:

# Função para validar certificado renovado
validate_certificate() {
    local domain="$1"
    local cert_path="/etc/letsencrypt/live/${domain}/fullchain.pem"

    if [ ! -f "${cert_path}" ]; then
        log "ERROR" "Certificado não encontrado: ${cert_path}"
        return 1
    fi

    # Verificar data de expiração
    local expiry_date=$(openssl x509 -enddate -noout -in "${cert_path}" | cut -d= -f2)
    local expiry_epoch=$(date -d "${expiry_date}" +%s)
    local current_epoch=$(date +%s)
    local days_remaining=$(( (expiry_epoch - current_epoch) / 86400 ))

    log "INFO" "Certificado ${domain} expira em ${days_remaining} dias"

    if [ ${days_remaining} -lt 30 ]; then
        log "WARN" "Certificado ${domain} expirará em menos de 30 dias"
    fi
}

# Recarga segura de serviços usando Docker
reload_docker_service() {
    local container="$1"
    local service="$2"
    docker exec "${container}" systemctl reload "${service}" 2>/dev/null || \
    docker exec "${container}" service "${service}" reload 2>/dev/null
}

# Exemplo de recarga para múltiplos serviços
for domain in $(ls /etc/letsencrypt/live/); do
    validate_certificate "${domain}"
done

systemctl reload nginx
systemctl reload postfix

6. Agendamento com Cron e Systemd Timers

Configuração com Cron

Agende a execução diária do script:

# Editar crontab: crontab -e
# Executar todos os dias às 03:00
0 3 * * * /usr/local/bin/renew_certs.sh

# Alternativa com redirecionamento de saída
0 3 * * * /usr/local/bin/renew_certs.sh >> /var/log/certbot-cron.log 2>&1

Configuração com Systemd Timer

Para maior controle, crie um service e timer do systemd:

# /etc/systemd/system/certbot-renew.service
[Unit]
Description=Certbot Renewal Service
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/renew_certs.sh
User=root
Group=root
StandardOutput=journal
StandardError=journal
# /etc/systemd/system/certbot-renew.timer
[Unit]
Description=Daily Certbot Renewal Timer
Requires=certbot-renew.service

[Timer]
OnCalendar=daily
RandomizedDelaySec=3600
Persistent=true

[Install]
WantedBy=timers.target

Ative e inicie o timer:

sudo systemctl daemon-reload
sudo systemctl enable certbot-renew.timer
sudo systemctl start certbot-renew.timer

7. Casos Especiais e Resolução de Problemas

Renovação Forçada e Testes

# Teste de renovação (dry-run)
certbot renew --dry-run

# Renovação forçada de um domínio específico
certbot renew --force-renewal --cert-name seudominio.com

# Listar todos os certificados gerenciados
certbot certificates

Script de Rollback Automático

#!/bin/bash
# rollback_certs.sh - Script de rollback em caso de falha

BACKUP_DIR="/var/backups/letsencrypt"
DATE=$(date +%Y%m%d_%H%M%S)

# Criar backup antes da renovação
backup_certificates() {
    mkdir -p "${BACKUP_DIR}"
    tar -czf "${BACKUP_DIR}/certs_${DATE}.tar.gz" /etc/letsencrypt/live/
}

# Restaurar backup
restore_certificates() {
    local latest_backup=$(ls -t ${BACKUP_DIR}/certs_*.tar.gz | head -1)
    if [ -n "${latest_backup}" ]; then
        tar -xzf "${latest_backup}" -C /
        log "INFO" "Certificados restaurados de: ${latest_backup}"
    fi
}

# Verificar limites de taxa (rate limits)
check_rate_limits() {
    local remaining=$(curl -s https://acme-v02.api.letsencrypt.org/directory | \
        python3 -c "import sys,json; print(json.load(sys.stdin).get('newOrder',''))" 2>/dev/null)
    if [ -z "${remaining}" ]; then
        log "WARN" "Não foi possível verificar rate limits"
    fi
}

Resolução de Problemas Comuns

  1. Porta 80 ocupada: Use certbot renew --standalone --pre-hook "systemctl stop nginx" --post-hook "systemctl start nginx"
  2. DNS pendente: Verifique propagação com dig +short TXT _acme-challenge.seudominio.com
  3. Renovação de wildcards: Requer plugin DNS (ex: certbot-dns-cloudflare)

Referências