Certificato autofirmato su Ubuntu: configurarlo bene
Crea su Ubuntu 24.04 un certificato TLS con SAN accettato da Chrome: comando openssl, nginx o Apache e fiducia nei client senza usare curl -k.
Cosa stai configurando
Un certificato TLS autofirmato che i browser e i client moderni accettano effettivamente, un subjectAltName corretto, permessi sicuri per la chiave e la configurazione necessaria per usarlo con nginx o Apache. Include anche l'aspetto che quasi tutte le guide trascurano: configurare correttamente la fiducia nei client, invece di ignorare gli avvisi e inserire curl -k direttamente negli script per sempre. Alla fine troverai una CA privata configurabile con cinque comandi, utile quando un servizio interno diventa sei.
Prima è necessario chiarire la scelta, perché un certificato autofirmato è lo strumento corretto molto meno spesso di quanto venga utilizzato. Se il servizio è raggiungibile da Internet con un vero nome DNS, interrompi la lettura e usa un certificato gratuito Let's Encrypt con certbot su nginx oppure l'equivalente per Apache. Non costa nulla, si rinnova automaticamente e i browser e i sistemi operativi più diffusi lo considerano già attendibile. Un certificato autofirmato su un sito pubblico abitua gli utenti a ignorare gli avvisi di sicurezza. È un'abitudine peggiore dell'uso di HTTP in chiaro.
Un certificato autofirmato è lo strumento corretto quando il servizio non è esposto a Internet: un pannello di amministrazione associato a un indirizzo del tunnel WireGuard sul tuo VPS, un server di staging su una rete privata, il traffico tra servizi backend, un'appliance in un laboratorio domestico oppure la sostituzione del certificato segnaposto che Webmin genera autonomamente sulla porta 10000. In ogni caso, Let's Encrypt non può emettere certificati per 10.8.0.1 o git.internal.lan. Nessuna CA pubblica inserirà in un certificato un indirizzo IP privato o un TLD inventato. Per questi nomi, la CA sei tu.
Tutto ciò che segue viene eseguito su un'installazione pulita di Ubuntu 24.04, che include OpenSSL 3.0.x (openssl version per conferma). Non è necessario l'accesso a Internet: la procedura funziona interamente in una rete isolata.
Perché il vecchio comando in una riga genera certificati rifiutati da Chrome
Il comando presente in ogni tutorial precedente al 2017 è simile a questo:
# Do not run this: shown so you recognise it in old guides
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout selfsigned.key -out selfsigned.crtPone una serie di domande interattive, inserisce il nome host nel campo Common Name e genera un certificato senza estensione subjectAltName. Chrome ha smesso di leggere il Common Name nella versione 58, nell'aprile 2017. Inoltre, RFC 2818 aveva già deprecato la verifica del CN nel 2000. Anche Firefox e Safari si comportano allo stesso modo. curl e il modulo ssl di Python continuano invece a usare il Common Name come fallback quando il certificato non contiene alcun SAN. Per questo un certificato privo di SAN può superare un rapido test curl e continuare a non funzionare in nessun browser. Per un browser, un certificato identifica il server tramite l'estensione SAN oppure non lo identifica affatto. Il browser lo comunica esattamente con queste parole:
NET::ERR_CERT_COMMON_NAME_INVALID
This server could not prove that it is git.internal.lan; its security
certificate does not specify Subject Alternative Names.Nessuna modifica al trust store risolve questo errore, perché il certificato non contiene effettivamente alcun nome. Se al momento visualizzi NET::ERR_CERT_COMMON_NAME_INVALID, il certificato non contiene alcun SAN, oppure contiene un SAN errato, e devi generarne uno nuovo. Fortunatamente, la correzione richiede un solo comando.
Creare un certificato accettato dai browser con un solo comando
OpenSSL ha introdotto il flag -addext nella versione 1.1.1. Non servono più le configurazioni elaborate dei vecchi tutorial per inserire un SAN. Su Ubuntu 24.04:
sudo openssl req -x509 -newkey rsa:4096 -sha256 -days 730 -noenc \
-keyout /etc/ssl/private/git.internal.key \
-out /etc/ssl/certs/git.internal.crt \
-subj "/CN=git.internal.lan" \
-addext "subjectAltName=DNS:git.internal.lan,IP:10.8.0.1"Funzione dei singoli flag:
-x509genera direttamente un certificato autofirmato invece di una richiesta di firma.-newkey rsa:4096genera una nuova chiave nella stessa operazione. RSA 4096 è compatibile anche con i client meno recenti. Se tutti i client che si connettono sono moderni,-newkey ec -pkeyopt ec_paramgen_curve:P-256è più piccolo e veloce.-noencè la sintassi di OpenSSL 3.x per il vecchio-nodes: la chiave non ha una passphrase. Entrambe le sintassi funzionano. Con una chiave protetta da passphrase, nginx resta in attesa dell'input a ogni avvio. Per una chiave server è quindi necessario usare questa opzione.-days 730indica una validità di due anni. Il motivo di questo valore è spiegato nella sezione sulla scadenza.-subjfornisce le risposte alle domande interattive direttamente sulla riga di comando. Il CN è ormai solo descrittivo, ma impostalo comunque sul nome principale: alcuni strumenti lo visualizzano.-addext "subjectAltName=..."è il flag fondamentale. Elenca ogni nome e ogni indirizzo IP che i client utilizzeranno: vociDNS:per i nomi host (sono ammessi anche wildcard comeDNS:*.internal.lan), vociIP:per gli indirizzi. Se qualcuno accederà ahttps://10.8.0.1, deve essere presente la voceIP:10.8.0.1. Un SAN contenente solo il nome DNS riproporrebbe il problemaNET::ERR_CERT_COMMON_NAME_INVALID.
Verifica che il SAN sia stato effettivamente inserito prima di configurare altro:
openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -ext subjectAltNameOutput corretto:
X509v3 Subject Alternative Name:
DNS:git.internal.lan, IP Address:10.8.0.1Se invece viene visualizzato No extensions in certificate, il certificato non contiene alcun SAN e i browser lo rifiuteranno. Rigeneralo invece di procedere.
Proteggere la chiave
Una chiave privata leggibile da ogni utente del sistema non è realmente privata. In un'installazione standard di Ubuntu 24.04, /etc/ssl/private è già 700 root:root, quindi solo root può anche solo elencarla; diventa 710 root:ssl-cert soltanto se è stato installato il pacchetto ssl-cert (apache2 lo consiglia). In ogni caso, impostare esplicitamente i permessi del file:
sudo chown root:root /etc/ssl/private/git.internal.key
sudo chmod 600 /etc/ssl/private/git.internal.keynginx e Apache leggono entrambi i certificati come root prima di abbandonare i privilegi, quindi la modalità root:root 600 è adatta. Se la chiave appartiene a un servizio eseguito con un proprio utente e che carica direttamente la chiave, ad esempio un'app Node, Gitea o un demone Python, chown invece a quell'utente del servizio, mantenendo comunque la modalità 600. Non usare mai la modalità 644, non conservare una copia in un repository git e non conservarne una copia in /tmp.
Collegarlo a nginx
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name git.internal.lan;
ssl_certificate /etc/ssl/certs/git.internal.crt;
ssl_certificate_key /etc/ssl/private/git.internal.key;
location / {
proxy_pass http://127.0.0.1:3000;
}
}sudo nginx -t && sudo systemctl reload nginxnginx -t deve stampare syntax is ok e test is successful prima che il reload esegua qualsiasi operazione. Se invece stampa SSL_CTX_use_PrivateKey_file() failed ... key values mismatch, il certificato e la chiave provengono da due generazioni diverse; consultare la sezione relativa alle modalità di errore.
Collegarlo ad Apache
sudo a2enmod ssl proxy proxy_httpssl da solo non è sufficiente: il virtual host seguente usa ProxyPass e, senza mod_proxy e mod_proxy_http, il test della configurazione termina con Invalid command 'ProxyPass', perhaps misspelled or defined by a module not included in the server configuration. Salva il virtual host come /etc/apache2/sites-available/git-internal.conf:
<VirtualHost *:443>
ServerName git.internal.lan
SSLEngine on
SSLCertificateFile /etc/ssl/certs/git.internal.crt
SSLCertificateKeyFile /etc/ssl/private/git.internal.key
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
</VirtualHost>sudo a2ensite git-internal
sudo apache2ctl configtest && sudo systemctl reload apache2configtest dovrebbe rispondere a Syntax OK. Ora esegui il test da una macchina client:
curl -v https://git.internal.lan/e otterrai un errore:
curl: (60) SSL certificate problem: self-signed certificateNon è un bug. È TLS che funziona: curl non ha mai visto il tuo certificato e rifiuta di comunicare con un server che non può autenticare. La sezione seguente contiene la soluzione corretta e non è quella adottata in questo momento da metà di Internet.
Fare in modo che i client si fidino del certificato ed evitare gli anti-pattern
Prima esaminiamo le correzioni errate, chiamandole con il loro nome. curl -k (o --insecure) inserito direttamente in uno script, verify=False in Python requests, NODE_TLS_REJECT_UNAUTHORIZED=0 in Node: nessuna di queste soluzioni rende attendibile il certificato. Disattivano la verifica del certificato, quindi il client comunica senza problemi con qualsiasi server che presenti un certificato qualsiasi, incluso un certificato inserito da un attaccante nel percorso di rete. Si mantiene il costo operativo di TLS, ma si perde l'autenticazione, che è lo scopo principale di TLS. Inoltre, questi flag tendono a propagarsi: vengono copiati in un cron job, poi in uno script di deploy e infine nel codice di produzione, finché nessuno ricorda più quali connessioni dovevano essere temporanee. Se un verify=False resta dopo la sessione di debug che lo ha richiesto, il design è errato.
La correzione consiste nell'insegnare al sistema operativo di ogni client che questo certificato è una root attendibile. Sui client Ubuntu e Debian:
sudo cp git.internal.crt /usr/local/share/ca-certificates/git.internal.crt
sudo update-ca-certificatesLa riga importante nell'output, seguita da un blocco Running hooks in /etc/ca-certificates/update.d..., è:
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.Queste righe nascondono due problemi frequenti. Il file deve terminare con .crt; un'estensione .pem viene ignorata senza avvisi e si ottiene 0 added senza alcun messaggio di errore. Inoltre, il contenuto deve essere in formato PEM e il file deve iniziare con -----BEGIN CERTIFICATE-----; prima convertire un certificato binario DER con openssl x509 -inform der -in file.der -out file.crt. Aggiungere come root il certificato autofirmato stesso funziona perché un certificato autofirmato è la propria root.
Dopo questa operazione, curl, wget, git, apt e qualsiasi altro client che usa OpenSSL con il bundle di sistema considerano attendibile il server senza alcun flag. Alcuni client includono un proprio archivio dei certificati attendibili e richiedono una configurazione specifica:
- Chrome/Chromium su Linux legge un database NSS, non l'archivio di sistema:
sudo apt install libnss3-tools, quindicertutil -d sql:$HOME/.local/share/pki/nssdb -A -t "C,," -n "git.internal" -i git.internal.crtper ogni utente. Questo percorso è quello predefinito di Chrome dalla versione 146; se l'utente dispone ancora di un~/.pki/nssdblegacy proveniente da un'installazione precedente, Chrome continua a leggere quello. Su tali macchine, impostare-dsusql:$HOME/.pki/nssdb. - Firefox dispone di un proprio archivio: Impostazioni → Privacy e sicurezza → Certificati → Importa. Su Windows e macOS è invece possibile impostare
security.enterprise_roots.enabledsutrueinabout:config, così Firefox legge l'archivio del sistema operativo. Mozilla documenta questa preferenza per Windows e macOS, non per Linux; su Ubuntu usare quindi l'importazione. - Python requests include un proprio bundle di CA (certifi) e ignora l'archivio di sistema: passare
verify="/usr/local/share/ca-certificates/git.internal.crt"oppure esportareREQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt. - Node.js usa per impostazione predefinita le proprie root incluse nel bundle: esportare
NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/git.internal.crtoppure fare in modo che legga l'archivio di sistema appena aggiornato con il flag--use-system-ca(Node 23.9 e versioni successive su Linux) oppureNODE_USE_SYSTEM_CA=1(Node 24.6 e Node 22.19 e versioni successive).
Sui client Windows, fare doppio clic su .crt e installarlo in Autorità di certificazione radice attendibili. Su macOS, aggiungerlo al portachiavi System con Accesso Portachiavi e impostarlo su Considera sempre attendibile.
Una root per molti servizi: una piccola CA privata
La gestione della fiducia per singolo certificato non è scalabile: sei servizi moltiplicati per quattro macchine client richiedono ventiquattro installazioni dei certificati di fiducia, e ogni nuovo servizio ne aggiunge altre. La soluzione è una CA privata: i client si fidano di una sola root e con questa si firma il certificato di ogni servizio.
L'opzione più semplice è mkcert, disponibile nei repository di Ubuntu 24.04 e in grado di gestire gli archivi NSS (Firefox e Chrome, con una limitazione descritta sotto) che update-ca-certificates non gestisce:
sudo apt install -y mkcert libnss3-tools
mkcert -install
mkcert git.internal.lan "*.internal.lan" 10.8.0.1mkcert -install crea una root e la registra nell'archivio di sistema, negli archivi NSS dei browser che riesce a individuare e nell'archivio Java quando è impostato JAVA_HOME. Node non legge l'archivio di sistema se non gli viene indicato esplicitamente, quindi avvia le applicazioni Node con --use-system-ca (oppure NODE_USE_SYSTEM_CA=1), come sopra, oppure esporta NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem". Il terzo comando genera git.internal.lan+2.pem e git.internal.lan+2-key.pem, pronti per essere inseriti negli snippet nginx o Apache riportati sopra. Il suo modello presuppone una macchina di sviluppo: la chiave root resta sulla macchina su cui è stato eseguito -install. È quindi ideale per un laptop di sviluppo, ma non è adatto a una flotta di server.
La limitazione, a ottobre 2026, è la seguente: mkcert cerca il database NSS di Chrome soltanto nel percorso legacy ~/.pki/nssdb, non in ~/.local/share/pki/nssdb, usato per impostazione predefinita da Chrome dalla versione 146. Su una macchina priva del database legacy, aggiungi manualmente la root a Chrome con il comando certutil della sezione precedente, usando -i "$(mkcert -CAROOT)/rootCA.pem".
Per i server, OpenSSL senza componenti aggiuntivi gestisce l'intera CA in cinque comandi:
openssl genrsa -out lab-ca.key 4096
openssl req -x509 -new -key lab-ca.key -sha256 -days 3650 \
-out lab-ca.crt -subj "/CN=Lab Internal CA" \
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
openssl genrsa -out git.key 2048
openssl req -new -key git.key -out git.csr -subj "/CN=git.internal.lan" \
-addext "subjectAltName=DNS:git.internal.lan,IP:10.8.0.1"
openssl x509 -req -in git.csr -CA lab-ca.crt -CAkey lab-ca.key \
-CAcreateserial -days 730 -sha256 -copy_extensions copy -out git.crtIl problema si trova nell'ultimo comando: per impostazione predefinita openssl x509 -req rimuove tutte le estensioni dalla CSR, incluso il SAN aggiunto con attenzione. -copy_extensions copy (un'opzione di OpenSSL 3.x, quindi disponibile su 24.04) trasferisce le estensioni; se la ometti, il certificato firmato non contiene alcun SAN e Chrome mostra di nuovo NET::ERR_CERT_COMMON_NAME_INVALID. Verifica con lo stesso controllo openssl x509 -noout -ext subjectAltName usato in precedenza.
Distribuisci lab-ca.crt ai client seguendo i passaggi precedenti per l'archivio di fiducia, una sola volta per ogni macchina. Proteggi lab-ca.key come una credenziale altamente sensibile: imposta la modalità 600 e, idealmente, conservala su una macchina diversa dai server per i quali firma i certificati, perché chiunque ne sia in possesso può generare un certificato per qualsiasi nome riconosciuto dai client.
Scadenza e rotazione
La durata dei certificati delle CA pubbliche si sta riducendo rapidamente. Il CA/Browser Forum ha limitato a 200 giorni, a partire da marzo 2026, la durata dei certificati appena emessi e considerati attendibili pubblicamente, rispetto ai precedenti 398 giorni. Il limite scenderà a 100 giorni nel 2027 e a 47 giorni entro marzo 2029. Queste regole si applicano però alle CA attendibili pubblicamente. La CA privata non è soggetta a questi limiti e i browser non li applicano alle root installate manualmente. Esiste tuttavia un limite concreto: le piattaforme Apple rifiutano qualsiasi certificato server TLS valido per più di 825 giorni, indipendentemente dall’autorità che lo ha emesso. Se iPhone o Mac devono connettersi, mantieni quindi i certificati leaf entro due anni. -days 730 rispetta questo limite ovunque; una root valida dieci anni con certificati leaf validi due anni è una configurazione interna pratica.
I certificati con validità lunga presentano un solo tipo di problema: smettono di funzionare in modo silenzioso e simultaneo, in una data che nessuno ricorda di avere scelto. Controlla i certificati disponibili:
openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -enddateInserisci il rinnovo in un calendario reale oppure configura cron perché invii un promemoria 30 giorni prima. openssl x509 -checkend 2592000 -in cert.crt restituisce un codice diverso da zero quando la scadenza rientra nel numero di secondi indicato. Se usi già Uptime Kuma per il monitoraggio dello stato, i relativi monitor HTTPS segnalano gratuitamente l'approssimarsi della scadenza del certificato.
La rotazione con una CA privata è semplice: esegui di nuovo i comandi per creare la CSR e firmarla, sostituisci i file e ricarica il web server. La root non è cambiata, quindi nessun client rileva la modifica.
Modalità di errore e stringhe visualizzate
NET::ERR_CERT_AUTHORITY_INVALID, è lo stato previsto prima di installare la relazione di trust, non un difetto del certificato. Se persiste dopo l’installazione della root: su Linux Chrome legge NSS invece dell’archivio di sistema (vedere il passaggio certutil) e, a partire da Chrome 146, il database si trova in ~/.local/share/pki/nssdb, a meno che non esista un ~/.pki/nssdb legacy; una root aggiunta all’altro percorso non viene quindi mai rilevata. In alternativa, il file copiato potrebbe non essere terminato con .crt e update-ca-certificates potrebbe aver indicato 0 added; oppure il server potrebbe presentare un certificato diverso da quello considerato attendibile. Confrontare le impronte digitali con openssl s_client -connect git.internal.lan:443 </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256.
NET::ERR_CERT_COMMON_NAME_INVALID, il certificato non contiene SAN oppure il SAN non include il nome visualizzato nella barra degli indirizzi. Il caso tipico è che il SAN elenchi DNS:git.internal.lan, mentre l’utente abbia aperto https://10.8.0.1. Le modifiche all’archivio di trust non possono risolvere questo problema; occorre riemettere il certificato includendo la voce mancante.
curl: (60) SSL certificate problem: self-signed certificate, curl non considera attendibile il certificato. La variante self-signed certificate in certificate chain indica la stessa cosa per un certificato firmato dalla CA privata. Correzione temporanea: curl --cacert lab-ca.crt https://...; correzione permanente: configurare l’archivio di trust. Non usare -k.
unable to load certificate ... Expecting: TRUSTED CERTIFICATE (oppure Expecting: CERTIFICATE REQUEST o no start line), confusione tra formati PEM. OpenSSL ha ricevuto il tipo di file errato: una chiave o una CSR quando si aspettava un certificato, oppure un file binario DER quando si aspettava PEM. head -1 filename indica quale file si possiede effettivamente; un certificato inizia con -----BEGIN CERTIFICATE-----. Per DER, convertire con openssl x509 -inform der -in file.der -out file.crt.
nginx: [emerg] SSL_CTX_use_PrivateKey_file(...) failed (SSL: error ... key values mismatch), il certificato e la chiave non corrispondono, di solito perché il comando di generazione è stato eseguito due volte e i file sono stati mescolati. Verificare openssl x509 -in git.internal.crt -noout -pubkey | sha256sum rispetto a openssl pkey -in git.internal.key -pubout | sha256sum; hash identici indicano una coppia corrispondente. Se differiscono, rigenerare entrambi insieme.
FAQ
Perché Chrome continua a mostrare "Non sicuro" dopo che ho creato un certificato autofirmato?
Se l'errore è NET::ERR_CERT_AUTHORITY_INVALID, il certificato è corretto: Chrome non ha ancora motivo di considerarlo attendibile. Installalo, oppure installa la CA privata principale, nell'archivio attendibile del client. Ricorda che su Linux Chrome usa il database NSS tramite certutil, in ~/.local/share/pki/nssdb a partire da Chrome 146, oppure un ~/.pki/nssdb legacy, se presente, non l'archivio di sistema. Se l'errore è NET::ERR_CERT_COMMON_NAME_INVALID, al certificato manca un Subject Alternative Name corrispondente all'URL. Devi quindi rigenerarlo con -addext "subjectAltName=...".
Come posso fare in modo che curl consideri attendibile un certificato autofirmato senza usare -k?
Copia il certificato, in formato PEM e con estensione .crt, in /usr/local/share/ca-certificates/ ed esegui sudo update-ca-certificates. L'output deve indicare 1 added. Da quel momento curl lo verifica come qualsiasi certificato pubblico. Per una singola richiesta senza modificare il sistema, curl --cacert /path/to/cert.crt esegue la verifica usando soltanto quel file. -k disabilita completamente la verifica e non deve essere inserito negli script.
Per quanto tempo può essere valido un certificato autofirmato?
Dal punto di vista tecnico, per tutto il tempo necessario. I limiti del CA/Browser Forum, 200 giorni attualmente e 47 entro il 2029, si applicano alle CA pubblicamente attendibili, non alla fiducia privata. In pratica, limita a 825 giorni la validità dei certificati server, perché i dispositivi Apple rifiutano certificati più lunghi indipendentemente dall'emittente. Una root privata con validità di dieci anni e certificati leaf di due anni (-days 730) è un'impostazione ragionevole. Pianifica comunque il rinnovo, perché un certificato interno scaduto può interrompere tutti i servizi senza messaggi evidenti, in una data che nessuno ricorda.
Devo usare un certificato autofirmato o Let's Encrypt?
Se il servizio ha un nome DNS pubblico ed è raggiungibile da Internet, usa sempre Let's Encrypt: è gratuito, automatizzato e già considerato attendibile dalla maggior parte dei browser e dei sistemi operativi. Se devono collegarsi dispositivi molto vecchi, consulta l'elenco di compatibilità di Let's Encrypt. Un certificato autofirmato, oppure una CA privata, serve per i casi in cui Let's Encrypt non può emettere certificati: IP privati, nomi host accessibili solo internamente come .lan, reti isolate e servizi nascosti intenzionalmente dietro una VPN. La scelta dipende dalla raggiungibilità e dalla denominazione, non dal livello di sicurezza. La crittografia è identica.
Perché il mio certificato viene rifiutato anche dopo averlo aggiunto a /usr/local/share/ca-certificates?
Controlla tre aspetti. Il nome del file deve terminare con .crt. Un'estensione .pem viene ignorata senza messaggi e update-ca-certificates restituisce 0 added. Il contenuto deve essere testo PEM che inizia con -----BEGIN CERTIFICATE-----, non un file binario DER. Inoltre, l'applicazione deve usare effettivamente l'archivio di sistema. Chrome su Linux, Firefox, Python requests, Node.js, salvo quando viene avviato con --use-system-ca, e Java mantengono ciascuno un archivio di certificati attendibili separato. Devi quindi aggiungere il certificato separatamente a ciascuno di questi archivi.