Why Certificates Instead Of Keys

SSH trusts on first use. Connect to a new machine and it shows you a fingerprint and asks if you are sure. Nobody checks. Everybody types yes. It is the same habit as clicking through a browser warning, and it is exactly what a machine in the middle needs you to do.

The other direction is no better. Every server keeps an authorized_keys file per user, and every one of them grows. Keys get added and never removed. Somebody leaves, and their key stays on a box nobody remembers until the day it matters. Rebuild a server and every client shouts that the host key changed, so people learn to delete the warning too.

An SSH certificate authority ends both. The CA signs each host’s key, and clients trust the CA, not a list of fingerprints. The CA signs each user’s key for a few hours, and servers trust the CA, not a list of keys. Nothing to accept on first connect, nothing to clean up when someone goes, and a user certificate that dies by itself by tomorrow.

This is a different setup from the X.509 side of step-ca covered in Your Own Certificate Authority: step-ca In A Container, which is why it has its own post. Same container, different keys, different files on every machine. Everything here was run with step-ca 0.30.2 and OpenSSH 10.0 in a Podman pod.

Without a CAWith step-ca
A client meets a new serverShows a fingerprint, you type yesChecks the host certificate against the CA, says nothing
A server is rebuiltEvery client warns the key changedSign the new key, clients never notice
A user gets accessTheir key goes in authorized_keys on each serverThe CA signs their key, every server accepts it
A user leavesFind and delete their key on every serverStop signing; their certificate is dead within 16 hours
What each server keepsOne authorized_keys per userOne CA public key
Who signs what, and who trusts whatstep-cahost CA keyuser CA keyClientuser cert, 16 hourstrusts the host CAServerhost cert, 30 daystrusts the user CAsigns user keysigns host keyssh, no prompteach checks theother's cert
The CA signs both sides. The client only has to trust the host CA key, and the server only has to trust the user CA key. Neither keeps a list of the other’s keys.

Switching The SSH CA On

The SSH keys are made when step-ca is first set up, or not at all. Set DOCKER_STEPCA_INIT_SSH=true on the first start1, and the log shows two extra keys:

SSH Host CA Key: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAy...
SSH User CA Key: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAy...
KeySignsWho trusts it
Host CAEach server’s host keyEvery client, in known_hosts
User CAEach person’s login keyEvery server, in sshd_config

It also adds an sshpop provisioner, which is what lets a host renew its own certificate later. If the CA is already running without SSH, there is no switch to flip. It is a new CA.

Signing The Host

Take the server’s existing host key, sign it, and give the certificate back to the server:

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

The order matters. With the provisioner flags after the hostname and key file, step refused it:

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

Put every flag first and it signs. The certificate, as ssh-keygen -L reads it:

FieldValue
Typeecdsa-sha2-nistp256-cert-v01@openssh.com host certificate
Key IDnas.home.example
Principalsnas.home.example
Valid30 days, 2026-10-09T18:34:30 to 2026-11-08T17:35:30

The principal is the name a client must use to connect. Connect by a name that is not on the certificate and the client will not accept it, so sign every name the host answers to.

The Server

Two lines in sshd_config2:

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

HostCertificate hands the host certificate to clients, and its key “must match a private host key already specified”2. TrustedUserCAKeys is the user CA’s public key, “trusted to sign user certificates for authentication”2. Run sshd -t before restarting. A typo in either path and sshd says Could not load host certificate, which is far better found now than from a locked-out session.

There is no authorized_keys on the test server at all. ls /home/damien/.ssh/authorized_keys comes back No such file or directory, and the logins below work anyway.

The User

The CA makes the key pair and signs it in one go:

step ssh certificate damien id_ecdsa \
  --provisioner admin --provisioner-password-file prov-pass
FieldValue
Typeuser certificate
Principalsdamien
Valid16 hours
Extensionspermit-agent-forwarding, permit-port-forwarding, permit-X11-forwarding, permit-pty, permit-user-rc

Sixteen hours is the point. It covers a working day and dies overnight. Revoking a user is mostly a matter of not signing for them tomorrow.

The Client

One line in known_hosts replaces every fingerprint it would otherwise collect3:

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

That says: any host under home.example whose certificate is signed by this key is genuine. The @cert-authority marker is what makes it a CA line rather than one host’s key.

What The Test Showed

All four from a client holding nothing but the user certificate and that one known_hosts line, with StrictHostKeyChecking=yes, so nothing could be accepted on the fly:

TestResult
Signed key with its certificate, as damienlogged in as damien, exit 0
Same key, certificate removedPermission denied (publickey,password,keyboard-interactive)
Certificate used to log in as rootPermission denied: the certificate only names damien
Client without the host CA lineHost key verification failed.

The third is the one people miss. A certificate does not open every account on the box, only the principals written into it. A key in someone’s authorized_keys cannot do that.

Renewing

A host renews its own certificate with the key it already holds. That is the sshpop provisioner, which “allows a client to renew, revoke, or rekey an SSH certificate using that certificate for authentication with the CA”4. No password5:

step ssh renew --force ssh_host_ecdsa_key-cert.pub ssh_host_ecdsa_key
SerialValid until
Signed151663258693294169972026-11-08 17:35:30
Renewed66789577897973167682026-11-08 17:35:59

A new serial and a fresh 30 days. Put it on a weekly timer and the host never lapses. Reload sshd after it, or the old certificate stays in memory.

Users renew by asking again. One step ssh certificate a day, or at the first login of the day, and that is the whole job.

Where It Falls Down

CaseWhat happensWhat to do
CA downExisting certificates keep working; nobody gets a new oneRun the CA on something that stays up; users have 16 hours of slack
Host certificate lapsesClients refuse the host, as if it were a strangerA renewal timer, and sshd reloaded after it
A user must go today, not tomorrowTheir certificate works until it expiresRevokedKeys in sshd_config takes a revoked-keys list2
Hostname not in the principalsClient refuses the hostSign every name it answers to
SSH CA not made at setupNo way to add itA new CA, set up with SSH on
The CA’s SSH keys stolenWhoever has them can log in anywhere and be any hostKeep the CA on its own box, backed up, offline if you can

The last one is the trade. Every authorized_keys file was its own small risk. Now there is one large one, in one place. That is better, because one place can be watched and locked down, but only if somebody does.

Trust Where It Can Be Checked

The fingerprint prompt and the authorized_keys file have something in common. Both ask a person to make a trust decision with no information, every day, forever. People are not good at that and never will be, so they stop deciding and start typing yes.

A CA moves that decision to one place, once, where it can be done properly. That is the whole idea, and it is the same one behind every standard worth having: do the careful thing once, write it down, and stop asking people to do it in their heads a hundred times a week. The machine checks the signature. You check the CA. That is a division of labour that holds up.


  1. smallstep/certificates — docker/entrypoint.sh — DOCKER_STEPCA_INIT_SSH=true adds --ssh to 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 — the “@cert-authority” marker “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 — the command used to renew the host certificate. ↩︎