Skip to main content
RunBook Academy

Secrets, PKI & CertificatesXI · SSH Keys, Host Trust and SSH CAsSSH

SSH public-key authentication — what actually proves identity

Intermediate⏱ ~22 min🧪 Lab requiredsshssh-keygen

What you'll learn

  • Trace the two-phase public-key exchange from key offer to signed proof
  • Explain what the authentication signature covers and why replay fails
  • Read authorized_keys as an authorisation decision made by the server
  • Choose key algorithms that OpenSSH 10 still accepts

Prerequisites

None — start here.

Practice

Verified against OpenSSL 3.5.x teaching target; 3.0+ minimum · OpenSSH 10.x teaching target; 8.2+ minimum for certificate workflows · OpenBao 2.6.x · Smallstep step-ca 0.30.x · Certbot / Pebble Certbot current release; Pebble 2.10.x ACME test server · Kubernetes (cross-course target) 1.36.x · PostgreSQL 17.x · 2026-08-26

Not yet marked complete on this device.

Public-key authentication in SSH is a possession proof. The client demonstrates that it holds a private key by signing a value that only this connection could have produced, and the server checks that signature against a public key it already decided to accept. Nothing secret crosses the wire, no password is compared, and the server can be fully compromised without ever learning the key.

What the client actually proves

By the time authentication starts, the transport is already encrypted and the server has already been authenticated to the client. Key exchange produced a shared secret and, as a by-product, a value known as the session identifier: a hash over the exchange that is unique to this TCP connection, this pair of ephemeral keys and this pair of peers. It never repeats, and neither side can force it to a chosen value. Everything that follows hangs off it.

The user authentication exchange defined in RFC 4252 then runs in two phases, and the split matters operationally.

  • The offer. The client sends a request naming the user, the service and one public key, with no signature attached. This costs nothing and lets the client discover which of its keys the server is prepared to accept before it spends a signature or prompts for a passphrase.
  • The proof. If the server replies that the key is acceptable, the client signs a blob containing the session identifier, the user name, the service name, the string that marks this as a public-key request, the algorithm name and the public key itself. The server verifies that signature with the public key it holds.
sequenceDiagram
    participant C as ssh client
    participant S as sshd-auth
    C->>S: offer user, service and one public key
    S-->>C: this key is acceptable
    C->>C: sign session identifier plus request fields
    C->>S: same request, now with signature
    S-->>C: success, or try the next key

The two-phase shape explains a behaviour engineers often misread. A client with six keys loaded will offer them one at a time, and a server that is counting authentication attempts sees six of them before the right key is found. That is not a brute-force attempt, it is the offer phase, and it is why a crowded agent can exhaust MaxAuthTries on a host where the correct key is last in the list. OpenSSH 10.5 added ssh -Z so that the order can be inspected directly rather than inferred from a debug trace.

authorized_keys is an authorisation list

~/.ssh/authorized_keys holds no secret. Every line in it is public material, and losing the whole file to an attacker costs you nothing directly. What the file expresses is a decision: this account will accept a proof of possession for this key. Authentication is cryptographic; authorisation is this file.

That distinction has consequences. The file lives inside the account it grants access to, so anyone who can write to it can grant themselves permanent access, which is why the server refuses to use a file that is group or world writable, or that sits under a directory with loose permissions. It also means that revoking access is a file-distribution problem across every host in the estate, and access survives exactly as long as the slowest configuration-management run.

# Read the file the server will actually consult, and fingerprint
# every key in it so entries can be matched against an inventory.
AUTHFILE="$HOME/.ssh/authorized_keys"
stat -c '%a %U %n' "$HOME/.ssh" "$AUTHFILE"
ssh-keygen -l -f "$AUTHFILE"

Each line may carry options before the key: a forced command, a source-address restriction, restrict to switch everything off and re-enable only what is named. Since OpenSSH 10.0 the AuthorizedKeysFile setting accepts glob patterns, so a fleet can drop one file per grantee into a directory instead of editing a shared file. Where the list has to be computed rather than stored, AuthorizedKeysCommand lets the server ask a program instead. All of these are still the same decision, made in the same place.

One detail changes how you read the resulting logs. OpenSSH 10.0 split the server into three binaries: sshd listens, sshd-session handles a connection, and sshd-auth handles the authentication phase specifically. The privilege boundary is the point, since the code that parses untrusted authentication material now runs in a separate executable from the code that owns the session. The practical consequence is that a log filter or an alert rule matching only the program name sshd will miss authentication events on a current release, and any AppArmor or SELinux policy written against a single binary needs revisiting.

Which algorithms are still real on OpenSSH 10

Two lists are easy to confuse. ssh -Q key reports the key and certificate types the binary understands; ssh -Q key-sig reports the signature algorithms it can negotiate. A key type is not a signature algorithm, and the RSA case is where that bites.

On the OpenSSH 10.2 build used to verify this course, ssh -Q key lists ssh-ed25519, the three ecdsa-sha2-nistp curves, ssh-rsa, the FIDO security-key variants and a -cert-v01@openssh.com form of each. It does not list ssh-dss. DSA was not deprecated in OpenSSH 10.0, it was removed: the build options that used to restore it no longer exist, so a host still holding a DSA key has an unauthenticatable key rather than a weak one.

# Key types this build understands, then the signature algorithms.
ssh -Q key
ssh -Q key-sig

ssh-rsa survives in the first list as a key type. The SHA-1 signature algorithm that shares its name does not belong in any current configuration. rsa-sha2-512 and rsa-sha2-256 are already the RSA defaults in HostKeyAlgorithms, CASignatureAlgorithms and PubkeyAcceptedAlgorithms, so there is nothing to enable and no reason to write ssh-rsa into a config file. For new key material, Ed25519 is the sane default: fixed small size, no parameter choices to get wrong, and no curve-selection argument to have.

Where the private key lives, and how it escapes

The private key is a file. On a workstation it sits in ~/.ssh, and if a passphrase was set, the file is encrypted with a key derived from that passphrase. The passphrase is not the credential and it does not protect a running session; it protects the file at rest, and it turns a copied key into an offline guessing problem rather than an immediate compromise. A key with no passphrase is a bearer token in a text file.

Which key gets used is worth controlling explicitly. By default a client will offer every identity it can find, including everything in the agent, which is how an unrelated key ends up being tried against a production host and counted as a failed attempt. Setting IdentitiesOnly yes alongside an explicit IdentityFile in a Host block confines each destination to the key you intended for it. That turns the offer phase from an enumeration into a single request, and it makes the server logs legible, because a failure now means the right key was refused rather than that five wrong ones were tried first.

The realistic leak paths are mundane. A key is copied to a jump host so that a second hop works. A key is baked into a container image so a job can reach a git remote. A key is committed to a repository inside a Terraform module. A laptop backup lands in cloud storage. In every case the key is compromised from the moment it is copied, not from the moment somebody uses it, and there is no way to tell the difference from the server side because both copies produce identical signatures.

Production discipline

  1. Inventory keys by fingerprint, not by comment. The comment field is free text that anybody can edit. Fingerprints from ssh-keygen -l are the only stable identifier, and they are what the server logs at LogLevel VERBOSE.
  2. Never let a private key be copied to reach a second hop. Use a jump host at the transport layer instead, which the agent lesson in this part covers in detail.
  3. Log at VERBOSE on anything that matters. Without it the authentication log records that a public key succeeded but not which one, which makes an access review guesswork.
  4. Treat a passphrase-free key as a secret in a file. It belongs in the same inventory, rotation schedule and leak-response procedure as an API token.
  5. Remove ssh-rsa and any DSA remnants from configuration. On OpenSSH 10 the DSA references are dead weight and the SHA-1 RSA reference is a downgrade waiting for a client that honours it.

Cross-course references

  • Linux for Production Sysadmins - Part XXVI (SSH) covers the server-side configuration of sshd, including the permission checks that decide whether authorized_keys is read at all.
  • Ansible for Production Sysadmins - Part XX (SSH) covers how a control machine authenticates to managed nodes, which is this exchange executed thousands of times without a human present.
  • Kubernetes for Production Sysadmins - Part LX (ServiceAccounts) covers the same question for workloads, where the possession proof is a short-lived projected token rather than a key file.

Quiz

Knowledge check · 4 questions

  1. Q1. During SSH public-key authentication, what does the client's signature cover?

  2. Q2. OpenSSH 10.0 removed DSA support outright, so no build option or configuration setting can restore ssh-dss.

  3. Q3. Explain why an authorized_keys file is not a secret, and what it does control.

  4. Q4. Decide what to check and what to change after a contractor departs.

    A contractor finished an engagement on 2026-08-14. Their key was installed by hand on an unknown number of hosts across three environments during a migration. Configuration management owns authorized_keys on 62 hosts but was bypassed on the 14 hosts built during the migration window.

Passing score: 75%. Answers are checked in this browser.