Alcuni link in questa pagina sono link di affiliazione: se ti registri tramite questi link, il sito riceve una commissione senza costi aggiuntivi per te. Prezzi e consigli non cambiano.
Quattro siti su una macchina legacy sono diventati irraggiungibili. Non lenti, non 500 - irraggiungibili, nel senso che l’handshake TLS falliva prima che una richiesta potesse esistere. Erano giù da cinque giorni prima che qualcuno se ne accorgesse, perché nessuno controllava l’unica cosa che contava.
La causa era banale: il rinnovo dei certificati viveva dentro un control panel, la licenza del panel è scaduta, e il suo cron di rinnovo giornaliero è morto con lei. Altri sette certificati sulla stessa macchina erano destinati a scadere nelle tre settimane successive.
La distro era a fine vita da anni, quindi certbot non si installava in modo pulito e i pacchetti del vendor erano spariti da tempo. Questa è la ricetta che ha risolto il problema in meno di trenta minuti senza toccare un solo file di vhost, e il monitoraggio che garantisce che non possa più succedere di nascosto.
Perché acme.sh e non certbot
certbot è un’applicazione Python con un albero di dipendenze. Su una distro attuale va benissimo. Su una a fine vita è l’intero problema: il Python di sistema è troppo vecchio per le wheel moderne di cryptography, pip vuole un compilatore che non vuoi su una macchina di produzione, e la via snap richiede un daemon che la distro non ha mai incluso.
acme.sh è uno script shell. Ha bisogno di bash e di curl oppure wget. Questo è l’intero elenco di dipendenze. Parla con l’API ACME via HTTPS tramite curl, quindi anche un OpenSSL antico non si trova nel percorso di firma, e bash 4.2 è già abbastanza recente.
C’è anche una piacevole ironia da sapere se stai scappando da un panel: diversi panel commerciali usano acme.sh sotto il cofano. Non stai sostituendo il meccanismo, lo stai solo tirando fuori da sotto qualcosa che ha smesso di funzionare.
Installazione, e correggere la CA predefinita
curl https://get.acme.sh | sh -s email=you@yourdomain.dev
source ~/.bashrc
Poi fai immediatamente il passo che la maggior parte delle guide salta. Dalla versione 3, acme.sh usa di default ZeroSSL, non Let’s Encrypt, e ZeroSSL richiede la registrazione dell’account con credenziali EAB. Se vuoi Let’s Encrypt, dillo una volta:
acme.sh --set-default-ca --server letsencrypt
acme.sh --register-account -m you@yourdomain.dev
Se salti questo passo, il tuo primo tentativo di emissione fallisce con un errore di registrazione che sembra un problema di rete.
Emettere senza toccare i vhost
L’obiettivo è zero modifiche alla configurazione del web server, perché su una macchina come questa le configurazioni sono generate da qualcosa che non controlli più, e una modifica manuale verrebbe sovrascritta.
Due cose lo rendono possibile.
Uno: usa la validazione webroot nella directory che il server serve già. Controlla prima se un percorso è già collegato - i panel di solito ne lasciano uno:
curl -s -o /dev/null -w '%{http_code}\n' \
http://yourdomain.dev/.well-known/acme-challenge/test
Qualsiasi cosa diversa da un errore di connessione significa che il percorso è raggiungibile e devi solo trovare a quale directory sul disco corrisponde. Poi:
acme.sh --issue -d yourdomain.dev -d www.yourdomain.dev \
-w /var/www/letsencrypt
Due: scrivi il certificato nei nomi di file che il server si aspetta già. Non puntare la configurazione alla directory di acme.sh; punta acme.sh ai nomi di file della configurazione. --install-cert fa esattamente questo, e il reload hook lo rende effettivo:
acme.sh --install-cert -d yourdomain.dev \
--fullchain-file /etc/ssl/sites/yourdomain.dev.crt \
--key-file /etc/ssl/sites/yourdomain.dev.key \
--reloadcmd "nginx -s reload && apachectl graceful"
Il contenuto dei file cambia, i percorsi non cambiano mai, e nulla nella configurazione del web server viene modificato.
I certificati scaduti non bloccano il rinnovo
La preoccupazione naturale: ogni sito reindirizza da HTTP a HTTPS, e HTTPS è rotto, quindi come fa la validazione a raggiungere il file di challenge?
Funziona. Let’s Encrypt segue il redirect verso HTTPS e non valida il certificato durante la challenge HTTP-01. Un certificato scaduto da tempo, uno autofirmato, un hostname che non corrisponde - niente di tutto questo blocca l’emissione. Non devi smontare prima i redirect, il che è un bene, perché su una configurazione generata non potresti farlo comunque.
Automatizzalo, poi controlla che non ci sia un rivale
acme.sh --install-cronjob
crontab -l | grep acme
Quella seconda riga è quella importante, ed è l’errore che abbiamo fatto noi. Il cron di rinnovo del panel era disabilitato - ma un secondo cron del panel che faceva lo stesso lavoro a un’altra ora stava quattro righe più sotto, non notato al primo passaggio. Due meccanismi di rinnovo in competizione sugli stessi file è un problema che emerge settimane dopo, sotto forma di un certificato valido su disco e scaduto nella memoria del server.
Leggi tutto il crontab. Commenta, non cancellare, così puoi vedere cosa c’era prima:
crontab -l > /root/crontab.bak-$(date +%Y%m%d)
acme.sh rinnova a 60 giorni per impostazione predefinita, quindi un’esecuzione fallita ha 30 giorni di tentativi prima che qualcosa diventi visibile agli utenti. È un margine generoso, ed è esattamente il motivo per cui un fallimento silenzioso può andare avanti per mesi.
Monitora il certificato che viene servito, non il file su disco
Questa è la parte che avrebbe intercettato il disservizio originale, e quasi nessuno la configura.
Controllare il file non basta: ti dice che il rinnovo ha scritto qualcosa, non che il processo in esecuzione l’ha preso in carico. Un reload fallito silenziosamente lascia un file fresco su disco e un certificato scaduto in memoria. Controlla l’handshake:
#!/usr/bin/env bash
# days until the served certificate expires, per domain
for d in "$@"; do
end=$(echo | openssl s_client -servername "$d" -connect "$d":443 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -z "$end" ]; then echo "CRITICAL $d: no TLS handshake"; continue; fi
days=$(( ( $(date -d "$end" +%s) - $(date +%s) ) / 86400 ))
[ "$days" -lt 25 ] && echo "WARN $d: $days days left" || echo "OK $d: $days days"
done
Eseguilo ogni giorno su tutti gli hostname che la macchina serve e genera un alert sotto i 25 giorni. Due proprietà rendono questo il controllo giusto: fallisce in modo evidente quando l’handshake fallisce del tutto, che è il vero disservizio, e dato che legge ciò che il server consegna a un client, un reload che non è avvenuto non può nascondersi dietro un file rinnovato.
Aggiungi un tail del log di rinnovo allo stesso job e hai tutta la storia in un solo alert:
tail -n 5 /var/log/acme-renew.log
Il risultato
Undici certificati rilasciati nuovamente in un solo passaggio, tutti validi per i prossimi tre mesi, quattro siti morti di nuovo online, rinnovo gestito da uno script shell di 200 KB con una riga di cron che possiamo leggere. Nessun file di vhost è stato modificato; sono cambiati solo i byte dentro i file dei certificati.
La lezione più ampia non riguarda i client ACME. È che la scadenza di una cosa non è il fallimento; il fallimento è il silenzio. Il rinnovo era rotto da circa due mesi prima che il primo sito andasse giù, e l’unico motivo per cui qualcuno se ne è accorto è stato un sito che si è spento davanti agli utenti. Un controllo dell’handshake di venti righe l’avrebbe segnalato nella prima settimana.
Se stai mettendo insieme una macchina nuova invece di salvare una vecchia, Caddy fa tutto questo senza alcuna configurazione - abbiamo descritto quello stack in Caddy e Docker in produzione. Questa ricetta è per la macchina che hai ereditato, quella in cui la configurazione è generata, la distro è congelata, e l’unico cambiamento accettabile è il più piccolo possibile.