tutorials6 min read
SSH the Grown-Up Way: Certificate Auth, No Passwords, Revocable Access
Your servers trust one CA public key instead of a pile of authorized_keys files — and every credential expires on its own. OpenSSH has had this since 5.4; here's how to actually turn it on.
Two CA key pairs (user + host), one signed cert per person with a short validity window and explicit principals, TrustedUserCAKeys on every server — and you can revoke access before expiry via KRLs instead of editing authorized_keys on ten boxes. Verified against the OpenSSH man pages current at review time; commands exercised on OpenSSH 9.9p1 (~1-2 hours for a 3-10 host lab, keep one break-glass static key per host while you flip switches).
Key sprawl dies and access becomes revocable. Right now, every person or deploy script that touches your lab has a line in authorized_keys on every server they can reach — and removing one means SSHing into each box to edit the file. With OpenSSH certificate auth, servers trust one CA public key instead of a pile of user keys, and every credential is a signed certificate with an explicit validity window that expires on its own. It's been in OpenSSH since 5.4 — most homelabs just never turned it on.
Before you start//
- Any Linux hosts running OpenSSH (certificates supported since 5.4). This procedure was verified against the current sshd_config(5) and ssh-keygen(1) man pages as published at review time (page dates Sep 2 / Aug 7, 2026), and every command below was exercised on OpenSSH 9.9p1 — if you're older than that,
ssh -Vfirst; anything ≥5.4 has the feature set used here. - Comfort with the command line. ~1–2 hours for a 3–10 host lab.
- Keep one break-glass static key per host in
authorized_keysuntil you've verified cert-only login works — then remove it. Don't flip switches and pray.
Step 1: Generate two CA key pairs on a trusted box//
ssh-keygen -t ed25519 -f user_ca # signs USER keys (people, deploy scripts)
ssh-keygen -t ed25519 -f host_ca # signs HOST keys (your servers' identities)
Why two CAs: separate trust roots so compromising one doesn't compromise both directions — a stolen user_ca lets an attacker log in anywhere; a stolen host_ca lets an attacker impersonate your servers. Store the private keys offline or in your vault: each CA key is access to everything that trusts it. (The public halves — user_ca.pub, host_ca.pub — are what get distributed.)
Step 2: Sign each server's host key, retire the fingerprint prompts//
On a trusted box, for each server:
ssh-keygen -s host_ca -I <hostname> -h -n <hostname>,<fqdn> -V +52w /etc/ssh/ssh_host_ed25519_key.pub
That produces ssh_host_ed25519_key-cert.pub. Install it next to the host key, then on the server set it in sshd_config(5):
HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub
On every client, add one line to ~/.ssh/known_hosts:
@cert-authority *.yourlab.example <contents of host_ca.pub>
Why: the -h flag makes it a host certificate, -n binds it to the exact hostnames it's valid for (a cert can't be used to impersonate nas.yourlab.example if it was signed only for router), and -V +52w gives it a year — re-sign on rotation. The @cert-authority line means clients now validate your servers against your CA instead of prompting on every new box. TOFU fingerprint prompts die.
Step 3: Sign user keys with principals and short validity//
For each person (or deploy identity), sign their existing key — no new keypair needed if they already have one:
ssh-keygen -s user_ca -I alice -n alice,deploy -V +8h ~/.ssh/id_ed25519.pub
That drops id_ed25519-cert.pub next to the public key; ssh picks it up automatically. No key ever lands in any server's authorized_keys.
Why -n alice,deploy: that's the cert's principal list — which local accounts this credential may map to. And note what happens if you omit -n entirely: per sshd_config(5), certificates that lack a principals list are not permitted via TrustedUserCAKeys at all, so you end up with a signed key that matches nobody — a dead credential that's easy to miss in an audit. Never sign without it (that's failure mode #2 below).
Step 4: Point every server at the CA, and gate each account//
On each server's sshd_config:
TrustedUserCAKeys /etc/ssh/user_ca.pub
AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u
%u is a token that expands to the username being authenticated — so /etc/ssh/auth_principals/alice lists which principals may log in as alice. For an actual account, put one line per allowed principal:
# /etc/ssh/auth_principals/alice
alice
deploy
Why: without AuthorizedPrincipalsFile, the cert's principal list is the only gate — any CA-signed cert whose principals happen to match a local username maps straight through. The per-host file lets host A allow deploy while host B (the NAS) doesn't, even though both trust the same CA.
Step 5: Cut the old ways, add revocation//
PermitRootLogin no
PasswordAuthentication no
RevokedKeys /etc/ssh/revoked_keys.krl
Build KRLs when you need to revoke before expiry — per ssh-keygen(1), -k builds a Key Revocation List:
# revoke a specific cert or key:
ssh-keygen -k -f revoked.krl path/to/offender.pub
# to revoke a certificate by serial number or key ID instead, feed a KRL spec (e.g. "serial:12345") and pass the signing CA with -s
# then distribute revoked.krl to every server (it's just a file)
The PermitRootLogin no line is the same move as step 1 of the router hygiene drill — here it means root goes through the same signed-cert path as everyone else. Why KRLs: expiry handles planned offboarding; a KRL handles "the laptop got stolen at 9am" without waiting for the cert's -V window to close or SSHing into every box to edit authorized_keys.
Verify it worked (non-negotiable)//
- Watch the cert get accepted:
ssh -v alice@host— the verbose output shows the certificate being offered and accepted, not a password prompt. - Delete the account's
authorized_keysentry for that user; confirm cert-only login still works (this is the moment you know the break-glass key can go). - Prove the principal gate: sign a test cert only for principal
deploy, then attempt to log in asalicewith it — it should fail, and vice versa. (Quick variant:ssh -vshows which principals were offered.) - Inspect any cert you doubt —
-Lfrom ssh-keygen(1) prints the full certificate including principals and validity window:
ssh-keygen -L -f id_ed25519-cert.pub
# ... Valid: from 2026-09-08T01:36 to 2026-09-08T09:37, Principals: alice deploy
That last one is your audit tool — a stray cert found on disk tells you exactly who it was for and when it stopped being valid.
When it breaks//
- Clock skew = silent failure. Certs have validity windows; if the server's clock is behind the cert's
valid aftertimestamp or ahead of its expiry, auth fails with no dramatic error — ssh just ends withPermission denied (publickey). NTP on every host is mandatory, not recommended. - Empty principals list = a dead credential. You signed without
-n. Per sshd_config(5), certificates that lack a list of principals "will not be permitted for authentication usingTrustedUserCAKeys", so the user just getsPermission denied (publickey)with no obvious reason. Find it withssh-keygen -L, re-sign with an explicit principal list, and revoke the old cert via KRL if it was ever distributed. - Forgot
AuthorizedPrincipalsFile— now any CA-signed principal that matches a local account name maps through on every server. Add the directive (Step 4), restart sshd, re-verify with test #3 above. - CA private key compromise = attacker can mint certs for anyone. Rotation means updating
TrustedUserCAKeyson all servers and the@cert-authorityentries on all clients — OpenSSH has no intermediate CAs, so trust both old and new CA during cutover, then drop the old one. - Restarting sshd kills your session. Do it over a console (or IPMI/iDRAC), or with an already-open second session you're not using for the restart.