Zelfondertekend certificaat maken op Ubuntu 24.04
Maak een zelfondertekend TLS-certificaat dat Chrome accepteert op Ubuntu 24.04. Gebruik de juiste OpenSSL SAN-configuratie en vertrouw het certificaat zonder curl -k te gebruiken.
Wat u bouwt
Een zelfondertekend TLS-certificaat dat moderne browsers en clients daadwerkelijk accepteren, correcte subjectAltName, veilige rechten voor sleutelbestanden, gekoppeld aan Nginx of Apache, plus het onderdeel dat bijna elke handleiding overslaat: zorgen dat uw clients het certificaat correct vertrouwen, in plaats van telkens waarschuwingen weg te klikken en curl -k hard te coderen in scripts. Aan het einde vindt u een private CA van vijf commando's voor wanneer één interne service er zes worden.
Eerst de afweging, want een zelfondertekend certificaat is minder vaak het juiste gereedschap dan men denkt. Als de service bereikbaar is vanaf het publieke internet onder een echte DNS-naam, stop dan met lezen en kies voor een gratis Let's Encrypt-certificaat met certbot op Nginx of het equivalent voor Apache. Dit kost niets, vernieuwt zichzelf en elke browser ter wereld vertrouwt het al. Een zelfondertekend certificaat op een publieke site traint uw gebruikers om beveiligingswaarschuwingen te negeren; dat is een slechtere gewoonte dan onversleuteld HTTP.
Zelfondertekend is de juiste keuze wanneer er geen publiek internet bij betrokken is: een beheerpaneel gebonden aan een WireGuard-tunneladres op uw VPS, een staging-omgeving op een privenetwerk, verkeer tussen backends, een home-lab-apparaat, of als vervanging voor het tijdelijke certificaat dat Webmin zelf genereert op poort 10000. Let's Encrypt kan sowieso geen certificaten uitgeven voor 10.8.0.1 of git.internal.lan; geen enkele publieke CA plaatst een privaat IP-adres of een verzonnen TLD in een certificaat. Voor die namen bent u zelf de CA.
Alles hieronder draait op een verse Ubuntu 24.04-server, die OpenSSL 3.0.x bevat (gebruik openssl version ter bevestiging). Niets hiervan vereist internettoegang; het werkt volledig offline.
Waarom de oude one-liner certificaten produceert die Chrome weigert
Het commando dat u in elke tutorial van vóór 2017 tegenkomt, ziet er als volgt uit:
# 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.crtHet stelt een reeks interactieve vragen, plaatst uw hostnaam in het Common Name-veld en produceert een certificaat zonder subjectAltName-extensie. Dat certificaat is bij aankomst al ongeldig. Chrome is in versie 58, in april 2017, gestopt met het uitlezen van de Common Name. RFC 2818 had het matchen op CN al in het jaar 2000 als verouderd gemarkeerd, en Firefox, Safari, curl en Python gedragen zich op dezelfde manier. Een certificaat identificeert zijn server via de SAN-extensie of helemaal niet, en de browser meldt dit in exact deze bewoordingen:
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.Geen enkele aanpassing aan de trust-store lost deze fout op, omdat het certificaat simpelweg geen naam bevat. Als u op dit moment naar NET::ERR_CERT_COMMON_NAME_INVALID kijkt, heeft uw certificaat geen SAN (of een onjuiste) en moet u een nieuw exemplaar aanmaken. Gelukkig is de oplossing slechts één commando.
Een certificaat genereren dat browsers accepteren: één commando
OpenSSL introduceerde de -addext-vlag in versie 1.1.1. Hierdoor zijn de complexe configuratiebestanden die in oude handleidingen werden gebruikt om een SAN toe te voegen niet langer nodig. Op 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"Wat elke vlag doet:
-x509genereert direct een zelfondertekend certificaat in plaats van een ondertekeningsverzoek (CSR).-newkey rsa:4096genereert in dezelfde stap een nieuwe sleutel. RSA 4096 is compatibel met alle legacy-clients; als alle clients modern zijn, is-newkey ec -pkeyopt ec_paramgen_curve:P-256kleiner en sneller.-noencis de schrijfwijze in OpenSSL 3.x voor de oude-nodes: geen wachtwoordzin op de sleutel. Beide schrijfwijzen werken. Een sleutel met een wachtwoordzin zorgt ervoor dat Nginx bij elke herstart blijft wachten op invoer; voor een serversleutel wilt u dit dus niet.-days 730, twee jaar; meer informatie over dit getal vindt u in de sectie over verloopdata.-subjbeantwoordt de interactieve vragen direct. De CN is tegenwoordig cosmetisch, maar stel deze toch in op de primaire naam; sommige tools tonen deze namelijk.-addext "subjectAltName=..."is de belangrijkste vlag. Vermeld elke naam en elk IP-adres dat clients zullen gebruiken:DNS:-vermeldingen voor hostnamen (wildcards zoalsDNS:*.internal.lanzijn toegestaan),IP:-vermeldingen voor adressen. Als iemand naarhttps://10.8.0.1navigeert, moet deIP:10.8.0.1-vermelding aanwezig zijn; een SAN met alleen DNS levert anders opnieuwNET::ERR_CERT_COMMON_NAME_INVALIDop.
Controleer of de SAN daadwerkelijk is toegevoegd voordat u verdergaat:
openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -ext subjectAltNameCorrecte uitvoer:
X509v3 Subject Alternative Name:
DNS:git.internal.lan, IP Address:10.8.0.1Als in plaats daarvan No extensions in certificate wordt getoond, bevat het certificaat geen SAN en zullen browsers het weigeren. Genereer het certificaat opnieuw in plaats van door te gaan.
Beveilig de private key
Een private key die leesbaar is voor elke gebruiker op het systeem is niet langer privé. Op Ubuntu is /etc/ssl/private standaard 710 root:ssl-cert, wat onbevoegde toegang door andere gebruikers voorkomt, maar stel de rechten van het bestand zelf expliciet in:
sudo chown root:root /etc/ssl/private/git.internal.key
sudo chmod 600 /etc/ssl/private/git.internal.keyZowel nginx als Apache lezen certificaten als root voordat ze hun privileges verlagen, dus root:root modus 600 volstaat voor deze services. Als de key bedoeld is voor een service die onder een eigen gebruiker draait en de key zelf inlaadt, zoals een Node-applicatie, Gitea of een Python-daemon, chown de eigenaar dan naar die specifieke servicegebruiker, eveneens met modus 600. Wat u nooit moet doen: modus 644 gebruiken, een kopie opslaan in een git-repository of een kopie plaatsen in /tmp.
Koppel het aan 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 moet syntax is ok en test is successful weergeven voordat de reload effect heeft. Als het in plaats daarvan SSL_CTX_use_PrivateKey_file() failed ... key values mismatch weergeeft, zijn het certificaat en de sleutel afkomstig van twee verschillende generatieruns; raadpleeg de sectie over foutmodi.
Koppel het aan Apache
sudo a2enmod ssl proxy proxy_httpssl is hier niet voldoende: de onderstaande vhost gebruikt ProxyPass, en zonder mod_proxy en mod_proxy_http mislukt de configuratietest met Invalid command 'ProxyPass', perhaps misspelled or defined by a module not included in the server configuration. Sla de vhost op als /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 hoort Syntax OK te antwoorden. Test nu vanaf een client-machine:
curl -v https://git.internal.lan/en u krijgt een foutmelding:
curl: (60) SSL certificate problem: self-signed certificateDat is geen bug. Dat is TLS in werking: curl kent uw certificaat niet en weigert te communiceren met een server die het niet kan verifiëren. De volgende sectie bevat de werkelijke oplossing, en dat is niet wat de helft van het internet op dit moment doet.
Clients laten vertrouwen en de anti-patronen om te vermijden
Eerst de onjuiste oplossingen, benoemd naar wat ze zijn. curl -k (of --insecure) ingebakken in een script, verify=False in Python requests, NODE_TLS_REJECT_UNAUTHORIZED=0 in Node; geen van deze maakt uw certificaat vertrouwd. Ze schakelen certificaatverificatie uit, wat betekent dat de client zonder problemen communiceert met elke server die elk certificaat presenteert, inclusief een certificaat dat door een aanvaller in het pad is geplaatst. U behoudt de overhead van TLS en verliest de authenticatie die het doel ervan was. Erger nog, deze vlaggen verspreiden zich: geplakt in een cron job, daarna een deploy-script, vervolgens in productiecode, totdat niemand meer weet welke verbindingen tijdelijk zouden zijn. Als een verify=False een debug-sessie overleeft, is het ontwerp onjuist.
De juiste oplossing is om elk client-OS te leren dat dit certificaat een vertrouwde root is. Op Ubuntu- en Debian-clients:
sudo cp git.internal.crt /usr/local/share/ca-certificates/git.internal.crt
sudo update-ca-certificatesDe regel die ertoe doet in de output (een Running hooks in /etc/ca-certificates/update.d... blok volgt hierop):
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.Twee valkuilen schuilen in deze regels. Het bestand moet eindigen op .crt, een .pem-extensie wordt stilzwijgend genegeerd en u krijgt 0 added zonder foutmelding. De inhoud moet bovendien PEM zijn; het bestand begint met -----BEGIN CERTIFICATE-----. Converteer een DER-binary eerst met openssl x509 -inform der -in file.der -out file.crt. Het toevoegen van het self-signed certificaat zelf als root werkt omdat een self-signed certificaat zijn eigen root is.
Daarna vertrouwen curl, wget, git, apt en al het andere dat OpenSSL gebruikt tegen de systeembundel, de server zonder enige vlag. Een handvol clients beheert eigen trust stores en vereist een individuele aanpak:
- Chrome/Chromium op Linux leest een NSS-database, niet de systeem-store:
sudo apt install libnss3-tools, daarnacertutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n "git.internal" -i git.internal.crtper gebruiker. - Firefox heeft een eigen store: Instellingen → Privacy & Beveiliging → Certificaten → Importeren, of zet
security.enterprise_roots.enabledoptrueinabout:configzodat deze de systeem-store leest. - Python requests levert een eigen CA-bundel (certifi) mee en negeert de systeem-store: geef
verify="/usr/local/share/ca-certificates/git.internal.crt"mee of exporteerREQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt. - Node.js: exporteer
NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/git.internal.crt.
Op Windows-clients dubbelklikt u op het .crt en installeert u dit in Trusted Root Certification Authorities; op macOS voegt u het toe aan de System-sleutelhanger in Keychain Access en markeert u dit als Always Trust.
Eén root voor vele services: een kleine private CA
Vertrouwen per certificaat schaalt niet: zes services maal vier client-machines betekent vierentwintig installaties van vertrouwensrelaties, en elke nieuwe service voegt er meer toe. De oplossing is een private CA; clients vertrouwen één root en u ondertekent het certificaat van elke service daarmee.
De gebruiksvriendelijke optie is mkcert, die in de Ubuntu 24.04 repositories zit en de NSS-stores (Chrome, Firefox) beheert die update-ca-certificates overslaat:
sudo apt install -y mkcert libnss3-tools
mkcert -install
mkcert git.internal.lan "*.internal.lan" 10.8.0.1mkcert -install maakt een root aan en registreert deze in elke trust store op die machine; het derde commando genereert git.internal.lan+2.pem en git.internal.lan+2-key.pem, klaar om in de bovenstaande nginx- of Apache-snippets te plaatsen. Het ontwerp gaat uit van een ontwikkelmachine; de root-key bevindt zich op de machine waar -install is uitgevoerd. Dit is ideaal voor een ontwikkel-laptop, maar ongeschikt voor een serverpark.
Voor servers voert standaard OpenSSL de volledige CA-procedure uit in vijf commando's:
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.crtDe valkuil zit in het laatste commando: openssl x509 -req verwijdert standaard alle extensies uit de CSR, inclusief de SAN die u zorgvuldig heeft toegevoegd. -copy_extensions copy (een OpenSSL 3.x optie, dus werkzaam op 24.04) neemt deze over; laat u dit weg, dan heeft het ondertekende certificaat geen SAN en begroet Chrome u opnieuw met NET::ERR_CERT_COMMON_NAME_INVALID. Controleer dit met dezelfde openssl x509 -noout -ext subjectAltName-check als voorheen.
Distribueer lab-ca.crt naar clients via de bovenstaande stappen voor trust-stores, één keer per machine, voor altijd. Bewaak lab-ca.key als de kroonjuwelen die het nu zijn: modus 600, bij voorkeur opgeslagen op een machine die niet een van de servers is waarvoor het certificaten ondertekent, aangezien iedereen die de key bezit een certificaat kan aanmaken voor elke naam die uw clients vertrouwen.
Verloop en rotatie
De levensduur van certificaten van publieke CA's neemt snel af. Het CA/Browser Forum heeft de geldigheidsduur van nieuw uitgegeven, publiek vertrouwde certificaten in maart 2026 beperkt tot 200 dagen (voorheen 398). Dit wordt verder teruggebracht naar 100 dagen in 2027 en 47 dagen in maart 2029. Deze regels zijn echter alleen bindend voor publiek vertrouwde CA's. Uw private CA valt hier niet onder en browsers dwingen deze limieten niet af voor handmatig geïnstalleerde root-certificaten. Er is wel één praktische beperking: Apple-platformen weigeren elk TLS-servercertificaat dat langer dan 825 dagen geldig is, ongeacht de uitgever. Als iPhones of Macs verbinding maken, houd de leaf-certificaten dan op een geldigheidsduur van twee jaar of minder. -days 730 voldoet overal aan deze eis; een root-certificaat van tien jaar met leaf-certificaten van twee jaar is een gangbare configuratie voor interne netwerken.
Certificaten met een lange levensduur falen op slechts één manier: geruisloos en tegelijkertijd, op een datum die niemand zich meer herinnert. Controleer wat u heeft:
openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -enddateZet de vernieuwing in een echte agenda of laat cron u 30 dagen van tevoren waarschuwen; openssl x509 -checkend 2592000 -in cert.crt geeft een non-zero exitcode zodra de verloopdatum binnen dat aantal seconden ligt. Als u al Uptime Kuma voor statusmonitoring gebruikt, markeren de HTTPS-monitors automatisch certificaten die bijna verlopen.
Rotatie bij een private CA is aangenaam saai: voer de CSR- en ondertekeningscommando's opnieuw uit, vervang de bestanden en herlaad de webserver. De root is niet gewijzigd, dus cliënten merken niets.
Foutmodi en de bijbehorende meldingen
NET::ERR_CERT_AUTHORITY_INVALID, de verwachte status voordat u vertrouwen instelt; dit is geen defect in het certificaat. Als dit blijft bestaan nadat u de root-CA heeft geïnstalleerd: op Linux leest Chrome de NSS-database in plaats van de systeemopslag (zie de certutil-stap); of het gekopieerde bestand eindigde niet op .crt en update-ca-certificates gaf de melding 0 added; of de server presenteert een ander certificaat dan het certificaat dat u heeft vertrouwd. Vergelijk de vingerafdrukken met openssl s_client -connect git.internal.lan:443 </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256.
NET::ERR_CERT_COMMON_NAME_INVALID, het certificaat bevat geen SAN, of de SAN dekt de naam in de adresbalk niet. Het klassieke geval: de SAN bevat DNS:git.internal.lan, maar de gebruiker navigeerde naar https://10.8.0.1. Wijzigingen in de trust-store lossen dit niet op; geef het certificaat opnieuw uit met de ontbrekende vermelding.
curl: (60) SSL certificate problem: self-signed certificate, curl vertrouwt het certificaat niet. De variant self-signed certificate in certificate chain betekent hetzelfde voor een certificaat dat is ondertekend door uw private CA. Tijdelijke oplossing: curl --cacert lab-ca.crt https://...; permanente oplossing: de trust-store. Gebruik niet -k.
unable to load certificate ... Expecting: TRUSTED CERTIFICATE (of Expecting: CERTIFICATE REQUEST, of no start line), PEM-verwarring. U heeft OpenSSL het verkeerde type bestand gegeven: een key of CSR waar een certificaat werd verwacht, of een DER-binair bestand waar PEM werd verwacht. head -1 filename toont u wat u daadwerkelijk heeft; een certificaat begint met -----BEGIN CERTIFICATE-----. Converteer DER-bestanden met openssl x509 -inform der -in file.der -out file.crt.
nginx: [emerg] SSL_CTX_use_PrivateKey_file(...) failed (SSL: error ... key values mismatch), het certificaat en de key horen niet bij elkaar. Dit gebeurt meestal omdat het generatiecommando twee keer is uitgevoerd en de bestanden zijn verwisseld. Controleer dit met openssl x509 -in git.internal.crt -noout -pubkey | sha256sum versus openssl pkey -in git.internal.key -pubout | sha256sum; overeenkomende hashes betekenen een bijpassend paar. Als ze verschillen, genereer beide dan opnieuw.
FAQ
Waarom geeft Chrome nog steeds "Niet veilig" aan nadat ik een zelfondertekend certificaat heb aangemaakt?
Als de foutmelding NET::ERR_CERT_AUTHORITY_INVALID is, is het certificaat in orde, maar heeft Chrome simpelweg nog geen reden om het te vertrouwen. Installeer het (of uw private CA-root) in de trust store van de client. Houd er rekening mee dat Chrome op Linux de NSS-database via certutil gebruikt, niet de systeem-store. Als de foutmelding NET::ERR_CERT_COMMON_NAME_INVALID is, mist het certificaat een Subject Alternative Name die overeenkomt met de URL; het moet opnieuw worden uitgegeven met -addext "subjectAltName=...".
Hoe zorg ik ervoor dat curl een zelfondertekend certificaat vertrouwt zonder -k?
Kopieer het certificaat (PEM-formaat, .crt-extensie) naar /usr/local/share/ca-certificates/ en voer sudo update-ca-certificates uit; de uitvoer moet 1 added aangeven. Vanaf dat moment verifieert curl het certificaat zoals elk publiek certificaat. Voor een eenmalig verzoek zonder het systeem aan te passen, verifieert curl --cacert /path/to/cert.crt alleen tegen dat bestand; -k schakelt verificatie volledig uit en hoort niet thuis in scripts.
Hoe lang kan een zelfondertekend certificaat geldig zijn?
Technisch gezien zo lang als u wilt. De limieten van het CA/Browser Forum (momenteel 200 dagen, 47 dagen vanaf 2029) zijn bindend voor publiek vertrouwde CA's, niet voor privaat vertrouwen. Beperk servercertificaten in de praktijk tot 825 dagen, omdat Apple-apparaten alles wat langer is weigeren, ongeacht de uitgever. Een private root van tien jaar met leaf-certificaten van twee jaar (-days 730) is een verstandige standaard; noteer de vernieuwing in uw agenda, want een verlopen intern certificaat legt alles geruisloos plat op een datum die niemand zich herinnert.
Moet ik een zelfondertekend certificaat of Let's Encrypt gebruiken?
Als de service een publieke DNS-naam heeft en bereikbaar is vanaf het internet, gebruik dan altijd Let's Encrypt: gratis, geautomatiseerd en al vertrouwd door elke client. Zelfondertekende certificaten (of een private CA) zijn bedoeld voor zaken waarvoor Let's Encrypt geen certificaten kan uitgeven: private IP-adressen, hostnamen die alleen intern werken zoals .lan, air-gapped netwerken en services die bewust achter een VPN zijn verborgen. De beslissing draait om bereikbaarheid en naamgeving, niet om de sterkte van de beveiliging; de cryptografie is identiek.
Waarom wordt mijn certificaat geweigerd, zelfs nadat ik het heb toegevoegd aan /usr/local/share/ca-certificates?
Controleer drie zaken. Het bestand moet eindigen op .crt; een .pem-extensie wordt geruisloos overgeslagen en update-ca-certificates rapporteert 0 added. De inhoud moet PEM-tekst zijn die begint met -----BEGIN CERTIFICATE-----, geen DER-binair bestand. Bovendien moet de applicatie daadwerkelijk de systeem-store gebruiken; Chrome op Linux, Firefox, Python requests, Node.js en Java onderhouden elk een eigen private trust store en vereisen dat het certificaat daar afzonderlijk aan wordt toegevoegd.