Vivian Voss

ssh-agent

ssh security unix

Technical Beauty ■ Episode 31

You have typed your passphrase four times this morning. Once to pull from GitHub. Once to deploy to staging. Once to SSH into production. Once because you mistyped it the third time and had to start over. By lunch, you will have typed it twelve more times, and by the end of the week you will have created a key without a passphrase because life, one feels, is too short.

Congratulations. Your private key is now a plaintext file on disk. Anyone who reads ~/.ssh/id_ed25519 owns every server you can reach. This is not a hypothetical. This is a Tuesday.

In 1995, a password-sniffing attack hit the network at Helsinki University of Technology. Tatu Ylonen, a researcher there, decided this was unacceptable and wrote SSH that same year. As part of that implementation, he wrote ssh-agent: a process that holds your private keys in memory and signs authentication challenges on your behalf. The key never leaves the process. Not to the client. Not to the server. Not to the wire. Never.

Thirty-one years later, that agent is still the authentication backbone of modern software delivery.

The Design

The entire agent is 2,624 lines of C in a single file: ssh-agent.c. Key storage, socket management, the full agent protocol, PKCS#11 smart card support, FIDO/U2F hardware key support, agent forwarding with destination constraints, and locking. In one file. Shorter than most React components one has had the pleasure of reviewing.

The API is a Unix domain socket. When the agent starts, it creates a socket, sets its permissions to owner-only (umask(0177)), and prints two environment variables:

SSH_AUTH_SOCK=/tmp/ssh-XXXXXXXXXX/agent.12345
SSH_AGENT_PID=12345

That is the entire interface. No configuration file. No service manager. No daemon registration. No YAML. The socket exists. Programs that know SSH_AUTH_SOCK can talk to it. Programs that do not, cannot. One does find this rather refreshing.

The Protocol: Five Operations List keyspublic keys Sign datasignature only Add key+ constraints Remove keyone or all Lockpassphrase The IETF draft describing the protocol is shorter than most framework tutorials. 2,624 Lines of C. One File. 31 Years. Key storage + socket + protocol + PKCS#11 + FIDO + forwarding + locking. Shorter than most React components one has had the pleasure of reviewing. No configuration file. No YAML. No cloud dependency. No subscription.

The Elegance

The authentication flow:

  1. Your SSH client connects to a server
  2. The server sends a challenge (data to be signed)
  3. The client forwards the challenge to the agent via the Unix socket
  4. The agent signs the challenge with the private key internally
  5. The agent returns only the signature
  6. The client sends the signature to the server

Step four is where the beauty lives. The agent calls the signing function internally. The result, a sequence of signature bytes, is sent back to the client. The private key material never crosses a process boundary. It is never serialised to any output channel. It exists in exactly one place: the agent's memory.

When the agent process terminates, the operating system reclaims the memory. The keys are gone. No cleanup script. No cache file to purge. No "secure delete" to trust. No residual state. The process was the vault, and the vault is demolished.

The agent also monitors its parent process. If the parent shell dies (detected by getppid() returning 1), the agent cleans up its socket and exits. No orphan processes accumulating in your process table. One does appreciate software that tidies up after itself.

The Constraints

ssh-add is the interface for managing keys in the agent:

ssh-add ~/.ssh/id_ed25519          # Add a key (passphrase prompt)
ssh-add -t 3600                     # Key expires after 1 hour
ssh-add -c                          # Confirm each signing operation
ssh-add -l                          # List loaded key fingerprints
ssh-add -D                          # Delete all keys

The -t flag stores an absolute expiry time. After that time, the key is automatically removed. No cron job. No external timer. The -c flag requires interactive confirmation before every signing operation. You see who is asking, and you decide whether to sign. For forwarded agents, this is the difference between convenience and a security incident.

The locking mechanism makes all keys inaccessible until the agent is unlocked with the correct passphrase. For stepping away from your desk, this is rather more civilised than terminating the agent and re-adding all keys when you return.

The Security

The agent takes security seriously at the implementation level:

  • Anti-tracing: platform_disable_tracing(0) at startup prevents other processes from attaching a debugger to read key material from memory
  • Privilege dropping: setegid(getgid()) at startup. The agent runs with minimal privileges
  • Memory hygiene: freezero() for PINs (zeroes memory before freeing). explicit_bzero() for lock password hashes, which prevents the compiler from optimising away the zeroing because the variable is "no longer used"
  • Socket permissions: created with umask(0177). Owner-only access. The directory is created with mkdtemp() for additional protection

These are not features listed on a marketing page. These are manners. The kind of quiet, disciplined engineering that distinguishes software built by people who understand what they are protecting.

Agent Forwarding

Agent forwarding (ssh -A or ForwardAgent yes) allows a remote machine to use your local agent. The remote sshd creates a Unix socket, sets SSH_AUTH_SOCK, and tunnels requests back through the SSH connection to your local agent. You can hop from server to server without copying keys.

The risk: anyone with root on the remote machine can access the forwarded socket while your session is active. The mitigations are characteristically minimal. ssh-add -c requires confirmation per use. ProxyJump avoids forwarding entirely by routing through an intermediate host without giving it agent access. Recent OpenSSH versions add destination constraints: keys can be restricted to specific hosts.

The design philosophy is consistent: provide the mechanism, make the risks visible, let the operator decide. No hand-holding. No default that pretends to be safe. Honest tooling for people who read the man page.

The Proof

Every CI/CD system on earth uses ssh-agent. GitHub Actions has a dedicated action. GitLab's documentation recommends eval $(ssh-agent -s) as the standard pattern. Jenkins, CircleCI, Buildkite, Travis CI: all use the agent protocol.

macOS integrated ssh-agent into the system keychain with Leopard in 2007. ssh-add --apple-use-keychain stores passphrases in Keychain, bridging the Unix tool with Apple's credential infrastructure. 1Password and Bitwarden now implement the agent protocol natively. An API designed by a Finnish researcher in 1995, communicating over a Unix socket named by an environment variable, is the authentication backbone of modern software delivery.

One does find this rather beautiful.

The Principle

ssh-agent is 2,624 lines of C. One file. One socket. One environment variable. No configuration file. No YAML. No cloud dependency. No subscription. The private key never leaves the process, the process never outlives the session, and the session never trusts more than it must.

Tatu Ylonen wrote it because typing passphrases was tedious. The best Unix tools often begin this way: someone finds a task annoying, writes a small programme to solve it, and designs it with enough discipline that it still works thirty-one years later. No rewrite. No framework migration. No breaking changes.

Technical beauty emerges from reduction. ssh-agent reduced authentication to five operations, one socket, and the guarantee that the secret never leaves the room.

2,624 lines of C. One file. One socket. One environment variable. Five protocol operations. No configuration. The private key never leaves the process. The process never outlives the session. Born from impatience in 1995, designed with discipline, still the authentication backbone of every CI/CD system on earth.