Skip to content
ecdsa.com
← All articles

SSH certificates vs authorized_keys: CAs and principals

authorized_keys scales by copying keys to every server and never forgetting to remove them. OpenSSH certificates replace that with one trusted CA, short-lived signed keys and names instead of fingerprints. Here is the full setup, tested on OpenSSH 10.0, with every failure it produces.

Short answer. With authorized_keys, every server lists every public key allowed to log in, and access lasts until someone deletes the line. With OpenSSH certificates, servers trust one user CA (TrustedUserCAKeys). Users get their own key signed with ssh-keygen -s, which adds an identity (-I), the accounts it may log into (principals, -n) and a validity window (-V). Clients trust one host CA (@cert-authority in known_hosts) instead of accepting each host's fingerprint on first use. Short-lived certificates make offboarding automatic, and Key Revocation Lists (RevokedKeys) handle the rest. The price is that you now run a CA and must protect its key.

What authorized_keys does not do

~/.ssh/authorized_keys is simple and has no moving parts, which is why it is everywhere. Its limits show up with more people and more servers:

  • Access never expires. A key added for a contractor in 2023 still works in 2026 unless someone removes it, on every server it was copied to. (expiry-time= exists per line, but someone has to set it.)
  • No identity. The line is a key and a comment. Which person, team or job it belongs to lives in a spreadsheet, if anywhere.
  • Distribution is your problem. Adding or removing someone means touching every server, with config management or by hand.
  • Host keys are trust on first use. Each client accepts each server's fingerprint the first time, and rebuilding a server triggers REMOTE HOST IDENTIFICATION HAS CHANGED everywhere.

OpenSSH certificates, available since OpenSSH 5.4 (2010), address all four. They are not X.509: the format is OpenSSH's own, a public key plus identity and constraints, signed by a CA key that is itself an ordinary SSH key. In 2025 the format description moved from the PROTOCOL.certkeys file in the OpenSSH sources to an Internet-Draft, now draft-ietf-sshm-cert in the IETF SSHM working group.

Two CAs, two directions

You need two separate signing keys, because they answer different questions:

  • The user CA answers "may this key log in as this account?" Servers trust it.
  • The host CA answers "is this really web01?" Clients trust it.
Creating the CAs (OpenSSH 10.0p2)
$ ssh-keygen -q -t ed25519 -f user_ca -C "Example user CA" -N ""
$ ssh-keygen -q -t ed25519 -f host_ca -C "Example host CA" -N ""

The -N "" keeps the lab simple. A real CA key belongs in an HSM, a hardware token, or a signing service (HashiCorp Vault's SSH secrets engine, smallstep, Teleport and cloud-provider equivalents all do this). Whoever holds the user CA key can log in as anyone the servers allow.

User certificates

Sign Alice's existing public key for eight hours, for the accounts alice and deploy:

ssh-keygen -s: issuing a user certificate
$ ssh-keygen -s user_ca -I alice@example.test -n alice,deploy -V -5m:+8h -z 1001 id_ed25519.pub
Signed user key /home/alice-laptop/id_ed25519-cert.pub: id "alice@example.test" serial 1001 for alice,deploy valid from 2026-10-20T13:14:08 to 2026-10-20T21:19:08
  • -I is the key ID. sshd logs it on every login, so make it a person or job, not a hostname.
  • -n lists the principals, the names this certificate may act as. The ssh-keygen manual says certificates without principals are "valid for all users or hosts", but sshd 10.0 refuses such a user certificate under TrustedUserCAKeys (Certificate lacks principal list). Always set -n.
  • -V -5m:+8h sets validity from five minutes ago (clock skew) to eight hours from now.
  • -z sets a serial number, which revocation can refer to.

ssh-keygen -L shows what was signed:

ssh-keygen -L -f id_ed25519-cert.pub
/home/alice-laptop/id_ed25519-cert.pub:
        Type: ssh-ed25519-cert-v01@openssh.com user certificate
        Public key: ED25519-CERT SHA256:9Mr4hhu4+c6LnVoj+n8tVj4AKD/AP037BQ5pyEAv07s
        Signing CA: ED25519 SHA256:aG9VR0NrG4he7YusGsggg/Gt3WviRbg3Anbfz4HJe60 (using ssh-ed25519)
        Key ID: "alice@example.test"
        Serial: 1001
        Valid: from 2026-10-20T13:14:08 to 2026-10-20T21:19:08
        Principals: 
                alice
                deploy
        Critical Options: (none)
        Extensions: 
                permit-X11-forwarding
                permit-agent-forwarding
                permit-port-forwarding
                permit-pty
                permit-user-rc

By default a user certificate grants all five extensions. -O clear removes them, and you can then add back only what you need, for example -O clear -O permit-pty. Critical options restrict the certificate. force-command replaces whatever command the client asks for, and source-address limits where it may be used from. With source-address=10.0.0.0/8, a login from 127.0.0.1 is refused. With an allowed source, the forced command runs no matter what the client asked for:

Critical options in practice (sshd log lines from LogLevel VERBOSE)
        Critical Options: 
                force-command echo backup only
                source-address 10.0.0.0/8
$ ssh deploy@web01.example.test
deploy@web01.example.test: Permission denied (publickey).
cert: Authentication tried for deploy with valid certificate but not from a permitted source address (127.0.0.1).
Refused by certificate options
...
$ ssh deploy@web01.example.test "rm -rf /tmp/anything"
backup only
Starting session: forced-command (key-option) 'echo backup only' for deploy from 127.0.0.1 port 48976 id 0

The ssh client finds the certificate automatically when it sits next to the private key as id_ed25519-cert.pub. Otherwise point to it with CertificateFile. ssh-add loads both into the agent.

Configuring sshd

The whole server side is a few lines. This drop-in file passed sshd -t and was used for every test on this page:

/etc/ssh/sshd_config.d/ca.conf
HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub
TrustedUserCAKeys /etc/ssh/user_ca.pub
AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u
RevokedKeys /etc/ssh/revoked_keys
PasswordAuthentication no
LogLevel VERBOSE
  • TrustedUserCAKeys holds one or more CA public keys. During a CA rotation, list both.
  • AuthorizedPrincipalsFile maps accounts to principals. Here /etc/ssh/auth_principals/deploy contains the single line deploy, so only certificates that list the principal deploy may log in as deploy. Without this option, sshd requires the account name itself among the principals. Mapping is more flexible: a principal such as oncall can be allowed on many accounts, and AuthorizedPrincipalsCommand can look it up in a directory.
  • RevokedKeys points to a revocation list (see below).
  • LogLevel VERBOSE logs the certificate key ID and serial of each login. This is the audit trail authorized_keys never had.

A login with Alice's certificate, from both sides:

ssh -v (client) and sshd log (server), abridged
Server host certificate: ssh-ed25519-cert-v01@openssh.com SHA256:IuzvI4QsLmSx1+6p3yrvlRx2wdl+LUCY1zXqSzssLAc, serial 0 ID "web01" CA ssh-ed25519 SHA256:+iie/YHPulJReHN2GQ8UvrorBVrtayD2T1MGxeg/QCQ valid from 2026-10-20T13:14:08 to 2027-10-19T13:19:08
Host 'web01.example.test' is known and matches the ED25519-CERT host certificate.
Offering public key: /home/alice-laptop/id_ed25519 ED25519-CERT SHA256:9Mr4hhu4+c6LnVoj+n8tVj4AKD/AP037BQ5pyEAv07s explicit
Server accepts key: /home/alice-laptop/id_ed25519 ED25519-CERT SHA256:9Mr4hhu4+c6LnVoj+n8tVj4AKD/AP037BQ5pyEAv07s explicit
Authenticated to web01.example.test ([127.0.0.1]:22) using "publickey".

Accepted publickey for deploy from 127.0.0.1 port 57432 ssh2: ED25519-CERT SHA256:9Mr4hhu4+c6LnVoj+n8tVj4AKD/AP037BQ5pyEAv07s ID alice@example.test (serial 1001) CA ED25519 SHA256:aG9VR0NrG4he7YusGsggg/Gt3WviRbg3Anbfz4HJe60

If you can't edit sshd_config, a single account can trust a CA through its own authorized_keys. The principals= option works like the principals file:

~deploy/.ssh/authorized_keys: trusting a CA for one account (key shortened)
cert-authority,principals="deploy-team" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK63...
$ ssh deploy@web01.example.test 'echo logged in as $(whoami)'
logged in as deploy

Host certificates

Sign each server's host key with the host CA. -h makes it a host certificate, and the principals are the names clients use to connect:

Signing a host key
$ ssh-keygen -s host_ca -I web01 -h -n web01.example.test,web01 -V -5m:+52w /etc/ssh/ssh_host_ed25519_key.pub
Signed host key /etc/ssh/ssh_host_ed25519_key-cert.pub: id "web01" serial 0 for web01.example.test,web01 valid from 2026-10-20T13:14:08 to 2027-10-19T13:19:08

Then point HostCertificate at the result (as in the sshd file above), and give clients one line in known_hosts (or /etc/ssh/ssh_known_hosts) instead of a fingerprint per host:

known_hosts: trust the host CA for a domain
@cert-authority *.example.test ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBtSsqoqxwcl31DTJg3kTPsv9eLuuw5gU1BcvYybODlu Example host CA

New and rebuilt servers are then trusted on the first connection, without a prompt, as long as their certificate lists the name the client used. That last condition is the usual surprise. Connecting under a name that is not a principal makes the client fall back to plain host keys, and with strict checking it stops:

Connecting as db01.example.test to a host certified only as web01
$ ssh deploy@db01.example.test
Certificate invalid: name is not a listed principal
No ED25519 host key is known for db01.example.test and you have requested strict checking.
Host key verification failed.

List every name clients use, including short names, CNAMEs, IP addresses if people connect by IP, and load-balancer names. ssh-keyscan -c fetches a server's certificate for inspection, and @revoked lines in known_hosts reject a specific host key or CA.

What failures look like

The client only ever says Permission denied (publickey). for user certificate problems. The reason is in the server log, at LogLevel VERBOSE:

Situationsshd log (OpenSSH 10.0p2)
Account not in the certificate's principals (after mapping)Certificate does not contain an authorized principal
Certificate issued without -n, no AuthorizedPrincipalsFileCertificate lacks principal list
Certificate past its -V windowCertificate invalid: expired
Serial or key in the KRLAuthentication key ED25519-CERT SHA256:... revoked by file /etc/ssh/revoked_keys
source-address does not matchcert: Authentication tried for deploy with valid certificate but not from a permitted source address (127.0.0.1).

Two things that look like certificate problems but are not:

  • Clock skew. Validity is checked against the server's clock. Issue certificates valid from a few minutes in the past (-V -5m:...) and keep servers on NTP.
  • Penalties from repeated failures. Since OpenSSH 9.8, sshd penalises addresses that keep failing authentication (PerSourcePenalties, on by default). While testing certificates from one machine, a failed attempt led to the next connection being dropped before authentication:
sshd 10.0p2 after a failed certificate login
srclimit_penalise: 127.0.0.1/32: activating ipv4 penalty of 19 seconds for penalty: failed authentication
$ ssh deploy@web01.example.test
Connection closed by 127.0.0.1 port 22
drop connection #0 from [127.0.0.1]:55080 on [127.0.0.1]:22 penalty: failed authentication

If ssh reports Connection closed right after a run of failures, wait out the penalty before you change your configuration again.

Revocation with KRLs

Short validity is the main revocation mechanism: an eight-hour certificate cleans itself up. For the rest, OpenSSH has Key Revocation Lists (KRLs, since OpenSSH 6.2), a compact binary format that can revoke by serial, key ID or key. Revoking Alice's certificate with serial 1001:

ssh-keygen -k: build a KRL, then test a certificate against it
$ printf 'serial: 1001\n' > /tmp/krl-spec
$ ssh-keygen -k -f /etc/ssh/revoked_keys -s user_ca.pub /tmp/krl-spec
Revoking from /tmp/krl-spec
$ ssh-keygen -Q -f /etc/ssh/revoked_keys id_ed25519-cert.pub
/home/alice-laptop/id_ed25519-cert.pub (alice@laptop): REVOKED

sshd reads RevokedKeys on each authentication, so the next login fails without a restart. Add -u to update an existing KRL instead of replacing it. Distribute the KRL with the same tooling as the CA key. A server with a stale KRL accepts what it does not know is revoked. That is one more reason to keep certificates short-lived and treat the KRL as a backstop for the remaining hours.

Side by side

authorized_keysCertificates
Granting accessCopy a key to each serverSign a key once
Removing accessDelete it on each serverLet it expire, or add it to the KRL
ExpiryOptional per line (expiry-time=)Every certificate has a validity window
Identity in logsKey fingerprintKey ID and serial
Restrictionsfrom=, command=, restrict per linesource-address, force-command, extensions per certificate
Host trustFingerprint per host (TOFU)One @cert-authority line
Critical secretNone centralThe CA private key
Moving partsNoneIssuance, CA protection, KRL distribution

The per-line restrictions in authorized_keys are real and tested on the same server:

authorized_keys options, OpenSSH 10.0p2 (keys shortened)
restrict,from="10.0.0.0/8",command="echo backup only" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
$ ssh deploy@web01.example.test id
deploy@web01.example.test: Permission denied (publickey).
/home/deploy/.ssh/authorized_keys:1: Authentication tried for deploy with correct key but not from a permitted host (host=127.0.0.1, ip=127.0.0.1, required=10.0.0.0/8).

expiry-time="20261001" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...
$ ssh deploy@web01.example.test id
deploy@web01.example.test: Permission denied (publickey).

For a handful of servers and people, authorized_keys with restrict and expiry-time is fine. Certificates pay off once onboarding, offboarding and host rebuilds happen weekly, or when you need to know who logged in, not just which key.

Migrating from authorized_keys

You don't need a flag day. sshd checks authorized_keys and trusted CAs side by side, so both kinds of login work during the move:

  1. Deploy the CA trust first. Roll out TrustedUserCAKeys, the principals files and an empty KRL to all servers, and leave authorized_keys alone.
  2. Issue certificates to a first team and let them use them for a few weeks. Each login now shows up in the log with ID ... (serial ...).
  3. Find the stragglers. A login through authorized_keys is logged without a certificate ID, for example Accepted publickey for deploy from 127.0.0.1 port 58770 ssh2: ED25519 SHA256:tT5X32Wds5InYaLSh7mV+QRqAtxA4KXwlKV3hkf0YkI. Those are usually automation: CI runners, backup jobs, monitoring. Give each one its own short-lived or narrowly restricted certificate.
  4. Switch off key files with AuthorizedKeysFile none once nothing legitimate uses them. Keep a break-glass path, such as console access or a separate emergency CA kept offline, for the day the signing service is down.
  5. Do the host side separately. Sign host keys and add the @cert-authority line to the clients' global /etc/ssh/ssh_known_hosts. The old per-host known_hosts entries can stay. They keep working until the host keys change.

Running it in practice

  • Issue certificates from your identity provider. A signing service checks the user's SSO login (OIDC) and returns a certificate with their principals for a few hours. Nobody handles the CA key, and leaving the company ends SSH access by the next day without touching a server.
  • Keep the user key on hardware if you can. ssh-keygen -t ed25519-sk creates a FIDO2-backed key. Certificates work with it like with any other key.
  • Rotate the CA with overlap. Add the new user CA as a second line in TrustedUserCAKeys, or the new host CA as a second @cert-authority line. Issue from the new CA, then remove the old one.
  • Watch the host side too. Re-sign host keys before their certificates expire. An expired host certificate makes clients fall back to plain host keys, which they usually don't have:
ssh to a host whose certificate expired yesterday
Certificate invalid: expired
No ED25519 host key is known for web01.example.test and you have requested strict checking.
Host key verification failed.

To inspect a key or certificate file without running anything, the SSH key inspector decodes OpenSSH keys and shows their type, size and fingerprint. The SSH server audit checks what a server offers, including its host-key and certificate algorithms and post-quantum key exchange.

Sources

  • OpenSSH manual pages: ssh-keygen(1) (CERTIFICATES and KEY REVOCATION LISTS sections), sshd_config(5) (TrustedUserCAKeys, AuthorizedPrincipalsFile, RevokedKeys, HostCertificate, PerSourcePenalties), sshd(8) (authorized_keys and known_hosts formats).
  • draft-ietf-sshm-cert: SSH Certificate Format (the successor of OpenSSH's PROTOCOL.certkeys). PROTOCOL.krl: the KRL format.
  • OpenSSH release notes: 5.4 (certificates), 6.2 (KRLs), 9.8 (PerSourcePenalties).

All output on this page comes from OpenSSH 10.0p2 (Debian 13 packages) in debian:trixie-20261005-slim on 20 October 2026, with test keys generated in the container.