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 Elegance
The authentication flow:
- Your SSH client connects to a server
- The server sends a challenge (data to be signed)
- The client forwards the challenge to the agent via the Unix socket
- The agent signs the challenge with the private key internally
- The agent returns only the signature
- 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 withmkdtemp()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.