SSD Nodes Learn
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-07-24

Zelfgetekend TLS-certificaat op Ubuntu maken

Maak een TLS-certificaat voor Ubuntu 24.04 dat door Chrome wordt geaccepteerd. Leer hoe u SAN gebruikt en het certificaat vertrouwt zonder curl -k te gebruiken.

Wat u bouwt

Een zelfgetekend TLS-certificaat dat door moderne browsers en clients wordt geaccepteerd — met de juiste subjectAltName, veilige sleutelrechten, en geïntegreerd in nginx of Apache. Daarnaast bevat dit de stap die bijna elke handleiding overslaat: het zorgen dat uw clients het certificaat daadwerkelijk vertrouwen, in plaats van dat zij beveiligingswaarschuwingen wegklikken en curl -k permanent in scripts hardcoderen. U eindigt met een private CA die slechts vijf commando's vereist, handig wanneer één interne service uitgroeit tot zes.

Eerst de besluitvorming, aangezien een zelfgetekend certificaat veel minder vaak de juiste keuze is dan vaak wordt aangenomen. Als de service via het publieke internet bereikbaar is onder een echte DNS-naam, stop dan met lezen en gebruik een gratis Let's Encrypt-certificaat met certbot op nginx of het Apache-equivalent. Het kost niets, het vernieuwt zichzelf en elke browser ter wereld vertrouwt het al. Een zelfgetekend certificaat op een publieke site leert gebruikers om beveiligingswaarschuwingen weg te klikken, wat een slechtere gewoonte is dan het gebruik van gewone HTTP.

Zelfgetekend is de juiste keuze wanneer er geen sprake is van het publieke internet: een admin-paneel gekoppeld aan een WireGuard-tunneladres op uw VPS, een staging-server in een privaat netwerk, service-to-service verkeer tussen backends, een home-lab apparaat, of het vervangen van het tijdelijke certificaat dat Webmin genereert voor zichzelf op poort 10000. Let's Encrypt kan sowieso geen certificaten uitgeven voor 10.8.0.1 of git.internal.lan — geen publieke CA plaatst een privaat IP of een verzonnen TLD in een certificaat. Voor die namen bent u de CA.

Alles hieronder wordt uitgevoerd op een schone Ubuntu 24.04-installatie met OpenSSL 3.0.x (gebruik openssl version om dit te bevestigen). Er is geen internettoegang nodig; alles werkt in een air-gapped omgeving.

Waarom de oude one-liner certificaten genereert die Chrome weigert

De opdracht die in elk tutorial van vóór 2017 wordt gegeven, 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.crt

Deze opdracht stelt een reeks interactieve vragen, plaatst uw hostname in het Common Name-veld en genereert een certificaat zonder subjectAltName extensie. Dat certificaat is direct onbruikbaar. Chrome is gestopt met het lezen van het Common Name in versie 58 in april 2017 — RFC 2818 heeft CN-matching al sinds het jaar 2000 afgeschaft — en Firefox, Safari, curl en Python werken op dezelfde manier. Een certificaat identificeert de server via de SAN-extensie of helemaal niet. De browser geeft dit aan met de volgende tekst:

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.

Het aanpassen van de trust-store lost deze fout niet op, omdat het certificaat geen gegevens bevat. Als u momenteel NET::ERR_CERT_COMMON_NAME_INVALID ziet, dan heeft uw certificaat geen SAN (of een onjuiste SAN) en moet u een nieuw certificaat aanmaken. De oplossing bestaat uit één opdracht.

Maak een certificaat aan dat door browsers wordt geaccepteerd: één commando

OpenSSL heeft de -addext flag uitgebreid in versie 1.1.1. Hierdoor zijn de complexe configuratiebestanden uit oudere handleidingen niet langer nodig om een SAN toe te voegen. 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"

De functie van elke flag:

  • -x509 genereert direct een self-signed certificaat in plaats van een signing request.
  • -newkey rsa:4096 genereert in dezelfde stap een nieuwe key. RSA 4096 is compatibel met legacy clients; als alle verbindingen modern zijn, is -newkey ec -pkeyopt ec_paramgen_curve:P-256 kleiner en sneller.
  • -noenc is de OpenSSL 3.x schrijfwijze van de oude -nodes: geen passphrase op de key. Beide schrijfwijzen werken. Een key met passphrase zorgt ervoor dat nginx blijft wachten op input bij elke boot; voor een server key is dit gewenst.
  • -days 730 — twee jaar; meer informatie over dit getal vindt u in de sectie over vervaldatum.
  • -subj beantwoordt de interactieve vragen direct. De CN is tegenwoordig alleen cosmetisch, maar stel deze toch in op de primaire naam; sommige tools tonen deze namelijk.
  • -addext "subjectAltName=..." is de belangrijkste flag. Vermeld elke naam en elk IP-adres dat clients gebruiken: DNS: vermeldingen voor hostnames (wildcards zoals DNS:*.internal.lan zijn toegestaan), IP: vermeldingen voor adressen. Als iemand naar https://10.8.0.1 navigeert, moet de IP:10.8.0.1 vermelding aanwezig zijn — een SAN met alleen DNS veroorzaakt opnieuw NET::ERR_CERT_COMMON_NAME_INVALID.

Controleer of de SAN correct is toegevoegd voordat u de configuratie voltooit:

openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -ext subjectAltName

Correcte output:

X509v3 Subject Alternative Name:
    DNS:git.internal.lan, IP Address:10.8.0.1

Als er in plaats daarvan No extensions in certificate wordt weergegeven, heeft het certificaat geen SAN en zullen browsers het weigeren. Genereer het certificaat opnieuw in plaats van door te gaan.

Lock down the key

A private key readable by every user on the box is not a private key. On Ubuntu, /etc/ssl/private is already 710 root:ssl-cert, which keeps casual eyes out, but set the file itself explicitly:

sudo chown root:root /etc/ssl/private/git.internal.key
sudo chmod 600 /etc/ssl/private/git.internal.key

nginx and Apache both read certificates as root before dropping privileges, so root:root mode 600 works for them. If the key is for a service that runs as its own user and loads the key itself — a Node app, Gitea, a Python daemon — chown it to that service user instead, still mode 600. What you never do: mode 644, a copy in a git repository, or a copy in /tmp.

Integreer het 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 nginx

nginx -t moet syntax is ok en test is successful weergeven voordat de reload effect heeft. Als SSL_CTX_use_PrivateKey_file() failed ... key values mismatch in plaats daarvan wordt weergegeven, komen het certificaat en de key uit twee verschillende generatie-runs — zie de sectie over foutmodi.

Wire it into Apache

sudo a2enmod ssl proxy proxy_http

ssl is hier alleen niet voldoende: de onderstaande vhost gebruikt ProxyPass. Zonder mod_proxy en mod_proxy_http faalt 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 apache2

configtest zou Syntax OK moeten beantwoorden. Test dit nu vanaf een clientmachine:

curl -v https://git.internal.lan/

en u krijgt een foutmelding:

curl: (60) SSL certificate problem: self-signed certificate

Dit is geen bug. Dit is TLS die correct werkt: curl herkent uw certificaat niet en weigert te communiceren met een server die niet geverifieerd kan worden. De volgende sectie bevat de definitieve oplossing — en dit is niet de methode die de helft van het internet op dit moment gebruikt.

Zorg dat clients het vertrouwen — en de anti-patterns die u moet weigeren

De verkeerde oplossingen eerst, benoemd naar wat ze zijn. curl -k (of --insecure) verwerkt in een script, verify=False in Python requests, NODE_TLS_REJECT_UNAUTHORIZED=0 in Node — geen van deze methoden zorgt ervoor dat uw certificaat wordt vertrouwd. Ze schakelen de certificaatverificatie uit. Dit betekent dat de client zonder problemen communiceert met elke server die elk certificaat presenteert, inclusief een certificaat dat door een aanvaller is geplaatst. U behoudt de overhead van TLS, maar verliest de authenticatie die het doel ervan was. Erger nog, deze flags verspreiden zich: eerst geplakt in een cron job, daarna in een deploy script, en vervolgens in de productiecode, totdat niemand meer weet welke verbindingen tijdelijk bedoeld waren. Als een verify=False langer bestaat dan de debugging-sessie waarvoor deze nodig was, is het ontwerp foutief.

De juiste oplossing is om elk client OS te instrueren 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-certificates

De relevante regel 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 zitten verborgen 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 PEM zijn; het bestand begint met -----BEGIN CERTIFICATE-----. Converteer een DER-binair bestand eerst met openssl x509 -inform der -in file.der -out file.crt. Het toevoegen van het zelfondertekende certificaat zelf als root werkt, omdat een zelfondertekend certificaat zijn eigen root is.

Daarna vertrouwen curl, wget, git, apt, en alles wat OpenSSL gebruikt tegenover de systeembundel de server zonder enige flags. Een aantal clients gebruikt een eigen trust store en vereist individuele behandeling:

  • Chrome/Chromium op Linux leest een NSS-database, niet de systeemstore: sudo apt install libnss3-tools, en vervolgens certutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n "git.internal" -i git.internal.crt per gebruiker.
  • Firefox heeft een eigen store: Instellingen → Privacy & Beveiliging → Certificaten → Importeren, of zet security.enterprise_roots.enabled op true in about:config zodat de systeemstore wordt gelezen.
  • Python requests bevat een eigen CA-bundel (certifi) en negeert de systeemstore: gebruik verify="/usr/local/share/ca-certificates/git.internal.crt" of exporteer REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt.
  • Node.js: exporteer NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/git.internal.crt.

Klik op Windows-clients dubbel op de .crt en installeer deze bij Vertrouwde uitgevers van de rootcertificaten; voeg deze op macOS toe aan de System keychain in Keychain Access en markeer deze als Altijd vertrouwen.

Eén root voor meerdere services: een kleine private CA

Vertrouwen per certificaat schaalt niet: zes services vermenigvuldigd met vier clientmachines resulteert in vierentwintig installaties. Elke nieuwe service verhoogt dit aantal. De oplossing is een private CA — clients vertrouwen één root, en u ondertekent het certificaat van elke service hiermee.

De gebruiksvriendelijke optie is mkcert. Deze zit in de Ubuntu 24.04 repos en beheert de NSS stores (Chrome, Firefox) waar update-ca-certificates tekortschiet:

sudo apt install -y mkcert libnss3-tools
mkcert -install
mkcert git.internal.lan "*.internal.lan" 10.8.0.1

mkcert -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, die direct gebruikt kunnen worden in de nginx of Apache snippets hierboven. Het ontwerp is gericht op een development machine — de root key staat op de machine waar -install is uitgevoerd. Dit is ideaal voor een dev laptop, maar niet geschikt voor een server fleet.

Voor servers volstaat standaard OpenSSL om de volledige CA in vijf commando's op te zetten:

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.crt

De valstrik zit in het laatste commando: openssl x509 -req verwijdert standaard alle extensions uit de CSR, inclusief de SAN die u handmatig heeft toegevoegd. -copy_extensions copy (een OpenSSL 3.x optie, dus werkend op 24.04) behoudt deze gegevens. Laat u deze optie weg, dan bevat het ondertekende certificaat geen SAN en geeft Chrome opnieuw NET::ERR_CERT_COMMON_NAME_INVALID. Verifieer dit met dezelfde openssl x509 -noout -ext subjectAltName check als eerder.

Distribueer lab-ca.crt naar clients via de trust-store stappen hierboven — slechts eenmaal per machine. Beveilig lab-ca.key als een kostbaar bezit: gebruik mode 600 en bewaar deze bij voorkeur op een machine die niet een van de servers is die hij ondertekent. Degene die de key bezit, kan namelijk een certificaat maken voor elke naam die uw clients zullen accepteren.

Expiry and rotation

De levensduur van publieke CA-certificaten neemt af. Het CA/Browser Forum heeft de limiet voor nieuw uitgegeven publiek vertrouwde certificaten in maart 2026 verlaagd naar 200 dagen (voorheen 398 dagen). In 2027 wordt dit 100 dagen en in maart 2029 wordt dit 47 dagen. Deze regels gelden echter alleen voor publiek vertrouwde CA's. Uw private CA valt hier niet onder en browsers handhaven deze regels niet voor handmatig geïnstalleerde roots. Er is één relevante beperking: Apple-platforms weigeren elk TLS-servercertificaat dat langer dan 825 dagen geldig is, ongeacht de uitgever. Als iPhones of Macs verbinding moeten maken, houd de leaf-certificaten dan op maximaal twee jaar. -days 730 voldoet aan deze eis; een root met een looptijd van tien jaar en leaves van twee jaar is een veilige interne configuratie.

Certificaten met een lange looptijd falen op slechts één manier: ze verlopen geruisloos en tegelijkertijd op een datum die niemand heeft vastgelegd. Controleer uw huidige status:

openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -enddate

Plan de vernieuwing in een echte agenda, of gebruik cron om u 30 dagen van tevoren te waarschuwen — openssl x509 -checkend 2592000 -in cert.crt geeft een non-zero exitcode zodra de vervaldatum binnen dat aantal seconden valt. Als u al Uptime Kuma voor statusmonitoring gebruikt, dan geven de HTTPS-monitors gratis een melding bij een naderende certificaatvervaldatum.

Rotatie met een private CA is eenvoudig: voer de CSR-and-sign commando's opnieuw uit, vervang de bestanden en herlaad de webserver. De root is niet gewijzigd, dus de client merkt niets.

Foutmodi, met de strings die u zult zien

NET::ERR_CERT_AUTHORITY_INVALID — de verwachte status voordat u vertrouwen instelt, geen defect in het certificaat. Als dit blijft bestaan nadat u het root-certificaat heeft geïnstalleerd: op Linux leest Chrome NSS in plaats van de systeemstore (zie stap certutil); of het gekopieerde bestand eindigde niet op .crt en update-ca-certificates gaf 0 added aan; of de server presenteert een ander certificaat dan het certificaat dat u heeft vertrouwd — vergelijk de fingerprints 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 heeft geen SAN, of de SAN de naam in de adresbalk dekt niet. Het klassieke geval: de SAN vermeldt DNS:git.internal.lan maar de gebruiker navigeert naar https://10.8.0.1. Wijzigingen in de trust-store lossen dit niet op; heruitgeven met de ontbrekende vermelding is noodzakelijk.

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 eigen private CA. Tijdelijke oplossing: curl --cacert lab-ca.crt https://...; permanente oplossing: de trust store. 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 gevoerd: een key of CSR waar een certificaat werd verwacht, of een DER-binair waar PEM werd verwacht. head -1 filename vertelt u wat u daadwerkelijk heeft — een certificaat begint met -----BEGIN CERTIFICATE-----. Converteer DER 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, 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 samen opnieuw.

FAQ

Waarom geeft Chrome nog steeds "Not secure" aan nadat ik een self-signed certificate heb aangemaakt?

Als de fout NET::ERR_CERT_AUTHORITY_INVALID is, dan is het certificaat correct. Chrome heeft simpelweg nog geen reden om het te vertrouwen. Installeer het certificaat (of uw private CA root) in de trust store van de client. Houd er rekening mee dat Chrome op Linux de NSS database gebruikt via certutil en niet de systeem-trust store. Als de fout NET::ERR_CERT_COMMON_NAME_INVALID is, dan ontbreekt een Subject Alternative Name die overeenkomt met de URL. Het certificaat moet dan opnieuw worden uitgegeven met -addext "subjectAltName=...".

Hoe laat ik curl een self-signed certificate vertrouwen zonder -k?

Kopieer het certificaat (PEM-formaat, .crt extensie) naar /usr/local/share/ca-certificates/ en voer sudo update-ca-certificates uit. De output moet 1 added aangeven. Vanaf dat moment verifieert curl het certificaat zoals elk publiek certificaat. Voor een eenmalige aanvraag zonder het systeem aan te passen, verifieert curl --cacert /path/to/cert.crt alleen tegen dat bestand; -k schakelt verificatie volledig uit en is ongeschikt voor scripts.

Hoe lang kan een self-signed certificate geldig zijn?

Technisch gezien zo lang als u wilt. De limieten van het CA/Browser Forum (tegenwoordig 200 dagen, 47 tegen 2029) gelden voor publiek vertrouwde CA's, niet voor private trust. Beperk servercertificaten in de praktijk tot 825 dagen, omdat Apple-apparaten alles wat langer duurt weigeren, ongeacht de uitgever. Een private root van tien jaar met tweejarige (-days 730) leaf-certificaten is een verstandige standaard. Plan de vernieuwing in de agenda, want een verlopen intern certificaat laat alles zonder waarschuwing uitvallen op een datum die niemand zich herinnert.

Moet ik een self-signed certificate gebruiken of Let's Encrypt?

Als de dienst een publieke DNS-naam heeft en bereikbaar is via het internet, gebruik dan altijd Let's Encrypt. Het is gratis, geautomatiseerd en wordt door elke client vertrouwd. Self-signed (of een private CA) is bedoeld voor zaken die Let's Encrypt niet kan uitgeven: private IP's, interne hostnames zoals .lan, air-gapped netwerken en diensten die bewust achter een VPN verborgen zijn. De keuze hangt af van bereikbaarheid en naamgeving, niet van cryptografische sterkte; 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 genegeerd en update-ca-certificates rapporteert 0 added. De inhoud moet PEM-tekst zijn die begint met -----BEGIN CERTIFICATE-----, geen DER-binair. En de applicatie moet daadwerkelijk de systeem-trust store gebruiken. Chrome op Linux, Firefox, Python requests, Node.js en Java gebruiken elk een eigen trust store en vereisen dat het certificaat apart wordt toegevoegd.

#openssl#tls#self-signed#ubuntu#security