Skip to content

ssh-agent Explained: Caching SSH Keys Without Leaking Them

How ssh-agent holds decrypted keys, why agent forwarding exposes them, and how key lifetimes, confirmation, locking and destination constraints keep them safe.

●By 10 min read

A passphrase on your SSH key is only useful if you do not type it a hundred times a day. ssh-agent is the compromise: you unlock the key once, the agent holds it in memory, and every ssh, scp and git call after that authenticates without asking. That convenience is also the risk. Whoever can talk to the agent can use your key.

This guide covers what the agent actually stores and exposes, how to run exactly one of it, and the controls most people never turn on: key lifetimes, per-use confirmation, locking, and destination constraints. For the server side, see our SSH security best practices.

What the Agent Does (and Never Does)

The agent is a small process that listens on a Unix domain socket. When you add a key, it decrypts the private key with your passphrase and keeps the plaintext in its own memory. When ssh needs to authenticate, it sends the agent the data to sign and gets a signature back. The private key never leaves the agent, not even to the ssh client.

$ eval "$(ssh-agent -s)"

Agent pid 48211

$ echo "$SSH_AUTH_SOCK"

/tmp/ssh-k3Qm9aP2xZ/agent.48210

$ ls -ld /tmp/ssh-k3Qm9aP2xZ

drwx------ 2 alice alice 60 Oct 2 09:14 /tmp/ssh-k3Qm9aP2xZ

$ ssh-add ~/.ssh/id_ed25519

Enter passphrase for /home/alice/.ssh/id_ed25519:

Identity added: /home/alice/.ssh/id_ed25519 (alice@laptop)

$ ssh-add -l

256 SHA256:Xq7u1mXcY2N0Jf3oK0bS1n5p7rD9hA4eLwV2tZ8cQ6s alice@laptop (ED25519)

$ ssh-add -L # public keys, ready to paste into authorized_keys

The socket is the key β€” anyone who can open $SSH_AUTH_SOCK can ask the agent to sign. The directory is mode 0700 so other users cannot, but root can, and so can any process running as you.

No exfiltration, but no limit either β€” an attacker using your agent cannot copy the private key out of it. They can, however, sign as many logins as they like for as long as the socket is reachable, which is usually all they need.

Two environment variables β€” SSH_AUTH_SOCK tells clients where to connect. SSH_AGENT_PID lets ssh-agent -k kill the right process. A new shell that does not inherit them cannot see the agent.

Run One Agent, Not Twenty

The classic mistake is eval "$(ssh-agent -s)" in ~/.bashrc: every new terminal starts another agent with no keys, and old ones never exit. Most desktops already run one (GNOME Keyring, KDE, or macOS launchd), so check before starting your own:

$ ssh-add -l

Could not open a connection to your authentication agent. # no agent in this shell

The agent has no identities. # agent running, no keys loaded

# On a headless box or a minimal desktop: one agent per login, managed by systemd

# ~/.config/systemd/user/ssh-agent.service

[Unit]

Description=SSH key agent

[Service]

Type=simple

Environment=SSH_AUTH_SOCK=%t/ssh-agent.socket

ExecStart=/usr/bin/ssh-agent -D -a $SSH_AUTH_SOCK

[Install]

WantedBy=default.target

$ systemctl --user enable --now ssh-agent.service

# ~/.bashrc or ~/.profile: point every shell at the same socket

export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent.socket"

-D keeps the agent in the foreground so systemd can supervise it, and -a binds a fixed socket path. %t expands to $XDG_RUNTIME_DIR, normally /run/user/1000, which is private to your user and cleared at logout. Then let ssh load keys on first use instead of at login:

# ~/.ssh/config

Host *

AddKeysToAgent yes

IdentitiesOnly yes

AddKeysToAgent β€” with yes, the first ssh that unlocks a key also hands it to the agent. confirm adds it with per-use confirmation, and ask prompts before adding. The SSH config generator writes these blocks for you.

IdentitiesOnly prevents lockouts β€” with many keys loaded, ssh offers each one in turn and the server disconnects after MaxAuthTries (6 by default) with "Too many authentication failures". IdentitiesOnly makes ssh offer only the IdentityFile configured for that host.

Pick an agent per host β€” IdentityAgent none in a Host block disables the agent for that host, and IdentityAgent ~/.ssh/work-agent.sock points it at a different one, which is a clean way to separate work and personal keys.

Lifetimes, Confirmation, and Locking

By default a key stays in the agent until the agent exits, which on a laptop that never logs out can mean weeks. Three built-in controls shrink that window:

# Expire this key after 4 hours (s, m, h, d, w suffixes)

$ ssh-add -t 4h ~/.ssh/id_ed25519

# Default lifetime for every key added to this agent

$ ssh-agent -D -a "$XDG_RUNTIME_DIR/ssh-agent.socket" -t 8h

# Require a confirmation dialog for each signature (needs an ssh-askpass program)

$ ssh-add -c ~/.ssh/id_ed25519_prod

# Lock the agent with a password; keys stay loaded but cannot be used

$ ssh-add -x

Enter lock password:

Again:

Agent locked.

$ ssh-add -X

Enter lock password:

Agent unlocked.

# Remove one key, or all of them

$ ssh-add -d ~/.ssh/id_ed25519_prod

$ ssh-add -D

All identities removed.

ControlProtects againstCost
ssh-add -tA key left loaded for days on an unattended machineRetype the passphrase after expiry
ssh-add -cSilent use of the agent by a compromised process or forwarded socketA dialog on every connection
ssh-add -xUse while you step away, without unloading keysManual; easy to forget
ssh-add -DAnything that runs after you are doneReload keys next time
FIDO key (ed25519-sk)Any remote use: every signature needs a physical touchHardware token, OpenSSH 8.2+ on both ends

Confirmation needs a GUI helper β€” without an askpass program (ssh-askpass, ssh-askpass-gnome, or openssh-askpass on RHEL/Fedora), a confirm-required key fails with agent refused operation instead of prompting.

Hardware keys change the threat model β€” with a FIDO-backed key, malware that reaches your agent still cannot sign without a touch on the token. Our SSH key setup tool covers generating one.

sponsored

Agent Forwarding: The Leak Most People Opt Into

ssh -A (or ForwardAgent yes) creates a socket on the remote host that tunnels back to your local agent, so you can hop from a bastion to another server with your laptop's key. While that session is open, root on the remote host can use your key, and so can anyone who has compromised your account there:

# On the bastion, as root, while alice is connected with -A

$ ls /tmp/ssh-*/

agent.30477

$ SSH_AUTH_SOCK=/tmp/ssh-a81Hc2Rw4N/agent.30477 ssh-add -l

256 SHA256:Xq7u1mXcY2N0Jf3oK0bS1n5p7rD9hA4eLwV2tZ8cQ6s alice@laptop (ED25519)

$ SSH_AUTH_SOCK=/tmp/ssh-a81Hc2Rw4N/agent.30477 ssh [email protected]

# ...logged in as alice, no passphrase, no trace on alice's laptop

For simply reaching an internal host through a bastion, you do not need forwarding at all. ProxyJump runs the second SSH connection from your machine, through a tunnel, so the bastion never sees an agent socket:

$ ssh -J [email protected] [email protected]

# ~/.ssh/config

Host db1.internal

ProxyJump bastion.example.com

ForwardAgent no

When you really need forwarding β€” for example, running git pull over SSH on a remote host: forward only to hosts you trust as much as your laptop, enable it per Host block rather than in Host *, and load a separate key with ssh-add -c so each use pops a dialog.

Disable it server-side where nobody needs it β€” AllowAgentForwarding no in sshd_config refuses forwarding requests on that server. It does not stop a user with shell access from running their own forwarder, so it is a policy default rather than a hard control.

Check what ssh will actually do β€” ssh -G db1.internal | grep -i forwardagent prints the effective setting after every config file and Host block is applied.

Destination Constraints (OpenSSH 8.9+)

Newer OpenSSH can restrict where a key loaded in the agent may be used, including through forwarding. The constraint is enforced by the agent itself, so a hijacked forwarded socket on the bastion cannot use the key for hosts you did not list:

# Usable to log in to the bastion, and from the bastion onward to db1 only

$ ssh-add -h bastion.example.com \

-h "bastion.example.com>db1.internal" \

~/.ssh/id_ed25519

$ ssh-add -l

256 SHA256:Xq7u1mXcY2N0Jf3oK0bS1n5p7rD9hA4eLwV2tZ8cQ6s alice@laptop (ED25519)

$ ssh -V

OpenSSH_9.6p1 Ubuntu-3ubuntu13, OpenSSL 3.0.13 30 Jan 2024

Host keys must be known β€” the agent identifies each hop by its host key, which ssh-add looks up in known_hosts when you add the key. Connect to every host in the chain once first so their keys are recorded.

Every hop needs a recent OpenSSH β€” both the client and the forwarding host must speak the session-binding extension. Ubuntu 22.04 and Debian 12 ship 8.9 or newer, but RHEL 9 ships 8.7, so a RHEL 9 bastion cannot enforce the constraint.

Defense in depth, not a replacement β€” constraints limit where a stolen agent session can go. They do not stop someone on the bastion from using the key to reach db1 while you are connected. Confirmation or a FIDO key closes that gap.

Debugging Agent Problems

SymptomLikely causeCheck
Could not open a connection to your authentication agentSSH_AUTH_SOCK unset or stale in this shellecho "$SSH_AUTH_SOCK"; ls -l "$SSH_AUTH_SOCK"
Too many authentication failuresAgent offers every key before the right oneIdentitiesOnly yes plus IdentityFile per host
agent refused operationConfirm-required key with no askpass, or a hardware key not touchedssh -v host 2>&1 | grep -i agent
Passphrase asked every timeAgent not running, or AddKeysToAgent not setssh-add -l
Works in terminal, fails in cron or systemdThose environments do not inherit SSH_AUTH_SOCKUse a dedicated unencrypted deploy key with a forced command instead

$ ssh -v [email protected] 2>&1 | grep -E 'Will attempt|Offering|Authenticated'

debug1: Will attempt key: /home/alice/.ssh/id_ed25519 ED25519 SHA256:Xq7u1mXc... agent

debug1: Offering public key: /home/alice/.ssh/id_ed25519 ED25519 SHA256:Xq7u1mXc... agent

Authenticated to db1.internal ([10.0.4.21]:22) using "publickey".

Frequently Asked Questions

Is it safe to use ssh-agent?

Yes, on a machine you trust. The private key never leaves the agent, and the socket is readable only by your user. The risks are root or malware on the same machine using the socket, and agent forwarding to hosts where someone else has root. Set a lifetime with ssh-add -t and avoid forwarding to untrusted hosts.

Can someone steal my private key from ssh-agent forwarding?

They cannot copy the key itself, because the agent only returns signatures. But root on the remote host can use your forwarded socket to log in anywhere your key is accepted for as long as your session is open. Use ProxyJump instead of forwarding, or add the key with ssh-add -c or a destination constraint.

How do I make ssh-agent forget keys automatically?

Add keys with a lifetime, for example ssh-add -t 4h, or start the agent with ssh-agent -t 8h to apply a default lifetime to every key. After the timeout the key is removed and you must enter the passphrase again. ssh-add -D removes all keys immediately.

Why does ssh say Too many authentication failures?

The agent holds several keys and ssh offers them one by one. The server counts each rejected key against MaxAuthTries, six by default, and disconnects before the right key is tried. Set IdentitiesOnly yes and an IdentityFile for that host in ~/.ssh/config so only the correct key is offered.

What is the difference between ssh -A and ssh -J?

ssh -A forwards your agent socket to the remote host so programs there can use your keys. ssh -J (ProxyJump) tunnels a second SSH connection from your own machine through the jump host, so the jump host never gets access to your agent. For simply reaching a host behind a bastion, ProxyJump is the safer choice.

sponsored

Related Tools

See also