SSH Hardening in 2026: Keys, Certificates, and Bastion Patterns
We had 140 engineers with 300 static public keys scattered across authorized_keys files nobody could audit. Moving to SSH certificates with short TTLs made access reviewable again.
Key takeaways
- We had 140 engineers with 300 static public keys scattered across authorized_keys files nobody could audit.
- Moving to SSH certificates with short TTLs made access reviewable again.
SSH Hardening in 2026: Keys, Certificates, and Bastion Patterns#
An auditor asked a fair question: "Which engineers can SSH into the payments database host, and how would you revoke someone who left last week?" The honest answer was that I'd have to grep authorized_keys files across 60 hosts, match public keys to people by memory, and hope nobody had copied a key onto a box we'd forgotten. That's not an access control system. That's a liability with a shell prompt.
The fix isn't more careful key management. It's getting out of the static-key business entirely and issuing short-lived SSH certificates instead.
Kill password auth and weak keys first#
Before anything clever, the baseline. In /etc/ssh/sshd_config on every host:
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
AuthenticationMethods publickey
And use ed25519, not RSA. It's shorter, faster, and there's no reason to generate a 4096-bit RSA key in 2026:
ssh-keygen -t ed25519 -C "kiril@devopsness" -f ~/.ssh/id_ed25519
That's table stakes. It does nothing about the audit problem, because you're still distributing public keys to hosts and hoping the set stays clean.
Certificates replace authorized_keys#
The shift is this: instead of each host trusting a list of individual public keys, every host trusts one certificate authority. When an engineer authenticates, they don't present a raw key. They present a certificate, signed by the CA, that says who they are, which principals (roles) they hold, and when it expires. Usually 8 hours from now.
You set up a CA key once, kept offline or in a signing service:
ssh-keygen -t ed25519 -f ssh_ca -C "devopsness-ssh-ca"
Every host gets configured to trust it, and that's the entire per-host trust config, one line pointing at the CA's public key:
# /etc/ssh/sshd_config
TrustedUserCAKeys /etc/ssh/ssh_ca.pub
No more per-user keys on hosts. To grant someone access, the CA signs them a cert. To revoke access, you stop signing. Since certs expire in 8 hours, the engineer who left last week already lost access automatically, the moment their last cert expired.
Signing a cert with real constraints#
The cert carries the authorization inside it. Here's the CA signing a user's key with a principal, an expiry, and forced restrictions:
ssh-keygen -s ssh_ca \
-I "kiril@devopsness" \
-n "db-readonly,web" \
-V +8h \
-O clear -O permit-pty \
-O source-address=10.20.0.0/16 \
~/.ssh/id_ed25519.pub
The -n db-readonly,web sets the principals this cert is valid for. On the host side, authorized_principals maps principals to who's allowed, so a cert with only web can't touch a database host that requires db-readonly. The -V +8h is the expiry. The -O clear drops all default permissions, then we add back only permit-pty, so no port forwarding or agent forwarding unless explicitly granted. The source-address pins the cert to our VPN range.
In practice you don't run ssh-keygen -s by hand. We front the CA with a service (HashiCorp Vault's SSH secrets engine, or Smallstep) that authenticates the engineer against our IdP, checks group membership, and mints the cert. The human never touches the CA private key.
The bastion still earns its place#
Certificates handle identity. The bastion handles network position. Our database and internal hosts have no public SSH port at all; the only inbound SSH comes from one hardened bastion in a public subnet. Engineers use a jump config so it's transparent:
# ~/.ssh/config
Host db-*.internal
ProxyJump bastion.devopsness.net
User deploy
Host bastion.devopsness.net
HostbasedAuthentication no
IdentityFile ~/.ssh/id_ed25519
ProxyJump (the -J flag) tunnels through the bastion without leaving the target host's traffic exposed and without agent-forwarding the private key onto the bastion, which is the old insecure pattern. The bastion logs every session, and because everyone connects with a certificate carrying their identity, the logs actually name people instead of showing an anonymous deploy user.
Answering the auditor#
The second half of the auditor's question was revocation. Beyond expiry, we keep a Key Revocation List for the panic case, when you need someone gone in the next 8 minutes, not the next 8 hours:
# /etc/ssh/sshd_config
RevokedKeys /etc/ssh/revoked_keys
Push a serial number into that list and the cert is dead on next connection, no waiting for TTL.
The call we'd make#
If you have more than a dozen engineers and more than a handful of hosts, move to SSH certificates. The authorized_keys model does not scale to a number you can audit, and "grep and hope" is not a revocation story you want to give a security reviewer. The setup cost is real, you're running a CA and a signing service, but 8-hour certs turn offboarding from a manual key hunt into a non-event. Keep the bastion regardless. Certificates prove who you are; the bastion controls where you can reach, and you want both.
Get the DevOps Troubleshooting Cheat Sheet
Subscribe and get our free one-page reference for the errors that eat an afternoon — CrashLoopBackOff, OOMKilled, Terraform state locks, and more — plus new guides as we publish them.
Distroless Docker Images: Smaller, Safer Production Containers
Our node image shipped 240 CVEs, most from OS packages we never called. Moving to distroless dropped the count to single digits and cut image size by 70%.
Agent Memory: Short-Term, Long-Term, and When You Need Neither
Most agents that "need memory" actually need a smaller context window and a database. Here's how we cut a support agent's token bill by 60 percent by deleting memory.
More from Linux
Explore more articles in this category
CERN Left Red Hat for Debian: The CPU Baseline Lesson
CERN is moving accelerator control computers to Debian because RHEL raised its minimum CPU level. Check your own fleet's x86-64-vN support before the next OS upgrade does it for you.
ext4 vs XFS vs Btrfs: Choosing a Filesystem for a Server
The default filesystem your distro picks is not always the right one for your workload. Here is what actually differs and when each one wins.
journald Log Management: Retention, Filtering, and Forwarding
journald is the default log sink on every systemd distro, and most of it runs on defaults nobody chose. Here is how to actually control it.
You might have missed
Evergreen posts worth revisiting.