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 CA | Met step-ca | |
|---|---|---|
| Een client ontmoet een nieuwe server | Toont een vingerafdruk, jij typt yes | Controleert het hostcertificaat tegen de CA, zegt niets |
| Een server wordt opnieuw opgebouwd | Elke client waarschuwt dat de sleutel is veranderd | Onderteken de nieuwe sleutel, clients merken niets |
| Een gebruiker krijgt toegang | Zijn sleutel gaat in authorized_keys op elke server | De CA ondertekent zijn sleutel, elke server accepteert hem |
| Een gebruiker vertrekt | Zoek en verwijder zijn sleutel op elke server | Stop met ondertekenen; zijn certificaat is binnen 16 uur dood |
| Wat elke server bijhoudt | Eén authorized_keys per gebruiker | Eén publieke CA-sleutel |
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...
| Sleutel | Ondertekent | Wie vertrouwt hem |
|---|---|---|
| Host-CA | De hostsleutel van elke server | Elke client, in known_hosts |
| Gebruikers-CA | De aanmeldsleutel van elke persoon | Elke 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:
| Veld | Waarde |
|---|---|
| Type | ecdsa-sha2-nistp256-cert-v01@openssh.com host certificate |
| Key ID | nas.home.example |
| Principals | nas.home.example |
| Geldig | 30 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
| Veld | Waarde |
|---|---|
| Type | user certificate |
| Principals | damien |
| Geldig | 16 uur |
| Extensies | permit-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:
| Test | Resultaat |
|---|---|
Ondertekende sleutel met zijn certificaat, als damien | logged in as damien, exit 0 |
| Zelfde sleutel, certificaat verwijderd | Permission denied (publickey,password,keyboard-interactive) |
Certificaat gebruikt om als root in te loggen | Permission denied: het certificaat noemt alleen damien |
| Client zonder de regel voor de host-CA | Host 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
| Serienummer | Geldig tot | |
|---|---|---|
| Ondertekend | 15166325869329416997 | 2026-11-08 17:35:30 |
| Vernieuwd | 6678957789797316768 | 2026-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
| Geval | Wat er gebeurt | Wat te doen |
|---|---|---|
| CA onbereikbaar | Bestaande certificaten blijven werken; niemand krijgt een nieuwe | Draai de CA op iets wat blijft draaien; gebruikers hebben 16 uur speling |
| Hostcertificaat verloopt | Clients weigeren de host, alsof het een vreemde is | Een vernieuwingstimer, en sshd daarna herladen |
| Een gebruiker moet vandaag weg, niet morgen | Zijn certificaat werkt tot het verloopt | RevokedKeys in sshd_config neemt een lijst met ingetrokken sleutels2 |
| Hostnaam niet in de principals | Client weigert de host | Onderteken elke naam waarop hij antwoordt |
| SSH-CA niet gemaakt bij de inrichting | Geen manier om hem toe te voegen | Een nieuwe CA, ingericht met SSH aan |
| De SSH-sleutels van de CA gestolen | Wie ze heeft kan overal inloggen en elke host zijn | Houd 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.
smallstep/certificates — docker/entrypoint.sh —
DOCKER_STEPCA_INIT_SSH=truevoegt--sshtoe aanstep ca init. ↩︎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”. ↩︎ ↩︎ ↩︎ ↩︎OpenBSD manual — sshd(8), SSH_KNOWN_HOSTS FILE FORMAT — de markering “@cert-authority” “to indicate that the line contains a certification authority (CA) key”. ↩︎
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.” ↩︎
Smallstep — step ssh renew — het commando waarmee het hostcertificaat werd vernieuwd. ↩︎