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.
| Control | Protects against | Cost |
|---|---|---|
ssh-add -t | A key left loaded for days on an unattended machine | Retype the passphrase after expiry |
ssh-add -c | Silent use of the agent by a compromised process or forwarded socket | A dialog on every connection |
ssh-add -x | Use while you step away, without unloading keys | Manual; easy to forget |
ssh-add -D | Anything that runs after you are done | Reload keys next time |
| FIDO key (ed25519-sk) | Any remote use: every signature needs a physical touch | Hardware 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.
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
| Symptom | Likely cause | Check |
|---|---|---|
| Could not open a connection to your authentication agent | SSH_AUTH_SOCK unset or stale in this shell | echo "$SSH_AUTH_SOCK"; ls -l "$SSH_AUTH_SOCK" |
| Too many authentication failures | Agent offers every key before the right one | IdentitiesOnly yes plus IdentityFile per host |
| agent refused operation | Confirm-required key with no askpass, or a hardware key not touched | ssh -v host 2>&1 | grep -i agent |
| Passphrase asked every time | Agent not running, or AddKeysToAgent not set | ssh-add -l |
| Works in terminal, fails in cron or systemd | Those environments do not inherit SSH_AUTH_SOCK | Use 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.