Skip to main content
RunBook Academy

← All runbooks in Secrets, PKI & Certificates

high riskcluster affecting~90 min

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-authority line in known_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.pub gives 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 RevokedKeys is not readable, public key authentication is refused for every user on that host. If the file named by RevokedHostKeys does 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.

  1. 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.
  2. 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.
  3. 3Distribute the new CA public key as an additional trust anchor, alongside the old one. For a user CA, the file named by TrustedUserCAKeys accepts one key per line, so both authorities can be listed at once. For a host CA, add a second @cert-authority line to the clients rather than editing the existing one.
  4. 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.
  5. 5Validate the configuration before reloading anything. sshd -t parses 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.
  6. 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.
  7. 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.pub sets identity, principal, lifetime and serial. Without -V a certificate is valid from the Unix epoch to the distant future; without -z the serial defaults to zero and cannot later be revoked by serial number.
  8. 8Confirm each reissued certificate names the new authority. ssh-keygen -L -f alice-cert.pub prints a Signing CA fingerprint. Compare it against the fingerprint of the new CA public key, not against the one you hoped for.
  9. 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.
  10. 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.
  11. 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.
  12. 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.
  13. 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 @revoked in client known_hosts where it was a host CA.
  14. 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 VERBOSE names the new CA fingerprint and the trust file it was read from, for a login performed after the change.
  • ssh-keygen -L -f on a reissued certificate shows a Signing CA fingerprint matching ssh-keygen -l -f on the new CA public key.
  • A client with only the new host CA in known_hosts connects 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 RevokedKeys is 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_hosts content 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_keys files 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

ActionReversible?What it costs if wrong
Generating the new CA keyYesNothing until it is published.
Adding the new CA as a second trust anchorYesTwo authorities are trusted, which is the intended state.
Reloading sshd after a bad editOnly from the consoleNew logins fail on that host until the file is fixed.
Issuing certificates without -VNoValid from the Unix epoch to the distant future, and unbounded.
Issuing certificates without -zNoSerial defaults to zero and cannot be revoked by serial number.
Deploying an unreadable revocation fileOnly from the consolePublic key authentication is refused for every user on that host.
Withdrawing the old CA earlyOnly from the consoleEvery 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

Configuration changeca-1, signing a user certificate with an explicit lifetime and serial
$ ssh-keygen -s user_ca -I "alice@runbook-lab" -n deploy -V -5m:+1h -z 1001 alice.pub
Signed 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:09

Illustrative 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

Read-only / Safeca-1, reading the issued certificate rather than trusting the signing command
$ ssh-keygen -L -f alice-cert.pub
alice-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-rc

Illustrative 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

SymptomCauseAction
Certificate invalid: name is not a listed principalCertificate principal does not include the login nameReissue with the correct -n value
Certificate invalid: expiredOverlap ended before reissue, or clock skewCheck both clocks before reissuing
Certificate never expiresSigned without -VReissue with an explicit window and revoke the old one
Cannot revoke a certificate by serialSerial defaulted to zeroRevoke by key identity, and always pass -z in future
Public key authentication fails for everyoneRevocation file missing or unreadableRestore the file from the console; check mode and ownership
Client suddenly prompts for host keyThe new host CA never reached that clientFix the @cert-authority line, never accept the prompt
Login works but you cannot say which CA allowed itLogLevel is below VERBOSERaise 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.

References

  1. ssh-keygen(1) manual page
  2. sshd_config(5) manual page
  3. ssh_config(5) manual page
  4. sshd(8) manual page, including the known_hosts file format
  5. OpenSSH release notes
  6. PROTOCOL.krl, the key revocation list format
  7. draft-ietf-sshm-cert, the SSH certificate format specification