Waarom certificaten in plaats van sleutels

SSH vertrouwt bij eerste gebruik. Maak verbinding met een nieuwe machine en hij toont je een vingerafdruk en vraagt of je het zeker weet. Niemand controleert het. Iedereen typt yes. Het is dezelfde gewoonte als een browserwaarschuwing wegklikken, en het is precies wat een machine in het midden nodig heeft dat je doet.

De andere richting is niet beter. Elke server houdt per gebruiker een authorized_keys-bestand bij, en elk ervan groeit. Sleutels worden toegevoegd en nooit verwijderd. Iemand vertrekt, en zijn sleutel blijft op een machine staan die niemand zich herinnert tot de dag dat het ertoe doet. Bouw een server opnieuw op en elke client roept dat de hostsleutel is veranderd, dus mensen leren ook die waarschuwing te verwijderen.

Een SSH-certificaatautoriteit maakt aan allebei een eind. De CA ondertekent de sleutel van elke host, en clients vertrouwen de CA, niet een lijst vingerafdrukken. De CA ondertekent de sleutel van elke gebruiker voor een paar uur, en servers vertrouwen de CA, niet een lijst sleutels. Niets te accepteren bij de eerste verbinding, niets op te ruimen als iemand vertrekt, en een gebruikerscertificaat dat morgen vanzelf dood is.

Dit is een andere opzet dan de X.509-kant van step-ca uit Je eigen certificaatautoriteit: step-ca in een container, en daarom heeft het een eigen bericht. Dezelfde container, andere sleutels, andere bestanden op elke machine. Alles hier is gedraaid met step-ca 0.30.2 en OpenSSH 10.0 in een Podman-pod.

Zonder CAMet step-ca
Een client ontmoet een nieuwe serverToont een vingerafdruk, jij typt yesControleert het hostcertificaat tegen de CA, zegt niets
Een server wordt opnieuw opgebouwdElke client waarschuwt dat de sleutel is veranderdOnderteken de nieuwe sleutel, clients merken niets
Een gebruiker krijgt toegangZijn sleutel gaat in authorized_keys op elke serverDe CA ondertekent zijn sleutel, elke server accepteert hem
Een gebruiker vertrektZoek en verwijder zijn sleutel op elke serverStop met ondertekenen; zijn certificaat is binnen 16 uur dood
Wat elke server bijhoudtEén authorized_keys per gebruikerEén publieke CA-sleutel
Wie ondertekent wat, en wie vertrouwt watstep-cahost-CA-sleutelgebruikers-CA-sleutelClientgebruikerscert, 16 uurvertrouwt de host-CAServerhostcert, 30 dagenvertrouwt de gebruikers-CAondertekent gebruikerssleutelondertekent hostsleutelssh, geen vraagelk controleerthet cert van de ander
De CA ondertekent beide kanten. De client hoeft alleen de host-CA-sleutel te vertrouwen, en de server alleen de gebruikers-CA-sleutel. Geen van beide houdt een lijst bij met de sleutels van de ander.

De SSH-CA aanzetten

De SSH-sleutels worden gemaakt als step-ca voor het eerst wordt ingericht, of helemaal niet. Zet DOCKER_STEPCA_INIT_SSH=true bij de eerste start1, en het log toont twee extra sleutels:

SSH Host CA Key: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAy...
SSH User CA Key: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAy...
SleutelOndertekentWie vertrouwt hem
Host-CADe hostsleutel van elke serverElke client, in known_hosts
Gebruikers-CADe aanmeldsleutel van elke persoonElke server, in sshd_config

Het voegt ook een sshpop-provisioner toe, en daarmee kan een host later zijn eigen certificaat vernieuwen. Draait de CA al zonder SSH, dan is er geen schakelaar om om te zetten. Het is een nieuwe CA.

De host ondertekenen

Neem de bestaande hostsleutel van de server, onderteken hem, en geef het certificaat terug aan de server:

step ssh certificate --host --sign \
  --provisioner admin --provisioner-password-file prov-pass \
  nas.home.example ssh_host_ecdsa_key.pub

De volgorde doet ertoe. Met de provisionervlaggen na de hostnaam en het sleutelbestand weigerde step het:

too many positional arguments were provided in 'step ssh certificate <key-id> <key-file>'

Zet elke vlag vooraan en hij ondertekent. Het certificaat, zoals ssh-keygen -L het leest:

VeldWaarde
Typeecdsa-sha2-nistp256-cert-v01@openssh.com host certificate
Key IDnas.home.example
Principalsnas.home.example
Geldig30 dagen, 2026-10-09T18:34:30 tot 2026-11-08T17:35:30

De principal is de naam die een client moet gebruiken om verbinding te maken. Maak verbinding met een naam die niet op het certificaat staat en de client accepteert het niet, dus onderteken elke naam waarop de host antwoordt.

De server

Twee regels in sshd_config2:

HostCertificate /etc/ssh/ssh_host_ecdsa_key-cert.pub
TrustedUserCAKeys /etc/ssh/ssh_user_ca_key.pub

HostCertificate geeft het hostcertificaat aan clients, en de sleutel ervan “must match a private host key already specified”2. TrustedUserCAKeys is de publieke sleutel van de gebruikers-CA, “trusted to sign user certificates for authentication”2. Draai sshd -t voor je herstart. Een typfout in een van beide paden en sshd zegt Could not load host certificate, en dat vind je veel liever nu dan vanuit een buitengesloten sessie.

Op de testserver is er helemaal geen authorized_keys. ls /home/damien/.ssh/authorized_keys geeft No such file or directory, en de aanmeldingen hieronder werken toch.

De gebruiker

De CA maakt het sleutelpaar en ondertekent het in één keer:

step ssh certificate damien id_ecdsa \
  --provisioner admin --provisioner-password-file prov-pass
VeldWaarde
Typeuser certificate
Principalsdamien
Geldig16 uur
Extensiespermit-agent-forwarding, permit-port-forwarding, permit-X11-forwarding, permit-pty, permit-user-rc

Zestien uur is precies de bedoeling. Het dekt een werkdag en sterft ’s nachts. Een gebruiker intrekken komt er vooral op neer dat je morgen niet meer voor hem ondertekent.

De client

Eén regel in known_hosts vervangt elke vingerafdruk die hij anders zou verzamelen3:

@cert-authority *.home.example ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAy...

Dat zegt: elke host onder home.example waarvan het certificaat door deze sleutel is ondertekend, is echt. De markering @cert-authority maakt er een CA-regel van in plaats van de sleutel van één host.

Wat de test liet zien

Alle vier vanaf een client met niets anders dan het gebruikerscertificaat en die ene regel in known_hosts, met StrictHostKeyChecking=yes, zodat er onderweg niets geaccepteerd kon worden:

TestResultaat
Ondertekende sleutel met zijn certificaat, als damienlogged in as damien, exit 0
Zelfde sleutel, certificaat verwijderdPermission denied (publickey,password,keyboard-interactive)
Certificaat gebruikt om als root in te loggenPermission denied: het certificaat noemt alleen damien
Client zonder de regel voor de host-CAHost key verification failed.

De derde is degene die mensen missen. Een certificaat opent niet elk account op de machine, alleen de principals die erin staan. Een sleutel in iemands authorized_keys kan dat niet.

Vernieuwen

Een host vernieuwt zijn eigen certificaat met de sleutel die hij al heeft. Dat is de sshpop-provisioner, die “allows a client to renew, revoke, or rekey an SSH certificate using that certificate for authentication with the CA”4. Geen wachtwoord5:

step ssh renew --force ssh_host_ecdsa_key-cert.pub ssh_host_ecdsa_key
SerienummerGeldig tot
Ondertekend151663258693294169972026-11-08 17:35:30
Vernieuwd66789577897973167682026-11-08 17:35:59

Een nieuw serienummer en verse 30 dagen. Zet het op een wekelijkse timer en de host verloopt nooit. Herlaad sshd daarna, anders blijft het oude certificaat in het geheugen.

Gebruikers vernieuwen door het opnieuw te vragen. Eén step ssh certificate per dag, of bij de eerste aanmelding van de dag, en dat is het hele werk.

Waar het tekortschiet

GevalWat er gebeurtWat te doen
CA onbereikbaarBestaande certificaten blijven werken; niemand krijgt een nieuweDraai de CA op iets wat blijft draaien; gebruikers hebben 16 uur speling
Hostcertificaat verlooptClients weigeren de host, alsof het een vreemde isEen vernieuwingstimer, en sshd daarna herladen
Een gebruiker moet vandaag weg, niet morgenZijn certificaat werkt tot het verlooptRevokedKeys in sshd_config neemt een lijst met ingetrokken sleutels2
Hostnaam niet in de principalsClient weigert de hostOnderteken elke naam waarop hij antwoordt
SSH-CA niet gemaakt bij de inrichtingGeen manier om hem toe te voegenEen nieuwe CA, ingericht met SSH aan
De SSH-sleutels van de CA gestolenWie ze heeft kan overal inloggen en elke host zijnHoud de CA op een eigen machine, geback-upt, offline als dat kan

De laatste is de ruil. Elk authorized_keys-bestand was een eigen klein risico. Nu is er één groot risico, op één plek. Dat is beter, want één plek kun je in de gaten houden en dichttimmeren, maar alleen als iemand dat ook doet.

Vertrouwen waar het te controleren is

De vingerafdrukvraag en het authorized_keys-bestand hebben iets gemeen. Beide vragen een mens om een vertrouwensbeslissing te nemen zonder informatie, elke dag, voor altijd. Mensen zijn daar niet goed in en zullen dat nooit worden, dus ze stoppen met beslissen en beginnen yes te typen.

Een CA verplaatst die beslissing naar één plek, één keer, waar ze goed gedaan kan worden. Dat is het hele idee, en het is hetzelfde idee achter elke standaard die iets waard is: doe het zorgvuldige werk één keer, schrijf het op, en stop met mensen vragen het honderd keer per week in hun hoofd te doen. De machine controleert de handtekening. Jij controleert de CA. Dat is een taakverdeling die standhoudt.


  1. smallstep/certificates — docker/entrypoint.sh — DOCKER_STEPCA_INIT_SSH=true voegt --ssh toe aan step ca init. ↩︎

  2. OpenBSD manual — sshd_config(5) — HostCertificate: “The certificate’s public key must match a private host key already specified by HostKey”; TrustedUserCAKeys: “public keys of certificate authorities that are trusted to sign user certificates for authentication”; RevokedKeys: “Keys listed in this file will be refused for public key authentication”. ↩︎ ↩︎ ↩︎ ↩︎

  3. OpenBSD manual — sshd(8), SSH_KNOWN_HOSTS FILE FORMAT — de markering “@cert-authority” “to indicate that the line contains a certification authority (CA) key”. ↩︎

  4. Smallstep — Provisioners, SSHPOP — “An SSHPOP provisioner allows a client to renew, revoke, or rekey an SSH certificate using that certificate for authentication with the CA.” ↩︎

  5. Smallstep — step ssh renew — het commando waarmee het hostcertificaat werd vernieuwd. ↩︎