Cosa protegge davvero TLS su una connessione HTTPS

Carica una pagina in http:// puro su una rete che non controlli, un aeroporto o il router di un bar, e tutto quello che invii viaggia come testo leggibile. Il percorso dell’URL, i cookie, i campi del form, la password in un POST di login: chiunque su quella rete con uno sniffer di pacchetti li legge mentre passano. TLS, Transport Layer Security, è il protocollo che blocca questo scenario. Si inserisce tra TCP e il protocollo applicativo, quindi quando HTTP inizia a parlare la connessione è già cifrata e legata a un’identità server verificata. HTTPS è semplicemente HTTP eseguito su quella connessione invece che su una in chiaro.

TLS unisce tre garanzie, e valgono solo insieme. La cifratura significa che un osservatore sulla rete vede testo cifrato e nient’altro. L’integrità significa che un bit alterato in transito viene rilevato e la connessione cade invece di consegnare dati modificati. L’autenticazione significa che il client può confermare di aver raggiunto example.com e non chiunque abbia risposto per primo su quell’indirizzo IP. Perdi una delle tre e le altre due smettono di valere granché: un canale cifrato verso un impostore non è un canale sicuro.

Come l’handshake TLS crea un canale cifrato

Prima che parta qualsiasi richiesta HTTP, client e server eseguono un handshake che concorda una chiave segreta condivisa senza mai mettere quella chiave sulla rete. TLS 1.3, la versione attuale e quella che dovresti usare, lo fa in un solo round trip, contro i due richiesti da TLS 1.2.

1
2
3
4
5
6
7
8
9
Client                                           Server
  |--- ClientHello + key_share ------------------->|
  |    (versioni supportate, cipher suite,         |
  |     un'ipotesi sul gruppo per lo scambio chiavi)|
  |<-- ServerHello + key_share --------------------|
  |<-- EncryptedExtensions, Certificate,           |
  |    CertificateVerify, Finished ----------------|
  |--- Finished ---------------------------------->|
  |======== dati applicativi, cifrati =============|

Il client invia un ClientHello: le versioni TLS e le cipher suite che supporta, un nonce casuale e un’ipotesi su quale gruppo per lo scambio di chiavi sceglierà il server, inviata come key_share nello stesso messaggio. Quell’ipotesi è la scorciatoia di TLS 1.3. Il server risponde con il proprio key_share nel ServerHello, ed entrambi i lati eseguono uno scambio Diffie-Hellman su quelle share per derivare in modo indipendente la stessa chiave di sessione simmetrica. La chiave in sé non attraversa mai la rete.

Da quel momento tutto il resto è già cifrato sotto quella chiave di sessione: il certificato del server, la prova che possiede la chiave privata corrispondente, i messaggi Finished che fanno il checksum dell’intero handshake. Un round trip, e la connessione è attiva.

TLS 1.2 richiedeva un secondo round trip perché lo scambio di chiavi e la negoziazione del cifrario giravano come passaggi separati invece che sovrapposti. Quel giro extra è pura latenza. Su un collegamento con 150ms di RTT aggiunge 150ms a ogni nuova connessione HTTPS, motivo per cui TLS 1.3 si è visto nei numeri di caricamento pagina e non solo nella postura di sicurezza.

TLS 1.2TLS 1.3
Round trip al primo byte cifrato21
Scambio chiaviNegoziato dopo la cipher suiteIpotizzato e inviato con ClientHello
Rinegoziazione a metà sessioneConsentitaRimossa
Scambio chiavi RSA staticoConsentitoRimosso (forward secrecy obbligatoria)
Cifrari deboli (RC4, CBC legacy)ConsentitiRimossi

Cosa dimostra un certificato e come funziona la catena di fiducia

Lo scambio di chiavi gestisce la cifratura. Dimostrare che il server è davvero example.com è compito del certificato. Un certificato lega una chiave pubblica a un nome di dominio e porta la firma di una Certificate Authority (CA), un’organizzazione la cui chiave pubblica è già presente nei trust store del tuo sistema operativo e del browser.

In pratica la catena arriva a tre livelli. Un certificato root CA, autofirmato e preinstallato ovunque, firma un certificato intermediate CA. L’intermedio firma il certificato foglia, quello per il tuo dominio. I server quasi mai presentano un certificato firmato direttamente da una root: servono la foglia più l’intermedio, e il client ricostruisce la catena fino a una root di cui si fida già.

Dimenticare di servire l’intermedio produce un guasto che sembra un fantasma. I browser che hanno una copia in cache di quell’intermedio si connettono senza problemi, quindi il sito funziona sul tuo portatile; tutti gli altri ricevono un errore di fiducia. È una delle configurazioni TLS sbagliate più comuni in produzione, e non si riproduce mai sulla macchina da cui hai testato.

Un certificato porta anche una finestra di validità (notBefore / notAfter) e gli hostname che copre, elencati nell’estensione Subject Alternative Name (SAN). SAN è il campo che i client leggono davvero oggi; il vecchio campo Common Name è vestigiale. Un certificato per example.com non copre api.example.com a meno che quel nome non sia anche nella lista SAN, o il certificato sia un wildcard per *.example.com.

Come ispezionare un certificato con openssl s_client

Non serve un browser per vedere tutto questo. openssl s_client apre una connessione TLS grezza e stampa cosa è stato negoziato:

1
openssl s_client -connect example.com:443 -servername example.com </dev/null

-servername imposta SNI (Server Name Indication), l’hostname inviato in chiaro nel ClientHello in modo che un server che ospita più domini su un solo IP sappia quale certificato presentare. Ometti quel flag e un server multi-tenant non ha modo di scegliere.

Per la sola data di scadenza invece dell’intero dump dell’handshake, reindirizza a openssl x509:

1
2
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates -subject -issuer

Questo stampa notBefore, notAfter, il subject (il dominio per cui è stato emesso il certificato) e l’issuer (la CA che l’ha firmato). Per uno script di monitoraggio che ha bisogno solo di un pass/fail, -checkend accetta una finestra in secondi e imposta l’exit code:

1
2
3
4
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -checkend 2592000 \
  && echo "OK: valido per almeno altri 30 giorni" \
  || echo "ATTENZIONE: scade entro 30 giorni"

curl -v è più rapido per un controllo veloce, perché stampa la versione TLS negoziata e la catena di certificati come parte della sua traccia dell’handshake in modalità verbose:

1
curl -v https://example.com 2>&1 | grep -E "SSL connection|subject:|expire date"

Come ottenere HTTPS su localhost con mkcert

Un certificato autofirmato grezzo, uno che generi e firmi tu stesso invece di ottenerlo da una CA, ti dà cifratura ma non autenticazione. Nessuno lo garantisce, quindi ogni browser mostra un avviso. Va bene per un test rapido con openssl. Fastidioso per lo sviluppo locale, dove vuoi che https://myapp.local si carichi senza dover cliccare per bypassare l’avviso.

mkcert risolve il problema generando una CA locale, installandola nei trust store del sistema operativo e del browser, ed emettendo certificati firmati da quella CA:

1
2
mkcert -install                          # genera e rende attendibile una CA locale
mkcert localhost 127.0.0.1 myapp.local   # emette un certificato per questi nomi

Ottieni una coppia certificato/chiave .pem nella directory corrente, di cui ogni browser sulla macchina si fida perché ora si fida della CA installata da mkcert. Punta la configurazione TLS del tuo server di sviluppo su questi due file e l’avviso sparisce.

Tieni rootCA-key.pem fuori dal controllo versione. Chiunque ottenga la chiave privata della CA locale di mkcert può generare un certificato per qualsiasi dominio di cui la tua macchina si fiderà, la stessa classe di esposizione descritta in Variabili d’Ambiente e Secrets in Docker per i secret nelle variabili d’ambiente: una chiave privata che trapela è una credenziale che è trapelata.

Come ottenere un certificato di produzione con Let’s Encrypt e certbot

In produzione serve un certificato firmato da una CA di cui il browser di ogni visitatore si fida già, e Let’s Encrypt è l’opzione gratuita e automatizzata che usa la maggior parte dei siti. certbot parla il protocollo ACME con i server di Let’s Encrypt e, con un plugin per il webserver, configura Nginx o Apache al posto tuo:

1
sudo certbot --nginx -d example.com -d www.example.com

Questo dimostra che controlli il dominio (servendo un token che certbot piazza, oppure tramite un record DNS, a seconda del tipo di challenge), ottiene il certificato e scrive le direttive ssl_certificate e ssl_certificate_key nella tua configurazione Nginx.

I certificati Let’s Encrypt durano 90 giorni. La finestra breve è deliberata: rende il rinnovo automatico l’unica strada percorribile. Certbot installa un timer systemd, o un cron job sui sistemi più vecchi, che gira due volte al giorno e rinnova tutto ciò che è entro 30 giorni dalla scadenza:

1
sudo certbot renew --deploy-hook "systemctl reload nginx"

--deploy-hook scatta solo su un rinnovo effettivo, non a ogni esecuzione del timer, quindi Nginx non viene ricaricato due volte al giorno per niente. Testa la pipeline prima di fidartene senza supervisione:

1
sudo certbot renew --dry-run

Come funziona la terminazione TLS su un reverse proxy o una CDN

La maggior parte dei servizi non termina mai TLS dentro il processo dell’applicazione. Un reverse proxy davanti alla tua app è la sede tipica: Nginx o Caddy tiene il certificato, decifra la connessione HTTPS in ingresso e inoltra la richiesta al backend su HTTP in chiaro sulla rete interna. Il codice della tua applicazione non vede mai un certificato.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
    }
}

fullchain.pem è il certificato foglia con l’intermedio già appeso, il file con la catena di fiducia pronto per essere servito così com’è. Usa questo invece di cert.pem, che è la foglia da sola e reintroduce il guasto da intermedio mancante visto prima.

Una CDN fa lo stesso lavoro un salto più in là. Termina TLS al PoP edge più vicino al visitatore, poi apre una seconda connessione TLS verso la tua origine. Due connessioni, due handshake, e il certificato che il browser del visitatore valida è quello edge della CDN. Il tuo certificato di origine deve soddisfare solo la CDN.

Quella divisione decide dove debuggare. Se curl diretto sull’origine mostra un certificato valido ma il dominio pubblico no, il problema sta al livello del proxy o della CDN e nessuna lettura dei log applicativi lo troverà.

Cosa aggiunge HSTS sopra TLS

TLS mette in sicurezza una connessione una volta che è HTTPS. Non fa nulla per la prima richiesta, che può ancora uscire su http:// in chiaro quando un utente digita un dominio senza protocollo o segue un vecchio link, ed è esattamente quella prima richiesta il punto dove un attaccante sul percorso può intercettare e forzare il downgrade. Strict-Transport-Security chiude questo varco: dopo una prima visita HTTPS riuscita, il browser riscrive ogni richiesta successiva verso quel dominio in HTTPS prima che lasci la macchina.

1
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

max-age è in secondi, e 63072000 sono due anni, comodamente sopra la soglia minima di un anno richiesta per l’iscrizione alla preload list. includeSubDomains estende la policy a ogni sottodominio. preload segna il dominio come idoneo per la lista integrata in Chrome, Firefox e Safari, le cui voci ottengono la riscrittura solo-HTTPS fin dalla primissima richiesta mai fatta, senza bisogno di una visita precedente.

Aggiungi preload solo quando ogni sottodominio serve davvero HTTPS. Uscire dalla preload list richiede mesi per propagarsi attraverso le release dei browser, e finché non succede, qualsiasi sottodominio che non può fare HTTPS resta irraggiungibile.

Quando TLS non basta, e cosa controllare per primo

TLS verifica il server verso il client. Non dice nulla su cosa quel server fa con la tua richiesta dopo, e non impedisce a un utente vittima di phishing di digitare una password in un dominio falso convincente che ha un certificato perfettamente valido. Let’s Encrypt emette per examp1e.com con la stessa facilità con cui emette per example.com. Un lucchetto significa che la connessione è privata, non che l’altro capo è onesto.

Tre guasti spiegano la maggior parte delle segnalazioni “il sito è giù” che si rivelano poi una configurazione TLS sbagliata: un certificato scaduto, un intermedio mancante e una lista SAN che non copre l’hostname richiesto. Tutti e tre sono visibili nell’output di openssl s_client di sopra, prima ancora di aprire un solo log applicativo.

Se termini TLS tu stesso, metti oggi stesso quel comando -checkend in un cron job o in un alert di monitoraggio. Il certificato che manda giù un sito è quello che nessuno ricordava esistesse.

Articoli correlati