Veelgestelde problemen
Een verlopen certificaat kondigt zich zelden aan. De browser zegt alleen dat de verbinding niet privé is, en jij mag raden welk van de twintig services op die ene server het probleem is.
Dit artikel laat je een TLS-certificaat opzetten met OpenSSL 3.x: een private CA, een server-certificaat met de juiste X.509 v3-extensies, en de OCSP-controle eromheen. Je leert waarom elke extensie er staat, zodat je hem later zelf kunt aanpassen in plaats van een tutorial te blijven volgen.
Vereisten
- Een Linux-server of VM waarop je root-toegang hebt (via `sudo`). De commando's hieronder zijn getest op Debian 12 en Ubuntu 24.04.
- OpenSSL 3.0 of nieuwer. Controleer met `openssl version`. Op Debian 12 staat standaard 3.0.11, op Ubuntu 24.04 staat 3.0.13.
- Basiskennis van het werken op de command line.
- Een editor naar keuze. `nano` volstaat.
Stappen
1. Maak de directorystructuur aan
OpenSSL verwacht een vaste indeling voor de bestanden die het aanmaakt en bijhoudt. Doe dit één keer goed, anders zoek je later naar een `.srl`-bestand dat ergens anders staat.```bash mkdir -p ~/ca/{certs,crl,newcerts,private} chmod 700 ~/ca/private touch ~/ca/index.txt echo 1000 > ~/ca/serial ```
`index.txt` is de database waarin OpenSSL bijhoudt welke certificaten zijn uitgegeven. `serial` bevat het volgende serienummer. Beide zijn leeg of minimaal bij de start, en dat is precies de bedoeling.
2. Schrijf het OpenSSL-configuratiebestand
Dit bestand bepaalt alles: de velden van je certificaten, de extensies, de paden. Zonder dit bestand werkt `openssl ca` niet, en met een slordig bestand krijg je certificaten die een browser weigert.Maak `~/ca/openssl.cnf`:
```ini [ ca ] default_ca = CA_default
[ CA_default ] dir = /home/USERNAME/ca certs = $dir/certs crl_dir = $dir/crl new_certs_dir = $dir/newcerts database = $dir/index.txt serial = $dir/serial private_key = $dir/private/ca.key.pem certificate = $dir/certs/ca.cert.pem default_md = sha256 default_days = 825 default_crl_days = 30 policy = policy_strict copy_extensions = none
[ policy_strict ] countryName = match stateOrProvinceName = match organizationName = match organizationalUnitName = optional commonName = supplied emailAddress = optional
[ req ] default_bits = 4096 distinguished_name = req_distinguished_name string_mask = utf8only default_md = sha256 x509_extensions = v3_ca
[ req_distinguished_name ] countryName = Country Name (2 letter code) stateOrProvinceName = State or Province Name localityName = Locality Name organizationName = Organization Name organizationalUnitName = Organizational Unit Name commonName = Common Name emailAddress = Email Address
[ v3_ca ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer basicConstraints = critical, CA:true keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[ server_cert ] basicConstraints = CA:FALSE nsCertType = server nsComment = "OpenSSL Generated Server Certificate" subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer:always keyUsage = critical, digitalSignature, keyEncipherment extendedKeyUsage = serverAuth subjectAltName = @alt_names
[ alt_names ] DNS.1 = example.internal DNS.2 = www.example.internal IP.1 = 10.0.0.10 ```
Vervang `/home/USERNAME/ca` door je eigen pad. Vervang ook de waarden onder `[ alt_names ]` door de hostnames en IP's die jouw certificaat moet dekken.
Twee dingen vallen op. `basicConstraints = critical, CA:true` in `v3_ca` markeert dit certificaat als een CA dat andere certificaten mag ondertekenen. Zonder die regel weigert een moderne client de hele keten. En `subjectAltName` is verplicht sinds RFC 6125: browsers kijken niet meer naar de Common Name. Staat je domein alleen in CN, dan krijg je een naam-mismatch.
`copy_extensions = none` is bewust streng. Het voorkomt dat een CSR stiekem zijn eigen extensies meestuurt en zo een certificaat krijgt met rechten die jij niet hebt goedgekeurd.
3. Genereer de private CA-sleutel en het CA-certificaat
De CA-sleutel is het kroonjuweel. Wie hem heeft, kan certificaten uitgeven voor elke naam die jij vertrouwt. Behandel hem alsof het de sleutel van je huis is.```bash cd ~/ca openssl genrsa -aes256 -out private/ca.key.pem 4096 chmod 400 private/ca.key.pem ```
De `-aes256` betekent dat de sleutel met een wachtwoord is versleuteld. Dat wachtwoord vraag je bij elke ondertekening opnieuw. Vervelend, maar het is het verschil tussen een gestolen bestand en een gestolen bestand waar iemand niets mee kan. Sla het wachtwoord op in je password manager, niet in een tekstbestand naast de sleutel.
Nu het CA-certificaat zelf, met een lange geldigheid:
```bash openssl req -config openssl.cnf \ -key private/ca.key.pem \ -new -x509 -days 7300 -sha256 -extensions v3_ca \ -out certs/ca.cert.pem ```
Deze stap vraagt om de velden uit `[ req_distinguished_name ]`. Vul ze in. Je krijgt een zelfondertekend certificaat dat twintig jaar meegaat. Die lange looptijd is normaal voor een root-CA: je wilt hem niet elk jaar opnieuw in elke truststore van elk systeem hoeven zetten.
4. Genereer de server-sleutel en een CSR
De server heeft een eigen sleutel nodig, los van de CA-sleutel. Dit is de sleutel die op de webserver of database terechtkomt.```bash openssl genrsa -out private/server.key.pem 2048 chmod 400 private/server.key.pem ```
2048 bits is voor een leaf-certificaat nog steeds acceptabel. Wil je 4096, dan kost dat meer CPU bij elke handshake en dat merk je op een drukke server. Voor een interne API is 2048 ruim voldoende.
Nu de Certificate Signing Request. Dit is het verzoek dat je bij de CA indient, met daarin je publieke sleutel en de naam die je wilt:
```bash openssl req -config openssl.cnf \ -key private/server.key.pem \ -new -sha256 \ -out server.csr.pem ```
Vul bij Common Name de hostname in die je in `alt_names` hebt gezet. De CSR bevat nog geen handtekening van de CA, dus hij is op zichzelf niets waard tot stap 5.
5. Onderteken de CSR met de CA
Hier gebeurt het echte werk. OpenSSL leest de CSR, past de extensies uit `[ server_cert ]` toe, en zet zijn handtekening eronder.```bash openssl ca -config openssl.cnf \ -extensions server_cert -days 825 -notext -md sha256 \ -in server.csr.pem \ -out certs/server.cert.pem ```
Je wordt om het wachtwoord van de CA-sleutel gevraagd en daarna om bevestiging. Driehonderdvijftig dagen is de maximale looptijd die publieke browsers accepteren; voor een private CA mag je langer, maar dan verlies je de gewoonte om te verlengen.
Wil je een certificaat voor client-authenticatie in plaats van een server, maak dan een `[ client_cert ]`-sectie met `extendedKeyUsage = clientAuth` en geef die mee met `-extensions`. Eén CA kan beide soorten uitgeven; het `extendedKeyUsage`-veld bepaalt waarvoor een cert gebruikt mag worden.
6. Controleer de keten
Voordat je iets in productie zet, kijk je of het certificaat de juiste velden heeft. Een verkeerde extensie betekent een weigering die je pas ontdekt als de eerste client verbinding maakt.```bash openssl x509 -in certs/server.cert.pem -noout -text ```
Je zoekt naar `X509v3 Subject Alternative Name` met je hostnames erin, `X509v3 Extended Key Usage: TLS Web Server Authentication`, en `X509v3 Basic Constraints: CA:FALSE`. Staat een van deze er niet, dan klopt je configuratiebestand niet.
Controleer ook of het certificaat tegen de CA valideert:
```bash openssl verify -CAfile certs/ca.cert.pem certs/server.cert.pem ```
De uitvoer moet `certs/server.cert.pem: OK` zijn. Alles anders betekent dat de handtekening niet klopt of dat de keten niet compleet is.
7. Test de verbinding lokaal
Een certificaat dat valideert is nog geen werkende TLS-verbinding. Test het met een echte server.```bash openssl s_server -accept 8443 \ -cert certs/server.cert.pem \ -key private/server.key.pem \ -www ```
Laat dit draaien in het ene terminalvenster en test in een ander:
```bash openssl s_client -connect localhost:8443 -CAfile ~/ca/certs/ca.cert.pem -servername example.internal ```
Let op `Verify return code: 0 (ok)` in de uitvoer. Zie je een andere code, dan is de keten niet compleet of vertrouwt de client de CA niet.
8. Zet OCSP op voor intrekking
Een certificaat intrekken is pas zinvol als clients kunnen controleren of het is ingetrokken. Zonder OCSP of CRL blijft een gestolen sleutel geldig tot de vervaldatum.```bash openssl ocsp -index ~/ca/index.txt \ -CA ~/ca/certs/ca.cert.pem \ -rsigner ~/ca/certs/ca.cert.pem \ -rkey ~/ca/private/ca.key.pem \ -port 8888 ```
Dit start een OCSP-responder op poort 8888. Een client vraagt dan de status op:
```bash openssl ocsp -issuer ~/ca/certs/ca.cert.pem \ -cert ~/ca/certs/server.cert.pem \ -url http://localhost:8888 \ -CAfile ~/ca/certs/ca.cert.pem ```
De regel `certs/server.cert.pem: good` betekent dat het certificaat geldig is. Wil je een certificaat intrekken, dan gebruik je `openssl ca -revoke certs/server.cert.pem` en daarna `openssl ca -gencrl` om de CRL bij te werken.
OCSP in productie draaien is meer werk dan dit: je wilt de responder achter een reverse proxy, met caching, en met een hoge beschikbaarheid. Voor een testomgeving is dit genoeg. Voor een publieke dienst kun je beter een publieke CA nemen die dit voor je regelt. Let's Encrypt doet dat gratis, met certificaten van negentig dagen en Certbot voor de automatisering. De prijs is dat je de verversing uit handen geeft en afhankelijk bent van hun uptime.
Verificatie
De volledige controle in één keer:```bash openssl verify -CAfile ~/ca/certs/ca.cert.pem ~/ca/certs/server.cert.pem && \ openssl x509 -in ~/ca/certs/server.cert.pem -noout -ext subjectAltName,extendedKeyUsage ```
Krijg je `OK` en zie je je hostnames en `TLS Web Server Authentication`, dan staat het certificaat goed. Voeg het CA-certificaat toe aan de truststore van je systeem en je browser of client vertrouwt de verbinding zonder waarschuwing.
Veelgestelde problemen
`unable to load CA private key` bij ondertekenen. Het pad in `openssl.cnf` klopt niet, of je hebt het wachtwoord verkeerd ingevoerd. Controleer de `dir`-regel; die moet absoluut zijn, geen `~`.`Verify return code: 20 (unable to get local issuer certificate)`. De client heeft het CA-certificaat niet in zijn truststore, of je stuurt alleen het leaf-certificaat mee zonder de keten. Bij een server geef je zowel `server.cert.pem` als `ca.cert.pem` mee in de certificate chain.
Browser weigert met `NET::ERR_CERT_COMMON_NAME_INVALID`. Je hebt de hostname alleen in Common Name gezet en niet in `subjectAltName`. Moderne clients negeren CN volledig. Zet de naam in `[ alt_names ]` en onderteken opnieuw.
Een private CA is prima voor intern verkeer, zolang je hem niet vergeet. Het CA-certificaat heeft een looptijd van twintig jaar, maar jij bent degene die over twintig jaar dat bestand nog moet terugvinden.