<?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>Linux on inetshell</title><link>https://inet.sh/en/tags/linux/</link><description>Recent content in Linux on inetshell</description><generator>Hugo</generator><language>en</language><lastBuildDate>Tue, 08 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://inet.sh/en/tags/linux/index.xml" rel="self" type="application/rss+xml"/><item><title>Persistent ssh-agent with systemd (user service)</title><link>https://inet.sh/en/posts/linux/ssh-agent-systemd/</link><pubDate>Tue, 08 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/linux/ssh-agent-systemd/</guid><description>Run ssh-agent as a systemd user service with a stable socket and load your id_ed25519 key on login, instead of starting a new agent per terminal.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># ~/.config/systemd/user/ssh-agent.service</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>Unit<span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="nv">Description</span><span class="o">=</span>SSH authentication agent
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">[</span>Service<span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="nv">ExecStart</span><span class="o">=</span>/usr/bin/ssh-agent -a %t/ssh-agent.socket -D
</span></span><span class="line"><span class="cl"><span class="nv">Type</span><span class="o">=</span>simple
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">[</span>Install<span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="nv">WantedBy</span><span class="o">=</span>default.target
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">systemctl --user daemon-reload
</span></span><span class="line"><span class="cl">systemctl --user <span class="nb">enable</span> --now ssh-agent
</span></span></code></pre></td></tr></table>
</div>
</div><p>And in your <code>~/.bashrc</code> or <code>~/.zshrc</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">SSH_AUTH_SOCK</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$XDG_RUNTIME_DIR</span><span class="s2">/ssh-agent.socket&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> ! ssh-add -l <span class="p">&amp;</span>&gt;/dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Adding keys...&#34;</span>
</span></span><span class="line"><span class="cl">  ssh-add -t 1d ~/.ssh/id_ed25519
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Log out and back in. Every shell now shares <strong>one</strong> agent on a stable socket, and you never spawn a fresh <code>ssh-agent</code> per terminal.</p>
<p>The classic pattern (<code>eval &quot;$(ssh-agent)&quot;</code> in <code>.bashrc</code>) starts one agent per shell, leaks orphaned processes, and loses keys between terminals. Letting <strong>systemd</strong> own the agent fixes all three: one process, one socket, one lifecycle.</p>
<h2 id="requirements">Requirements</h2>
<p><code>openssh</code> (ships <code>ssh-agent</code> and <code>ssh-add</code>) and a systemd user session (any modern distro with logind). Check:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">ssh-agent -V 2&gt;/dev/null<span class="p">;</span> ssh-add -l<span class="p">;</span> <span class="nb">echo</span> <span class="s2">&#34;XDG_RUNTIME_DIR=</span><span class="nv">$XDG_RUNTIME_DIR</span><span class="s2">&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>If <code>$XDG_RUNTIME_DIR</code> is empty you don&rsquo;t have a real user session (e.g. you got here via <code>su</code>); log in over SSH or on the console as your own user.</p>
<h2 id="1-create-the-user-service">1. Create the user service</h2>
<p>A <strong><code>--user</code></strong> service lives in <code>~/.config/systemd/user/</code> and runs as your account, no root:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">mkdir -p ~/.config/systemd/user
</span></span><span class="line"><span class="cl">cat &gt; ~/.config/systemd/user/ssh-agent.service <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">[Unit]
</span></span></span><span class="line"><span class="cl"><span class="s">Description=SSH authentication agent
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">[Service]
</span></span></span><span class="line"><span class="cl"><span class="s">ExecStart=/usr/bin/ssh-agent -a %t/ssh-agent.socket -D
</span></span></span><span class="line"><span class="cl"><span class="s">Type=simple
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">[Install]
</span></span></span><span class="line"><span class="cl"><span class="s">WantedBy=default.target
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></td></tr></table>
</div>
</div><ul>
<li><code>%t</code> expands to the user runtime dir (<code>$XDG_RUNTIME_DIR</code>, usually <code>/run/user/&lt;uid&gt;</code>), so the socket lands at <code>/run/user/&lt;uid&gt;/ssh-agent.socket</code>.</li>
<li><code>-a</code> pins that socket path (instead of a random one under <code>/tmp</code>), which is what we export next.</li>
<li><code>-D</code> keeps the agent in the foreground; that&rsquo;s correct for <code>Type=simple</code>.</li>
</ul>
<blockquote>
<p>If you&rsquo;d rather have it apply to <strong>all</strong> users on the box, drop the file in <code>/etc/systemd/user/ssh-agent.service</code> (needs root). For your own account, <code>~/.config/systemd/user/</code> is the clean choice. Pick one location, not both.</p></blockquote>
<h2 id="2-export-the-socket-and-load-the-key-on-login">2. Export the socket and load the key on login</h2>
<p>Add this to your <code>~/.bashrc</code> or <code>~/.zshrc</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">SSH_AUTH_SOCK</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$XDG_RUNTIME_DIR</span><span class="s2">/ssh-agent.socket&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> ! ssh-add -l <span class="p">&amp;</span>&gt;/dev/null<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Adding keys...&#34;</span>
</span></span><span class="line"><span class="cl">  ssh-add -t 1d ~/.ssh/id_ed25519
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span></code></pre></td></tr></table>
</div>
</div><ul>
<li>The <code>export</code> must come <strong>before</strong> the <code>ssh-add</code>, or the client won&rsquo;t know which agent to talk to.</li>
<li><code>ssh-add -l</code> exits 1 when the agent has no keys and 2 when it can&rsquo;t reach the socket; the <code>if !</code> covers both and (re)loads.</li>
<li><code>-t 1d</code> makes the key expire after a day. Drop <code>-t</code> if you want it to live until you restart the agent.</li>
</ul>
<h2 id="3-enable-and-verify">3. Enable and verify</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">systemctl --user daemon-reload
</span></span><span class="line"><span class="cl">systemctl --user <span class="nb">enable</span> --now ssh-agent
</span></span><span class="line"><span class="cl">systemctl --user status ssh-agent   <span class="c1"># active (running)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Open a new terminal (or <code>source ~/.bashrc</code>) and confirm the key is loaded:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">ssh-add -L   <span class="c1"># lists the public keys held by the agent</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="what-each-piece-does">What each piece does</h2>
<table>
  <thead>
      <tr>
          <th>Piece</th>
          <th>What it does</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ssh-agent.service</code> (<code>--user</code>)</td>
          <td>Keeps one agent alive, owned by systemd</td>
      </tr>
      <tr>
          <td><code>-a %t/ssh-agent.socket</code></td>
          <td>Socket at a fixed, predictable path</td>
      </tr>
      <tr>
          <td><code>SSH_AUTH_SOCK</code> in the rc</td>
          <td>Points every shell at that socket</td>
      </tr>
      <tr>
          <td><code>ssh-add -t 1d id_ed25519</code></td>
          <td>Loads the key on login, with optional expiry</td>
      </tr>
  </tbody>
</table>
<h2 id="traps">Traps</h2>
<ul>
<li><strong>Use your key&rsquo;s real name.</strong> Many guides say <code>id_rsa</code>; modern keys are <code>id_ed25519</code>. If the file doesn&rsquo;t exist, <code>ssh-add</code> fails quietly inside the <code>if</code>.</li>
<li><strong>Lingering:</strong> by default the user service dies when you close your last session. If you need the agent alive without an active login (cron, a CI runner on the box), enable lingering: <code>loginctl enable-linger $USER</code>.</li>
<li><strong>The socket must match.</strong> The unit&rsquo;s <code>-a %t/ssh-agent.socket</code> and the rc&rsquo;s <code>SSH_AUTH_SOCK=&quot;$XDG_RUNTIME_DIR/ssh-agent.socket&quot;</code> point at the <strong>same</strong> file. Change one, change the other.</li>
<li><strong><code>&amp;&gt;/dev/null</code> is bash/zsh.</strong> Under a strict <code>/bin/sh</code> (dash) use <code>&gt;/dev/null 2&gt;&amp;1</code>.</li>
<li><strong>Passphrase:</strong> if your key has a passphrase, the login <code>ssh-add</code> prompts once per expiry. With <code>-t 1d</code>, once a day.</li>
<li><strong>Don&rsquo;t mix agents.</strong> If a profile still does <code>eval &quot;$(ssh-agent)&quot;</code>, remove it; otherwise you end up with two agents and a <code>SSH_AUTH_SOCK</code> that flips between them.</li>
</ul>
<h2 id="see-also">See also</h2>
<ul>
<li>Original Server Fault thread: <a href="https://serverfault.com/questions/672346/straight-forward-way-to-run-ssh-agent-and-ssh-add-on-login-via-ssh">https://serverfault.com/questions/672346/straight-forward-way-to-run-ssh-agent-and-ssh-add-on-login-via-ssh</a></li>
<li>Reference gist (magnetikonline): <a href="https://gist.github.com/magnetikonline/b6255da90606fe9c5c25d3333c98c90d">https://gist.github.com/magnetikonline/b6255da90606fe9c5c25d3333c98c90d</a></li>
</ul>
]]></content:encoded></item></channel></rss>