●moray: ssh, sftp & terminal

ssh-agent Explained (and When Not to Forward It)

What ssh-agent holds, how to load a key for a session, and why agent forwarding hands your identity to whoever runs the server you hop through.

Keys · Bartłomiej Żabicki · updated 2026-07-27 · 9 min read

A passphrase on your private key is good security and terrible ergonomics. Type it forty times a day and you will quietly remove it. The SSH agent exists to resolve that tension — and its one optional feature, agent forwarding, is among the easiest ways to hand your identity to someone else.

What the agent is

ssh-agent is a small background process that holds decrypted private keys in memory and performs signatures on request. Your SSH client never receives the key itself; it asks the agent "sign this challenge" and gets a signature back.

That gives you two things:

  • You unlock the key once per session instead of once per connection.
  • Programs that need to authenticate never handle the key material directly.

The agent talks over a Unix socket, and its location lives in an environment variable:

echo $SSH_AUTH_SOCK
# /private/tmp/com.apple.launchd.k3Jd9/Listeners

If that variable is empty, no agent is reachable — which is the cause of most "why is it asking again?" confusion.

Loading a key

# start an agent if your system does not run one (macOS and most desktop Linux do)
eval "$(ssh-agent -s)"

# add a key — prompts for the passphrase once
ssh-add ~/.ssh/id_ed25519

# what is loaded right now?
ssh-add -l

# forget everything
ssh-add -D

# forget after 4 hours
ssh-add -t 4h ~/.ssh/id_ed25519

ssh-add -t is underused. A key that expires at the end of the working day limits the damage if your laptop is taken while unlocked.

Better than remembering to run ssh-add at all, let the config do it:

Host *
    AddKeysToAgent yes

The key is added to the agent the first time it is used, so you are prompted once and never again that session.

On macOS

macOS runs an agent for you and integrates with the Keychain, so the passphrase survives reboots:

ssh-add --apple-use-keychain ~/.ssh/id_ed25519
Host *
    AddKeysToAgent yes
    UseKeychain yes

UseKeychain yes tells SSH to look up the passphrase from the Keychain rather than prompting. Note it is macOS-only — putting it in a config you sync to Linux produces a warning on every connection.

Why "too many keys" breaks logins

This is the agent's most common side effect, and it does not look like an agent problem.

Servers default to MaxAuthTries 6. Your client offers the agent's keys first, in order, before anything named in your config — and every offer counts as an attempt. Load seven keys and a server can disconnect you before your correct key is ever tried. The error you get is Permission denied (publickey), which sends most people looking at authorized_keys.

ssh-add -l | wc -l      # how many is it offering?

The fix is per-host, not global:

Host web1
    IdentityFile ~/.ssh/prod_ed25519
    IdentitiesOnly yes

IdentitiesOnly yes restricts the client to the keys you explicitly named, even when the agent holds others. Full checklist in fixing Permission denied (publickey).

Agent forwarding, and why to avoid it

Now the dangerous part. -A, or ForwardAgent yes, makes your agent's socket available on the server you connect to, so you can hop onward from there using your local keys.

ssh -A deploy@bastion       # convenient, and a bad idea on a shared box

Here is what you have actually done. The socket on that server is a live channel back to your agent, and anyone who can read it can ask your agent to sign anything. That includes:

  • root on that machine
  • any process running as your user there
  • an administrator you have never met
  • anything that has already compromised the host

They cannot copy your key — the agent never sends it — but for as long as your session is open they can authenticate as you to every server that trusts that key. The requests look identical to your own, so nothing in any log distinguishes them.

The blast radius is not the one server you logged into. It is every machine your key opens.

What to do instead

Use ProxyJump. It tunnels an end-to-end connection through the intermediate host, so your key is only ever used against the machine you actually meant to reach, and the bastion never gets a socket:

Host bastion
    HostName bastion.example.com
    User deploy

Host db
    HostName db.internal
    User deploy
    ProxyJump bastion

Details and the multi-hop form in jump hosts and ProxyJump.

If you genuinely cannot avoid it

Some environments still require forwarding. Constrain it:

# require confirmation for every signature
ssh-add -c ~/.ssh/id_ed25519
# forward only to the one host that needs it — never Host *
Host legacy-bastion
    ForwardAgent yes

With -c, each signing request pops a confirmation dialog, so an unexpected one is visible rather than silent. It turns invisible abuse into something you would notice — a mitigation, not a fix.

Two rules worth keeping: never put ForwardAgent yes under Host *, and never forward to a machine whose administrators you would not hand a key to.

Agent, Keychain, hardware: which is which

They solve overlapping problems and get conflated constantly.

HoldsSurvives rebootGated by
ssh-agentDecrypted keys in memoryNoWhatever unlocked it
Apple KeychainEncrypted keys/passphrases on diskYesFace ID / Touch ID
Hardware token (ed25519-sk, YubiKey)Key that cannot be extractedYesPhysical touch

On a phone or tablet the agent is largely beside the point: there is no long-lived shell session to populate, and the platform already offers something stronger. A key stored in the device Keychain is encrypted at rest, gated behind biometrics, and never handed to the app as raw bytes — which is how Moray does it, and why there is no passphrase to retype on a touchscreen.

Hardware-backed keys are the strongest option where they fit. ssh-keygen -t ed25519-sk creates a key whose private half physically cannot leave the token, so even a fully compromised laptop cannot exfiltrate it — the attacker can only use it while the token is plugged in and touched.

The short version

  • The agent holds decrypted keys and signs on request; it never sends the key.
  • AddKeysToAgent yes beats remembering ssh-add. ssh-add -t 8h limits exposure.
  • Too many loaded keys causes Permission denied (publickey) — fix with IdentitiesOnly yes.
  • Do not forward the agent. Use ProxyJump. If forced to, scope it to one host and use -c.
  • On Apple platforms, the Keychain does the agent's job better and with biometrics.

Next: SSH keys explained for choosing and storing the keys the agent holds.

Written by Bartłomiej Żabicki

iOS developer writing Swift since 2015, with a homelab he administers from an iPhone more often than he would like to admit. Moray is the SSH client he wanted to exist.

Frequently asked

Why does SSH keep asking for my passphrase?
Because nothing is holding the decrypted key. Start an agent and add the key with ssh-add, or set AddKeysToAgent yes in your config so it is loaded on first use. On macOS, --apple-use-keychain stores the passphrase so the agent is populated after a reboot.
Is agent forwarding safe?
Not on a machine you do not fully trust. Anyone with root on that host can use the forwarded socket to authenticate as you to any server your key opens, for as long as your session lasts. Use ProxyJump instead, and if you genuinely must forward, add -c so every signature needs confirmation.
What is the difference between the agent and the Keychain?
The agent is a process holding decrypted keys in memory for the length of a session. Apple's Keychain is encrypted storage on disk, gated behind Face ID or Touch ID. A key kept in the Keychain does not need an agent at all, which is how SSH clients on iPhone and iPad avoid the problem.

try it yourself

Moray does all of this natively

A real terminal, dual-pane SFTP, tunnels and jump hosts on iPhone, iPad and Mac. Keys stay in the Keychain, hosts sync over your own iCloud, and there is no account to make.