Certificato HTTPS scaduto o mal configurato su VPS con Laravel: diagnosi, fix e hardening TLS nel 2025

Certificato HTTPS scaduto o mal configurato su VPS con Laravel: diagnosi, fix e hardening TLS nel 2025

A luglio 2025 il titolare di un portale B2B per la distribuzione di componenti elettronici - ospitato su un VPS Hetzner CPX31 con Laravel 10 - mi ha scritto: "Il sito non si apre da nessun browser da undici giorni." Undici giorni. Non undici minuti, non undici ore. Il portale serviva circa 400 clienti B2B con ordini giornalieri, e per quasi due settimane nessun cliente era riuscito ad accedere. Chrome mostrava ERR_CERT_DATE_INVALID senza possibilità di bypass, perché il server inviava un header HSTS con max-age=31536000 - un anno di strict transport security che impediva qualunque connessione non TLS. Il certificato era scaduto l'11 giugno 2025 e nessuno se n'era accorto fino al 22 giugno, quando un cliente ha chiamato il commerciale per lamentarsi.

Il danno stimato dal titolare: circa 180 ordini non completati nel periodo di downtime, equivalenti a circa 75.000 euro di fatturato. Per un certificato Let's Encrypt gratuito che non si era rinnovato automaticamente. E il danno reputazionale: per undici giorni, ogni cliente che cercava di accedere al portale vedeva un warning di sicurezza del browser - il tipo di messaggio che fa pensare "questo sito è stato hackerato" anche quando il problema è solo un certificato scaduto. Recuperare la fiducia dei clienti B2B dopo un'impressione del genere richiede settimane di comunicazione proattiva.

Perché un certificato HTTPS scaduto blocca completamente un sito con HSTS attivo?

Senza HSTS, un browser che incontra un certificato scaduto mostra un warning ma permette all'utente di procedere (su Chrome: "Advanced → Proceed to site"). Con HSTS attivo - l'header Strict-Transport-Security che dice al browser "per i prossimi N secondi, accetta solo connessioni HTTPS da questo dominio" - il browser non mostra nemmeno l'opzione di procedere. La connessione viene rifiutata senza appello. Il rollout sicuro di HSTS raccomandato dalla community di sicurezza prevede una progressione graduale: max-age=60 (1 minuto) → max-age=86400 (1 giorno) → max-age=604800 (1 settimana) → max-age=31536000 (1 anno) solo dopo aver verificato che il rinnovo automatico funziona senza problemi. Il sito del cliente era passato direttamente a 1 anno - senza che nessuno avesse mai verificato il rinnovo Certbot.

Il primo passo diagnostico è sempre verificare lo stato del certificato dall'esterno:

# Controllare scadenza e catena del certificato
echo | openssl s_client -connect portale.it:443 -servername portale.it 2>/dev/null | openssl x509 -noout -dates -subject -issuer

# Verificare quale certificato Nginx sta servendo
nginx -T 2>/dev/null | grep ssl_certificate

# Stato di Certbot e tentativi di rinnovo
certbot certificates
cat /var/log/letsencrypt/letsencrypt.log | tail -50

Sul VPS del cliente, certbot certificates mostrava il certificato come "EXPIRED" da 11 giorni. Il log di Certbot rivelava il motivo: il rinnovo via HTTP-01 challenge richiedeva che il server web rispondesse sulla porta 80, ma una regola UFW aggiunta tre mesi prima durante un "hardening" aveva bloccato la porta 80 per "forzare HTTPS". L'ironia: l'hardening aveva causato il downtime.

Il fix immediato:

# Riaprire porta 80 (necessaria per HTTP-01 challenge di Let's Encrypt)
ufw allow 80/tcp

# Forzare il rinnovo
certbot renew --force-renewal

# Verificare il nuovo certificato
echo | openssl s_client -connect portale.it:443 -servername portale.it 2>/dev/null | openssl x509 -noout -dates

# Ricaricare Nginx con il nuovo certificato
nginx -t && systemctl reload nginx

La porta 80 deve restare aperta anche se tutto il traffico viene rediretto a HTTPS. La redirect corretta in Nginx è un server block dedicato che ascolta sulla porta 80 e restituisce un 301:

# /etc/nginx/sites-enabled/redirect-http.conf
server {
    listen 80;
    listen [::]:80;
    server_name portale.it www.portale.it;

    # Let's Encrypt challenge (deve rispondere su HTTP)
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    # Tutto il resto → HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

Bloccare la porta 80 a livello firewall - come aveva fatto chi aveva "hardenizzato" il server del cliente - è l'errore che vedo ripetere più spesso. Il ragionamento è "se voglio solo HTTPS, blocco HTTP" - logicamente sensato, operativamente catastrofico perché rompe il rinnovo dei certificati. La porta 80 serve per due cose: la challenge Let's Encrypt e la redirect a HTTPS. Entrambe sono necessarie, entrambe sono sicure (la redirect non espone dati, la challenge è un token temporaneo).

Un ulteriore livello di protezione che aggiungo su ogni dominio: il record DNS CAA (Certificate Authority Authorization), che specifica quali CA sono autorizzate a emettere certificati per il dominio. Se un attaccante riesce a richiedere un certificato da un'altra CA (un attacco reale documentato in contesti di DNS hijacking), il CAA record lo blocca a livello di validazione:

; DNS CAA record: solo Let's Encrypt può emettere certificati per questo dominio
portale.it.  IN CAA  0 issue "letsencrypt.org"
portale.it.  IN CAA  0 issuewild "letsencrypt.org"
``` La challenge HTTP-01 di Let's Encrypt richiede che il server risponda su porta 80 con un file temporaneo in `.well-known/acme-challenge/`. Senza porta 80 aperta, nessun rinnovo automatico.

Stai cercando un **Consulente Informatico** esperto per risolvere problemi di certificati HTTPS e configurazione TLS sulla tua infrastruttura? Nel mio [profilo professionale](/pages/about) trovi l'esperienza concreta su VPS Hetzner, OVH, Contabo e Digital Ocean.

## Le tre misconfigurazioni TLS che trovo su ogni VPS di PMI

Oltre al certificato scaduto, l'audit TLS sul VPS del cliente ha rivelato tre problemi che trovo su quasi tutti i server che prendo in carico:

**TLS 1.0 e 1.1 ancora abilitati.** La configurazione Nginx di default su Debian 11 include `ssl_protocols TLSv1 TLSv1.1 TLSv1.2;` - protocolli deprecati dalla RFC 8996 nel 2021. TLS 1.0 è vulnerabile a BEAST e POODLE. TLS 1.1 non ha cipher suite moderne. Nel 2025, [TLS 1.3 rappresenta circa il 73% del traffico cifrato globale](https://comparecheapssl.com/the-state-of-ssl-key-statistics-and-trends-shaping-web-security/) e non c'è nessun browser o client B2B moderno che richieda TLS 1.0 o 1.1. Rimuoverli non rompe nulla e chiude due superfici di attacco.

**Cipher suite deboli.** Cipher come `DES-CBC3-SHA` o `RC4-MD5` ancora presenti nella configurazione - residui di un'epoca in cui la compatibilità con Internet Explorer 6 era un requisito. Ogni cipher debole è un potenziale vettore di downgrade attack: un attaccante man-in-the-middle può forzare la negoziazione su un cipher debole anche se il server supporta cipher forti.

**OCSP stapling disabilitato.** Senza OCSP stapling, il browser deve contattare la CA (Let's Encrypt) per verificare che il certificato non sia stato revocato - un round-trip aggiuntivo di 100-300 ms su ogni nuova sessione TLS. Con lo stapling, il server include la risposta OCSP (prefirmata dalla CA) direttamente nel TLS handshake, eliminando il round-trip. Impatto su performance e privacy: il browser non deve mai contattare la CA, la connessione è più veloce e la CA non viene informata di chi visita il sito.

La [checklist SSL hardening per Nginx nel 2026](https://letsecure.me/nginx-ssl-hardening-checklist-2026/) riassume i controlli più importanti in ordine di priorità: TLS 1.3 come protocollo primario, HSTS con preload (solo dopo aver verificato il rinnovo), OCSP stapling, e session cache per performance.

## Hardening TLS su Nginx: la configurazione che produce A+ su SSL Labs

Dopo aver risolto l'emergenza, il passo successivo è hardenizzare la configurazione TLS perché non basta avere un certificato valido - bisogna che il server negozi solo protocolli e cipher suite sicuri. TLS 1.0 e 1.1 sono [deprecati formalmente dalla RFC 8996 dal 2021](https://usavps.com/post/hsts-on-your-vps-common-misconfigurations-and-how-to-fix-them/) e nel 2025 non c'è nessun motivo per supportarli. TLS 1.3 è ormai il 73% del traffico cifrato globale. La configurazione Nginx che applico su ogni VPS:

```nginx
# /etc/nginx/snippets/ssl-hardened.conf
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;  # TLS 1.3 gestisce i cipher autonomamente

# Cipher suite per TLS 1.2 (TLS 1.3 non è configurabile in Nginx - OpenSSL li gestisce)
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';

# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;

# HSTS graduale (partire da 1 giorno, NON da 1 anno)
add_header Strict-Transport-Security "max-age=86400" always;

# Session cache per performance
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;

# Certificati ECDSA per handshake più veloci
ssl_certificate /etc/letsencrypt/live/portale.it/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/portale.it/privkey.pem;

Un dettaglio che fa la differenza in performance: ssl_certificate_key dovrebbe puntare a un certificato ECDSA anziché RSA. Let's Encrypt supporta ECDSA via certbot --key-type ecdsa, e l'handshake ECDSA P-256 è significativamente più veloce di RSA 2048 - su server a basse risorse come un VPS da 4 vCPU la differenza si misura in millisecondi per ogni nuova connessione, e su un portale con centinaia di utenti concorrenti il risparmio cumulativo è tangibile. Sul VPS del cliente ho rigenerato il certificato in ECDSA:

# Rigenerare il certificato in ECDSA P-256
certbot certonly --nginx --key-type ecdsa -d portale.it -d www.portale.it --force-renewal

Il test su Qualys SSL Labs dopo la configurazione completa ha restituito A+ - il grado massimo - con TLS 1.3 come protocollo preferito, forward secrecy su tutti i cipher, OCSP stapling attivo e HSTS presente.

Il punto critico: max-age=86400 (1 giorno) anziché 31536000 (1 anno). Se il certificato scade di nuovo o la configurazione TLS si rompe, il browser del cliente rifiuterà la connessione solo per 24 ore - non per un anno intero. Una volta verificato che il rinnovo automatico funziona per almeno 3 cicli consecutivi (90 giorni), si può alzare progressivamente: 1 settimana → 1 mese → 6 mesi → 1 anno. Per il monitoraggio proattivo della scadenza dei certificati - l'alert che avrebbe evitato le 11 giornate di downtime - ho descritto la regola Prometheus CertificatoInScadenza nel mio articolo sul monitoring proattivo per Laravel su VPS unmanaged.

Il cron di Certbot: verificare che funzioni, non dare per scontato

Let's Encrypt emette certificati con validità di 90 giorni. Certbot è configurato per rinnovare automaticamente ogni certificato quando mancano meno di 30 giorni alla scadenza. Il rinnovo avviene tramite un timer systemd (certbot.timer) o un cron job in /etc/cron.d/certbot. Ma su un VPS che ha subìto aggiornamenti di sistema, cambi di configurazione del firewall, o migrazioni di web server, il rinnovo automatico può rompersi silenziosamente - come è successo sul VPS del cliente.

La verifica è semplice:

# Dry run: simula il rinnovo senza modificare nulla
certbot renew --dry-run

# Verificare il timer systemd
systemctl list-timers | grep certbot

# Se non c'è il timer, verificare il cron
cat /etc/cron.d/certbot

Se il dry run fallisce, il problema va risolto prima che il certificato scada. Le cause più comuni che ho riscontrato sui VPS delle PMI: porta 80 bloccata dal firewall (come nel nostro caso - è il primo in classifica), web root errata nella configurazione di Certbot (il --webroot-path punta a una directory che Nginx non serve), conflitto tra il plugin --nginx e una configurazione Nginx custom con include multipli, path di Certbot cambiato dopo un aggiornamento del sistema operativo (visto nell'articolo sul monitoring con Prometheus dove il Certbot era passato da /usr/bin a /snap/bin), permessi errati sulla directory .well-known, e DNS che punta a un IP diverso da quello del server (capita dopo migrazioni a nuovo provider dove il DNS non è stato aggiornato per l'HTTP challenge).

Per chi gestisce server con applicazioni PHP e vuole un hardening completo - non solo TLS ma anche SSH, MySQL, PHP-FPM e firewall - ho scritto una guida dedicata all'hardening di server Debian e Ubuntu per applicativi PHP delle PMI.

Il risultato sul VPS del cliente: dal fix del certificato al completamento dell'hardening TLS sono passate 3 ore. Il portale è tornato online con un grado A+ su SSL Labs, HSTS graduale a 1 giorno (alzato a 1 settimana dopo il primo rinnovo riuscito, poi a 1 mese), rinnovo Certbot verificato con dry run e monitorato tramite l'alert Prometheus CertificatoInScadenza che scatta 14 giorni prima della scadenza. I 75.000 euro di fatturato persi non si recuperano - ma il titolare ora ha la certezza che lo scenario non si ripeterà.

Un aspetto che vale la pena sottolineare per le PMI soggette a NIS2: un'interruzione di servizio causata da un certificato scaduto può configurare un "incidente significativo" ai sensi dell'Articolo 23, specialmente se il servizio interrotto è un portale B2B da cui dipende la supply chain di altre aziende. La documentazione dell'incidente e delle azioni correttive è obbligatoria - e un report che dice "il certificato è scaduto perché nessuno controllava il rinnovo automatico" è il tipo di finding che un auditor NIS2 non perdona.

Se il tuo VPS ha un certificato Let's Encrypt e non hai mai verificato che il rinnovo automatico funzioni, fallo adesso con certbot renew --dry-run. Se il dry run fallisce, hai al massimo 60 giorni dal prossimo rinnovo programmato per risolvere il problema - poi il certificato scade e, se hai HSTS attivo con un max-age lungo, il sito scompare completamente senza possibilità di fallback HTTP. Contattami se hai bisogno di un audit della configurazione TLS: in mezza giornata verifico certificati, rinnovo automatico, cipher suite, HSTS, record CAA e ti porto ad A+ su SSL Labs con una configurazione che si mantiene da sola.

Ultima modifica: