Tools: Latest: Why SSH Key Management Is Broken and How Certificates Fix It

Tools: Latest: Why SSH Key Management Is Broken and How Certificates Fix It

The Root Problem: Trust Doesn't Scale with authorized_keys

How SSH Certificates Work (The 30-Second Version)

Step-by-Step: Setting Up SSH Certificates

Step 1: Create the CA Key Pair

Step 2: Sign Host Keys (No More "Trust This Fingerprint?")

Step 3: Tell Clients to Trust the Host CA

Step 4: Sign User Keys (The Big Win)

Step 5: Tell Servers to Trust the User CA

Offboarding Is Now Trivial

The Gotchas I Ran Into

Inspecting Certificates

Prevention: Making This Sustainable

Is It Worth the Effort? If you manage more than a handful of servers, you already know the pain. Every new developer who joins the team needs their public key added to authorized_keys on every server they need access to. Someone leaves? Good luck remembering all the places their key was authorized. And let's not talk about the first time you SSH into a new server and have to decide whether to trust that host fingerprint you definitely didn't verify. SSH certificates solve all of this, and they've been baked into OpenSSH since version 5.4 (2010). Yet almost nobody uses them. Let's fix that. The traditional SSH model works fine for one person with three servers. But it falls apart fast: This isn't a hypothetical problem. It's a daily operational headache. Instead of distributing individual public keys everywhere, you create a Certificate Authority (CA) — really just another SSH key pair. Then: That's it. One trust anchor instead of a web of individual keys. If you've used TLS certificates, this mental model maps directly. You need two CAs in practice — one for signing user certificates and one for signing host certificates. Keeping them separate limits blast radius if one is compromised. These private keys are the crown jewels. Treat them like you'd treat a root TLS CA key — offline storage, restricted access, the works. Grab each server's existing host public key and sign it: This produces /etc/ssh/ssh_host_ed25519_key-cert.pub. Copy it back to the server. Then tell sshd to present the certificate. Add to /etc/ssh/sshd_config: Restart sshd and that server now proves its identity cryptographically. On each client machine (or in a shared config), add the Host CA's public key to known_hosts: Now when you SSH to any .example.com host, the client verifies the host certificate against the CA. No more blindly typing yes. No more manually managing known_hosts files. This is where it gets really good. Instead of copying user public keys to every server: This creates id_ed25519-cert.pub alongside their existing key. SSH uses it automatically. Add to /etc/ssh/sshd_config on every server: Restart sshd. That's it. Alice can now SSH into any server without her public key existing in any authorized_keys file. The server validates her certificate against the CA. When someone leaves the team, you just stop signing new certificates for them. If you're using short-lived certificates (and you should be — 8-24 hours is a good range), their access expires automatically. For immediate revocation, OpenSSH supports a revocation list: Is it as seamless as short-lived certs expiring on their own? No. But it's still one operation versus hunting through dozens of authorized_keys files. After rolling this out across a few environments, here's what bit me: Then create /etc/ssh/auth_principals/deploy containing the principals allowed to log in as the deploy user. When things go wrong (and they will during setup), you can inspect any certificate: This dumps the certificate details — principals, validity window, CA fingerprint, and any extensions. It's the first thing to check when authentication fails unexpectedly. SSH certificates are only better than authorized_keys if you actually automate the signing workflow. A few practical tips: For a solo developer with two servers? Probably not. Stick with authorized_keys. For a team of any size managing more than a few machines? Absolutely. The upfront cost is maybe an afternoon of setup. The ongoing payoff is dramatically simpler access management, proper host verification, and offboarding that doesn't keep you up at night. SSH certificates have been available for over fifteen years. The tooling is mature, the documentation is solid, and the operational benefits are real. The only mystery is why more teams aren't using them yet. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to ? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

authorized_keys # Generate the User CA key (keep this extremely safe) ssh-keygen -t ed25519 -f user_ca -C "user-ca@yourorg" # Generate the Host CA key ssh-keygen -t ed25519 -f host_ca -C "host-ca@yourorg" # Generate the User CA key (keep this extremely safe) ssh-keygen -t ed25519 -f user_ca -C "user-ca@yourorg" # Generate the Host CA key ssh-keygen -t ed25519 -f host_ca -C "host-ca@yourorg" # Generate the User CA key (keep this extremely safe) ssh-keygen -t ed25519 -f user_ca -C "user-ca@yourorg" # Generate the Host CA key ssh-keygen -t ed25519 -f host_ca -C "host-ca@yourorg" # Sign the server's host key with your Host CA # -s: signing key, -I: certificate identity (for logging) # -h: this is a HOST certificate (not user) # -n: principals (hostnames this cert is valid for) # -V: validity period ssh-keygen -s host_ca \ -I "webserver-prod-01" \ -h \ -n "webserver-prod-01.example.com,10.0.1.50" \ -V +52w \ /etc/ssh/ssh_host_ed25519_key.pub # Sign the server's host key with your Host CA # -s: signing key, -I: certificate identity (for logging) # -h: this is a HOST certificate (not user) # -n: principals (hostnames this cert is valid for) # -V: validity period ssh-keygen -s host_ca \ -I "webserver-prod-01" \ -h \ -n "webserver-prod-01.example.com,10.0.1.50" \ -V +52w \ /etc/ssh/ssh_host_ed25519_key.pub # Sign the server's host key with your Host CA # -s: signing key, -I: certificate identity (for logging) # -h: this is a HOST certificate (not user) # -n: principals (hostnames this cert is valid for) # -V: validity period ssh-keygen -s host_ca \ -I "webserver-prod-01" \ -h \ -n "webserver-prod-01.example.com,10.0.1.50" \ -V +52w \ /etc/ssh/ssh_host_ed25519_key.pub /etc/ssh/ssh_host_ed25519_key-cert.pub /etc/ssh/sshd_config # Present our signed host certificate to connecting clients HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub # Present our signed host certificate to connecting clients HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub # Present our signed host certificate to connecting clients HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub known_hosts # Add to ~/.ssh/known_hosts or /etc/ssh/ssh_known_hosts # The @cert-authority directive tells SSH to trust any host # certificate signed by this CA for matching hosts @cert-authority *.example.com ssh-ed25519 AAAA...your-host-ca-public-key... # Add to ~/.ssh/known_hosts or /etc/ssh/ssh_known_hosts # The @cert-authority directive tells SSH to trust any host # certificate signed by this CA for matching hosts @cert-authority *.example.com ssh-ed25519 AAAA...your-host-ca-public-key... # Add to ~/.ssh/known_hosts or /etc/ssh/ssh_known_hosts # The @cert-authority directive tells SSH to trust any host # certificate signed by this CA for matching hosts @cert-authority *.example.com ssh-ed25519 AAAA...your-host-ca-public-key... .example.com known_hosts # Sign a developer's public key with the User CA # -n: principals (usernames they can log in as) # -V: valid for 8 hours — short-lived certs are the sweet spot ssh-keygen -s user_ca \ -I "alice@yourorg" \ -n "alice,deploy" \ -V +8h \ ~/.ssh/id_ed25519.pub # Sign a developer's public key with the User CA # -n: principals (usernames they can log in as) # -V: valid for 8 hours — short-lived certs are the sweet spot ssh-keygen -s user_ca \ -I "alice@yourorg" \ -n "alice,deploy" \ -V +8h \ ~/.ssh/id_ed25519.pub # Sign a developer's public key with the User CA # -n: principals (usernames they can log in as) # -V: valid for 8 hours — short-lived certs are the sweet spot ssh-keygen -s user_ca \ -I "alice@yourorg" \ -n "alice,deploy" \ -V +8h \ ~/.ssh/id_ed25519.pub id_ed25519-cert.pub /etc/ssh/sshd_config # Trust any user certificate signed by our User CA TrustedUserCAKeys /etc/ssh/user_ca.pub # Trust any user certificate signed by our User CA TrustedUserCAKeys /etc/ssh/user_ca.pub # Trust any user certificate signed by our User CA TrustedUserCAKeys /etc/ssh/user_ca.pub authorized_keys # Create or update the revoked keys file ssh-keygen -k -f /etc/ssh/revoked_keys -s user_ca id_ed25519.pub # Create or update the revoked keys file ssh-keygen -k -f /etc/ssh/revoked_keys -s user_ca id_ed25519.pub # Create or update the revoked keys file ssh-keygen -k -f /etc/ssh/revoked_keys -s user_ca id_ed25519.pub sshd_config RevokedKeys /etc/ssh/revoked_keys RevokedKeys /etc/ssh/revoked_keys RevokedKeys /etc/ssh/revoked_keys authorized_keys valid after AuthorizedPrincipalsFile # In sshd_config, restrict which principals can log into this server AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u # In sshd_config, restrict which principals can log into this server AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u # In sshd_config, restrict which principals can log into this server AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u /etc/ssh/auth_principals/deploy ssh-keygen -L -f id_ed25519-cert.pub ssh-keygen -L -f id_ed25519-cert.pub ssh-keygen -L -f id_ed25519-cert.pub authorized_keys authorized_keys - Onboarding is manual. Every new team member's public key needs to land on every relevant server. That's N users × M servers worth of configuration. - Offboarding is terrifying. Did you remove that key from every server? Are you sure? What about that one bastion host nobody remembers setting up in 2021? - Host verification is a joke. Be honest — when was the last time you actually verified an SSH host fingerprint? You type yes and move on like the rest of us. - No expiration. SSH keys live forever by default. That contractor who helped out for two weeks three years ago? Their key might still be valid. - The CA signs user and host public keys, producing certificates. - Servers trust the CA, not individual user keys. - Clients trust the CA, not individual host fingerprints. - Principals matter. If you sign a user cert with -n alice but the user tries to log in as deploy, it fails. Think carefully about which principals each person needs. - Clock skew kills you. Certificates have validity windows. If your server's clock is off by a few minutes and the cert's valid after timestamp is in the server's future, authentication fails silently. Use NTP. Seriously. - Don't lose the CA key. If you lose the CA private key, you need to redistribute trust from scratch. If someone else gets it, they can sign certificates for anyone. Protect it accordingly. - AuthorizedPrincipalsFile is your friend. Without it, any user certificate signed by the CA can log in as any principal. You almost certainly want to restrict this per-server. - Automate cert signing. Don't make developers email you their public keys. Build or use a signing service. Projects like step-ca from Smallstep or HashiCorp Vault's SSH secrets engine handle this well. - Keep cert lifetimes short. 8-24 hours for user certs. This dramatically reduces the risk window if a key is compromised. - Use separate CAs for users and hosts. I mentioned this earlier but it bears repeating. - Automate host cert renewal. Host certs with a 52-week lifetime should get renewed automatically, not when someone notices SSH connections failing. - Log the certificate IDs. The -I identity string shows up in auth logs. Use meaningful identifiers so you can trace access.