Vai al contenuto

Hestia Control Panel come baseline cliente: SSL, mail e backup in un’ora

Hestia Control Panel come baseline cliente: SSL, mail e backup in un’ora

<a href=Hestia Control Panel come baseline cliente: SSL, mail e backup in un’ora” title=”Hestia Control Panel come baseline cliente: SSL, mail e backup in un’ora” loading=”eager” decoding=”async” />

Agosto 2023, settimo nuovo cliente di hosting WordPress in tre mesi. Il giorno in cui mi sono accorto che il problema non era più il singolo setup, ma il fatto che ognuno mi prendeva fra le quattro e le sei ore. Tre clienti in fila, una settimana intera persa solo a configurare server. Insostenibile. La sera stessa ho aperto un repo runbook interno e ho cominciato a scrivere il template di onboarding HestiaCP: server vuoto, password root, e in un’ora il cliente è in produzione con SSL, mail e backup. Vedi anche gli altri post di Production.

Il problema concreto

Cos’è un onboarding di hosting WordPress per un cliente editoriale piccolo, in pratica? Dieci, dodici cose:

  • Utenza dedicata sul server, con quota disco e quota mail
  • Dominio principale e i suoi alias, con DNS allineati
  • Certificato Let’s Encrypt valido, con autorenew testato
  • Mail server funzionante: caselle, IMAP/SMTP autenticato, webmail
  • DKIM, SPF e DMARC configurati e propagati
  • WordPress installato, con utenza admin e DB dedicato
  • Backup giornaliero off-site cifrato, con retention almeno 14 giorni
  • Tuning di nginx + PHP-FPM ragionevole per WordPress
  • fail2ban con regole che proteggano /wp-login.php e SSH senza bannare il cliente
  • Object cache Redis attivo
  • Monitoraggio base (uptime + alert mail)
  • Documentazione del setup in markdown nel repo runbook interno

Fatto a mano da zero, sono almeno quattro ore se non interviene niente di strano. Ma qualcosa interviene quasi sempre: un MX che non si propaga, un certbot che fallisce per via di una rule firewall, un plugin di cache che entra in conflitto con fastcgi_cache. Ed eccoti che il cliente, che pensava di essere online la sera, lo è il giorno dopo. Non sostenibile sopra i cinque server in produzione.

Cosa ho provato prima del template

Tre tentativi seri, prima di arrivare a HestiaCP come baseline.

cPanel commerciale. Lo conoscevo dagli anni in software house, ma le licenze a uso commerciale costano: per un cliente piccolo che paga 80 euro al mese di hosting, il costo licenza si mangia metà del margine. E poi cPanel è un walled garden: ogni customizzazione un po’ fuori standard, tipo fastcgi_cache con bypass cookie custom, è una battaglia con WHM e con i template proprietari. Cassato.

ISPConfig. Lo avevo provato in azienda anni prima. Open source, multi-server nativo. Ma due cose mi hanno fatto desistere: la GUI sembrava ferma al 2010, e il codice di backend (PHP procedurale con un po’ di OO sopra) era difficile da estendere. Per il volume che avevo davanti, gli scappellotti che mi sarei dovuto dare per ogni override non valevano la pena. Cassato.

Plesk. Stessi problemi di cPanel sul lato licenze, in più una sensazione di vendor lock-in più forte. Cassato.

A quel punto la scelta era fra tre alternative: HestiaCP, niente control panel (Ansible + tutto a mano), o un fork più piccolo tipo VestaCP. VestaCP era moribondo già nel 2022, niente da fare. Niente control panel l’avevo provato su un paio di server: funziona se hai tempo, ma alle 23:00 quando il certbot di un cliente non si rinnova non vuoi essere davanti a journalctl invece che a letto. HestiaCP era la risposta: open source MIT, scritto in bash leggibile, attivo e maintained, con un install script che ti tira su nginx + PHP-FPM + MariaDB + exim + dovecot + roundcube + bind + fail2ban in 20 minuti.

Il template di install

Il file che ho scritto si chiama bootstrap.sh nel repo runbook interno. È bash puro, idempotente per quanto possibile, parametrizzato. La firma è:

./bootstrap.sh <hostname> <admin-email> <ssh-pubkey-path>

Internamente fa tre fasi.

Fase 1: install HestiaCP. Scarica lo script ufficiale da hestiacp.com e lo lancia con i flag che ho deciso essere lo standard:

bash hst-install-debian.sh \
  --apache=no \
  --phpfpm=yes \
  --multiphp=yes \
  --vsftpd=no \
  --proftpd=no \
  --named=yes \
  --mysql=yes \
  --postgresql=no \
  --exim=yes \
  --dovecot=yes \
  --sieve=yes \
  --clamav=no \
  --spamassassin=yes \
  --iptables=yes \
  --fail2ban=yes \
  --quota=yes \
  --hostname=<hostname> \
  --email=<admin-email> \
  --interactive=no

Le scelte non banali: niente Apache (nginx puro davanti a PHP-FPM), niente ClamAV (RAM mangiata su mail di clienti editoriali piccoli, il valore non vale il costo), MariaDB sì. Quote disco attive perché altrimenti il primo cliente che ti carica un dump da 10 GB ti riempie il filesystem in silenzio.

Fase 2: post-install. Una volta che Hestia è su, applico una serie di modifiche standard:

  • Pool PHP-FPM separati per versione, con pm = ondemand invece di pm = dynamic (riduce la RAM idle del 30-40% sui server piccoli)
  • Template nginx clonato dal default di Hestia in nginx-romiltec, con fastcgi_cache configurato di default in stato bypass (vedi sotto sul bug)
  • unattended-upgrades attivo per le security update Debian
  • Cron giornaliero per il backup restic (vedi fase 3)
  • Object cache Redis: install di redis-server, namespace prefix:<dominio-hash> per ogni sito, plugin Redis Object Cache settato di default
  • Monitoring: node_exporter Prometheus su porta 9100, accessibile solo da subnet privata Romiltec
  • fail2ban: regola WordPress con threshold rilassato (10 fail in 10 minuti, ban 1 ora), perché la prima versione era 3 in 5 e bannava redattori legittimi (vedi sotto)

Fase 3: backup off-site. Lo strato che mi ha tolto più sonno nei mesi precedenti. Il default di HestiaCP è v-backup-user: ti tira fuori un tar dei file utente con dump SQL allegato, e te lo butta in /backup/. Stesso server. Il datacenter brucia? Buonanotte. Il template installa restic sopra, configurato verso Backblaze B2 zona EU. La scelta restic + B2 EU non è ovvia, la spiego sotto.

Perché restic, perché B2 EU

Restic invece di duplicity, borgbackup, rsync+cron. Tre motivi pratici. Il primo è che restic fa deduplicazione a livello di blocchi: se un cliente carica un PDF da 5 MB nei suoi wp-content/uploads, e il giorno dopo non cambia niente, lo snapshot incrementale costa qualche kilobyte. Il secondo è che lo snapshot è cifrato lato client con un master key, prima ancora di partire verso il bucket: il fornitore di storage non legge mai i contenuti in chiaro. Il terzo è che il restore è semplice: restic restore latest --target /tmp/restore e te ne stai in pace. Borg fa cose simili, ma il protocollo richiede ssh+borg server o un repository su filesystem locale, e il flusso verso uno storage object come B2 è meno diretto. Duplicity usa cifratura GPG e ha overhead di gestione delle chiavi più pesante. Rsync cifrato non ha deduplicazione e non ha snapshot.

B2 invece di S3, Wasabi, Storj. Backblaze B2 fa storage object S3-compatible a circa un quarto del prezzo di S3 standard, senza costi di egress fino a 3x lo stored al mese (politica che non vedi mai su AWS). Per backup è il caso d’uso giusto: scrivi tanto, leggi raramente, ma quando leggi (disaster recovery vero) non vuoi una bolletta extra. Wasabi era allineato come prezzo ma più giovane e con qualche storia di disservizio nelle fonti tech che seguivo. Storj distribuito è interessante ma lo sentivo ancora sperimentale per backup di clienti.

Zona EU. Backblaze B2 ha tre regioni: us-west, us-east, eu-central. Per i clienti editoriali italiani la zona eu-central è la scelta che allinea data residency UE e governance interna. Niente trasferimento extra-UE, niente sovrapposizione di giurisdizioni, niente discussione con il loro DPO sul tema.

Il cron del backup è uno script wrapper che fa tre cose: dump SQL del cliente in /var/lib/restic-staging/, snapshot restic dei web/ e dello staging SQL, eliminazione dello staging. Retention: 14 giornalieri, 8 settimanali, 6 mensili. Schedulato alle 04:00 con minuti scaglionati per cliente, per non far saturare la banda di upload quando i server hanno tanti utenti.

SSL e mail: i due automatismi che fanno la differenza

Hestia integra Let’s Encrypt nativamente con v-add-letsencrypt-domain. Il template, in fase di add cliente (v-add-user + v-add-web-domain), invoca subito anche il letsencrypt. Il certificato si rinnova automaticamente via cron Hestia. Il punto è che Hestia gestisce sia il rinnovo che la rigenerazione delle config nginx: niente certbot.timer da debuggare a mano.

La parte mail richiede tre setup DNS lato dominio cliente:

  • Record MX verso il sottodominio mail standardizzato del server
  • Record SPF (v=spf1 mx ~all)
  • Record DKIM, generato da Hestia con v-add-mail-domain-dkim (chiave RSA 2048, selettore mail)
  • Record DMARC, opzionale ma raccomandato (v=DMARC1; p=quarantine; rua=mailto:postmaster@<dominio-cliente>)

Hestia genera la chiave DKIM e ti dà il record TXT da incollare nel DNS del cliente. Per i clienti che hanno il DNS gestito su Cloudflare (la maggior parte), il template include uno script Python che usa l’API Cloudflare per scrivere i record automaticamente: lo lancio con il token API del cliente già configurato in fase di onboarding, e i tre record (MX, SPF, DKIM) vanno a posto in trenta secondi. DMARC lo aggiungo manualmente dopo qualche giorno di osservazione dei log.

Roundcube come webmail ce l’hai out of the box su HestiaCP. Il cliente accede dal sottodominio standard del server, login con email completa e password. Un dettaglio: ho disabilitato POP3 sui pool dovecot, lasciando solo IMAP. POP3 non lo usa più nessuno, e ridurre la superficie aiuta su fail2ban.

Cosa è andato storto in produzione

Due cose, con un cliente reale ciascuna, nei primi tre mesi del template.

Bug 1: fastcgi_cache in stato bypass per default

Il template iniziale, sezione nginx, aveva fastcgi_cache configurato ma con la condizione di bypass set $no_cache 1; di default. L’idea era attivo lo cache esplicitamente per ogni cliente quando ho verificato che non ci sono problemi di privacy/cookie sensitive. In pratica significava che ogni cliente partiva senza cache, e il TTFB su WordPress vanilla con qualche plugin pesante era 800-1200 ms.

Me ne sono accorto quando un cliente, due settimane dopo l’onboarding, mi ha mandato uno screenshot di PageSpeed Insights con TTFB 1.4 secondi. È normale? No. Sono entrato sul server, ho verificato che il cache non stava lavorando, e ho corretto il template: fastcgi_cache attivo di default per le request GET non autenticate, con bypass su cookie wordpress_logged_in_* e comment_*. Dopo il fix, TTFB del cliente sceso a 220 ms. Ho riapplicato il template a tutti i clienti precedenti, uno per uno, con uno script.

Lezione: il default deve essere funziona bene, non funziona safe ma lento. Il bypass è esplicito quando serve, non implicito sempre.

Bug 2: backup non escludeva la cache di WordPress

Il primo template includeva tutto web/<dominio>/public_html/ nel backup restic. Sembrava giusto: tutti i file del sito, niente esclusioni. Sbagliato. WordPress, con plugin tipo Autoptimize o WP Super Cache, scrive un sacco di roba in wp-content/cache/ e dentro wp-content/uploads/cache/ per le immagini ottimizzate. Roba rigenerabile. E la rigenerazione cambia i nomi dei file ogni volta, quindi la deduplicazione restic non aiuta: ogni snapshot porta dietro file nuovi dal punto di vista del checksum.

Me ne sono accorto guardando le statistiche restic dopo un mese: il repository di un cliente che pesava 800 MB di sito reale era cresciuto a 4.2 GB di stored su B2. Allarme su Grafana, scrematura. Ho aggiunto al template di backup un file excludes.txt standard:

**/wp-content/cache/
**/wp-content/uploads/cache/
**/wp-content/plugins/wp-super-cache/cache/
**/wp-content/uploads/wpcf7_uploads/
**/.git/
**/node_modules/
**/vendor/
*.log

Riapplicato il template, snapshot ricostruiti. Lo storage medio è sceso del 28% sul fleet, e gli incrementi giornalieri sono diventati prevedibili: qualche megabyte per snapshot per cliente, non più decine.

Lezione: un backup non è completo per definizione, è completo per scelta. Decidi cosa salvi, e perché.

I numeri dopo tre mesi

Da fine maggio a fine agosto 2023, il template ha onboardato dodici clienti. Tempi misurati a cronometro:

  • Setup medio: 58 minuti dal bootstrap.sh al cliente che ha le credenziali in mano
  • Setup minimo: 47 minuti (cliente con DNS già pronto su Cloudflare)
  • Setup massimo: 1h 24min (cliente che usava un DNS non standard, dovuto fare i record a mano e aspettare propagazione)
  • TTFB tipico WordPress vanilla post-template, da Milano: 180-260 ms
  • Dimensione media snapshot restic dopo cleanup: -28% rispetto alla v1
  • Tempi di ripristino test (un sito completo da zero da B2): 18 minuti

Il cliente medio paga circa 80-150 euro al mese di hosting. Sessanta minuti di onboarding sono sostenibili a quel prezzo. Quattro ore non lo erano.

Cosa rifarei diversamente

Una. Avrei messo da subito il monitoraggio uptime con alert. Nei primi tre clienti del template non c’era: ho aggiunto un check blackbox_exporter Prometheus solo dopo che il quarto cliente mi ha chiamato alle 22:00 per dire il sito non risponde. Il sito rispondeva, era il suo Wi-Fi, ma il punto è che non avevo dati per dirgli con sicurezza guarda, da Milano e Francoforte sta in piedi. Un alert preventivo è sempre un alert in meno dal cliente.

Due. La prima versione di fail2ban aveva una regola WordPress troppo aggressiva su /wp-login.php: tre fail in cinque minuti, ban di un’ora. In teoria ragionevole. In pratica, il primo cliente con tre redattori freelance che lavoravano da bar con Wi-Fi pubblico mi ha chiamato dopo tre giorni: non riusciamo a entrare in WordPress. Stessa subnet del bar, ban incrociati. Ho rilassato a dieci fail in dieci minuti, ban di un’ora, e ho aggiunto al template una whitelist di IP fissa che il cliente può popolare in fase di onboarding (uffici, VPN, IP statici dei collaboratori principali).

Tre. Avrei strutturato la fase 2 (post-install) come Ansible role invece che bash. A fine 2023 ce l’avevo solo come script bash da 600 righe. Funzionava, ma debugging e idempotenza erano fragili. L’ho riscritto in Ansible nel 2024, ed è stata una delle cose che mi ha permesso di affrontare il rolling upgrade PHP del fleet senza diventare matto.

Hestia non è la piattaforma definitiva. È il control panel che ti toglie il plumbing standard di un hosting WordPress da sopra le spalle, e ti lascia il tempo di costruire le tre cose che il cliente paga davvero: SSL che non scade, mail che non finisce in spam, backup che esiste davvero quando il datacenter brucia. Il template non è un prodotto: è un asset di metodo, vive dentro il repo runbook interno e si aggiorna ogni volta che incontro un nuovo bordo tagliente. Per Romiltec di metà 2023, era esattamente la baseline che serviva.