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.
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.
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.
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.
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.
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.
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.
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 latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
Static service tokens leaked into logs and never rotated. SPIFFE identities plus SPIRE-issued SVIDs gave us short-lived certs and killed the shared-secret sprawl.
We moved 40 services off the nginx Ingress controller onto Gateway API without a single dropped connection. Here's the routing overlap trick that made it boring.
Explore more articles in this category
Our proxy topped out at 40k connections while the CPU sat half-idle. The bottleneck was kernel defaults tuned for 2009, not the hardware.
When a service is slow and every dashboard looks green, bpftrace lets you watch the kernel directly. These one-liners found our tail latency.
A cron job silently stopped running for three weeks and nobody knew until the backups were missing. systemd timers give you the logging and status cron never did.
Evergreen posts worth revisiting.