Waarom een eigen CA draaien
Kijk wat er tegenwoordig op een thuisnetwerk staat. Een NAS, Proxmox, Home Assistant, een printer, de beheerpagina van een router. Elk ervan serveert HTTPS met een zelfondertekend certificaat. Elke browser die langskomt geeft een waarschuwing. Mensen klikken die weg. Dan klikken ze de volgende weg, en de keer daarna, tot het wegklikken van een certificaatwaarschuwing gewoon is wat je doet. Dat is de gewoonte die een aanvaller nodig heeft.
Een publieke CA lost het niet op. Let’s Encrypt geeft niet uit voor localhost “because nobody uniquely owns it”1, en hetzelfde geldt voor elke naam die alleen binnen het huis bestaat. Zelfs voor een echt domein gaat elk certificaat dat het uitgeeft naar de openbare Certificate Transparency-logs2. Haal er een voor nas.yourname.uk en de naam van je NAS is nu openbaar.
Je eigen CA lost beide op. Die ondertekent welke namen je binnen ook gebruikt. Er wordt nergens iets gepubliceerd. Zodra elk apparaat zijn root vertrouwt, stoppen de waarschuwingen, en betekent een waarschuwing weer iets.
step-ca is de open source CA van Smallstep, en hij draait in één container. Alles hier is gedraaid op step-ca 0.30.2 in Podman, met Caddy 2.11.7 en nginx 1.30.5 als clients.
| Toepassing | Wat step-ca ervoor levert | Hoofdstuk |
|---|---|---|
| Elk apparaat vertrouwt je certificaten | Eén root om te installeren, één keer | De root vertrouwen |
| Interne HTTPS voor alles wat ACME spreekt | Hetzelfde protocol als Let’s Encrypt, voor interne namen | Interne HTTPS met ACME |
| Diensten zonder ACME-client | Certificaten die zichzelf op een timer vernieuwen | Diensten die geen ACME kunnen |
| Telefoons en andere clientcertificaten | Sleutels die een apparaat bewaart om binnen te komen, en een manier om ze in te trekken | Eigen bericht: de standaarden werken tegen |
| SSH-aanmeldingen | Certificaten in plaats van authorized_keys | Eigen bericht: een andere opzet |
Twee daarvan hebben een eigen bericht, omdat elk een opzet op zich is. Clientcertificaten van step-ca behandelt telefoons en mTLS, waar vijf standaardinstellingen anders moeten. SSH-aanmeldingen ondertekend door step-ca behandelt SSH, dat andere sleutels en andere bestanden op elke machine gebruikt.
Hem starten
Het image initialiseert zichzelf bij de eerste start vanuit een paar omgevingsvariabelen3. Twee ervan bepalen dingen die je later niet kunt veranderen zonder opnieuw te beginnen, dus zet ze op dag één.
| Variabele | Doet |
|---|---|
DOCKER_STEPCA_INIT_NAME | De naam van de uitgever op elk certificaat |
DOCKER_STEPCA_INIT_DNS_NAMES | De namen waarop de CA antwoordt |
DOCKER_STEPCA_INIT_ACME=true | Voegt een ACME-provisioner toe voor interne HTTPS |
DOCKER_STEPCA_INIT_SSH=true | Maakt ook SSH-CA-sleutels voor hosts en gebruikers |
De schakelaars voor ACME en SSH worden gelezen door het entrypoint van het image4. Zet ze nu aan, ook als je er vandaag maar één wilt. SSH later toevoegen betekent een nieuwe CA bouwen.
podman run -d --name step-ca \
-v step:/home/step \
-p 9000:9000 \
-e DOCKER_STEPCA_INIT_NAME="Home CA" \
-e DOCKER_STEPCA_INIT_DNS_NAMES="localhost,ca.home.example" \
-e DOCKER_STEPCA_INIT_ACME=true \
-e DOCKER_STEPCA_INIT_SSH=true \
docker.io/smallstep/step-ca:0.30.2
Hij bouwt een root, een intermediate die door die root is ondertekend, de SSH-sleutels en drie provisioners, en serveert dan op poort 9000:
X.509 Root Fingerprint: cd2f8d4c60af32a0a534781cd63b4e2a9ce362e2c1ae5983b56a370bfa2c2489
SSH Host CA Key: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAy...
SSH User CA Key: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAy...
Serving HTTPS on :9000 ...
| Provisioner | Type | Ondertekent |
|---|---|---|
admin | JWK | Alles waar je met het wachtwoord om vraagt |
acme | ACME | Certificaten voor clients die bewijzen dat de naam van hen is |
sshpop | SSHPOP | Vernieuwingen van SSH-hostcertificaten |
De provisioner is hoe de CA “wie vraagt erom” opvat. Elke provisioner heeft zijn eigen regels, en het meeste van wat volgt is het instellen van die regels.
Zet de versie vast. latest was 0.30.2 op de dag dat dit werd geschreven, en een CA is het laatste wat je bij een pull onder je voeten wilt zien veranderen.
Twee wachtwoorden, niet één
De Docker-handleiding van Smallstep zegt dat één gegenereerd wachtwoord zowel “the encrypted CA keys and the default CA provisioner” dekt, bewaard in secrets/password3. Op dit image gebeurt dat niet. Probeer met dat wachtwoord een certificaat uit te geven en de provisioner weigert het:
failed to decrypt JWE: invalid password
Het eigen entrypoint van het image legt het uit. Het genereert twee willekeurige wachtwoorden, één voor de CA-sleutels en één voor de provisioner. Dat van de provisioner wordt één keer naar het log geschreven en daarna vernietigd4:
echo "👉 Your CA administrative password is: $(< $STEPPATH/provisioner_password )"
...
shred -u $STEPPATH/provisioner_password
| Wachtwoord | Ontgrendelt | Waar het terechtkomt |
|---|---|---|
secrets/password | De sleutels van de root en de intermediate | In het volume, gebruikt bij elke start van de CA |
| Provisionerwachtwoord | De admin-provisioner, die elk certificaat ondertekent waarom je vraagt | Het log van de eerste start, en verder nergens |
Lees dus bij de eerste start het log en bewaar dat tweede wachtwoord op een veilige plek. Raak je het kwijt, dan kun je geen certificaat meer uitgeven zonder de provisioner er met de hand uit te bewerken. Zet DOCKER_STEPCA_INIT_PASSWORD voor de eerste start als je er liever zelf een kiest; het entrypoint gebruikt het dan voor beide4.
De root vertrouwen
Niets vertrouwt je CA tot je het dat vertelt. Een Debian-machine die Caddy om een pagina vraagt die door step-ca is ondertekend:
$ curl https://nas.home.example/
curl: (60) SSL certificate problem: unable to get local issuer certificate
Kopieer de root erin, en hetzelfde verzoek komt erdoor:
cp root_ca.crt /usr/local/share/ca-certificates/home-ca.crt
update-ca-certificates
$ curl https://nas.home.example/
internal service, certificate from step-ca
Dat is het hele werk. Eén keer per apparaat. step certificate install doet hetzelfde voor welk systeem het ook op draait5. Alleen de root gaat in een trust store. De intermediate reist mee met elk certificaat, en een telefoon die een clientcertificaat gaat tonen heeft hem ook nodig; dat staat in het bericht over clientcertificaten.
Behandel de root als de voordeursleutel die hij nu is. Alles wat de privésleutel van de root heeft, kan een certificaat maken voor elke naam, en elk apparaat dat hem vertrouwt zal het geloven.
Interne HTTPS met ACME
ACME is het protocol dat Let’s Encrypt gebruikt, en step-ca spreekt het voor interne namen6. Alles met een ACME-client krijgt interne certificaten op dezelfde manier als publieke, vernieuwingen inbegrepen. Voor Caddy zijn het twee globale regels:
{
acme_ca https://ca.home.example:9000/acme/acme/directory
acme_ca_root /etc/caddy/root_ca.crt
}
nas.home.example {
reverse_proxy 192.0.2.30:5000
}
Wat Caddy bij de eerste start logde:
"msg":"trying to solve challenge","identifier":"nas.home.example","challenge_type":"tls-alpn-01"
"msg":"certificate obtained successfully","identifier":"nas.home.example"
| Controle | Resultaat |
|---|---|
| Challenge | tls-alpn-01, beantwoord door Caddy op poort 443 |
| Uitgever | Home CA Intermediate CA |
| Looptijd | 24 uur, van 17:31 tot 17:32 de volgende dag |
| Vernieuwing | Caddy’s eigen taak, niets in te stellen |
Een certificaat van 24 uur klinkt alarmerend. Dat is het niet. Een ACME-client vernieuwt ruim voor het verloopt, elke dag, zonder dat er iemand aan te pas komt. Een gestolen sleutel is één dag waard. Dat is de standaard waar step-ca omheen is gebouwd, en hier is hij precies goed.
De challenge vereist wel dat de CA de naam kan bereiken waarvoor hij ondertekent. step-ca moet nas.home.example kunnen opzoeken en er verbinding mee maken op poort 443 voor tls-alpn-01, of poort 80 voor http-01. Interne DNS die beide kanten gebruiken is het eerste om goed te krijgen.
Diensten die geen ACME kunnen
Genoeg apparatuur heeft geen ACME-client: de webinterface van een hypervisor, een printer, een oud apparaat. Geef voor die het certificaat uit met de admin-provisioner en vernieuw het met step ca renew:
step ca certificate proxmox.home.example pve.crt pve.key
step ca renew --force pve.crt pve.key
| Serienummer | Geldig tot | |
|---|---|---|
| Uitgegeven | 241943140971… | 2026-10-10 17:33:03 |
Na step ca renew | 206409156462… | 2026-10-10 17:33:06 |
Nieuw serienummer, verse 24 uur, en geen wachtwoord gevraagd. Het huidige certificaat bewijst wie er vraagt, en daar gaat het om: vernieuwen vereist geen geheim op de machine buiten de sleutel die er al is. Het certificaat moet wel nog geldig zijn, dus een vernieuwing die haar moment mist betekent opnieuw met de hand uitgeven.
Laat je het draaien, dan vernieuwt het zichzelf. --daemon vernieuwt standaard na twee derde van de looptijd, en --exec voert na elke vernieuwing een commando uit, en daar hoort de reload7:
step ca renew --daemon --exec "systemctl reload nginx" pve.crt pve.key
Draai dat vanuit een systemd-unit en de machine zorgt voor zichzelf. Vergeet het, en binnen 24 uur geeft de dienst dezelfde waarschuwing waar je vanaf wilde.
De CA is het makkelijke deel
step-ca aan de praat krijgen kostte één commando. Een Debian-machine hem laten vertrouwen kostte er twee. Caddy had twee regels nodig en vroeg daarna nergens meer om. Dat is het deel dat mensen jarenlang uitstellen, levend met een browserwaarschuwing op elke interne pagina, en het is de goedkoopste klus op deze lijst.
Wat kost, is alles wat een certificaat raakt nadat het is uitgegeven: de root op elk apparaat, de vernieuwingen die niemand in de gaten houdt, die ene dienst die geen ACME kan en onthouden moet worden. Niets ervan is moeilijk. Alles ervan is het soort klus dat alleen goed gebeurt als iemand er eigenaar van is.
Een thuisnetwerk is een klein landgoed. Het verdient wat een echt landgoed krijgt: één root, zorgvuldig bewaard, bewust vertrouwd, en een waarschuwing die iets betekent als hij verschijnt. Als zodanig is de CA niet het slimme stuk. Het slimme stuk is nooit meer iets wegklikken.
Let’s Encrypt — Certificates for localhost — kan niet uitgeven voor “localhost” “because nobody uniquely owns it, and it’s not rooted in a top level domain”. ↩︎
Let’s Encrypt — Certificate Transparency logs — “Let’s Encrypt submits all certificates we issue to CT logs.” ↩︎
Smallstep — Run a private CA in Docker — de
DOCKER_STEPCA_INIT_*-variabelen; zegt dat het gegenereerde wachtwoord is voor “the encrypted CA keys and the default CA provisioner”. ↩︎ ↩︎smallstep/certificates — docker/entrypoint.sh — genereert aparte bestanden
passwordenprovisioner_password, schrijft het provisionerwachtwoord één keer weg en vernietigt het; één opgegeven wachtwoord wordt naar beide geschreven. ↩︎ ↩︎ ↩︎Smallstep — step certificate install — installeert een rootcertificaat in de trust store van het systeem. ↩︎
Smallstep — ACME basics — de ACME-provisioner van step-ca en de challengetypen die hij ondersteunt. ↩︎
Smallstep — step ca renew — met
--daemon, “By default, it will renew the certificate before 2/3 of the validity period of the certificate has elapsed”;--execzorgt voor “certificate reloads on your services”. ↩︎