← News
/PLAYBOOK · 2026-09-11

SSH Certificate Auth in Practice: Replace Long-Lived Keys with an SSH CA

Replace per-host pubkey distribution with SSH certificate auth: stand up User CA and Host CA, issue 8-hour short-lived login certs, configure TrustedUserCAKeys, AuthorizedPrincipalsFile, and KRL revocation. Covers validity windows, clock skew, and principal mismatch. Commands are copy-paste ready.

Scope

Once server count hits dozens, stuffing pubkeys into every authorized_keys fails in three ways: leftover keys after offboarding, no expiry so a leak stays valid forever, and new hosts needing per-machine pubkey rollout. SSH certificate auth introduces a self-hosted CA. Short-lived certs replace long-lived keys. Per-host authorization becomes a one-time CA issue. This is the standard pattern for scaled SSH access control. Applies to ops teams, cloud host fleets, bastions, and CI/CD deploy accounts.

Prerequisites

How it works

Trust moves from each server's authorized_keys list up to a CA public key. Targets only need TrustedUserCAKeys pointing at the CA pubkey. Any cert signed by that CA private key, within validity and principal scope, logs in. No user pubkeys need to live on the server.

Procedure

1. Generate CA keypairs

On the issuer, create User CA and Host CA. Private keys must be mode 600, never committed, never shipped with code.

`sudo mkdir -p /etc/ssh/ca && sudo chmod 700 /etc/ssh/ca
sudo ssh-keygen -t ed25519 -f /etc/ssh/ca/user_ca -C "user-ca" -N ""
sudo ssh-keygen -t ed25519 -f /etc/ssh/ca/host_ca -C "host-ca" -N ""
sudo chmod 600 /etc/ssh/ca/user_ca /etc/ssh/ca/host_ca
ls -l /etc/ssh/ca/`

Flags: -t ed25519 sets the algorithm; -N "" is an empty passphrase (for automated issuance; the private key file itself must stay tightly protected).

2. Trust the User CA on targets

Distribute user_ca.pub to /etc/ssh/ on each server and declare trust in sshd_config.

`# Execute on each target server
sudo scp ca_server:/etc/ssh/ca/user_ca.pub /etc/ssh/user_ca.pub
sudo tee -a /etc/ssh/sshd_config >/dev/null <<'EOF'

# SSH certificate authentication
TrustedUserCAKeys /etc/ssh/user_ca.pub
EOF
sudo sshd -t && sudo systemctl reload sshd`

To require that cert principals exist as system accounts, add AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u and list allowed principals in that file (one per line).

3. Issue user login certs

User generates a keypair and submits the pubkey to the CA. Validity: 8 hours to 7 days recommended.

`# User side: generate keypair
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -C "alice" -N ""
# Submit id_ed25519.pub to the CA (e.g. via ticket or internal platform)

# CA side: issue certificate, 8-hour validity, principal name alice
sudo ssh-keygen -s /etc/ssh/ca/user_ca \
  -I "alice@corp-20260911" \
  -n alice \
  -V +8h \
  -O clear_allow_extensions \
  /tmp/alice.pub
# Produces /tmp/alice-cert.pub; return it to the user`

Key flags: -I is Key ID (lands in logs for audit); -n is allowed login principals, comma-separated; -V +8h is relative validity. Absolute form: -V 20260911:20260912.

User places the cert next to the private key as <keyname>-cert.pub. ssh loads it automatically:

`mv alice-cert.pub ~/.ssh/id_ed25519-cert.pub
ssh alice@web01.example.com`

4. Issue host certs (optional, strongly recommended)

`# On the target server, issue its own host certificate, 52-week validity
sudo ssh-keygen -s /etc/ssh/ca/host_ca \
  -I "host-web01" -h \
  -n web01.example.com,10.0.0.11 \
  -V +52w \
  /etc/ssh/ssh_host_ed25519_key.pub

sudo tee -a /etc/ssh/sshd_config >/dev/null <<'EOF'
HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub
EOF
sudo sshd -t && sudo systemctl reload sshd

# Client known_hosts declares the host CA to skip per-host fingerprint confirmation
echo "@cert-authority *.example.com $(cat /etc/ssh/ca/host_ca.pub)" >> ~/.ssh/known_hosts`

Verification

Inspect the cert. Principals, validity, and type must be correct:

`ssh-keygen -L -f ~/.ssh/id_ed25519-cert.pub`

Confirm sshd loaded the trusted CA:

`sudo sshd -T | grep -i trustedusercakeys
# Expected: trustedusercakeys /etc/ssh/user_ca.pub`

On login, auth logs should show cert success:

`sudo journalctl -u ssh -n 20 | grep -i "Accepted publickey"
# Expected to contain: Accepted publickey for alice ... ED25519-CERT SHA256:...`

Client debug: ssh -vvv alice@web01.example.com. Look for Offering public key and Server accepts key.

FAQ

FAQ 1: After cert auth, do old authorized_keys logins still work?

Yes. They run in parallel. sshd tries certs first, then falls back to authorized_keys. Keep the old keys during cutover. After everyone is on certs, set AuthorizedKeysFile none to force cert-only. That also kills key logins without a cert. Confirm the cert path works before flipping it.

FAQ 2: Cert is unexpired but login says Permission denied (publickey)?

Check in this order:

Fastest debug: client ssh -vvv plus server sudo /usr/sbin/sshd -ddd -p 2222.

FAQ 3: How to revoke an issued, unexpired cert?

SSH certs cannot be revoked individually. Short TTL usually means wait it out. For immediate revoke, use a KRL:

`# CA side: generate revocation list
sudo ssh-keygen -k -f /etc/ssh/ca/revoked_keys -s /etc/ssh/ca/user_ca /tmp/alice.pub
# Load on the target server
sudo tee -a /etc/ssh/sshd_config >/dev/null <<'EOF'
RevokedKeys /etc/ssh/revoked_keys
EOF
sudo sshd -t && sudo systemctl reload sshd`

Revoked certs die as soon as the KRL is live. ssh -vvv shows key revoked.

Summary

SSH cert auth turns per-host pubkey distribution into CA-centralized issuance. Three wins: short TTL shrinks leak window from forever to hours; KRL revokes in one shot; Key ID ties each login to an issuance record. Rollout order: Host CA first to kill fingerprint prompts, then User CA in parallel with existing pubkeys, then disable pubkey auth. CA private keys stay offline or in a hardware module. A leaked CA key equals loss of every server.