An ssh-agent is a program that holds private keys for public key authentication and answers signing requests on their behalf. ssh finds it through SSH_AUTH_SOCK, the path of a Unix-domain socket, so the key is unlocked once and then borrowed by every connection that follows. The private key itself is never handed to a client.
That is a genuine security property and a genuine liability in the same move. The key never sits on disk in decrypted form, but the agent is a live oracle: anything that can reach the socket can sign, for as long as the identity stays loaded.
The socket is the capability
The agent protocol has no verb that exports a key. Asking for identities (message 11) returns message 12 with a public key blob and a comment — 51 bytes in the run behind this note, never private material. Asking it to sign an arbitrary challenge (message 13) returns message 14 with an 88-byte Ed25519 signature. An undefined opcode (99) comes back as message 5, SSH_AGENT_FAILURE. Renaming the private key file away did not stop the next signature, which is the point: the material lives in the agent’s memory.
That memory is protected by file permissions on the socket, and by nothing stronger. The socket is a Unix socket, mode 0600, owned by the uid that started the agent. A third process launched with nothing but SSH_AUTH_SOCK in its environment listed the identity and produced a signature without ever seeing the key file or the passphrase.
Unlock once, then bound the lifetime
ssh-add is the only step that needs the passphrase. It reads the key file, which should be mode 0600 — ssh-add ignores identity files readable by anyone else — and hands the decrypted identity to the agent. From then on the agent answers for you. ssh-add -t 8h sets a maximum lifetime, given in seconds or in the time format from sshd_config(5); in the run behind this note ssh-add -t 2 still listed the identity immediately afterwards and reported no identities three seconds later.
ssh-add -c goes further and demands confirmation for every use, routed through ssh-askpass. A signing request with no tty to answer the prompt was refused, which is exactly the behaviour you want when the request comes from a host that cannot reach your screen. Neither flag removes an identity; ssh-add -D deletes all of them, and killing the agent is the blunt version.
What the agent refuses to do
Modern ssh-agent narrows its own surface. By default it refuses signature requests for FIDO keys whose application string does not start with ssh: and whose data does not look like an SSH authentication request or an ssh-keygen signature — the default exists so that forwarding access to a hardware key does not also forward the ability to authenticate to websites. It also permits only local clients to load PKCS#11 or FIDO provider libraries unless allow-remote-pkcs11 is set.
ssh-add -h adds destination constraints, restricting a key to specific source and destination host pairs, and the whole connection path including forwarding is tested against them. That is a far better answer than hoping a bastion is trustworthy.
When it breaks
The classic failure is a leaked SSH_AUTH_SOCK. Any process with that one variable and the same uid holds the unlocked identity: no key file, no passphrase, no prompt. It presents as a key that keeps working after you rotate it on disk, because the agent still holds the old one. Revocation is the agent’s lifetime, so bound it with ssh-add -t and delete identities when the work is done.
The second failure is an agent that outlives the session that created it. An agent started at login with ssh-add and no lifetime keeps signing long after you have moved on, and on a shared machine its socket under $HOME/.ssh/agent/ is reachable by anything running as you. Rotating the key file does not help: the loaded copy stays valid until the agent dies or the identity is deleted.
Check it quickly
Confirm which socket you are talking to and what it holds:
echo "$SSH_AUTH_SOCK"
ls -l "$SSH_AUTH_SOCK"
ssh-add -l
ssh-add -t 8h ~/.ssh/id_ed25519
ssh-add -D
The listing should show one fingerprint per loaded identity and nothing else; printing nothing with exit status 1 means no identity is loaded. The ls -l line should show a socket with mode 0600 owned by you — anything world-accessible means another account can sign as you — and the final command should leave ssh-add -l reporting no identities at all.
Sources
- OpenBSD: ssh-agent, holds private keys for public key authentication, socket location, and restrictions on remote clients, checked 2026-09-16.
- OpenBSD: ssh-add, -t lifetime, -c confirmation, and -h destination constraints, checked 2026-09-16.
- OpenBSD: ssh, SSH_AUTH_SOCK and the askpass environment variables, checked 2026-09-16.