← All runbooks in Secrets, PKI & Certificates
Runbook: Rotate an SSH Certificate Authority
1 · Prerequisites
Confirm every item is in place before any state change.
- secrets-pki-lab-17-build-ssh-certificate-authority
- secrets-pki-lab-18-issue-and-revoke-ssh-certificates
- Console, serial or out-of-band access to every host in scope, independent of SSH, confirmed working before the first change
- A complete inventory of the hosts that trust the user CA and the clients that trust the host CA, including the ones nobody maintains
- Configuration management or an equivalent mechanism that can place a file on every host and report which hosts it failed to reach
- Authority to change sshd configuration in the maintenance window, and a named approver for the withdrawal step
- A change record, because the withdrawal step is the one that can lock an entire estate out of itself
2 · Pre-checks
Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.
- · Prove out-of-band access works before you touch SSH. Open a console session on at least one host in each group and leave it open. An estate that can only be reached over SSH has no rollback path for an SSH trust change, and discovering that after the reload is too late.
- · Establish which authority you are rotating. The user CA is trusted by servers through
TrustedUserCAKeys. The host CA is trusted by clients through an@cert-authorityline inknown_hosts. They have different populations and different failure modes. Rotate one, finish it, then start the other. - · Record the current trust anchors and their fingerprints.
ssh-keygen -l -f /etc/ssh/ca/user_ca.pubgives you the fingerprint you will match against server log lines later, and the value you will restore in a rollback. - · Confirm the inventory is complete. Any host that does not receive the new CA public key during the overlap will stop accepting logins the moment the old CA is withdrawn. The hosts nobody maintains are precisely the ones that will be missed.
- · Check the revocation files before you rely on them. If the file named by
RevokedKeysis not readable, public key authentication is refused for every user on that host. If the file named byRevokedHostKeysdoes not exist or is not readable, host authentication is refused for every host on that client. - · Confirm the certificate lifetimes currently in issue. The overlap period must outlast the longest certificate signed by the old authority, or the withdrawal step will invalidate credentials that had not yet expired.
- · Confirm no automation issues certificates outside the process you are about to change. A forgotten signing job that keeps using the old key will quietly extend the overlap for as long as it runs.
3 · Procedure
Execute each step in order. Verify the expected output of a step before moving to the next.
- 1Freeze issuance from the outgoing authority and announce the window. Every certificate signed from this point onwards extends the overlap you will have to wait out, so stop the signing path first and confirm it is stopped.
- 2Generate the new authority key on the host that will hold it, and never copy the private half anywhere else. Protect it with a passphrase or a hardware token according to your standard. Publish only the public half.
- 3Distribute the new CA public key as an additional trust anchor, alongside the old one. For a user CA, the file named by
TrustedUserCAKeysaccepts one key per line, so both authorities can be listed at once. For a host CA, add a second@cert-authorityline to the clients rather than editing the existing one. - 4Write the trust file atomically, never in place. Build the new content beside the target and move it over in one operation. A half-written trust file on a running server is a lockout with a very short window and a very long recovery.
- 5Validate the configuration before reloading anything.
sshd -tparses the configuration and exits non-zero if it cannot. Reload only after it is clean, and verify with a brand new connection from a second terminal rather than by reconnecting the session you are depending on. - 6Prove that both authorities are accepted before you issue anything from the new one. Authenticate with an existing certificate signed by the old CA and confirm it still works. The overlap only exists if it has been demonstrated.
- 7Reissue certificates from the new authority with an explicit validity window and an explicit serial.
ssh-keygen -s new_user_ca -I "alice@runbook-lab" -n deploy -V -5m:+1h -z 1001 alice.pubsets identity, principal, lifetime and serial. Without-Va certificate is valid from the Unix epoch to the distant future; without-zthe serial defaults to zero and cannot later be revoked by serial number. - 8Confirm each reissued certificate names the new authority.
ssh-keygen -L -f alice-cert.pubprints aSigning CAfingerprint. Compare it against the fingerprint of the new CA public key, not against the one you hoped for. - 9Verify from the server side which authority actually authorised the login. With
LogLevel VERBOSE, sshd names the certificate identity, the serial, the CA fingerprint and the trust file it came from. That log line is the only independent proof that the new authority is in use. - 10Reissue host certificates and confirm clients accept them without a first-contact prompt. A client that suddenly prompts to accept a host key has not received the new host CA, and the prompt is the warning, not the inconvenience.
- 11Wait out the remaining lifetime of every certificate signed by the old authority. The overlap ends when the last one expires or is reissued, whichever is later. Shortening this window is the most common way a rotation becomes an outage.
- 12Withdraw the old authority by removing its line from the trust files, atomically, one group at a time. Verify each group before starting the next, and keep the console session open throughout.
- 13Revoke the old authority explicitly rather than only removing trust. Add its key to the key revocation list so a stray copy of the old trust file cannot quietly restore it, and mark it
@revokedin clientknown_hostswhere it was a host CA. - 14Confirm the old authority now fails, and read the server log to see why. A refusal you cannot explain from the log is not a completed rotation.
4 · Verification
Confirm the procedure actually fixed the problem.
- ✓A certificate signed by the new authority authenticates successfully from a client that has never held a certificate from the old one.
- ✓The sshd log at
LogLevel VERBOSEnames the new CA fingerprint and the trust file it was read from, for a login performed after the change. - ✓
ssh-keygen -L -fon a reissued certificate shows aSigning CAfingerprint matchingssh-keygen -l -fon the new CA public key. - ✓A client with only the new host CA in
known_hostsconnects with no first-contact prompt, proving the host certificate chain of trust reaches it. - ✓After withdrawal, a certificate signed by the old authority is refused, and the server log explains the refusal rather than leaving it to inference.
- ✓Configuration management reports every host in the inventory as converged, and the count matches the inventory rather than the number of hosts that answered.
- ✓An out-of-band console login still works on a sample host from each group, confirming the fallback path was not damaged by the change.
- ✓The file named by
RevokedKeysis present and readable on every host, verified after the change rather than before.
5 · Rollback
If verification fails, undo the procedure in reverse order.
- ↶If the reload breaks authentication, restore the previous trust file from the copy taken in the pre-checks, run
sshd -t, and reload again from the console session you kept open. - ↶If the withdrawal step locks users out, re-add the old CA public key line to the trust file and reload. This is why the old public key is retained until the rotation is signed off.
- ↶If a revocation list was deployed and public key authentication now fails for everyone on a host, check that the file is present and readable; an unreadable revocation file refuses every user by design.
- ↶If clients begin prompting to accept host keys, restore the previous
known_hostscontent rather than instructing anyone to accept the prompt. - ↶If only part of the estate converged, stop the rollout, leave both authorities trusted, and treat the divergence as a configuration management incident rather than pressing on.
- ↶If the new CA private key is suspected to have been mishandled at any point, abandon the rotation, destroy the new key, and start again with a fresh one. A CA key with an uncertain history is not a CA key.
6 · Escalation
When the runbook isn't enough, contact:
- · Any host cannot be reached out of band and is not converged: stop the withdrawal step entirely and escalate to the owner of that host, because withdrawing trust will strand it.
- · Configuration management cannot enumerate the estate: escalate to the platform owner. A rotation without a reliable inventory is a lockout waiting for a schedule.
- · The old authority private key is believed to be compromised: this is no longer a rotation, it is an incident. Escalate to the security owner and shorten the overlap deliberately, accepting the disruption.
- · Third-party or vendor systems trust the CA and cannot be updated in the window: escalate to the service owner before the withdrawal, not after.
- · A host is discovered still issuing certificates from the old authority: escalate to whoever owns that automation, because the overlap cannot end until it stops.
- · Users report intermittent failures rather than consistent ones: stop and escalate, because intermittent SSH trust failures usually mean the estate is not converged and you are seeing two populations.
An SSH certificate authority is a single key with a single signature and no
structure underneath it. There are no intermediates, no chains, no CRL and
no OCSP. A server decides whether to accept a user certificate by looking
up the signing key in the file named by TrustedUserCAKeys; a client
decides whether to accept a host certificate by looking up the signing key
in an @cert-authority line in known_hosts. That is the whole
mechanism, and it is why rotation is a distribution problem rather than a
cryptography problem.
Because the trust anchor is a file on every participant, the only safe rotation has an overlap in the middle. Both authorities are trusted at once, everything is reissued under the new one, and only then is the old one withdrawn. Reversing that order, or shortening the overlap because the change window is tight, locks people out of the hosts they need in order to fix it.
Rotate one authority at a time. A user CA and a host CA have different populations, different files and different symptoms, and doing both in one window means every failure has two candidate causes.
When this runbook applies, and when it does not
It applies to a planned replacement of an SSH user or host certificate authority across an estate that is already using certificates, with a maintenance window and an inventory.
It does not apply when:
- The CA private key is believed to be compromised. An overlap gives an attacker a window in which their forged certificates are still accepted. That is an incident with a deliberately short and disruptive cutover, not a scheduled rotation.
- The estate is not using certificates yet. Rolling out certificates
for the first time is a build project with a pilot group. Running a
rotation procedure against
authorized_keysfiles achieves nothing. - You cannot reach the hosts out of band. Every step below assumes a path that does not depend on the trust you are changing. Without it there is no rollback, only an incident.
- The inventory is incomplete. A rotation is only as good as the list of participants. Withdrawing trust from an estate you cannot enumerate strands whatever you missed.
Blast radius
| Action | Reversible? | What it costs if wrong |
|---|---|---|
| Generating the new CA key | Yes | Nothing until it is published. |
| Adding the new CA as a second trust anchor | Yes | Two authorities are trusted, which is the intended state. |
| Reloading sshd after a bad edit | Only from the console | New logins fail on that host until the file is fixed. |
Issuing certificates without -V | No | Valid from the Unix epoch to the distant future, and unbounded. |
Issuing certificates without -z | No | Serial defaults to zero and cannot be revoked by serial number. |
| Deploying an unreadable revocation file | Only from the console | Public key authentication is refused for every user on that host. |
| Withdrawing the old CA early | Only from the console | Every unexpired certificate signed by it stops working at once. |
Step 1 - Record what is trusted now, and match it to a fingerprint
The fingerprint is how you will recognise the right authority in a server log later, so capture it before anything changes.
CA_DIR=/etc/ssh/ca
# Fingerprint each authority. Keep both lines in the change record.
ssh-keygen -l -f "$CA_DIR/user_ca_2026.pub"
ssh-keygen -l -f "$CA_DIR/user_ca_2027.pub"
cp -a /etc/ssh/ca/user_ca_trusted "/root/user_ca_trusted.pre-rotation"
The fingerprint printed here is the same string sshd writes into its
authentication log and the same string ssh-keygen -L reports on an issued
certificate, which is what makes the three observations comparable. Keep
the copy of the trust file: it is the rollback.
The server configuration that consumes it is small enough to read in full, and every line in it matters during a rotation:
HostKey /etc/ssh/ssh_host_ed25519_key
HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub
TrustedUserCAKeys /etc/ssh/ca/user_ca_trusted
RevokedKeys /etc/ssh/ca/revoked.krl
PubkeyAuthentication yes
PasswordAuthentication no
LogLevel VERBOSE
TrustedUserCAKeys names a file that holds one public key per line, which
is exactly the property the overlap depends on. LogLevel VERBOSE is what
makes the verification step possible; a rotation performed at the default
log level cannot be proved.
Step 2 - Publish the new authority alongside the old one
Build the file beside its target and move it into place in a single operation. A trust file caught half-written by an incoming connection is a lockout.
CA_DIR=/etc/ssh/ca
cat "$CA_DIR/user_ca_2026.pub" "$CA_DIR/user_ca_2027.pub" > "$CA_DIR/user_ca_trusted.new"
chmod 0644 "$CA_DIR/user_ca_trusted.new"
mv "$CA_DIR/user_ca_trusted.new" "$CA_DIR/user_ca_trusted"
For a host CA the equivalent lives on the clients, and again the new line is added rather than substituted:
@cert-authority *.lab.example ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...
@cert-authority *.lab.example ssh-ed25519 AAAAC3NzaC1lZDI1NTE6...
Only one marker is permitted per line, and @revoked on a key means it is
never accepted for authentication or as a certification authority. That
second property is what makes revocation of an old host CA meaningful
rather than advisory.
Step 3 - Validate, reload, and test from a different terminal
SSHD_UNIT=ssh # Debian and Ubuntu; use sshd on the Red Hat family
sshd -t
systemctl reload "$SSHD_UNIT"
Keep the console session open and open a brand new SSH connection from a second terminal to test. Reconnecting the session you are relying on is not a test; it is the thing you are protecting.
Step 4 - Prove the overlap before issuing anything new
Authenticate with an existing certificate signed by the outgoing authority and confirm it still works. If it does not, the trust file is wrong and you have just discovered it while the old authority is still deployed, which is the cheapest possible moment.
Step 5 - Reissue from the new authority, with a window and a serial
$ ssh-keygen -s user_ca -I "alice@runbook-lab" -n deploy -V -5m:+1h -z 1001 alice.pubSigned user key alice-cert.pub: id "alice@runbook-lab" serial 1001 for deploy valid from 2026-08-26T21:17:09 to 2026-08-26T22:22:09Illustrative output
-V is not optional. Without it a certificate is valid from the Unix epoch
to the distant future, which converts a short-lived credential system into
a permanent one. -z is equally load-bearing: the default serial is zero,
and a revocation list revokes certificates by serial number for values that
do not include zero, so a certificate issued without a serial can only be
revoked by key identity.
Host certificates use -h, and their principals are hostnames:
ssh-keygen -s host_ca -I "sshd.lab.example" -h -n sshd.lab.example \
-V -5m:+52w /etc/ssh/ssh_host_ed25519_key.pub
Wildcards in principals are supported for host certificates and are not
supported for user certificates. Since OpenSSH 10.3 an empty principals
list matches nothing at all rather than acting as a wildcard, and
certificates that carry no principals are never accepted through
TrustedUserCAKeys.
Step 6 - Confirm the certificate names the authority you expect
$ ssh-keygen -L -f alice-cert.pubalice-cert.pub:
Type: ssh-ed25519-cert-v01@openssh.com user certificate
Public key: ED25519-CERT SHA256:/hOjaMxlXaTaWekzkFCIxR4PRU/c7eLkooB565Y7X6I
Signing CA: ED25519 SHA256:6x39cg8OAp7PZgisHreCLRO7g0vp6s0VZIw+DcTgKtE (using ssh-ed25519)
Key ID: "alice@runbook-lab"
Serial: 1003
Valid: from 2026-08-26T19:21:30 to 2026-08-26T20:21:30
Principals:
deploy
Critical Options: (none)
Extensions:
permit-X11-forwarding
permit-agent-forwarding
permit-port-forwarding
permit-pty
permit-user-rcIllustrative output
Compare the Signing CA fingerprint against the new CA public key, not
against the one you intended to use. During an overlap both keys are
present on the signing host and the wrong one is one shell history entry
away.
Step 7 - Read the server log for the decisive evidence
The client says it connected. The server says which authority let it in, and which file that authority was read from. Only the second answers the question this rotation exists to settle.
Accepted certificate ID "alice@runbook-lab" (serial 1001) signed by ED25519 CA SHA256:6x39cg8... via /etc/ssh/ca/user_ca_trusted
Accepted publickey for deploy from 192.0.2.41 port 51328 ssh2: ED25519-CERT SHA256:... ID alice@runbook-lab (serial 1001) CA ED25519 SHA256:...
Those two lines are the shape sshd emits at LogLevel VERBOSE, with the
file path and the source address of this procedure substituted in. The
detail that matters is via, which names the trust file the accepted
authority was read from, and the CA fingerprint immediately before it.
Since OpenSSH 10.0 the server is split across sshd, sshd-session and
sshd-auth, so these lines may be attributed to any of the three depending
on how your logging is configured. Search on the message, not on the
process name.
Step 8 - Withdraw the old authority, one group at a time
Only when every certificate signed by the outgoing authority has been reissued or has expired. Remove its line from the trust file using the same atomic write as before, verify the group, and only then move to the next.
Then revoke it, so that a stray copy of an old trust file cannot silently restore it:
printf 'serial: 1001\n' | ssh-keygen -k -f revoked.krl -s user_ca_2026.pub -z 1 -
ssh-keygen -Qf revoked.krl alice-cert.pub
A revocation list can revoke by serial number, by serial range, by key identity or by the key itself. The test command exits zero only when nothing presented to it has been revoked, which makes it usable directly in a verification script.
Common pitfalls
| Symptom | Cause | Action |
|---|---|---|
Certificate invalid: name is not a listed principal | Certificate principal does not include the login name | Reissue with the correct -n value |
Certificate invalid: expired | Overlap ended before reissue, or clock skew | Check both clocks before reissuing |
| Certificate never expires | Signed without -V | Reissue with an explicit window and revoke the old one |
| Cannot revoke a certificate by serial | Serial defaulted to zero | Revoke by key identity, and always pass -z in future |
| Public key authentication fails for everyone | Revocation file missing or unreadable | Restore the file from the console; check mode and ownership |
| Client suddenly prompts for host key | The new host CA never reached that client | Fix the @cert-authority line, never accept the prompt |
| Login works but you cannot say which CA allowed it | LogLevel is below VERBOSE | Raise it before the next group |
Verification
The rotation is verified when a certificate signed by the new authority
authenticates from a client that never held one from the old authority, and
when the server log for that login names the new CA fingerprint and the
trust file it was read from. Those two together are the independent
channel; the signing command’s own success message is not. Confirm that
ssh-keygen -L on a reissued certificate reports a Signing CA
fingerprint matching ssh-keygen -l on the new public key. Confirm a
client carrying only the new host CA connects with no first-contact prompt.
After withdrawal, confirm that a certificate signed by the old authority is
now refused and that the server log explains the refusal in words rather
than leaving you to infer it. Finally, check that configuration management
reports the full inventory as converged, and that an out-of-band console
login still works on a sample host in each group, because the fallback path
is the one thing that must never be collateral damage.
Rollback
Every step has the same rollback: restore the previous file from the copy
taken in the pre-checks, run sshd -t, and reload from the console session
you deliberately kept open. Adding the old CA public key line back to the
trust file undoes a premature withdrawal in seconds, which is why the old
public key is retained until the rotation is signed off rather than deleted
at the moment trust is removed. If a revocation file has locked out public
key authentication on a host, the fix is on the console: confirm the file
exists, is readable by the server, and is a valid revocation list. If
clients have begun prompting for host keys, restore the previous
known_hosts content rather than telling anyone to accept the prompt. If
only part of the estate converged, stop, leave both authorities trusted,
and treat the divergence as a configuration management problem. If the new
private key was handled in any way you cannot fully account for, destroy it
and begin again with a fresh one.