<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Svyatoslav Pavlov</title>
    <description>The latest articles on DEV Community by Svyatoslav Pavlov (@wolfhound1995).</description>
    <link>https://dev.to/wolfhound1995</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4060150%2Fade29fc0-19eb-4bbf-9bf8-0fc51e252710.jpg</url>
      <title>DEV Community: Svyatoslav Pavlov</title>
      <link>https://dev.to/wolfhound1995</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wolfhound1995"/>
    <language>en</language>
    <item>
      <title>Every way to give an AI agent SSH access, ranked by blast radius</title>
      <dc:creator>Svyatoslav Pavlov</dc:creator>
      <pubDate>Thu, 13 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/wolfhound1995/every-way-to-give-an-ai-agent-ssh-access-ranked-by-blast-radius-1248</link>
      <guid>https://dev.to/wolfhound1995/every-way-to-give-an-ai-agent-ssh-access-ranked-by-blast-radius-1248</guid>
      <description>&lt;p&gt;Sooner or later you'll want an AI agent to touch a real server — tail a log, restart a service, run the deploy. Every way of arranging that answers the same underlying question: &lt;em&gt;when&lt;/em&gt; the agent gets confused or compromised — prompt injection, a poisoned tool result, a plan that skipped a step — &lt;strong&gt;what does it cost you?&lt;/strong&gt; That's the blast radius, and it's the right axis to rank the options on, because the failure isn't hypothetical. A model that reads untrusted input (logs, filenames, web pages, other tools' output) can be steered by it. You don't get to prevent that; you get to decide what it's worth when it happens.&lt;/p&gt;

&lt;p&gt;Here are the five arrangements people actually use, worst to best. The tradeoffs of each are real, including the good ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Paste the private key into the agent's environment
&lt;/h2&gt;

&lt;p&gt;The direct route: &lt;code&gt;id_ed25519&lt;/code&gt; in an env var, a config blob, a mounted file, so the agent can run &lt;code&gt;ssh&lt;/code&gt; itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blast radius: everything, forever.&lt;/strong&gt; An SSH key is a pure bearer credential — whoever holds the bytes &lt;em&gt;is&lt;/em&gt; you, on every host that trusts the key, with a full shell, from anywhere on the internet. And it's the one arrangement you can't walk back: once the key has passed through the model's context, a transcript, or a tool call you didn't read, you can't prove it didn't leak. The only honest remediation is rotating the key on every host that trusts it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest upside:&lt;/strong&gt; it works in thirty seconds with zero dependencies, which is why it's everywhere. That's the entire upside.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Forward your agent into the box the AI runs on
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ForwardAgent yes&lt;/code&gt; into the VM or devbox where the coding agent lives, so anything there can authenticate as you without holding the key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blast radius: your full identity, live, for the duration of the session.&lt;/strong&gt; The key bytes stay home — that's genuinely better than option 1, and when the session closes the exposure ends. But while it's open, any process on that box that can reach the forwarded socket can sign as you, to every host your keys reach, and nothing on your side shows it happening. You've lent the box live use of your identity; whether the borrower is the agent following instructions or something the agent was injected into is invisible from where you sit. &lt;a href="https://termal.in/blog/ssh-agent-forwarding-risks/" rel="noopener noreferrer"&gt;We've written up the mechanics and the mitigations&lt;/a&gt; — &lt;code&gt;ssh-add -c&lt;/code&gt; at minimum, so every signature needs a local click.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest upside:&lt;/strong&gt; revocation is closing the session, and no secret is ever at rest on the agent's side. As a &lt;em&gt;supervised, short-lived&lt;/em&gt; arrangement it beats a pasted key by a wide margin. As a standing one, it's option 1 with a timer.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A raw SSH-MCP server holding a key, no policy
&lt;/h2&gt;

&lt;p&gt;A standalone MCP server with an &lt;code&gt;ssh_exec&lt;/code&gt; tool and a key (or password) in its own config. The model never sees the credential — it sees tools. This is the shape of most minimal SSH-MCP servers you'll find.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blast radius: a full shell on every host the server's key reaches.&lt;/strong&gt; Getting the key out of the model's context is a real improvement — a leaked transcript no longer contains a credential, and revocation means rotating one known key, not auditing everywhere the model's context went. But the &lt;em&gt;capability&lt;/em&gt; is unbounded: any tool call can run anything, on any configured host, and a steered model doesn't need the key when &lt;code&gt;ssh_exec("curl … | sh")&lt;/code&gt; is on the menu. These servers also tend to ship with no session visibility and no audit trail, so your record of what an agent did is whatever the agent says it did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest upside:&lt;/strong&gt; simple, self-hosted, credential out of the context window. The gap is that it treats &lt;em&gt;key custody&lt;/em&gt; as the whole problem, when unbounded execution is most of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A scoped credential per task
&lt;/h2&gt;

&lt;p&gt;The classical ops answer: a deploy key limited to one repo, a &lt;code&gt;command=&lt;/code&gt;-restricted line in &lt;code&gt;authorized_keys&lt;/code&gt;, a dedicated low-privilege user, or — the serious version — short-lived certificates minted per task with the principals and TTL baked in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blast radius: one task's worth of capability, expiring on its own.&lt;/strong&gt; This is a genuinely strong position. A stolen deploy credential deploys; it doesn't read the production database two hops over. Short-lived certs mean stolen material goes stale in minutes, and &lt;code&gt;command=&lt;/code&gt; restrictions bound even a live credential to one action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest tradeoffs:&lt;/strong&gt; the operational burden is real and it compounds. Somebody has to mint, distribute, and track every scoped credential; a certificate flow needs CA infrastructure most teams don't run; and scoped credentials sprawl — the forced-command entry for a task finished in March is still in &lt;code&gt;authorized_keys&lt;/code&gt; in August, because revoking something requires remembering it exists. Under deadline pressure, "just this once" broadens the scope, and the scheme quietly degrades toward option 1. Observability is also not included: a scoped credential bounds what the agent &lt;em&gt;can&lt;/em&gt; do, but records nothing about what it &lt;em&gt;did&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. A custodian with per-host policy, live watch, and audit
&lt;/h2&gt;

&lt;p&gt;The agent never holds any credential. A custodian — a broker the human controls — holds the keys or mints the certificates, signs on the agent's behalf, and wraps execution in controls: the agent reaches only enrolled hosts, each host carries a command policy (full shell, an allowlist, or blocked), a human can watch the session live, and every command lands in an audit log attributed to the agent. &lt;a href="https://termal.in/blog/what-is-an-mcp-server/" rel="noopener noreferrer"&gt;The MCP explainer&lt;/a&gt; covers why "who holds the credential" is the load-bearing question; this is the arrangement where the answer is "not the agent, ever."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blast radius: the policy, minus what output can carry.&lt;/strong&gt; Compromise the agent's context entirely and what you hold is the ability to &lt;em&gt;request&lt;/em&gt; allowed commands on listed hosts — requests that appear in a live view and an audit log as they happen. No credential to exfiltrate, revocation is a toggle, and the record distinguishes the agent's actions from yours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest limits — because this option has them too:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Policy doesn't stop exfiltration through allowed output.&lt;/strong&gt; If &lt;code&gt;cat&lt;/code&gt; is on the allowlist, &lt;code&gt;cat .env&lt;/code&gt; returns your secrets into the model's context, and from there they can go wherever the model's output goes. A custodian bounds &lt;em&gt;actions&lt;/em&gt;, not the information content of results. Allowlists need to be written with reads in mind, not just writes — and secrets on disk are their own problem to fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A full-access host is still a full shell.&lt;/strong&gt; The policy is only as tight as you set it; "full" on production recreates option 3 for that host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The custodian is now the thing to protect.&lt;/strong&gt; Compromise the machine it runs on and you're back to holding keys. Concentrating custody is the right trade — one hardened place beats copies everywhere — but it's a trade, not a free lunch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watching reduces dwell time, not probability.&lt;/strong&gt; The live view and audit log mean a steered agent gets caught in minutes instead of discovered in a postmortem. Injection still happens; you've bounded what it's worth and how long it runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The ranking, compressed
&lt;/h2&gt;

&lt;p&gt;What a compromise costs, top to bottom: &lt;strong&gt;(1)&lt;/strong&gt; your identity, everywhere, unrevocably → &lt;strong&gt;(2)&lt;/strong&gt; your identity, live, until you notice → &lt;strong&gt;(3)&lt;/strong&gt; a full shell behind one revocable credential → &lt;strong&gt;(4)&lt;/strong&gt; one task's capability, self-expiring, if the hygiene holds → &lt;strong&gt;(5)&lt;/strong&gt; the policy you wrote, observed and revocable, minus whatever allowed output carries.&lt;/p&gt;

&lt;p&gt;If you're assembling option 4 or 5 yourself from certificates, forced commands and &lt;code&gt;tlog&lt;/code&gt;, that's a respectable project. The reason we built &lt;a href="https://termal.in/features/" rel="noopener noreferrer"&gt;Termalin&lt;/a&gt; is that almost nobody finishes that project: it's an SSH client whose built-in MCP server &lt;em&gt;is&lt;/em&gt; the custodian — keys stay in the one-unlock agent, per-host policy is a setting (full / allowlist / blocked), agent sessions mirror into a live watch grid, and every agent command is tagged in output-only recordings and the audit log. The same pattern works &lt;a href="https://termal.in/blog/give-a-local-llm-safe-ssh-access/" rel="noopener noreferrer"&gt;with a local model&lt;/a&gt; as with a hosted one, and the full custody story is written up on the &lt;a href="https://termal.in/security/" rel="noopener noreferrer"&gt;security page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Whatever you pick, pick it deliberately. The worst arrangement on this list is also the one you get by not choosing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Termalin is a free, cross-platform SSH client with a built-in MCP server, a key custodian and per-host agent policy — &lt;a href="https://termal.in/download/" rel="noopener noreferrer"&gt;download it&lt;/a&gt;, or read how it &lt;a href="https://termal.in/security/" rel="noopener noreferrer"&gt;handles keys safely&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>ssh</category>
      <category>security</category>
      <category>mcp</category>
    </item>
    <item>
      <title>SSH port forwarding, explained: local, remote and dynamic tunnels</title>
      <dc:creator>Svyatoslav Pavlov</dc:creator>
      <pubDate>Fri, 07 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/wolfhound1995/ssh-port-forwarding-explained-local-remote-and-dynamic-tunnels-5kh</link>
      <guid>https://dev.to/wolfhound1995/ssh-port-forwarding-explained-local-remote-and-dynamic-tunnels-5kh</guid>
      <description>&lt;p&gt;SSH port forwarding is one of those features people copy off Stack Overflow, get working once, and never quite understand — so the next time they need it, it's back to guessing which of &lt;code&gt;-L&lt;/code&gt;, &lt;code&gt;-R&lt;/code&gt; and &lt;code&gt;-D&lt;/code&gt; to use. The three flags aren't hard; they're just badly named. Here's the one mental model that makes them obvious, an example for each, and the flags and traps that decide whether the tunnel survives the afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one question that picks the flag
&lt;/h2&gt;

&lt;p&gt;Every SSH tunnel opens a &lt;strong&gt;listening port on one machine&lt;/strong&gt; and quietly ships whatever hits it to &lt;strong&gt;somewhere reachable from the other machine&lt;/strong&gt;. So the only question is: &lt;em&gt;which side opens the port you connect to?&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-L&lt;/code&gt; (local)&lt;/strong&gt; — the port opens on &lt;strong&gt;your&lt;/strong&gt; machine. You connect locally; traffic comes out on the server's side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-R&lt;/code&gt; (remote)&lt;/strong&gt; — the port opens on the &lt;strong&gt;server&lt;/strong&gt;. Someone connects there; traffic comes out on your side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-D&lt;/code&gt; (dynamic)&lt;/strong&gt; — the port opens on &lt;strong&gt;your&lt;/strong&gt; machine, but it's a SOCKS proxy: the destination is decided per-connection, not fixed up front.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else is detail. Keep "which side opens the port" in your head and you'll never reach for the wrong flag again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local forwarding (&lt;code&gt;-L&lt;/code&gt;) — pull a remote service to localhost
&lt;/h2&gt;

&lt;p&gt;The most common one. A database, an internal dashboard, a metrics UI — something bound to &lt;code&gt;127.0.0.1&lt;/code&gt; on the server, or on a machine only the server can reach. You want it on your laptop as if it were local.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;ssh&lt;/span&gt; -L &lt;span class="m"&gt;5432&lt;/span&gt;:localhost:5432 deploy@db.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it left to right: open &lt;code&gt;5432&lt;/code&gt; &lt;strong&gt;on my machine&lt;/strong&gt;, and forward anything that arrives to &lt;code&gt;localhost:5432&lt;/code&gt; &lt;strong&gt;as seen from the server&lt;/strong&gt;. Now &lt;code&gt;psql -h 127.0.0.1&lt;/code&gt; on your laptop talks to the server's Postgres — through the encrypted SSH connection, without exposing the database to the network at all.&lt;/p&gt;

&lt;p&gt;The middle host is resolved &lt;em&gt;on the server side&lt;/em&gt;, which is what makes this powerful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="c1"&gt;# reach a database that only the bastion can see&lt;/span&gt;
&lt;span class="k"&gt;ssh&lt;/span&gt; -L &lt;span class="m"&gt;5432&lt;/span&gt;:10.0.0.9:5432 deploy@bastion.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here &lt;code&gt;10.0.0.9&lt;/code&gt; is a private address your laptop can't route to — but the bastion can. The tunnel bridges the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remote forwarding (&lt;code&gt;-R&lt;/code&gt;) — expose your machine to the server's side
&lt;/h2&gt;

&lt;p&gt;The mirror image, and the one people find backwards. The port opens &lt;strong&gt;on the server&lt;/strong&gt;; connections to it come back out on &lt;strong&gt;your&lt;/strong&gt; end. Two classic uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="c1"&gt;# let the server (or its network) reach a dev server running on your laptop&lt;/span&gt;
&lt;span class="k"&gt;ssh&lt;/span&gt; -R &lt;span class="m"&gt;8080&lt;/span&gt;:localhost:3000 deploy@app.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now anything hitting &lt;code&gt;localhost:8080&lt;/code&gt; on the server is piped to &lt;code&gt;localhost:3000&lt;/code&gt; on your laptop — handy for showing a teammate on the box your work-in-progress, or letting a remote service call back into something only you're running.&lt;/p&gt;

&lt;p&gt;This is also the trick for reaching a machine stuck behind NAT with no inbound access: have &lt;em&gt;it&lt;/em&gt; open a reverse tunnel out to a server you control, and you connect through the server. The machine that can't accept connections makes the outbound one instead.&lt;/p&gt;

&lt;p&gt;One catch: by default the server binds the forwarded port to &lt;code&gt;localhost&lt;/code&gt;, so only the server itself can use it. To let &lt;em&gt;other&lt;/em&gt; machines on the server's network reach it, the server's &lt;code&gt;sshd_config&lt;/code&gt; needs &lt;code&gt;GatewayPorts yes&lt;/code&gt; (or &lt;code&gt;clientspecified&lt;/code&gt;) — a server-side setting, not something you can force from the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dynamic forwarding (&lt;code&gt;-D&lt;/code&gt;) — a SOCKS proxy in one flag
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;-L&lt;/code&gt; forwards to one fixed destination. &lt;code&gt;-D&lt;/code&gt; opens a &lt;strong&gt;SOCKS proxy&lt;/strong&gt; instead, so the destination is chosen per request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;ssh&lt;/span&gt; -D &lt;span class="m"&gt;1080&lt;/span&gt; deploy@jump.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point your browser (or &lt;code&gt;curl --socks5 127.0.0.1:1080&lt;/code&gt;) at &lt;code&gt;127.0.0.1:1080&lt;/code&gt; and every request is routed &lt;em&gt;from the server&lt;/em&gt;. It's the quick way to browse an internal network, test what a service looks like from a datacenter's IP, or reach a handful of internal hosts without a &lt;code&gt;-L&lt;/code&gt; for each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flags that make it usable
&lt;/h2&gt;

&lt;p&gt;A bare tunnel command also opens a shell you don't want and dies the moment you close it. In practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;ssh&lt;/span&gt; -N -L &lt;span class="m"&gt;5432&lt;/span&gt;:localhost:5432 deploy@db.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-N&lt;/code&gt;&lt;/strong&gt; — don't run a remote command. You only want the tunnel, not a shell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-f&lt;/code&gt;&lt;/strong&gt; — background the process after connecting (pair with &lt;code&gt;-N&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-o ExitOnForwardFailure=yes&lt;/code&gt;&lt;/strong&gt; — if the port can't be bound (already in use, permission denied), fail loudly instead of connecting anyway and silently forwarding nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keepalives&lt;/strong&gt; — a long-lived tunnel dies to idle timeouts without &lt;code&gt;ServerAliveInterval 30&lt;/code&gt;. Add it or the tunnel drops the moment you stop using it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Make it permanent in ssh_config
&lt;/h2&gt;

&lt;p&gt;Anything you'd pass with &lt;code&gt;-L&lt;/code&gt;/&lt;code&gt;-R&lt;/code&gt;/&lt;code&gt;-D&lt;/code&gt; has a config directive, so a saved host carries its tunnels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;Host&lt;/span&gt; db-tunnel
    &lt;span class="k"&gt;HostName&lt;/span&gt; db.example.com
    &lt;span class="k"&gt;User&lt;/span&gt; deploy
    &lt;span class="k"&gt;LocalForward&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt; localhost:5432
    &lt;span class="k"&gt;ServerAliveInterval&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
    &lt;span class="k"&gt;RequestTTY&lt;/span&gt; &lt;span class="no"&gt;no&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;ssh -N db-tunnel&lt;/code&gt; brings the database to &lt;code&gt;127.0.0.1:5432&lt;/code&gt; with no flags to remember. &lt;code&gt;RemoteForward&lt;/code&gt; and &lt;code&gt;DynamicForward&lt;/code&gt; work the same way. (More on the config file itself in &lt;a href="https://termal.in/blog/ssh-config-file-guide/" rel="noopener noreferrer"&gt;the ~/.ssh/config guide&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Three traps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bind address matters.&lt;/strong&gt; &lt;code&gt;-L 5432:...&lt;/code&gt; listens on &lt;code&gt;127.0.0.1&lt;/code&gt; only. To let other devices on &lt;em&gt;your&lt;/em&gt; LAN use the tunnel, prefix a bind address: &lt;code&gt;-L 0.0.0.0:5432:...&lt;/code&gt; — and understand you've just exposed it to your local network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Address already in use"&lt;/strong&gt; means the local port is taken (often a previous tunnel that didn't exit). Pick another local port or kill the old one; &lt;code&gt;ExitOnForwardFailure&lt;/code&gt; turns the silent failure into an error you'll actually notice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The middle host is resolved on the far side.&lt;/strong&gt; &lt;code&gt;localhost&lt;/code&gt; in &lt;code&gt;-L a:localhost:b&lt;/code&gt; means the &lt;em&gt;server's&lt;/em&gt; localhost, not yours. Getting this backwards is the single most common port-forwarding mistake.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When juggling tunnels by hand gets old
&lt;/h2&gt;

&lt;p&gt;Forwarding is a great primitive, and every developer should know the three flags cold. The friction shows up later: you keep a scratchpad of &lt;code&gt;ssh -N -L ...&lt;/code&gt; commands, you re-run them after every laptop sleep, you forget which local port maps to which service, and you can't tell at a glance whether a tunnel is even still up.&lt;/p&gt;

&lt;p&gt;That's where a client that treats tunnels as first-class state helps. &lt;a href="https://termal.in/features/" rel="noopener noreferrer"&gt;Termalin&lt;/a&gt; keeps local, remote and dynamic forwards attached to the host as saved config — start them with a click, see which are live, and let them re-establish with the connection instead of retyping the command. The same host list (with its keys, jump hosts and keepalives) syncs across your machines end-to-end encrypted, and each server is verified on first connect. The command line teaches you how tunnels work; a client keeps you from rebuilding them by hand every morning.&lt;/p&gt;

&lt;p&gt;Either way, the model is the whole trick: &lt;strong&gt;decide which side opens the port, and the rest of the flag writes itself.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Termalin is a free, cross-platform SSH client with built-in tunnels, SFTP and a key manager — &lt;a href="https://termal.in/download/" rel="noopener noreferrer"&gt;download it&lt;/a&gt;, or see how it &lt;a href="https://termal.in/security/" rel="noopener noreferrer"&gt;handles your keys safely&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ssh</category>
      <category>portforwarding</category>
      <category>tunneling</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to give a local LLM safe SSH access to your servers</title>
      <dc:creator>Svyatoslav Pavlov</dc:creator>
      <pubDate>Fri, 07 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/wolfhound1995/how-to-give-a-local-llm-safe-ssh-access-to-your-servers-36n1</link>
      <guid>https://dev.to/wolfhound1995/how-to-give-a-local-llm-safe-ssh-access-to-your-servers-36n1</guid>
      <description>&lt;p&gt;Local models are having a moment. Ollama on a workstation, an open-weight model behind your own endpoint, a self-hosted coding agent — you run them for good reasons: privacy, cost, no rate limits, works on a plane. And once a model is already handling your prompts, the obvious next step is to point it at your infrastructure: let it tail a log, restart a service, run the migration.&lt;/p&gt;

&lt;p&gt;Here's the trap that rides along with it: &lt;strong&gt;"it's local, so it's safe" is not true.&lt;/strong&gt; Running the model on your own hardware keeps your &lt;em&gt;prompts&lt;/em&gt; off someone else's servers. It does nothing about what happens when that model gets a shell.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "local" doesn't make server access safe
&lt;/h2&gt;

&lt;p&gt;Two things stay exactly as dangerous whether the model runs in a datacenter or on the box under your desk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key is still a bearer credential.&lt;/strong&gt; Drop &lt;code&gt;id_ed25519&lt;/code&gt; into the agent's environment so it can run &lt;code&gt;ssh&lt;/code&gt; itself, and whoever — or whatever — holds those bytes &lt;em&gt;is you&lt;/em&gt;, on every host that trusts the key. Running the model locally doesn't change that; it just moves the copy to a machine you happen to own. And you still can't take it back: once the key has passed through the model's context — an env var, a mounted file, a tool call, a transcript — you can no longer prove it didn't end up somewhere it shouldn't. The only honest answer to "did the model see my key?" is to rotate it on every host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A local model still gets confused.&lt;/strong&gt; Prompt injection doesn't need a cloud model. A poisoned log line, a malicious filename, a booby-trapped tool result, an over-eager plan — a local model acts on all of them just as readily as a hosted one. If anything the risk is &lt;em&gt;worse&lt;/em&gt;, because people trust local setups more, wire them up with fewer guardrails, and hand them broader access, precisely because "it's on my machine." That's the wrong instinct: the model's judgement isn't better because the weights are local.&lt;/p&gt;

&lt;p&gt;So the goal is the same as for any agent — let the model &lt;em&gt;do&lt;/em&gt; the work without ever &lt;em&gt;holding&lt;/em&gt; the credential, and make every action scoped, visible and reversible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to do it badly
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Paste the raw key.&lt;/strong&gt; The direct route, and the worst: no scope, no expiry, and the irreversibility above. The key that deploys to staging can usually also reach the production database two hops over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mint a long-lived token.&lt;/strong&gt; A deploy token or "automation" credential that expires never. It feels tidier than a key, but it's the same bearer problem with extra steps. If revoking something means remembering it exists, it'll outlive the experiment that created it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Give it its own root login and walk away.&lt;/strong&gt; The right instinct — a separate identity — done the wrong way. Drop a public key into &lt;code&gt;authorized_keys&lt;/code&gt; for a bot user and you've built access with no audit trail: nothing distinguishes the model's commands from anyone else's, and when something breaks at 2 a.m. you're reconstructing its session from bash history and vibes.&lt;/p&gt;

&lt;p&gt;The common thread: the model holds a standing credential, and observability is an afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern that works: a custodian, not a copy
&lt;/h2&gt;

&lt;p&gt;Flip it. The model should never hold the credential at all.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The model asks; a custodian signs.&lt;/strong&gt; A broker holds the keys (or mints short-lived certificates) and authenticates on the model's behalf. Compromise the model's context and you get the ability to &lt;em&gt;request&lt;/em&gt; actions through the broker — not the ability to impersonate you from anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope is explicit.&lt;/strong&gt; The model reaches only the hosts you've listed. A new host is a new decision, not a default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commands are policed.&lt;/strong&gt; Per host, decide whether the model gets a full shell, an allowlist of commands, or nothing — so "restart nginx" is allowed on the web tier and "anything at all" never is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A human can watch — live.&lt;/strong&gt; Not just in the post-mortem, and with the record distinguishing "the model did this" from "I did this."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revocation is a toggle, not a rotation.&lt;/strong&gt; Because nothing was ever shared, turning access off costs nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is exotic — it's roughly how certificate-based SSH already works on serious infra teams. The catch has always been that wiring it up yourself is a project, so people skip to one of the bad options above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it to a local model
&lt;/h2&gt;

&lt;p&gt;MCP — the Model Context Protocol — is the clean way to do this, and it's client-agnostic: any MCP-capable client can drive it, so it doesn't matter whether your model is Claude, an open-weight model behind Ollama, or something you host yourself.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run your local model through an MCP-capable client&lt;/strong&gt; (a coding agent or chat client that speaks MCP).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Point it at an MCP server that fronts SSH&lt;/strong&gt; — one that authenticates &lt;em&gt;for&lt;/em&gt; the model rather than handing it a key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn on only the hosts you want,&lt;/strong&gt; and set each host's command policy (full / allowlist / blocked).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the session in view&lt;/strong&gt; and let it write to an audit log, so every command the model runs is attributable and replayable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model gets tools — open a session, run a command, read or write a file — and the custodian does the authenticating. Your key never enters the model's environment, on your machine or anywhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Termalin does it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://termal.in/features/" rel="noopener noreferrer"&gt;Termalin&lt;/a&gt; is an SSH client with a built-in MCP server, so this pattern is the default rather than a project.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On your machine,&lt;/strong&gt; register the bundled local server with your agent (&lt;code&gt;claude mcp add termalin -- &amp;lt;path&amp;gt;/termalin-mcp&lt;/code&gt;, or the equivalent for any MCP client). The model reaches only the hosts you enable — agent access is off by default — and authentication goes through Termalin's key custodian: you unlock your keys once, Termalin signs on the model's behalf, and no key file is ever there for the model to read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per host, you set the policy&lt;/strong&gt; — full access, an allowlist of commands, or blocked — so a local model that gets confused can't run something you never authorized on that box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You watch it happen.&lt;/strong&gt; Agent sessions run as live terminal tabs; the watch grid mirrors them, and the tiles a model is driving glow. Every command is marked in the session recording — output only, never your keystrokes — and written to an audit log with the device and IP it came from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No app running?&lt;/strong&gt; Point the model at Termalin's hosted MCP endpoint with an API key that's scoped to specific servers, carries an expiry, is command-policed, and authenticates each run with a short-lived certificate — so even a leaked key can't reach an unlisted host or run outside its allowed commands.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either way, the property you wanted holds: your local model can operate your servers, and it has never seen a key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with one boring host
&lt;/h2&gt;

&lt;p&gt;Don't begin with production. Enroll a low-stakes box — a staging server, a toy VPS — set it to an allowlist, and give the model a real chore: tail the log until the error shows, fix the config, restart the service. Keep the grid open while it works. What you learn in the first hour — how it behaves, where it hesitates, what it does with ambiguity — tells you whether the second host gets enrolled, and with how much rope.&lt;/p&gt;

&lt;p&gt;That's the quiet payoff of the custodian model: you expand one host at a time, because no step you take is one you can't take back — no matter where the model is running.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Termalin is a free, cross-platform SSH client with a built-in MCP server, a key custodian and per-host agent policy — &lt;a href="https://termal.in/download/" rel="noopener noreferrer"&gt;download it&lt;/a&gt;, or read how it &lt;a href="https://termal.in/security/" rel="noopener noreferrer"&gt;handles keys safely&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ssh</category>
      <category>aiagents</category>
      <category>localllm</category>
      <category>security</category>
    </item>
    <item>
      <title>SSH key types in 2026: Ed25519 vs RSA vs ECDSA — which to use</title>
      <dc:creator>Svyatoslav Pavlov</dc:creator>
      <pubDate>Thu, 06 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/wolfhound1995/ssh-key-types-in-2026-ed25519-vs-rsa-vs-ecdsa-which-to-use-1go3</link>
      <guid>https://dev.to/wolfhound1995/ssh-key-types-in-2026-ed25519-vs-rsa-vs-ecdsa-which-to-use-1go3</guid>
      <description>&lt;p&gt;When you run &lt;code&gt;ssh-keygen&lt;/code&gt;, it asks nothing about the algorithm and quietly picks one for you — and that default has changed over the years. If you're generating a key today and want the short answer: &lt;strong&gt;use Ed25519.&lt;/strong&gt; The longer answer, and the cases where it's not that simple, are below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line recommendation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"you@device-2026"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Ed25519 keys are small, fast, and as strong as a 3072-bit RSA key while being a fraction of the size. Unless you have a specific reason not to (one real case below), this is the key to make.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four you'll meet
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ed25519 — the modern default.&lt;/strong&gt; An elliptic-curve signature scheme (Curve25519) designed by Daniel J. Bernstein and collaborators. Fixed size (~68-character public key), fast to generate and verify, and immune to the RNG and parameter footguns that plagued older ECDSA. Supported by OpenSSH since 6.5 (2014), so every current server and service — GitHub, GitLab, your VPS — accepts it. This is the right default in 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RSA — the compatibility fallback.&lt;/strong&gt; The old workhorse. Still perfectly secure &lt;strong&gt;at 3072 or 4096 bits&lt;/strong&gt; (&lt;code&gt;ssh-keygen -t rsa -b 4096&lt;/code&gt;). Its one advantage is reach: some ancient appliance, a legacy jump host, or an enterprise system stuck on an old OpenSSH may not know Ed25519. If you hit &lt;code&gt;no matching host key type&lt;/code&gt;, an RSA key is your escape hatch. The catch: bigger keys, slower, and &lt;strong&gt;anything under 2048 bits is unsafe&lt;/strong&gt; — regenerate old 1024-bit keys now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ECDSA — skip it.&lt;/strong&gt; Elliptic-curve like Ed25519, but it depends on a per-signature random value that, if the random number generator is weak, leaks your private key (this is how a certain game console's signing key was recovered in 2010). Ed25519 was specifically designed to remove that failure mode. ECDSA isn't broken if implemented perfectly, but there's no reason to choose it over Ed25519 today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DSA — never.&lt;/strong&gt; Capped at 1024 bits, disabled by default in modern OpenSSH, being removed entirely. If you find a &lt;code&gt;id_dsa&lt;/code&gt; in your &lt;code&gt;~/.ssh&lt;/code&gt;, retire it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick decision
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Making a key today, normal servers&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Ed25519&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Must reach an old/legacy system that rejects Ed25519&lt;/td&gt;
&lt;td&gt;RSA 4096&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You have an existing ECDSA or 1024-bit key&lt;/td&gt;
&lt;td&gt;Regenerate as Ed25519&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anything DSA&lt;/td&gt;
&lt;td&gt;Delete it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Things worth doing while you're here
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Always set a passphrase.&lt;/strong&gt; A private key with no passphrase is a plaintext password to your servers — anyone who copies the file is you. Add one at generation; unlock it once per session with an agent (&lt;code&gt;AddKeysToAgent yes&lt;/code&gt; in your &lt;a href="https://termal.in/blog/ssh-config-file-guide/" rel="noopener noreferrer"&gt;ssh_config&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Name your keys by device, not by "id".&lt;/strong&gt; &lt;code&gt;~/.ssh/id_ed25519&lt;/code&gt; on five machines is five keys you can't tell apart. &lt;code&gt;laptop_ed25519&lt;/code&gt;, &lt;code&gt;work-desktop_ed25519&lt;/code&gt; makes revocation a one-line edit to &lt;code&gt;authorized_keys&lt;/code&gt; instead of a guessing game.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rotate on a schedule, and when a device is lost.&lt;/strong&gt; A key is a credential; treat it like one. When a laptop dies or is stolen, the fix is removing its public key from every server's &lt;code&gt;authorized_keys&lt;/code&gt; — which is far easier when keys are named per device and you have a list of your servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a client helps
&lt;/h2&gt;

&lt;p&gt;Generating one key is easy. The friction shows up at scale: which key is on which server, which passphrase goes with which file, and un-trusting a lost device across a fleet. &lt;a href="https://termal.in/features/" rel="noopener noreferrer"&gt;Termalin&lt;/a&gt; generates Ed25519 and RSA keys, shows each key's fingerprint, and unlocks them once through a built-in agent so passphrases aren't retyped all day — and because it keeps the list of your hosts and keys together (synced across machines, end-to-end encrypted), rotating a compromised key is a visible, finite task rather than an archaeology dig through five &lt;code&gt;~/.ssh&lt;/code&gt; folders.&lt;/p&gt;

&lt;p&gt;The algorithm choice, though, stays simple: &lt;strong&gt;Ed25519 unless something old forces your hand.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Termalin is a free, cross-platform SSH client that generates and manages your keys with a one-unlock agent — &lt;a href="https://termal.in/download/" rel="noopener noreferrer"&gt;download it&lt;/a&gt;, or read the &lt;a href="https://termal.in/security/" rel="noopener noreferrer"&gt;security model&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ssh</category>
      <category>sshkeys</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The ~/.ssh/config file: stop retyping the same SSH flags</title>
      <dc:creator>Svyatoslav Pavlov</dc:creator>
      <pubDate>Thu, 06 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/wolfhound1995/the-sshconfig-file-stop-retyping-the-same-ssh-flags-568i</link>
      <guid>https://dev.to/wolfhound1995/the-sshconfig-file-stop-retyping-the-same-ssh-flags-568i</guid>
      <description>&lt;p&gt;If you've ever typed &lt;code&gt;ssh -i ~/.ssh/work_ed25519 -p 2222 -o ServerAliveInterval=30 deploy@203.0.113.10&lt;/code&gt; more than twice, you've already earned back the ten minutes it takes to learn &lt;code&gt;ssh_config&lt;/code&gt;. It's the single highest-leverage file in your SSH setup: it turns that whole line into &lt;code&gt;ssh web1&lt;/code&gt;, and it's read automatically by &lt;code&gt;ssh&lt;/code&gt;, &lt;code&gt;scp&lt;/code&gt;, &lt;code&gt;sftp&lt;/code&gt;, &lt;code&gt;rsync&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt;, and anything else that shells out to SSH.&lt;/p&gt;

&lt;p&gt;Here's the practical tour — the directives that actually earn their place, and the traps worth knowing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it lives and how it's read
&lt;/h2&gt;

&lt;p&gt;Your personal config is &lt;code&gt;~/.ssh/config&lt;/code&gt; (create it if it doesn't exist; &lt;code&gt;chmod 600&lt;/code&gt;). There's also a system-wide &lt;code&gt;/etc/ssh/ssh_config&lt;/code&gt;. The rules that trip people up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;First match wins, not most specific.&lt;/strong&gt; SSH reads top to bottom and the &lt;em&gt;first&lt;/em&gt; value it sees for each option is the one it uses. Put your specific hosts at the top and broad &lt;code&gt;Host *&lt;/code&gt; defaults at the bottom.&lt;/li&gt;
&lt;li&gt;Every setting lives under a &lt;code&gt;Host&lt;/code&gt; block (a pattern), indented by convention (indentation isn't required, but read the file your future self will thank you for).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The building block: a host alias
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;Host&lt;/span&gt; web1
    &lt;span class="k"&gt;HostName&lt;/span&gt; &lt;span class="m"&gt;203&lt;/span&gt;.0.113.10
    &lt;span class="k"&gt;User&lt;/span&gt; deploy
    &lt;span class="k"&gt;Port&lt;/span&gt; &lt;span class="m"&gt;2222&lt;/span&gt;
    &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/work_ed25519
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;ssh web1&lt;/code&gt; expands to the full command. So does &lt;code&gt;scp file.tar web1:/srv/&lt;/code&gt;, &lt;code&gt;sftp web1&lt;/code&gt;, and &lt;code&gt;rsync -a ./dist/ web1:/var/www/&lt;/code&gt;. The alias is the whole point: name the server once, use the name everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Directives that earn their place
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ProxyJump — reach servers through a bastion.&lt;/strong&gt; The modern replacement for the old &lt;code&gt;ProxyCommand ... netcat&lt;/code&gt; incantation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;Host&lt;/span&gt; db1
    &lt;span class="k"&gt;HostName&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;.0.0.5          &lt;span class="c1"&gt;# private address, only reachable from the bastion&lt;/span&gt;
    &lt;span class="k"&gt;User&lt;/span&gt; deploy
    &lt;span class="k"&gt;ProxyJump&lt;/span&gt; bastion

&lt;span class="k"&gt;Host&lt;/span&gt; bastion
    &lt;span class="k"&gt;HostName&lt;/span&gt; bastion.example.com
    &lt;span class="k"&gt;User&lt;/span&gt; jump
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ssh db1&lt;/code&gt; now transparently hops through &lt;code&gt;bastion&lt;/code&gt; to reach a host with no public address. Chain them with commas (&lt;code&gt;ProxyJump a,b,c&lt;/code&gt;) for deeper networks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keepalives — stop idle sessions from dying.&lt;/strong&gt; The fix for &lt;code&gt;client_loop: send disconnect: Broken pipe&lt;/code&gt; when you look away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;Host&lt;/span&gt; *
    &lt;span class="k"&gt;ServerAliveInterval&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
    &lt;span class="k"&gt;ServerAliveCountMax&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A packet every 30 seconds keeps NAT routers and firewalls from forgetting your connection; four missed replies (two minutes) before it gives up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IdentitiesOnly — offer only the right key.&lt;/strong&gt; If you carry several keys, SSH offers them all and a strict server replies &lt;code&gt;Too many authentication failures&lt;/code&gt;. Pin one key per host and stop the guessing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;Host&lt;/span&gt; github.com
    &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/personal_ed25519
    &lt;span class="k"&gt;IdentitiesOnly&lt;/span&gt; &lt;span class="no"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;AddKeysToAgent — unlock a passphrase-protected key once:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;Host&lt;/span&gt; *
    &lt;span class="k"&gt;AddKeysToAgent&lt;/span&gt; &lt;span class="no"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Patterns and wildcards — configure a fleet in one block:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;Host&lt;/span&gt; *.staging.example.com
    &lt;span class="k"&gt;User&lt;/span&gt; deploy
    &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/staging_ed25519

&lt;span class="k"&gt;Host&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;.0.*
    &lt;span class="k"&gt;ProxyJump&lt;/span&gt; bastion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A realistic config, top to bottom
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Specific hosts first (first match wins)&lt;/span&gt;
&lt;span class="k"&gt;Host&lt;/span&gt; web1
    &lt;span class="k"&gt;HostName&lt;/span&gt; &lt;span class="m"&gt;203&lt;/span&gt;.0.113.10
    &lt;span class="k"&gt;User&lt;/span&gt; deploy
    &lt;span class="k"&gt;Port&lt;/span&gt; &lt;span class="m"&gt;2222&lt;/span&gt;
    &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/work_ed25519

&lt;span class="k"&gt;Host&lt;/span&gt; db1
    &lt;span class="k"&gt;HostName&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;.0.0.5
    &lt;span class="k"&gt;User&lt;/span&gt; deploy
    &lt;span class="k"&gt;ProxyJump&lt;/span&gt; bastion

&lt;span class="k"&gt;Host&lt;/span&gt; bastion
    &lt;span class="k"&gt;HostName&lt;/span&gt; bastion.example.com
    &lt;span class="k"&gt;User&lt;/span&gt; jump
    &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/work_ed25519

&lt;span class="k"&gt;Host&lt;/span&gt; github.com
    &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/personal_ed25519
    &lt;span class="k"&gt;IdentitiesOnly&lt;/span&gt; &lt;span class="no"&gt;yes&lt;/span&gt;

&lt;span class="c1"&gt;# Broad defaults last&lt;/span&gt;
&lt;span class="k"&gt;Host&lt;/span&gt; *
    &lt;span class="k"&gt;ServerAliveInterval&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
    &lt;span class="k"&gt;ServerAliveCountMax&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
    &lt;span class="k"&gt;AddKeysToAgent&lt;/span&gt; &lt;span class="no"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Split it up as it grows: &lt;code&gt;Include ~/.ssh/config.d/*&lt;/code&gt; at the top of the file lets you keep one file per project or client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two traps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"First match wins" bites you&lt;/strong&gt; when a &lt;code&gt;Host *&lt;/code&gt; block near the top sets a value you meant to override below — it won't override, because the earlier value already won. Defaults go at the &lt;em&gt;bottom&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comments are &lt;code&gt;#&lt;/code&gt; at line start.&lt;/strong&gt; Trailing comments after a value can be parsed as part of the value on some options — keep comments on their own lines.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When the config file isn't enough
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ssh_config&lt;/code&gt; is excellent and everyone should have one. Its edges show when the setup gets bigger than a text file: you're syncing it across machines by hand, sharing sanitized versions with teammates, remembering which passphrase goes with which key, or trying to see at a glance which of forty hosts is even online.&lt;/p&gt;

&lt;p&gt;That's the seam &lt;a href="https://termal.in/features/" rel="noopener noreferrer"&gt;Termalin&lt;/a&gt; fills. It keeps the same per-host settings — hostname, user, port, key, jump host, keepalives — but attached to the host as structured data you organize into folders and tags, unlocks your keys once through a built-in agent, verifies each server on first connect, and shows live status for every host in the list. It imports your existing PuTTY sessions, and its host list syncs across your machines end-to-end encrypted. The file scales to a point; a client that keeps the state for you scales past it.&lt;/p&gt;

&lt;p&gt;Either way, the principle holds: &lt;strong&gt;name a server once, and never retype its flags again.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Termalin is a free, cross-platform SSH client that manages per-host settings, keys and jump hosts for you — &lt;a href="https://termal.in/download/" rel="noopener noreferrer"&gt;download it&lt;/a&gt;, or read how it &lt;a href="https://termal.in/security/" rel="noopener noreferrer"&gt;handles keys safely&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ssh</category>
      <category>sshconfig</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to let an AI agent deploy to your server — without handing it your SSH keys</title>
      <dc:creator>Svyatoslav Pavlov</dc:creator>
      <pubDate>Wed, 05 Aug 2026 07:24:51 +0000</pubDate>
      <link>https://dev.to/wolfhound1995/how-to-let-an-ai-agent-deploy-to-your-server-without-handing-it-your-ssh-keys-4blo</link>
      <guid>https://dev.to/wolfhound1995/how-to-let-an-ai-agent-deploy-to-your-server-without-handing-it-your-ssh-keys-4blo</guid>
      <description>&lt;p&gt;The coding part is mostly solved. An agent can branch, patch, write the tests, open the PR. Then the change has to land on an actual server — a service restarted, a migration run, an nginx config edited — and the autonomy stops cold. Deploying needs SSH, SSH needs credentials, and nobody in their right mind wants to paste a private key into an agent's environment.&lt;/p&gt;

&lt;p&gt;That hesitation is correct, and it's worth being precise about why. An SSH private key is a bearer credential: whoever holds the bytes &lt;em&gt;is&lt;/em&gt; you. Sharing it with an agent is not like sharing it with a colleague, because you can't take it back. Once a key has passed through an agent's context — an env var, a mounted file, a config blob — you can no longer prove it didn't end up in a log, a transcript, or a tool call you never read. The only honest answer to "did the model see my key?" is to rotate the key, on every host that trusts it. That's the irreversibility problem, and it's why "just give the agent a key" feels wrong even to people who can't articulate the threat model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to do it badly
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Paste the raw key.&lt;/strong&gt; The direct route: drop &lt;code&gt;id_ed25519&lt;/code&gt; into the agent's config or environment and let it run &lt;code&gt;ssh&lt;/code&gt; itself. Beyond the irreversibility above, an SSH key has no scope and no expiry — the key that deploys to staging can usually also read the production database credentials two hops away. And revocation means rotation, which means touching &lt;code&gt;authorized_keys&lt;/code&gt; on every box, which means you'll put it off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mint a long-lived token.&lt;/strong&gt; A deploy token, a PAT, an "automation" credential that expires never. It feels more hygienic than a key, but it's the same bearer problem with extra steps. The rule of thumb: if revoking a credential requires &lt;em&gt;remembering it exists&lt;/em&gt;, it will outlive the experiment it was created for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Give the agent its own root account.&lt;/strong&gt; This one at least shows the right instinct — a separate identity you can revoke separately. But done the usual way (drop a public key into &lt;code&gt;authorized_keys&lt;/code&gt; for a &lt;code&gt;deploy-bot&lt;/code&gt; user, walk away), it ships with no audit trail. Nothing distinguishes the agent's commands from anyone else's, nothing records what it actually did, and when something breaks at 2 a.m. you're reconstructing an agent's session from bash history and vibes.&lt;/p&gt;

&lt;p&gt;The common thread: in all three, the agent holds a standing credential, and observability is an afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  What good looks like: a custodian, not a copy
&lt;/h2&gt;

&lt;p&gt;Flip the model. The agent should never hold the credential at all.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The agent asks; a custodian signs.&lt;/strong&gt; A broker process holds the keys — or mints short-lived certificates — and performs the authentication on the agent's behalf. The agent's environment contains nothing worth stealing: compromise the context and you get the ability to &lt;em&gt;request&lt;/em&gt; actions through the broker, not the ability to impersonate you from anywhere on the internet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope is explicit.&lt;/strong&gt; The custodian only reaches the hosts you've listed. A new host is a new decision, not a default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A human can watch.&lt;/strong&gt; Live, while it happens — not just in the post-mortem. And the record must distinguish "the agent did this" from "I did this".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revocation is a toggle, not a rotation.&lt;/strong&gt; Because nothing was ever shared, turning access off costs nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is exotic — it's roughly how certificate-based SSH already works at companies with real infrastructure teams. The catch has always been that wiring it up yourself is a project, so most people skip straight to one of the bad options above.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Termalin implements it
&lt;/h2&gt;

&lt;p&gt;Termalin is an SSH client with a built-in &lt;a href="https://termal.in/mcp/" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt;. Your agent — Claude, or anything that speaks the Model Context Protocol — gets tools like &lt;code&gt;hosts_list&lt;/code&gt;, &lt;code&gt;ssh_exec&lt;/code&gt;, SFTP reads and writes, and persistent sessions. Termalin does the authenticating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On your machine.&lt;/strong&gt; Register the bundled local server with your agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add termalin &lt;span class="nt"&gt;--&lt;/span&gt; &amp;lt;path&amp;gt;/termalin-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent reaches only the hosts you've enabled in &lt;strong&gt;Settings → MCP&lt;/strong&gt; — agent access is off by default, and the host inventory is written with agent-only auth, so no passwords land on disk. Authentication goes through Termalin's &lt;strong&gt;key custodian&lt;/strong&gt;: you unlock your keys once, and Termalin signs on the agent's behalf. No &lt;code&gt;ssh-add&lt;/code&gt;, no key file for the agent to read, no key touching disk at all.&lt;/p&gt;

&lt;p&gt;And you can actually watch. Agent sessions run as live terminal tabs, the &lt;strong&gt;watch grid&lt;/strong&gt; mirrors every open session side by side, and the tiles an agent is driving glow. Letting the agent type into a session &lt;em&gt;you&lt;/em&gt; already have open is a separate consent toggle, not a default. Every agent command is marked in the session recording — which captures output only, never your keystrokes — and written to the audit log with the device and IP it came from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No app running.&lt;/strong&gt; If your agent lives where your desktop isn't — CI, a cloud sandbox — create an API key in the &lt;a href="https://termal.in/account" rel="noopener noreferrer"&gt;web cabinet&lt;/a&gt; and point it at the hosted MCP endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"termalin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://termal.in/api/v1/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer tk_live_…"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hosted endpoint reaches only servers enrolled with the tunnel agent, and it authenticates each run with a &lt;strong&gt;short-lived certificate&lt;/strong&gt; — again, no standing key is ever handed out. The API keys themselves are scoped to specific servers, carry an expiry (30, 90 or 365 days), and can be revoked any time; hosted runs are rate-limited and time-boxed per key.&lt;/p&gt;

&lt;p&gt;Either way, the property you wanted holds: the agent can deploy, and it has never seen a key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with one boring host
&lt;/h2&gt;

&lt;p&gt;Don't begin with production. Enroll one low-stakes box — the staging server, a toy VPS. Give the agent a real chore: deploy the branch, tail the log until the error shows up, fix the config, restart the service. Keep the watch grid open while it works. What you learn in the first hour — how it behaves, where it hesitates, what it does with ambiguity — tells you whether the second host gets enrolled.&lt;/p&gt;

&lt;p&gt;That's the quiet payoff of the custodian model: you get to expand one host at a time, because no step you take is one you can't take back.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Termalin's Free tier is the full desktop app, and every new account starts with a 14-day Pro trial — &lt;a href="https://termal.in/download/" rel="noopener noreferrer"&gt;download it&lt;/a&gt;, or start with the &lt;a href="https://termal.in/mcp/" rel="noopener noreferrer"&gt;MCP docs&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>ssh</category>
      <category>security</category>
    </item>
    <item>
      <title>How to record SSH sessions — without keylogging yourself</title>
      <dc:creator>Svyatoslav Pavlov</dc:creator>
      <pubDate>Wed, 05 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/wolfhound1995/how-to-record-ssh-sessions-without-keylogging-yourself-3bm4</link>
      <guid>https://dev.to/wolfhound1995/how-to-record-ssh-sessions-without-keylogging-yourself-3bm4</guid>
      <description>&lt;p&gt;Sooner or later you want a record of what happened in a terminal. A deploy went sideways at 2 a.m. and the postmortem needs facts, not memories. A new teammate wants to see how the migration was actually run. An auditor wants proof. Or — the new reason — an AI agent worked on your server, and "what exactly did it do?" deserves a better answer than a summary written by the same agent.&lt;/p&gt;

&lt;p&gt;Recording SSH sessions is a solved problem several times over. What's &lt;em&gt;not&lt;/em&gt; solved by default is doing it without quietly building a keylogger aimed at yourself. Let's go through the options, then the trap. (If you'd rather see how a client does this out of the box, that's &lt;a href="https://termal.in/ssh-session-recording/" rel="noopener noreferrer"&gt;SSH session recording&lt;/a&gt; in Termalin — this guide covers the DIY routes.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The quick ways to record a session
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;script&lt;/code&gt; — always there.&lt;/strong&gt; Ships with every Unix since before you were born:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;script &lt;span class="nt"&gt;-T&lt;/span&gt; timing.log session.log
&lt;span class="c"&gt;# ... work ...&lt;/span&gt;
&lt;span class="nb"&gt;exit
&lt;/span&gt;scriptreplay &lt;span class="nt"&gt;-t&lt;/span&gt; timing.log session.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Records everything printed to the terminal, replays with original timing. Crude but dependable; the files are plain text plus a timing track.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;asciinema — the pretty one.&lt;/strong&gt; &lt;code&gt;asciinema rec demo.cast&lt;/code&gt; records the terminal as a compact JSON event stream you can replay in a browser, embed in docs, or share. Perfect for demos and how-tos; less ideal as an audit trail (it's trivially editable).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;tlog — the server-side one.&lt;/strong&gt; Red Hat's &lt;code&gt;tlog&lt;/code&gt; wraps the login shell and journals every session to systemd-journald or Elasticsearch. Being server-side, it catches &lt;em&gt;every&lt;/em&gt; way in — handy for the "all access to this box is recorded" requirement. Pairing it with a session-recording-aware SSSD setup is the classic RHEL compliance stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;auditd tty logging — the heavyweight.&lt;/strong&gt; &lt;code&gt;pam_tty_audit&lt;/code&gt; records TTY input at the kernel level. Note the word &lt;em&gt;input&lt;/em&gt;. We'll come back to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: recording input records your secrets
&lt;/h2&gt;

&lt;p&gt;Here's the thing nobody mentions until an incident: a terminal session has two streams. &lt;strong&gt;Output&lt;/strong&gt; — what the server printed — is what you almost always want: commands appear there anyway (your shell echoes them), along with everything they printed. &lt;strong&gt;Input&lt;/strong&gt; — the raw keystrokes — adds exactly one category of data output doesn't have: &lt;em&gt;the things that were deliberately not echoed&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;What isn't echoed? Passwords at &lt;code&gt;sudo&lt;/code&gt; prompts. Passphrases. The token you pasted into a login prompt. The database password you typed into &lt;code&gt;psql&lt;/code&gt;. Record keystrokes and your audit trail is now a credentials file with excellent timestamps — stored for years, synced to log servers, readable by whoever reads logs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pam_tty_audit&lt;/code&gt; has a &lt;code&gt;disable=*&lt;/code&gt; / &lt;code&gt;enable=root&lt;/code&gt; dance and a &lt;code&gt;log_passwd&lt;/code&gt; option specifically because of this; the default without care is unsafe. Homegrown wrappers that tee stdin are unsafe, period. If your recording setup can answer the question "what was the sudo password?", it's not an audit trail — it's an incident waiting for a retention policy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule: record output, never input.&lt;/strong&gt; You lose nothing forensically — every command that ran is visible in the output stream — and you stop hoarding secrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage is part of the design
&lt;/h2&gt;

&lt;p&gt;Recordings are as sensitive as shell history times ten: they contain config files you &lt;code&gt;cat&lt;/code&gt;ted, database rows you selected, environment dumps. Two boring rules cover most of it: store them &lt;strong&gt;encrypted&lt;/strong&gt;, and decide &lt;strong&gt;who can replay&lt;/strong&gt; before the first recording exists, not after. If recordings sync anywhere (they should — a laptop-only audit trail dies with the laptop), the sync should be end-to-end encrypted so the storage provider is not silently a member of your security team.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent angle
&lt;/h2&gt;

&lt;p&gt;If AI agents touch your servers, recording stops being a compliance checkbox and becomes the core of the trust model. The properties that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The recording is made by the client, not narrated by the agent.&lt;/strong&gt; What ran is what you replay — not what the model chose to summarize.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent actions are labeled.&lt;/strong&gt; At 2 a.m. you need "the agent did this, I did that" to be a filter, not a reconstruction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output-only still applies.&lt;/strong&gt; The agent's session shouldn't capture your keystrokes when you step in to type into it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Termalin does it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://termal.in/ssh-session-recording/" rel="noopener noreferrer"&gt;Termalin&lt;/a&gt; records sessions &lt;strong&gt;output-only by design&lt;/strong&gt; — keystrokes are never captured, so there is nothing password-shaped to leak. Recording is a per-host toggle; replay is built in (with timing, like &lt;code&gt;scriptreplay&lt;/code&gt; but with a UI), and every command an &lt;a href="https://termal.in/mcp/" rel="noopener noreferrer"&gt;agent&lt;/a&gt; ran through the MCP server is marked as the agent's in the recording and the audit log, with the device and IP it came from. Recordings sync end-to-end encrypted on Pro — the server stores ciphertext it cannot replay.&lt;/p&gt;

&lt;p&gt;The takeaway works with any stack, ours included: record everything the server said, nothing you typed blind, encrypt the archive — and when an agent drives, make the recording the ground truth.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Termalin's Free tier records and replays locally with no host limits — &lt;a href="https://termal.in/download/" rel="noopener noreferrer"&gt;download it&lt;/a&gt;, or see &lt;a href="https://termal.in/ssh-session-recording/" rel="noopener noreferrer"&gt;session recording&lt;/a&gt; in detail.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ssh</category>
      <category>sessionrecording</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>SSH clients for AI agents in 2026: the landscape</title>
      <dc:creator>Svyatoslav Pavlov</dc:creator>
      <pubDate>Wed, 05 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/wolfhound1995/ssh-clients-for-ai-agents-in-2026-the-landscape-7bo</link>
      <guid>https://dev.to/wolfhound1995/ssh-clients-for-ai-agents-in-2026-the-landscape-7bo</guid>
      <description>&lt;p&gt;A year ago "SSH client for AI agents" wasn't a product category. Then coding agents got good enough to deploy what they write, the Model Context Protocol gave them a standard way to hold tools, and suddenly several teams — ours included — are building terminals where the operator isn't always human.&lt;/p&gt;

&lt;p&gt;Full disclosure up front: &lt;strong&gt;we build Termalin&lt;/strong&gt;, one of the products below. This post exists because when we asked AI assistants what's out there, they assembled answers from app-store listings and directory scraps — nobody had mapped the field. So here's the map, with the competition described as fairly as we can manage. Facts checked August 5, 2026; this space moves fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three desktop clients
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Termalin&lt;/strong&gt; (that's us) — a cross-platform SSH client for &lt;strong&gt;Windows, macOS and Linux&lt;/strong&gt;: tabbed terminal, SFTP with a built-in editor, dual-pane file manager (host↔host, S3), snippets, live server stats. The agent layer is a built-in MCP server with a &lt;strong&gt;key custodian&lt;/strong&gt;: the agent requests, Termalin signs — no key ever enters the agent's environment. Every agent session is mirrored live in a &lt;strong&gt;watch grid&lt;/strong&gt;, and agent commands are marked in output-only recordings and the audit log. Two things nobody else on this list has: a &lt;strong&gt;hosted MCP endpoint&lt;/strong&gt; (your agent works from CI or a cloud sandbox with &lt;em&gt;no desktop running anywhere&lt;/em&gt; — auth by scoped, expiring API key, servers reached through a keyless tunnel agent), and an account-based &lt;strong&gt;end-to-end encrypted sync&lt;/strong&gt; where the server only ever stores ciphertext. Phones and tablets are covered by a web cabinet (a PWA with a real terminal) rather than a native app. Free tier is the full desktop app with &lt;strong&gt;no host limits&lt;/strong&gt;; Pro is a subscription ($8/mo billed yearly).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gumpbox&lt;/strong&gt; — an "AI-first SSH client" for the &lt;strong&gt;Apple ecosystem only&lt;/strong&gt; (macOS, iOS and iPadOS 26+). It's the most opinionated about safe execution: agent actions can run in &lt;strong&gt;gVisor-sandboxed&lt;/strong&gt; Alpine containers on the target, consent sheets show full server identity before an operation, and there's a tidy set of admin tools (processes, cron, tunnels, audit log, workflows). Sync is via iCloud. Pricing is friendly: free for one server, &lt;strong&gt;$19.99 one-time&lt;/strong&gt; for unlimited. It's a young solo-developer product (about three months of public history at the time of writing), and if your fleet or your team includes Windows or Linux desktops, it's simply not an option.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VibeShell&lt;/strong&gt; — a Tauri-based desktop app for Windows, macOS and Linux with a polished terminal workspace: split panes, Finder-style SFTP, themes, snippets, session recording. Its agent story is an &lt;strong&gt;Agent Gateway&lt;/strong&gt; exposing ~28 MCP tools plus a "skill installer" that wires up Claude Code, Codex, Cursor and a dozen other tools; it can also launch coding-agent CLIs in native tabs with live Git diffs. Sync is do-it-yourself end-to-end encryption through a &lt;strong&gt;GitHub Gist or WebDAV&lt;/strong&gt; file. The repository is public, which we respect — but note there's &lt;strong&gt;no license file&lt;/strong&gt; (source-visible isn't open source), and the README itself currently states that saved credentials are &lt;strong&gt;not encrypted at rest&lt;/strong&gt; (Keychain storage is planned). A capable tool for tinkerers who read the code they run; a harder sell where credential handling is audited.&lt;/p&gt;

&lt;h2&gt;
  
  
  The headless option: standalone MCP servers
&lt;/h2&gt;

&lt;p&gt;If you don't want a GUI at all, several standalone MCP servers speak SSH: &lt;strong&gt;mcp-ssh&lt;/strong&gt; (AiondaDotCom), &lt;strong&gt;SSH-MCP&lt;/strong&gt;, &lt;strong&gt;Aegis&lt;/strong&gt; (zero-trust flavored, with command firewalls), and others on the MCP directories. You configure hosts and keys yourself, point Claude Desktop or your agent at the server, and get command execution and file transfer with no visual layer.&lt;/p&gt;

&lt;p&gt;They're lightweight and scriptable, and the good ones take policy seriously. The tradeoff is structural: &lt;strong&gt;the credentials live wherever the MCP server runs&lt;/strong&gt;, usually as plain key files in the agent's reach, and there's no shared screen where a human watches the session as it happens. You audit after the fact, from logs — if logging was configured.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Termalin&lt;/th&gt;
&lt;th&gt;Gumpbox&lt;/th&gt;
&lt;th&gt;VibeShell&lt;/th&gt;
&lt;th&gt;Standalone MCP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Platforms&lt;/td&gt;
&lt;td&gt;Windows · macOS · Linux (+ web/PWA)&lt;/td&gt;
&lt;td&gt;Apple only&lt;/td&gt;
&lt;td&gt;Windows · macOS · Linux&lt;/td&gt;
&lt;td&gt;anywhere headless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent works without the desktop app&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes — hosted endpoint&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (it &lt;em&gt;is&lt;/em&gt; headless)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keys reachable by the agent&lt;/td&gt;
&lt;td&gt;Never (custodian signs)&lt;/td&gt;
&lt;td&gt;Keychain-held&lt;/td&gt;
&lt;td&gt;Plaintext at rest (per README, for now)&lt;/td&gt;
&lt;td&gt;Usually plain key files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Watch agent live&lt;/td&gt;
&lt;td&gt;Watch grid, marked recordings&lt;/td&gt;
&lt;td&gt;Consent sheets per action&lt;/td&gt;
&lt;td&gt;Shared visible sessions&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sandboxed execution on target&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;gVisor containers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Varies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sync&lt;/td&gt;
&lt;td&gt;E2E-encrypted, own account&lt;/td&gt;
&lt;td&gt;iCloud&lt;/td&gt;
&lt;td&gt;DIY Gist/WebDAV&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile&lt;/td&gt;
&lt;td&gt;Web cabinet (PWA)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Native iOS/iPadOS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;In development&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;Free (no host limits) + $8/mo Pro&lt;/td&gt;
&lt;td&gt;Free (1 server) + $19.99 once&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Honest recommendations, including against ourselves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;All-Apple, one server or a personal fleet, love one-time pricing&lt;/strong&gt; → Gumpbox is genuinely good, and its sandboxing is the strongest containment story here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want to read the source and assemble your own sync&lt;/strong&gt; → VibeShell, once you've made peace with the license situation and checked how your credentials are stored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pure headless automation, no humans in the loop by design&lt;/strong&gt; → a standalone MCP server with strict &lt;code&gt;authorized_keys&lt;/code&gt; restrictions is the smallest moving part.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixed-OS machines, agents that must run from CI or the cloud, keys that must never be agent-readable, or a team that needs an audit trail&lt;/strong&gt; → that's the corner we built Termalin for.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where this category is going
&lt;/h2&gt;

&lt;p&gt;Three predictions, since we're here: consent UX will converge (every product above is inventing per-action approval independently); &lt;em&gt;"can the agent act when my laptop is closed"&lt;/em&gt; will separate toys from infrastructure; and key custody will become the line auditors actually check — "the agent could read the key but we trust it" is not going to survive contact with a SOC 2 review.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Termalin is free with no host limits on Windows, macOS and Linux — &lt;a href="https://termal.in/download/" rel="noopener noreferrer"&gt;download it&lt;/a&gt;, read the &lt;a href="https://termal.in/mcp/" rel="noopener noreferrer"&gt;MCP docs&lt;/a&gt;, or see how the &lt;a href="https://termal.in/mcp/" rel="noopener noreferrer"&gt;hosted endpoint&lt;/a&gt; works when your agent lives in the cloud.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>mcp</category>
      <category>ssh</category>
      <category>comparison</category>
    </item>
    <item>
      <title>How to connect Claude Code to a server over SSH — a hands-on walkthrough</title>
      <dc:creator>Svyatoslav Pavlov</dc:creator>
      <pubDate>Tue, 04 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/wolfhound1995/how-to-connect-claude-code-to-a-server-over-ssh-a-hands-on-walkthrough-1c51</link>
      <guid>https://dev.to/wolfhound1995/how-to-connect-claude-code-to-a-server-over-ssh-a-hands-on-walkthrough-1c51</guid>
      <description>&lt;p&gt;Claude Code is comfortable in a repo. The moment the task leaves the repo — check why the service won't start, free up disk space, fix the nginx config &lt;em&gt;on the actual box&lt;/em&gt; — it needs SSH, and most people solve that by pasting a private key somewhere into the agent's environment. We wrote about &lt;a href="https://termal.in/blog/let-an-ai-agent-deploy-without-your-ssh-keys/" rel="noopener noreferrer"&gt;why that's the one move you can't undo&lt;/a&gt;; this post is the practical version. Twenty minutes, one VPS, no key ever leaves your side.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Termalin&lt;/strong&gt; — the desktop app, &lt;a href="https://termal.in/download/" rel="noopener noreferrer"&gt;free tier is fine&lt;/a&gt;. It's an SSH client with a built-in MCP server; that server is what Claude Code will talk to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code&lt;/strong&gt; on the same machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One host&lt;/strong&gt; saved in Termalin — pick something low-stakes for the first run. Staging, a toy VPS, the box that only runs your RSS reader.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1 — turn agent access on
&lt;/h2&gt;

&lt;p&gt;Agent access is &lt;strong&gt;off by default&lt;/strong&gt;. In Termalin, open &lt;strong&gt;Settings → MCP&lt;/strong&gt; and enable it, then tick the hosts the agent may reach. Nothing outside that list exists as far as the agent is concerned — a new host is a decision you make in the app, not something the agent can talk its way into.&lt;/p&gt;

&lt;p&gt;Two things happen under the hood. Termalin writes a host inventory for the MCP server with &lt;strong&gt;agent-only auth entries — no passwords or keys land in that file&lt;/strong&gt;. And authentication stays with the app's key custodian: you unlock your keys once, Termalin signs on the agent's behalf. There is no key file for the agent to read, so there's nothing for it to leak.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — register the server with Claude Code
&lt;/h2&gt;

&lt;p&gt;One command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;claude mcp add termalin -- &amp;lt;path&amp;gt;/termalin-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole integration. Restart Claude Code and it picks up the tools: &lt;code&gt;hosts_list&lt;/code&gt;, &lt;code&gt;ssh_exec&lt;/code&gt;, persistent sessions (&lt;code&gt;session_open&lt;/code&gt;, &lt;code&gt;session_exec&lt;/code&gt;), SFTP reads and writes, and — if you allow it — tools that drive the live Termalin window itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — give it a real chore
&lt;/h2&gt;

&lt;p&gt;Don't start with a demo command; start with a task you'd actually do. For example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Disk usage on &lt;em&gt;staging-1&lt;/em&gt; keeps creeping up. Find what's eating the space, clean up anything that's obviously safe to remove, and tell me what you did.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Watch what happens in Claude Code: it calls &lt;code&gt;hosts_list&lt;/code&gt;, sees &lt;code&gt;staging-1&lt;/code&gt;, opens a session, and starts working — &lt;code&gt;df -h&lt;/code&gt;, then &lt;code&gt;du&lt;/code&gt; down the suspicious paths, then a look at whatever it finds (in our runs it's almost always logs nobody rotated or a Docker image graveyard). It cleans up, verifies with another &lt;code&gt;df -h&lt;/code&gt;, and reports back.&lt;/p&gt;

&lt;p&gt;The part that makes this comfortable is that none of it is invisible. Every session the agent opens appears in Termalin as a &lt;strong&gt;live terminal tab&lt;/strong&gt;, and the &lt;strong&gt;watch grid&lt;/strong&gt; shows all open sessions side by side with the agent-driven tiles glowing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft962blfae9hnbr5uy4dj.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft962blfae9hnbr5uy4dj.gif" alt="The watch grid mirroring live sessions, with agent-driven tiles highlighted" width="760" height="428"&gt;&lt;/a&gt; You're not reading a summary after the fact — you're watching the commands land as they run, and you can step in at any moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the boundaries are
&lt;/h2&gt;

&lt;p&gt;A few defaults worth knowing before you point it at anything you care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your keys never touch disk&lt;/strong&gt; for the agent — the custodian signs; the agent requests. Revoking access is a toggle in Settings, not a key rotation across your fleet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent's reach is the allowlist&lt;/strong&gt; you set in Step 1, nothing more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typing into &lt;em&gt;your&lt;/em&gt; session is separate.&lt;/strong&gt; By default the agent opens its own sessions. Letting it act inside a session you already have open (on Pro) is its own consent toggle — it never rides along silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recordings mark agent activity.&lt;/strong&gt; Session recordings capture output only — never your keystrokes — and every agent command is tagged as the agent's, with the device and IP it came from in the audit log. At 2 a.m. you can tell "it did this" from "I did this".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  No desktop around? Use the hosted endpoint
&lt;/h2&gt;

&lt;p&gt;If the agent runs where your desktop isn't — CI, a cloud sandbox — you don't ship the app with it. Create an API key in the &lt;a href="https://termal.in/account" rel="noopener noreferrer"&gt;web cabinet&lt;/a&gt; and point the agent at the hosted MCP endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"termalin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://termal.in/api/v1/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer tk_live_…"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hosted endpoint only reaches servers you've enrolled with the tunnel agent, and each run authenticates with a short-lived certificate — same custodian idea, no standing credential anywhere. Keys are scoped to specific servers, expire on a schedule you pick (30, 90 or 365 days), and die instantly when revoked. File tools are deliberately modest there: text files up to 512 KB — config-editing territory, not a data channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The habit that makes it stick
&lt;/h2&gt;

&lt;p&gt;Treat the first week like onboarding a new teammate who types very fast. Keep the watch grid open. Give it chores with a clear done-state — "make the health check pass", "get the cert renewed" — and read how it gets there. You'll learn quickly where it's careful and where it needs a tighter leash, and you can widen the allowlist one boring host at a time.&lt;/p&gt;

&lt;p&gt;The asymmetry is the point: the agent gets real reach, and you keep the one thing that can't be taken back.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Termalin's Free tier is the full desktop app, and every new account starts with a 14-day Pro trial — &lt;a href="https://termal.in/download/" rel="noopener noreferrer"&gt;download it&lt;/a&gt;, or read the &lt;a href="https://termal.in/mcp/" rel="noopener noreferrer"&gt;MCP docs&lt;/a&gt; first.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>mcp</category>
      <category>ssh</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>SSH errors, decoded: the ten you'll actually hit and how to fix them</title>
      <dc:creator>Svyatoslav Pavlov</dc:creator>
      <pubDate>Tue, 04 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/wolfhound1995/ssh-errors-decoded-the-ten-youll-actually-hit-and-how-to-fix-them-gh4</link>
      <guid>https://dev.to/wolfhound1995/ssh-errors-decoded-the-ten-youll-actually-hit-and-how-to-fix-them-gh4</guid>
      <description>&lt;p&gt;SSH error messages are terse on purpose — the protocol won't tell a stranger &lt;em&gt;why&lt;/em&gt; it turned them away. That's good security and bad ergonomics: the message you see is often two steps removed from the actual problem. This is a field guide to the ten errors you'll actually meet, what each one really means, and the fastest route to a fix.&lt;/p&gt;

&lt;p&gt;One tool before we start: &lt;code&gt;ssh -v user@host&lt;/code&gt; (add up to &lt;code&gt;-vvv&lt;/code&gt;). Nearly every mystery below stops being a mystery once you see which step of the handshake it dies on.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;Connection refused&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The machine is reachable, but nothing is listening on the port you knocked on. This one is almost never about you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sshd isn't running — &lt;code&gt;systemctl status sshd&lt;/code&gt; from a console (VPS providers have a web console for exactly this moment);&lt;/li&gt;
&lt;li&gt;it listens on a non-standard port — try &lt;code&gt;ssh -p 2222&lt;/code&gt;, or check &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; for &lt;code&gt;Port&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;a firewall is actively rejecting (rather than dropping) the port.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you &lt;em&gt;just&lt;/em&gt; rebuilt the server, it's the firewall. If it worked yesterday, sshd crashed or the port changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;Connection timed out&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Different from refused: your packets are vanishing. The host is down, the IP is wrong, or — most often on cloud boxes — a security group / network ACL silently drops port 22. Check the provider's firewall rules first, then whether you're on a network that blocks outbound 22 (hotel and office guest Wi-Fi love this; try port 443-based access or a different network to confirm).&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;Permission denied (publickey)&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The all-time champion. The server only accepts key auth, and it didn't accept any key you offered. In order of likelihood:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wrong user.&lt;/strong&gt; &lt;code&gt;ubuntu&lt;/code&gt; on Ubuntu images, &lt;code&gt;ec2-user&lt;/code&gt; on Amazon Linux, &lt;code&gt;root&lt;/code&gt; on most bare VPSes. The same key with the wrong username produces exactly this error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your key wasn't offered.&lt;/strong&gt; &lt;code&gt;ssh -v&lt;/code&gt; shows every key tried. If your key isn't in the list, point at it explicitly: &lt;code&gt;ssh -i ~/.ssh/id_ed25519 user@host&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The key isn't on the server.&lt;/strong&gt; Its public half must be one line in &lt;code&gt;~user/.ssh/authorized_keys&lt;/code&gt; — for &lt;em&gt;that&lt;/em&gt; user, not root's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-side permissions.&lt;/strong&gt; sshd refuses to honor &lt;code&gt;authorized_keys&lt;/code&gt; if the home dir, &lt;code&gt;~/.ssh&lt;/code&gt; (700), or the file itself (600) is group-writable. This is the classic "I copied the key and it still doesn't work".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;Permission denied&lt;/code&gt; when you expected a password prompt
&lt;/h2&gt;

&lt;p&gt;If you wanted password auth and never got asked, the server has &lt;code&gt;PasswordAuthentication no&lt;/code&gt; — most cloud images ship that way, and it's the right default. Use the provider's console or your existing key to get in, then add the new key properly rather than switching passwords back on.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. &lt;code&gt;Host key verification failed&lt;/code&gt; / &lt;code&gt;REMOTE HOST IDENTIFICATION HAS CHANGED&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The scary banner with the ASCII skull energy. The server presented a different key than the one you trusted before. Two explanations: the machine was legitimately rebuilt/reinstalled (overwhelmingly the common case), or something between you and the server is impersonating it (the case the warning exists for).&lt;/p&gt;

&lt;p&gt;If — and only if — you know the server was rebuilt: &lt;code&gt;ssh-keygen -R hostname&lt;/code&gt; removes the stale entry, and the next connect re-trusts. Don't blindly script that removal into your workflow; the one time the warning is real, it's the only warning you get.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. &lt;code&gt;Too many authentication failures&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Your agent enthusiastically offered six keys, the server's &lt;code&gt;MaxAuthTries&lt;/code&gt; said enough. Tell the client to offer only the key you mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;IdentitiesOnly&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/the_right_key user@host
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…or make it permanent in &lt;code&gt;~/.ssh/config&lt;/code&gt; per host. If you carry many keys, &lt;code&gt;IdentitiesOnly yes&lt;/code&gt; under &lt;code&gt;Host *&lt;/code&gt; will save you this error forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. &lt;code&gt;WARNING: UNPROTECTED PRIVATE KEY FILE&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Your &lt;em&gt;private&lt;/em&gt; key is readable by others, and the client refuses to use it. &lt;code&gt;chmod 600 ~/.ssh/id_ed25519&lt;/code&gt;. On Windows this appears after copying keys between machines or out of a cloud drive — fix it via the file's Security properties, or keep keys out of synced folders entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. &lt;code&gt;client_loop: send disconnect: Broken pipe&lt;/code&gt; (the session that dies when you look away)
&lt;/h2&gt;

&lt;p&gt;Idle connections dropped by a NAT router or aggressive firewall between you and the server. Keepalives fix it client-side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;Host&lt;/span&gt; *
    &lt;span class="k"&gt;ServerAliveInterval&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
    &lt;span class="k"&gt;ServerAliveCountMax&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two minutes of silence tolerated, packets every 30 seconds so the NAT table never forgets you. If sessions die &lt;em&gt;under load&lt;/em&gt; rather than idle, that's a different animal — look at MTU (VPNs especially) rather than keepalives.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. &lt;code&gt;no matching key exchange method found&lt;/code&gt; / &lt;code&gt;no matching cipher&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;You're talking to something old — a router, a switch, an appliance stuck on legacy crypto. Modern OpenSSH removed those algorithms for good reason. Re-enable them &lt;em&gt;for that one host only&lt;/em&gt;, never globally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;Host&lt;/span&gt; ancient-switch
    &lt;span class="k"&gt;KexAlgorithms&lt;/span&gt; +diffie-hellman-group14-sha1
    &lt;span class="k"&gt;Ciphers&lt;/span&gt; +aes256-cbc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error message names the algorithms the server offered; add the least-bad one it lists.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. &lt;code&gt;ssh_exchange_identification: read: Connection reset by peer&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The TCP connection opened and was immediately slammed shut, before SSH even said hello. Usual suspects: fail2ban or DenyHosts has banned your IP (check &lt;code&gt;/etc/hosts.deny&lt;/code&gt; and fail2ban's jail from a console), sshd is overloaded (&lt;code&gt;MaxStartups&lt;/code&gt; exhausted by a scanner hammering the port), or a load balancer health check is eating connections. If it's fail2ban and it's your own server, the fix is a console login and &lt;code&gt;fail2ban-client unban &amp;lt;your-ip&amp;gt;&lt;/code&gt; — then move SSH off scanners' radar or tighten the jail so you're not next week's collateral.&lt;/p&gt;




&lt;h2&gt;
  
  
  Make the fixes stick
&lt;/h2&gt;

&lt;p&gt;Half of these are one-off fixes; the other half deserve a line in &lt;code&gt;~/.ssh/config&lt;/code&gt; so they never come back — keepalives, &lt;code&gt;IdentitiesOnly&lt;/code&gt;, per-host ports and identities. Config beats memory.&lt;/p&gt;

&lt;p&gt;The other thing that beats memory is a client that keeps state for you. &lt;a href="https://termal.in/features/" rel="noopener noreferrer"&gt;Termalin&lt;/a&gt; stores per-host settings — port, user, key, keepalives — with the host itself, verifies servers on first connect and warns loudly when a fingerprint changes, and its status bar shows the machine's vitals live, so "is the box even up?" stops being a diagnostic step. The Free tier has no host limits — &lt;a href="https://termal.in/download/" rel="noopener noreferrer"&gt;grab it&lt;/a&gt; and retire a few of these errors permanently.&lt;/p&gt;

</description>
      <category>ssh</category>
      <category>troubleshooting</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
