Let's Encrypt con Certbot su Nginx: automazione completa per multi-dominio

Let's Encrypt con Certbot su Nginx: automazione completa per multi-dominio

Nel gennaio 2024 ho ereditato la gestione di otto VPS Linux (cinque Hetzner, due OVH, uno Digital Ocean) per un cliente del settore servizi digitali che ospita una quarantina di siti web e applicazioni per i propri clienti finali. La prima cosa che ho controllato dopo l'accesso SSH è stata la scadenza dei certificati TLS. Il risultato era un bollettino di guerra: 3 certificati già scaduti (i siti servivano la pagina di errore "La tua connessione non è privata" del browser), 7 certificati in scadenza entro 15 giorni, 12 certificati rinnovati manualmente dall'amministratore precedente con certbot certonly senza alcun cronjob di rinnovo automatico, e 18 certificati gestiti da un cronjob certbot renew che girava ogni 3 mesi - ma il timer era rotto perché il cronjob era stato inserito nel crontab dell'utente deploy che non aveva i permessi di root per scrivere nella directory di Certbot. Nessuno se ne era accorto perché nessuno controllava.

Ho ricostruito l'intero sistema di gestione dei certificati in una giornata di lavoro, implementando un'automazione completa che da due anni gestisce 40 certificati TLS su 8 server senza un singolo rinnovo manuale e senza un singolo certificato scaduto. L'approccio è volutamente semplice - Certbot, un timer systemd, uno script di verifica scadenze e un webhook verso il canale di monitoring - perché la complessità è il nemico della manutenzione, e un sistema di gestione certificati che nessuno capisce è un sistema che prima o poi romperà.

Perché i certificati TLS scadono ancora nel 2025, nonostante Let's Encrypt sia gratuito e automatico?

La risposta non è tecnica - è organizzativa. Let's Encrypt ha reso i certificati TLS gratuiti e rinnovabili automaticamente dal 2016, e Certbot rende il processo di rinnovo un singolo comando. Eppure, nel mio lavoro di consulenza infrastrutturale vedo certificati scaduti con frequenza allarmante. I motivi sono sempre gli stessi: il rinnovo automatico è stato configurato una volta e mai verificato, il cronjob è nel crontab di un utente che non ha i permessi giusti, il rinnovo funziona per il dominio principale ma fallisce silenziosamente per i sottodomini, il server ha una configurazione Nginx che blocca la challenge HTTP-01 di Let's Encrypt, oppure semplicemente nessuno ha impostato un alert che segnali quando un certificato sta per scadere. In tutti questi casi, il problema viene scoperto quando un cliente chiama perché il browser mostra un errore di sicurezza - il peggior modo possibile per scoprire che il tuo TLS è scaduto.

Il danno non è solo tecnico. Un certificato scaduto su un sito e-commerce significa transazioni perse (nessun browser moderno permette di procedere su HTTPS con certificato scaduto senza un click esplicito dell'utente su "Accetta il rischio"), penalizzazione SEO (Google declassa i siti con problemi di sicurezza), e soprattutto perdita di fiducia del cliente. Nel mio profilo professionale trovi il dettaglio dell'esperienza che porto nella gestione di infrastrutture multi-server per PMI - e la gestione dei certificati TLS è uno dei primi interventi che faccio in ogni subentro, perché è il primo segnale visibile della qualità della manutenzione infrastrutturale.

Configurazione base: Certbot con plugin Nginx su Debian 12

L'installazione di Certbot con il plugin Nginx su Debian 12 è standard e ben documentata nella guida ufficiale di Certbot per Nginx. Il plugin Nginx gestisce sia l'emissione del certificato sia la configurazione del server block, riducendo il margine di errore nella configurazione manuale delle direttive SSL:

# Installazione Certbot con plugin Nginx
apt-get update
apt-get install -y certbot python3-certbot-nginx

# Emissione certificato per un singolo dominio
certbot --nginx -d esempio.it -d www.esempio.it \
  --email [email protected] --agree-tos --non-interactive

# Emissione certificato wildcard (richiede validazione DNS)
certbot certonly --manual --preferred-challenges dns \
  -d esempio.it -d '*.esempio.it' \
  --email [email protected] --agree-tos

Il plugin --nginx fa due cose in automatico: configura il server block Nginx per il dominio con il certificato appena emesso (aggiungendo le direttive ssl_certificate e ssl_certificate_key), e configura il redirect da HTTP a HTTPS. Per un singolo dominio, il processo richiede meno di 30 secondi. Il certificato wildcard (*.esempio.it) è utile quando hai molti sottodomini (staging, api, admin, cdn) e non vuoi emettere un certificato separato per ciascuno - ma richiede la validazione DNS-01 invece della HTTP-01, il che significa che devi aggiungere un record TXT alla zona DNS del dominio. Per l'automazione del wildcard, l'integrazione con il plugin DNS del tuo provider (Cloudflare, Hetzner DNS, OVH) è essenziale per evitare che il rinnovo ogni 90 giorni richieda un intervento manuale - la tratto nella sezione dedicata più avanti nell'articolo.

Rinnovo automatico: timer systemd, non cronjob

Il primo errore che correggo in ogni server che eredito è il cronjob di Certbot. La maggior parte delle guide online suggerisce di aggiungere una riga nel crontab di root: 0 0,12 * * * certbot renew --quiet. Funziona, ma ha due problemi: primo, il cronjob non produce alcun feedback se il rinnovo fallisce (l'opzione --quiet sopprime tutto l'output, inclusi gli errori); secondo, il cronjob non ha un meccanismo nativo di retry se il server era spento o sotto manutenzione all'ora programmata.

La soluzione che uso è un timer systemd, che Certbot installa automaticamente su Debian 12 come certbot.timer:

# Verifica che il timer systemd di Certbot sia attivo
systemctl status certbot.timer

# Output atteso:
# ● certbot.timer - Run certbot twice daily
#   Loaded: loaded (/lib/systemd/system/certbot.timer; enabled)
#   Active: active (waiting)
#   Trigger: [prossima esecuzione]

# Se non è attivo, abilitalo
systemctl enable --now certbot.timer

Il timer systemd ha due vantaggi rispetto al cronjob: esegue il rinnovo con un offset randomico (per distribuire il carico sui server Let's Encrypt e evitare che migliaia di server facciano la richiesta nello stesso secondo), e se il server era offline all'ora programmata, systemd esegue il rinnovo al prossimo avvio. Il timer è configurato per eseguire certbot renew due volte al giorno - un intervallo aggressivo ma innocuo, perché Certbot rinnova solo i certificati che scadono entro 30 giorni e ignora gli altri.

Il passaggio critico che manca nella configurazione di default è il hook post-rinnovo. Certbot rinnova il certificato sul filesystem, ma Nginx continua a usare quello vecchio caricato in memoria fino al prossimo reload. Senza un reload, il certificato è rinnovato ma il sito continua a servire quello scaduto. La configurazione del hook è una riga nel file /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh:

#!/bin/bash
# Hook post-rinnovo: ricarica Nginx con il nuovo certificato
# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

set -euo pipefail

# Verifica che la configurazione Nginx sia valida prima del reload
nginx -t 2>/dev/null || {
    echo "ERRORE: configurazione Nginx non valida, reload annullato" >&2
    exit 1
}

systemctl reload nginx

echo "Nginx ricaricato con certificato rinnovato: $(date)"

Il nginx -t prima del reload è una precauzione che ho imparato dopo un incidente in cui un rinnovo di certificato era avvenuto contemporaneamente a una modifica manuale della configurazione Nginx - il reload aveva caricato una configurazione rotta e il sito era andato offline. Con il test preventivo, il reload viene annullato se la configurazione non è valida, preservando il servizio con il vecchio certificato (ancora valido per 30 giorni) e dando tempo al team di correggere il problema.

Lo script di monitoring: alert 30 giorni prima della scadenza

Il rinnovo automatico è necessario ma non sufficiente. Serve un sistema di monitoring che verifichi indipendentemente la validità dei certificati e invii un alert quando qualcosa non funziona - perché il rinnovo automatico può fallire silenziosamente per una dozzina di motivi: rate limit di Let's Encrypt raggiunto, validazione DNS non configurata per un nuovo sottodominio, permessi file cambiati, plugin Nginx aggiornato con breaking change.

Lo script che uso controlla ogni certificato dal punto di vista del client (non dal filesystem del server) usando openssl s_client:

#!/bin/bash
# Controllo scadenza certificati TLS per tutti i domini gestiti
# Eseguito giornalmente via cron

set -euo pipefail

DOMAINS=(
    "esempio.it"
    "www.esempio.it"
    "api.esempio.it"
    "app.altrodominio.it"
    # ... tutti i domini gestiti
)

SOGLIA_GIORNI=30
WEBHOOK_URL="https://hooks.monitoring.local/tls-alert"

for domain in "${DOMAINS[@]}"; do
    # Controlla il certificato dal punto di vista del client
    scadenza=$(echo | openssl s_client -servername "${domain}" \
        -connect "${domain}:443" 2>/dev/null \
        | openssl x509 -noout -enddate 2>/dev/null \
        | cut -d= -f2)

    if [ -z "${scadenza}" ]; then
        # Impossibile connettersi o leggere il certificato
        curl -s -X POST "${WEBHOOK_URL}" \
            -H "Content-Type: application/json" \
            -d "{\"domain\": \"${domain}\", \"status\": \"UNREACHABLE\"}"
        continue
    fi

    # Calcola i giorni alla scadenza
    scadenza_epoch=$(date -d "${scadenza}" +%s)
    oggi_epoch=$(date +%s)
    giorni_rimasti=$(( (scadenza_epoch - oggi_epoch) / 86400 ))

    if [ "${giorni_rimasti}" -lt "${SOGLIA_GIORNI}" ]; then
        curl -s -X POST "${WEBHOOK_URL}" \
            -H "Content-Type: application/json" \
            -d "{\"domain\": \"${domain}\", \"days_left\": ${giorni_rimasti}, \"expiry\": \"${scadenza}\", \"status\": \"EXPIRING\"}"
    fi
done

Questo script gira su un server separato (non sullo stesso server che ospita i siti) e controlla i certificati dall'esterno, esattamente come farebbe un utente che visita il sito. Se il rinnovo automatico ha funzionato ma l'hook di reload Nginx ha fallito, lo script lo rileva perché il certificato servito dal server è ancora quello vecchio. Se il server è irraggiungibile, lo script lo rileva. Se il certificato è stato rinnovato ma con un sottodominio mancante, lo script lo rileva controllando ogni dominio singolarmente.

Sul canale di monitoring del cliente, l'alert arriva come: "ATTENZIONE: il certificato di api.esempio.it scade tra 12 giorni. Il rinnovo automatico potrebbe aver fallito." Il team ha 12 giorni per investigare e risolvere - tempo più che sufficiente per capire cosa è successo senza panico.

Wildcard con automazione DNS: Cloudflare e Hetzner

Per i clienti che hanno molti sottodomini, il certificato wildcard è la scelta più efficiente perché un singolo certificato copre esempio.it e *.esempio.it. Ma il wildcard richiede la validazione DNS-01, che a sua volta richiede l'aggiunta di un record TXT alla zona DNS del dominio ad ogni rinnovo. Senza automazione, il rinnovo del wildcard è manuale - e manuale significa che prima o poi qualcuno dimenticherà di farlo.

La soluzione è il plugin DNS di Certbot specifico per il tuo provider. Per Cloudflare, il più diffuso tra i miei clienti:

# Installazione plugin Cloudflare per Certbot
apt-get install -y python3-certbot-dns-cloudflare

# Configurazione delle credenziali API Cloudflare
cat > /etc/letsencrypt/cloudflare.ini << 'EOF'
dns_cloudflare_api_token = <token-cloudflare-con-permesso-dns-edit>
EOF
chmod 600 /etc/letsencrypt/cloudflare.ini

# Emissione wildcard con validazione DNS automatica
certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d esempio.it -d '*.esempio.it' \
  --email [email protected] --agree-tos --non-interactive

Con questa configurazione, il rinnovo del wildcard è completamente automatico: Certbot aggiunge il record TXT via API Cloudflare, Let's Encrypt verifica, Certbot rimuove il record TXT, e l'hook di deploy ricarica Nginx. L'intero processo avviene senza intervento umano, ogni 60-90 giorni, da due anni senza un singolo fallimento.

Ho documentato un approccio complementare alla gestione TLS nel contesto più ampio del piano di disaster recovery per infrastrutture PMI, dove il rinnovo automatico dei certificati è uno degli elementi del runbook operativo. La regola che applico è che nessun componente dell'infrastruttura deve dipendere dall'intervento manuale di una persona specifica per continuare a funzionare - e i certificati TLS sono il primo posto dove questa regola viene violata quando manca una cultura operativa strutturata. Se gestisci più VPS con decine di domini e il tuo approccio alla gestione dei certificati è "certbot renew e speriamo", contattami per una sessione di automazione: in mezza giornata configuriamo il timer systemd, lo script di monitoring, l'automazione DNS e l'integrazione con il tuo canale di alerting preferito - e da quel momento i certificati diventano un problema risolto che non devi più pensare.

Ultima modifica: