What it solves
With plain keys, every server keeps a list of allowed keys, and every laptop keeps a list of host keys. With 50 servers, adding or removing a person means changing 50 files, and old keys tend to stay forever.
With certificates, servers trust one CA key. You sign a person's public key for a set time, and when the certificate expires, the access ends. These are OpenSSH certificates, not the X.509 kind used for websites, and you do not need any outside company or service.
Create the CA
ssh-keygen -t ed25519 -f user_ca -C "user-ca"
This is a normal key pair. Anyone holding the private half user_ca can log in as anyone on every server that trusts it, so give it a strong passphrase and keep it off your daily machine and off the servers. Use a separate CA for hosts, as shown below.
Sign a user's public key
The person sends you id_ed25519.pub, never the private key:
ssh-keygen -s user_ca -I "alice@example.com" -n deploy,alice -V +8h id_ed25519.pub
| Option | Meaning |
|---|---|
-s user_ca | The CA private key used for signing. |
-I | The key ID. It is written in the server log at each login, so make it name the person. |
-n | The principals: the names this certificate is valid for. |
-V | The validity. +8h, +1d, or a range such as -5m:+1d, which also covers a server whose clock runs a little behind. |
The result is id_ed25519-cert.pub. Return it to the person, who puts it next to the private key. ssh then finds it by name, with no extra configuration. Kept somewhere else, it needs a CertificateFile line in ~/.ssh/config. To read one:
ssh-keygen -L -f id_ed25519-cert.pub
Without -V, a certificate never expires, which removes much of the benefit.
Make the server trust the CA
Copy user_ca.pub, only the public part, to /etc/ssh/user_ca.pub on the server. Add this to the sshd configuration, then run sudo sshd -t and reload:
TrustedUserCAKeys /etc/ssh/user_ca.pub
The user can now log in without any line in authorized_keys. The server log shows who it was:
Accepted publickey for deploy from 203.0.113.50 port 56276 ssh2: ED25519-CERT SHA256:... ID alice@example.com (serial 0) CA ED25519 SHA256:...
Principals
By default, the server requires the account name to be one of the certificate's principals. A certificate for alice does not open the account deploy.
To use roles instead of account names, list the accepted principals for each account:
AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u
Then /etc/ssh/auth_principals/deploy holds one name per line, for example ops, and you sign with -n ops. With this setting on, the account name stops being accepted automatically: only the names in the file count, and an account with no file refuses certificates.
Limit what a certificate can do
Options are set at signing time. Here the certificate works only from one network and cannot open a terminal:
ssh-keygen -s user_ca -I alice -n ops -V +1h -O source-address=203.0.113.0/24 -O no-pty id_ed25519.pub
-O clear removes all default permissions, and you can then add back the ones you want, for example -O clear -O permit-pty.
When a login fails
The client only says Permission denied (publickey). The reason is in the server log:
| Log line | Cause |
|---|---|
Certificate invalid: name is not a listed principal | The account is not in the certificate's -n list. |
Certificate invalid: expired | Past its end date. Sign it again. |
Refused by certificate options | A limit such as source-address does not match. |
Authentication key ... revoked by file ... | The key is on the revocation list. |
Revoke a certificate before it expires
Keep certificates short, so you rarely need this. For the urgent case, create a key revocation list from the public key and copy it to each server:
ssh-keygen -k -f revoked.krl id_ed25519.pub
ssh-keygen -Q -f revoked.krl id_ed25519.pub
RevokedKeys /etc/ssh/revoked.krl
Add later keys to the same list with -u. Take care with this file: if sshd cannot read it, public key login is refused for all users.
For keys without certificates, see how to revoke an SSH key when someone leaves.
Host certificates
Host certificates solve the other direction. Your users stop seeing the unknown host prompt, because they trust a CA and not each host key. Sign the server's public host key with -h, and list the names people use to connect:
ssh-keygen -t ed25519 -f host_ca -C "host-ca"
ssh-keygen -s host_ca -I server1 -h -n server1.example.com -V +52w ssh_host_ed25519_key.pub
On the server, with ssh_host_ed25519_key-cert.pub in /etc/ssh/:
HostKey /etc/ssh/ssh_host_ed25519_key
HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub
On each client, add one line to ~/.ssh/known_hosts, with the contents of host_ca.pub:
@cert-authority *.example.com ssh-ed25519 AAAA...
The pattern must match the name you connect with. When it does not, ssh ignores the line and treats the host as unknown, or refuses it if StrictHostKeyChecking is yes. Host certificates also expire, so renew them before that date. See known_hosts explained for how the rest of that file works.
When certificates are worth it
For a few servers and a few people, plain keys are simpler. Certificates begin to pay off when people join and leave often, or when you want short-lived access with a name in the log. The hard part is the process around the CA: who may sign, where the private key lives, and how a person gets a new certificate each day. ssh-keygen gives you the signing, not that process.
Questions
What is the difference between an SSH key and an SSH certificate?
A certificate is a public key signed by a CA, with a list of allowed user names and an expiry date. A server that trusts the CA accepts it without an authorized_keys entry for that key.
How do I check when an SSH certificate expires?
Run ssh-keygen -L -f id_ed25519-cert.pub. The Valid line shows the start and the end of the validity period.
Do SSH certificates replace authorized_keys?
For certificate logins, yes: the server only needs TrustedUserCAKeys. Plain keys listed in authorized_keys keep working next to them.
Sources
- ssh-keygen(1), CERTIFICATES section, OpenBSD manual
- sshd_config(5), OpenBSD manual
- sshd(8), SSH_KNOWN_HOSTS FILE FORMAT, OpenBSD manual