Certificato self-signed Ubuntu 24.04 con SAN
Guida per creare certificati TLS con OpenSSL su Ubuntu 24.04 che Chrome accetta senza errori. Configurazione corretta per nginx, Apache e CA privata.
Cosa stai costruendo
Un certificato TLS self-signed che i browser e i client moderni accettano correttamente — con subjectAltName corretto, permessi delle chiavi appropriati e configurato su nginx o Apache. Include anche l'aspetto che quasi tutte le guide trascurano: far sì che i client si fidino correttamente del certificato, evitando di ignorare gli avvisi e di inserire curl -k nei vari script. Alla fine, otterrai una CA privata composta da cinque comandi, utile quando i servizi interni passano da uno a sei.
Per prima cosa, la scelta: un certificato self-signed è lo strumento corretto molto meno spesso di quanto si pensi. Se il servizio è raggiungibile da internet tramite un nome DNS reale, interrompi la lettura e ottieni un certificato Let's Encrypt gratuito con certbot su nginx o l'equivalente per Apache. Non ha costi, si rinnova automaticamente e ogni browser lo riconosce già come affidabile. Usare un certificato self-signed su un sito pubblico abitua gli utenti a ignorare gli avvisi di sicurezza, un'abitudine peggiore rispetto all'uso di HTTP semplice.
L'approccio self-signed è corretto quando non è coinvolta la rete internet pubblica: un pannello di amministrazione collegato a un indirizzo di un tunnel WireGuard sul tuo VPS, un server di staging su una rete privata, traffico service-to-service tra backend, un dispositivo per home-lab, o per sostituire il certificato predefinito che Webmin genera per se stesso sulla porta 10000. Let's Encrypt non può emettere certificati per 10.8.0.1 o git.internal.lan: nessuna CA pubblica inserisce un IP privato o un TLD inventato in un certificato. Per quei nomi, la CA sei tu.
Tutto quanto segue viene eseguito su un sistema Ubuntu 24.04 appena installato, che include OpenSSL 3.0.x (usa openssl version per confermare). Nessun passaggio richiede l'accesso a internet; tutto funziona in modalità air-gapped.
Perché il vecchio comando produce certificati rifiutati da Chrome
Il comando presente in ogni tutorial precedente al 2017 è il seguente:
# 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.crtQuesto comando pone una serie di domande interattive, inserisce l'hostname nel campo Common Name e genera un certificato privo di estensione subjectAltName. Tale certificato non è valido. Chrome ha smesso di leggere il Common Name nella versione 58, nell'aprile 2017 — l'RFC 2818 aveva già deprecato il matching tramite CN nell'anno 2000 — e Firefox, Safari, curl e Python si comportano allo stesso modo. Un certificato identifica il server tramite l'estensione SAN o non lo identifica affatto; il browser segnala l'errore con queste esatte 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 può risolvere questo errore, poiché il certificato non contiene alcun nome. Se visualizzi NET::ERR_CERT_COMMON_NAME_INVALID in questo momento, il certificato non possiede l'estensione SAN (o possiede quella errata) ed è necessario generarne uno nuovo. Fortunatamente, la soluzione richiede un solo comando.
Genera un certificato accettato dai browser: un solo comando
OpenSSL ha introdotto il flag -addext nella versione 1.1.1. Non è più necessario utilizzare configurazioni complesse per iniettare 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 di ogni flag:
-x509genera direttamente un certificato self-signed invece di una richiesta di firma.-newkey rsa:4096genera una nuova chiave nella stessa operazione. RSA 4096 è compatibile con i client legacy; se tutti i client sono moderni,-newkey ec -pkeyopt ec_paramgen_curve:P-256è più piccolo e veloce.-noencè la sintassi OpenSSL 3.x per il vecchio-nodes: nessuna passphrase sulla chiave. Entrambe le sintassi sono valide. Una chiave con passphrase blocca nginx in attesa di input ad ogni avvio; per una chiave server è necessario evitare questo problema.-days 730— due anni; dettagli su questo valore nella sezione scadenza.-subjrisponde automaticamente alle domande interattive. Il CN è solo estetico, ma impostalo comunque con il nome principale; alcuni tool lo visualizzano.-addext "subjectAltName=..."è il flag fondamentale. Elenca ogni nome e ogni IP che i client utilizzeranno: vociDNS:per gli hostname (i wildcard comeDNS:*.internal.lansono validi), vociIP:per gli indirizzi. Se un client accederà ahttps://10.8.0.1, l'entryIP:10.8.0.1deve essere presente — un SAN con solo DNS causerà nuovamente l'erroreNET::ERR_CERT_COMMON_NAME_INVALID.
Verifica che il SAN sia stato applicato correttamente prima di configurare il sistema:
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 l'output mostra invece No extensions in certificate, il certificato non ha il SAN e i browser lo rifiuteranno; rigenera il certificato invece di procedere.
Proteggere la chiave
Una chiave privata leggibile da ogni utente sul sistema non è una chiave privata. Su Ubuntu, /etc/ssl/private è già 710 root:ssl-cert, il che protegge dai tentativi di accesso accidentali, ma è necessario impostare esplicitamente i permessi del file:
sudo chown root:root /etc/ssl/private/git.internal.key
sudo chmod 600 /etc/ssl/private/git.internal.keySia nginx che Apache leggono i certificati come root prima di ridurre i privilegi, quindi la modalità root:root 600 è corretta per loro. Se la chiave è destinata a un servizio che viene eseguito con un proprio utente e carica la chiave autonomamente — come un'app Node, Gitea o un daemon Python — assegna la proprietà a chown a quell'utente di servizio, mantenendo la modalità 600. Cosa non fare mai: usare la modalità 644, salvare una copia in un repository git o in /tmp.
Configurazione in 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 abbia effetto. Se stampa invece SSL_CTX_use_PrivateKey_file() failed ... key values mismatch, il certificato e la chiave appartengono a due sessioni di generazione diverse — consultare la sezione failure-modes.
Configurazione in Apache
sudo a2enmod ssl proxy proxy_httpssl da solo non è sufficiente: il vhost seguente utilizza ProxyPass. Senza mod_proxy e mod_proxy_http, il test della configurazione fallisce con l'errore Invalid command 'ProxyPass', perhaps misspelled or defined by a module not included in the server configuration. Salva il vhost 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 riceverai un errore:
curl: (60) SSL certificate problem: self-signed certificateNon si tratta di un bug. È il funzionamento corretto di TLS: curl non riconosce il tuo certificato e rifiuta la connessione con un server che non può autenticare. La sezione successiva contiene la soluzione definitiva — e non è il metodo utilizzato dalla maggior parte degli utenti in questo momento.
Rendere il certificato affidabile — anti-pattern da evitare
Le soluzioni errate, identificate correttamente. curl -k (o --insecure) integrato in uno script, verify=False in Python requests, NODE_TLS_REJECT_UNAUTHORIZED=0 in Node — nessuna di queste opzioni rende il certificato affidabile. Queste soluzioni disabilitano la verifica del certificato; il client accetterà quindi qualsiasi server presenti qualsiasi certificato, incluso uno inserito da un attaccante. Si mantiene l'overhead di TLS ma si perde l'autenticazione, che ne è lo scopo principale. Peggio ancora, questi flag si propagano: vengono incollati in un cron job, poi in uno script di deploy, infine nel codice di produzione, finché nessuno ricorda quali connessioni dovessero essere temporanee. Se un verify=False persiste oltre la sessione di debugging che lo ha generato, il design è errato.
La soluzione corretta consiste nell'istruire ogni OS client affinché consideri questo certificato come una root trusted. Sui client Ubuntu e Debian:
sudo cp git.internal.crt /usr/local/share/ca-certificates/git.internal.crt
sudo update-ca-certificatesLa riga rilevante nell'output (segue un blocco Running hooks in /etc/ca-certificates/update.d...):
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.Due criticità si trovano in queste righe. Il file deve terminare con .crt — un'estensione .pem viene ignorata silenziosamente e si ottiene 0 added senza messaggi di errore. Il contenuto deve essere in formato PEM — il file inizia con -----BEGIN CERTIFICATE-----; convertire prima il binario DER con openssl x509 -inform der -in file.der -out file.crt. Aggiungere il certificato self-signed come root funziona perché un certificato self-signed è la propria root.
Dopo questa operazione, curl, wget, git, apt e qualsiasi altro strumento che utilizzi OpenSSL contro il bundle di sistema si fiderà del server senza l'uso di flag. Alcuni client utilizzano store di fiducia proprietari e richiedono una gestione individuale:
- Chrome/Chromium su Linux legge un database NSS, non lo store di sistema:
sudo apt install libnss3-tools, poicertutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n "git.internal" -i git.internal.crtper ogni utente. - Firefox ha il proprio store: Settings → Privacy & Security → Certificates → Import, oppure impostare
security.enterprise_roots.enabledsutrueinabout:configper leggere lo store di sistema. - Python requests include il proprio bundle CA (certifi) e ignora lo store di sistema: passare
verify="/usr/local/share/ca-certificates/git.internal.crt"o esportareREQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt. - Node.js: esportare
NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/git.internal.crt.
Sui client Windows, fare doppio clic su .crt e installarlo in Trusted Root Certification Authorities; su macOS, aggiungerlo al System keychain in Keychain Access e impostarlo su Always Trust.
Un'unica root per molti servizi: una piccola CA privata
La fiducia basata sul singolo certificato non è scalabile: sei servizi per quattro client richiedono ventiquattro installazioni di fiducia, e ogni nuovo servizio aumenta il carico. La soluzione è una CA privata: i client si fidano di una sola root e si firma il certificato di ogni servizio con essa.
L'opzione più semplice è mkcert, disponibile nei repository di Ubuntu 24.04, che gestisce i database NSS (Chrome, Firefox) 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 in tutti i database di fiducia della macchina; 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 sopra indicati. Il design è pensato per una macchina di sviluppo: la chiave root risiede sulla macchina che ha eseguito -install; è quindi ideale per un laptop di sviluppo, ma non è adatta per un parco server.
Per i server, OpenSSL standard gestisce l'intera CA con 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.crtL'errore si trova nell'ultimo comando: openssl x509 -req rimuove tutte le estensioni dal CSR di default, incluso il SAN aggiunto manualmente. -copy_extensions copy (un'opzione di OpenSSL 3.x, quindi valida su 24.04) le mantiene; se omessa, il certificato firmato non avrà il SAN e Chrome mostrerà nuovamente l'errore NET::ERR_CERT_COMMON_NAME_INVALID. Verificare con lo stesso controllo openssl x509 -noout -ext subjectAltName usato in precedenza.
Distribuire lab-ca.crt ai client tramite i passaggi per il trust-store indicati sopra — una sola volta per macchina. Proteggere lab-ca.key come un segreto critico: permessi 600, idealmente conservato su una macchina diversa da quelle che deve certificare, poiché chiunque ne sia in possesso può generare un certificato per qualsiasi nome accettato dai client.
Scadenza e rotazione
La durata dei certificati delle CA pubbliche si sta riducendo — il CA/Browser Forum ha limitato i nuovi certificati emessi con fiducia pubblica a 200 giorni nel marzo 2026 (rispetto ai precedenti 398), passando a 100 giorni nel 2027 e a 47 giorni entro il marzo 2029 — ma queste regole si applicano solo alle CA con fiducia pubblica. La vostra CA privata non è soggetta a tali regole e i browser non le applicano ai root installati manualmente. Esiste un limite reale applicabile: le piattaforme Apple rifiutano qualsiasi certificato TLS del server con validità superiore a 825 giorni, indipendentemente dall'emittente; pertanto, se iPhone o Mac devono connettersi, mantenete i certificati leaf a due anni o meno. -days 730 supera questo limite ovunque; una root di dieci anni con leaf di due anni è una configurazione interna ideale.
I certificati a lunga durata falliscono in un solo modo: silenziosamente, tutti contemporaneamente, in una data che nessuno ricorda di aver scelto. Verificate i vostri certificati:
openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -enddateInserite il rinnovo in un calendario reale, oppure utilizzate cron per ricevere un avviso 30 giorni prima — openssl x509 -checkend 2592000 -in cert.crt restituisce un codice di uscita non nullo quando la scadenza è entro quel numero di secondi. Se utilizzate già Uptime Kuma per il monitoraggio dello stato, i suoi monitor HTTPS segnalano gratuitamente l'imminente scadenza del certificato.
La rotazione con una CA privata è un processo semplice: eseguite nuovamente i comandi di CSR-and-sign, sostituite i file e ricaricate il web server. La root non è cambiata, quindi nessun client noterà alcuna variazione.
Modalità di errore e stringhe visualizzate
NET::ERR_CERT_AUTHORITY_INVALID — è lo stato atteso prima dell'installazione del certificato di fiducia, non un difetto del certificato stesso. Se persiste dopo l'installazione della root: su Linux, Chrome utilizza il database NSS invece del repository di sistema (vedere il passaggio certutil); oppure il file copiato non terminava con .crt e update-ca-certificates ha restituito 0 added; oppure il server presenta un certificato diverso da quello di cui ti sei fidato — confronta gli fingerprint 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 possiede un SAN, oppure il SAN non include il nome presente nella barra degli indirizzi. Il caso classico: il SAN elenca DNS:git.internal.lan ma l'utente ha navigato verso https://10.8.0.1. Le modifiche al trust-store non possono risolvere questo errore; rigenera il certificato con la voce mancante.
curl: (60) SSL certificate problem: self-signed certificate — curl non si fida del certificato. La variante self-signed certificate in certificate chain indica lo stesso problema per un certificato firmato dalla propria CA privata. Soluzione temporanea: curl --cacert lab-ca.crt https://...; soluzione permanente: aggiungere il certificato al trust store. Non usare -k.
unable to load certificate ... Expecting: TRUSTED CERTIFICATE (o Expecting: CERTIFICATE REQUEST, o no start line) — confusione del formato PEM. È stato fornito a OpenSSL il tipo di file errato: una key o un CSR dove era richiesto un certificato, oppure un binario DER dove era richiesto un PEM. head -1 filename indica il formato effettivo — un certificato inizia con -----BEGIN CERTIFICATE-----. Per il formato DER, converti 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 key non sono accoppiati, solitamente perché il comando di generazione è stato eseguito due volte e i file si sono mescolati. Verifica con openssl x509 -in git.internal.crt -noout -pubkey | sha256sum rispetto a openssl pkey -in git.internal.key -pubout | sha256sum; hash identici indicano una coppia corretta. Se differiscono, rigenera entrambi i file contemporaneamente.
FAQ
Perché Chrome continua a mostrare "Not secure" dopo aver creato un certificato self-signed?
Se l'errore è NET::ERR_CERT_AUTHORITY_INVALID, il certificato è corretto — Chrome non ha ancora motivi per fidarsi. Installa il certificato (o la tua CA root privata) nel trust store del client. Ricorda che su Linux Chrome utilizza il database NSS tramite certutil, non il trust store di sistema. Se l'errore è NET::ERR_CERT_COMMON_NAME_INVALID, il certificato non possiede un Subject Alternative Name corrispondente all'URL e deve essere riemesso con -addext "subjectAltName=...".
Come posso far fidare curl di un certificato self-signed senza usare -k?
Copia il certificato (formato PEM, estensione .crt) in /usr/local/share/ca-certificates/ ed esegui sudo update-ca-certificates — l'output deve riportare 1 added. Da quel momento curl lo verificherà come un certificato pubblico. Per una singola richiesta senza modificare il sistema, curl --cacert /path/to/cert.crt effettua la verifica solo rispetto a quel file; -k disabilita completamente la verifica e non deve essere utilizzato negli script.
Per quanto tempo può essere valido un certificato self-signed?
Tecnicamente per qualsiasi durata — i limiti del CA/Browser Forum (attualmente 200 giorni, 47 entro il 2029) si applicano alle CA pubblicamente fidate, non alla fiducia privata. In pratica, limita i certificati del server a 825 giorni, poiché i dispositivi Apple rifiutano qualsiasi certificato di durata superiore indipendentemente dall'emittente. Una root privata decennale con certificati leaf di due anni (-days 730) è un'impostazione predefinita corretta; pianifica il rinnovo, poiché un certificato interno scaduto interrompe tutti i servizi silenziosamente in una data che nessuno ricorda.
Devo usare un certificato self-signed 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à fidato da ogni client. Il certificato self-signed (o una CA privata) serve per ciò che Let's Encrypt non può emettere: IP privati, hostname interni come .lan, reti air-gapped e servizi deliberatamente nascosti dietro una VPN. La scelta dipende dalla raggiungibilità e dalla denominazione, non dalla forza crittografica — la crittografia è identica.
Perché il mio certificato viene rifiutato anche dopo averlo aggiunto a /usr/local/share/ca-certificates?
Verifica tre elementi. Il file deve terminare con .crt — un'estensione .pem viene ignorata silenziosamente e update-ca-certificates riporta 0 added. Il contenuto deve essere testo PEM che inizia con -----BEGIN CERTIFICATE-----, non binario DER. L'applicazione deve utilizzare effettivamente il trust store di sistema — Chrome su Linux, Firefox, Python requests, Node.js e Java utilizzano ciascuno un proprio trust store privato e richiedono l'aggiunta separata del certificato.