<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Agent · XB Field Notes</title><link>https://www.xbcatm.com/tags/agent/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://www.xbcatm.com/tags/agent/index.xml" rel="self" type="application/rss+xml"/><item><title>The ssh agent passes keys without exposing them</title><link>https://www.xbcatm.com/posts/linux-ssh-agent-forwarding/</link><pubDate>Wed, 09 Feb 2028 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/linux-ssh-agent-forwarding/</guid><description>An ssh agent holds your keys in memory so clients never need to type a passphrase or hand over the private key file.</description><content:encoded>&lt;p&gt;Agent forwarding is not the agent. Forwarding asks &lt;code&gt;sshd&lt;/code&gt; on the remote host to create its own socket and point &lt;code&gt;SSH_AUTH_SOCK&lt;/code&gt; at it, so programs on that host can ask your agent back home to sign. The remote never holds the key, and it never needs to: it holds full use of it.&lt;/p&gt;
&lt;p&gt;That is the standard way to reach a machine behind a bastion, and it is also a capability handed to every hop in the path. Most people who reach for &lt;code&gt;-A&lt;/code&gt; wanted &lt;code&gt;ProxyJump&lt;/code&gt; instead, which relays TCP and leaves the agent at home.&lt;/p&gt;
&lt;h2 id="the-remote-gets-the-socket-not-the-key"&gt;The remote gets the socket, not the key&lt;/h2&gt;
&lt;p&gt;ssh_config(5) is blunt about the trade. &lt;code&gt;ForwardAgent&lt;/code&gt; takes yes, no (the default), an explicit socket path, or the name of an environment variable, and the man page warns that users able to bypass file permissions on the remote host for the agent&amp;rsquo;s socket can reach the local agent through the forwarded connection. The important half is the next sentence: an attacker cannot obtain key material from the agent, but they can perform operations on the keys that authenticate using the loaded identities.&lt;/p&gt;
&lt;p&gt;The run behind this note shows exactly that shape. A third process given nothing but &lt;code&gt;SSH_AUTH_SOCK&lt;/code&gt; — no key file, no passphrase — listed the identity and obtained an 88-byte signature. That is the whole capability a forwarded agent confers on a hop.&lt;/p&gt;
&lt;h2 id="forwarding-is-opt-in-per-hop"&gt;Forwarding is opt-in per hop&lt;/h2&gt;
&lt;p&gt;The chain of hosts that can sign is exactly the set of hops that ran ssh with forwarding enabled. &lt;code&gt;ssh -A bastion&lt;/code&gt; exposes the agent to bastion; a second &lt;code&gt;ssh -A build-box&lt;/code&gt; from there exposes it to build-box as well, while &lt;code&gt;ssh build-box&lt;/code&gt; without &lt;code&gt;-A&lt;/code&gt; stops at bastion. Nothing cascades by accident, but nothing warns you either.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ProxyJump&lt;/code&gt; is the safer shape. ssh_config(5) describes it as connecting to the target by first making an ssh connection to the jump host and then establishing a TCP forwarding to the ultimate target from there. The jump hosts forward packets rather than signatures, so the agent never leaves the workstation, and &lt;code&gt;ssh -G&lt;/code&gt; confirms that &lt;code&gt;-o ProxyJump=bastion&lt;/code&gt; leaves forwardagent at no.&lt;/p&gt;
&lt;h2 id="what-a-server-can-and-cannot-enforce"&gt;What a server can and cannot enforce&lt;/h2&gt;
&lt;p&gt;On the server side, &lt;code&gt;sshd -T&lt;/code&gt; reports &lt;code&gt;allowagentforwarding yes&lt;/code&gt; when the configuration says nothing, and &lt;code&gt;AllowAgentForwarding no&lt;/code&gt; once you set it. The man page adds a warning worth reading twice: disabling agent forwarding does not improve security unless users are also denied shell access, because they can always install their own forwarders. Treat the directive as hygiene, not as a boundary.&lt;/p&gt;
&lt;p&gt;What does work is narrowing the identity. &lt;code&gt;ssh-add -c&lt;/code&gt; demands confirmation for every use, and a signature request with no tty to answer the prompt was refused. &lt;code&gt;ssh-add -h 'bastion&amp;gt;prod-db'&lt;/code&gt; constrains a key to specific source and destination host pairs and tests the whole path, forwarding included, against them. &lt;code&gt;ssh-add -t&lt;/code&gt; bounds how long any of this remains possible.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;The classic failure is a compromised bastion. Whoever owns that host inherits your identity for as long as the agent holds the key: they can authenticate to every host the key opens, and they can use the forwarded agent to reach further hosts you never intended. Rotating the key on disk does not interrupt it, because the loaded copy in your agent is still the old one and still signs.&lt;/p&gt;
&lt;p&gt;The quieter failure is forwarding left on globally. A &lt;code&gt;Host *&lt;/code&gt; stanza with &lt;code&gt;ForwardAgent yes&lt;/code&gt; turns every connection into a delegation, including the one to a colleague&amp;rsquo;s test box, and the socket that appears on the remote is owned by your account there and reachable by root on that machine. Forwarding should be a per-host decision, and it should not outlive the task that needed it.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Find out what you are actually delegating:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ssh -G prod-db | grep -iE &amp;#39;^(forwardagent|proxyjump)&amp;#39;
grep -rin forwardagent ~/.ssh/config
ssh-add -l
ssh -o ForwardAgent=no -J bastion,build-box prod-db
sshd -T | grep -i allowagentforwarding
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first line prints the effective values, so &lt;code&gt;forwardagent yes&lt;/code&gt; for a host you do not control is the finding; the last one, run on the server, shows whether the server will accept forwarding at all. &lt;code&gt;ssh-add -l&lt;/code&gt; should list only the identities the task needs, because everything loaded is delegated along with them.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh_config.5"&gt;OpenBSD: ssh_config&lt;/a&gt;, ForwardAgent and its warning, and ProxyJump as TCP forwarding from the jump host, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/sshd_config.5"&gt;OpenBSD: sshd_config&lt;/a&gt;, AllowAgentForwarding, its default, and why disabling it is not a boundary, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh-add.1"&gt;OpenBSD: ssh-add&lt;/a&gt;, -c confirmation, -t lifetime, and -h destination constraints for forwarded keys, checked 2026-09-16.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>