<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://techdelver.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://techdelver.com/" rel="alternate" type="text/html" /><updated>2026-08-13T10:51:26-05:00</updated><id>https://techdelver.com/feed.xml</id><title type="html">Techdelver</title><subtitle>Notes on Linux, self-hosting, and infrastructure.</subtitle><entry><title type="html">The Best Ways to Use SSH</title><link href="https://techdelver.com/linux/ssh/2026/08/13/the-best-ways-to-use-ssh.html" rel="alternate" type="text/html" title="The Best Ways to Use SSH" /><published>2026-08-13T10:00:00-05:00</published><updated>2026-08-13T10:00:00-05:00</updated><id>https://techdelver.com/linux/ssh/2026/08/13/the-best-ways-to-use-ssh</id><content type="html" xml:base="https://techdelver.com/linux/ssh/2026/08/13/the-best-ways-to-use-ssh.html"><![CDATA[<p>Most people learn <code class="language-plaintext highlighter-rouge">ssh user@host</code>, get a shell, and stop there. That’s a shame,
because SSH is less a remote-login tool than a general-purpose encrypted
transport that happens to <em>also</em> give you a shell. Here are the parts worth
knowing, roughly in order of how much time they’ll save you.</p>

<h2 id="1-put-everything-in-sshconfig">1. Put everything in <code class="language-plaintext highlighter-rouge">~/.ssh/config</code></h2>

<p>This is the highest-leverage change you can make. Stop typing flags.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host web1
    HostName web1.example.com
    User deploy
    IdentityFile ~/.ssh/id_ed25519

Host *.example.com
    User deploy
    IdentitiesOnly yes
</code></pre></div></div>

<p>Now <code class="language-plaintext highlighter-rouge">ssh web1</code> works, and so does <code class="language-plaintext highlighter-rouge">scp file web1:</code>, <code class="language-plaintext highlighter-rouge">rsync</code>, <code class="language-plaintext highlighter-rouge">git</code>, and anything
else that shells out to SSH. Wildcards and <code class="language-plaintext highlighter-rouge">Match</code> blocks let you set defaults
per-network.</p>

<p>One option deserves special mention:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host *
    IdentitiesOnly yes
</code></pre></div></div>

<p>Without it, your agent offers every key it has, in order, on every connection.
Servers commonly cap authentication attempts at six — so if you have seven keys
loaded, connections start failing with a baffling “Too many authentication
failures” that has nothing to do with the key you actually wanted.</p>

<h2 id="2-connection-multiplexing">2. Connection multiplexing</h2>

<p>Every new SSH connection pays for a TCP handshake plus a key exchange. If you’re
running a loop of remote commands, or using Ansible, or just typing <code class="language-plaintext highlighter-rouge">ssh web1</code>
forty times an hour, that adds up.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host *
    ControlMaster auto
    ControlPath ~/.ssh/cm-%r@%h:%p
    ControlPersist 10m
</code></pre></div></div>

<p>The first connection opens a real session and leaves a socket behind. Every
subsequent connection to the same host rides the existing one and comes up
essentially instantly. <code class="language-plaintext highlighter-rouge">ControlPersist 10m</code> keeps it alive ten minutes after you
log out.</p>

<p>Make sure <code class="language-plaintext highlighter-rouge">~/.ssh/</code> isn’t world-writable, and be aware the socket path has a
length limit on some systems — if you get “unix_listener: too long”, shorten it
to something like <code class="language-plaintext highlighter-rouge">~/.ssh/cm-%C</code> (a hash).</p>

<h2 id="3-proxyjump-instead-of-agent-forwarding">3. <code class="language-plaintext highlighter-rouge">ProxyJump</code> instead of agent forwarding</h2>

<p>The old way to reach a machine behind a bastion was to SSH to the bastion and SSH
again from there, forwarding your agent so the second hop could authenticate.
Don’t do that. Agent forwarding lets <strong>root on the bastion</strong> use your agent to
authenticate as you, anywhere. It’s a real risk on a host you don’t fully trust.</p>

<p><code class="language-plaintext highlighter-rouge">ProxyJump</code> builds the tunnel client-side instead. Your key never touches the
intermediate host:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-J</span> bastion.example.com target.internal
</code></pre></div></div>

<p>Or permanently:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host target
    HostName target.internal
    ProxyJump bastion.example.com
</code></pre></div></div>

<p>Chain multiple hops with commas: <code class="language-plaintext highlighter-rouge">-J host1,host2,host3</code>.</p>

<p>If you genuinely need agent forwarding, at least confine it to specific hosts and
use <code class="language-plaintext highlighter-rouge">ssh-add -c</code> so the agent prompts for confirmation on every use.</p>

<h2 id="4-port-forwarding-all-three-directions">4. Port forwarding, all three directions</h2>

<p>This is where SSH stops being a login tool.</p>

<p><strong>Local (<code class="language-plaintext highlighter-rouge">-L</code>) — pull a remote service to your machine.</strong> A database bound to
localhost on the server, reachable in your local client:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-L</span> 5432:localhost:5432 dbhost
</code></pre></div></div>

<p><strong>Remote (<code class="language-plaintext highlighter-rouge">-R</code>) — push a local service out to the server.</strong> Useful for showing a
colleague your dev server, or for reaching a machine behind NAT:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-R</span> 8080:localhost:3000 public-host
</code></pre></div></div>

<p>By default <code class="language-plaintext highlighter-rouge">-R</code> binds only to the remote loopback; you need <code class="language-plaintext highlighter-rouge">GatewayPorts yes</code> in
the server’s <code class="language-plaintext highlighter-rouge">sshd_config</code> for others to reach it.</p>

<p><strong>Dynamic (<code class="language-plaintext highlighter-rouge">-D</code>) — a full SOCKS5 proxy.</strong> This one is underrated:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-D</span> 1080 <span class="nt">-N</span> <span class="nt">-q</span> myhost
</code></pre></div></div>

<p>Point a browser’s SOCKS proxy at <code class="language-plaintext highlighter-rouge">localhost:1080</code> and all its traffic exits from
that host. It’s a poor man’s VPN that requires zero server configuration. <code class="language-plaintext highlighter-rouge">-N</code>
means “don’t run a command” and <code class="language-plaintext highlighter-rouge">-q</code> quiets it.</p>

<h2 id="5-escape-sequences">5. Escape sequences</h2>

<p>Your connection hangs. You mash Ctrl-C and nothing happens, because Ctrl-C is
being dutifully forwarded to a remote host that isn’t listening.</p>

<p>Type <code class="language-plaintext highlighter-rouge">Enter</code>, then <code class="language-plaintext highlighter-rouge">~.</code> — that’s tilde, period. The client kills the connection
locally. This works when nothing else does, and it’s worth committing to muscle
memory.</p>

<p>Others: <code class="language-plaintext highlighter-rouge">~?</code> lists all sequences, <code class="language-plaintext highlighter-rouge">~C</code> opens a command line where you can add
port forwards to an already-running session, and <code class="language-plaintext highlighter-rouge">~^Z</code> backgrounds the client.</p>

<p>The tilde is only recognized immediately after a newline. If you’re two hops
deep, double it — <code class="language-plaintext highlighter-rouge">~~.</code> sends it to the second machine.</p>

<h2 id="6-move-data-without-a-copy-tool">6. Move data without a copy tool</h2>

<p>SSH is a pipe, so anything that speaks stdin/stdout works over it.</p>

<p>Copy a directory, preserving permissions, without a staging file:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">tar </span>czf - ./data | ssh host <span class="s1">'tar xzf - -C /dest'</span>
</code></pre></div></div>

<p>Clone a disk:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh host <span class="s1">'dd if=/dev/sda bs=4M'</span> | <span class="nb">gzip</span> <span class="nt">-d</span> <span class="o">&gt;</span> disk.img
</code></pre></div></div>

<p>Or just use <code class="language-plaintext highlighter-rouge">rsync</code>, which uses SSH as its transport by default and is the
correct answer for anything you might need to resume:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rsync <span class="nt">-avz</span> <span class="nt">--progress</span> ./data/ host:/dest/
</code></pre></div></div>

<p>Prefer <code class="language-plaintext highlighter-rouge">rsync</code> over <code class="language-plaintext highlighter-rouge">scp</code> generally — <code class="language-plaintext highlighter-rouge">scp</code> is deprecated in spirit, its
semantics around remote path expansion have been a persistent source of
vulnerabilities, and modern OpenSSH quietly reimplemented it on top of SFTP.</p>

<h2 id="7-mount-a-remote-filesystem">7. Mount a remote filesystem</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sshfs host:/remote/path ~/mnt
</code></pre></div></div>

<p>Latency makes this bad for compiling, but excellent for poking at remote files
with local tools. Unmount with <code class="language-plaintext highlighter-rouge">fusermount -u ~/mnt</code>.</p>

<h2 id="8-run-commands-properly">8. Run commands properly</h2>

<p>A non-interactive command works as you’d expect:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh host <span class="s1">'systemctl status nginx'</span>
</code></pre></div></div>

<p>Two things bite people. First, <strong>quoting is evaluated twice</strong> — once by your
local shell, once remotely. When it gets hairy, use a here-doc:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh host bash <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">EOF</span><span class="sh">'
for f in /var/log/*.log; do
    echo "</span><span class="nv">$f</span><span class="sh">: </span><span class="si">$(</span><span class="nb">wc</span> <span class="nt">-l</span> &lt; <span class="s2">"</span><span class="nv">$f</span><span class="s2">"</span><span class="si">)</span><span class="sh">"
done
</span><span class="no">EOF
</span></code></pre></div></div>

<p>The quotes around <code class="language-plaintext highlighter-rouge">'EOF'</code> are essential — they stop the <em>local</em> shell expanding
anything.</p>

<p>Second, <code class="language-plaintext highlighter-rouge">ssh host 'long-running-thing &amp;'</code> won’t survive your disconnect the way
you hope. Use <code class="language-plaintext highlighter-rouge">nohup</code>, <code class="language-plaintext highlighter-rouge">setsid</code>, or better, a systemd unit. For interactive work,
<code class="language-plaintext highlighter-rouge">tmux</code> on the remote end is the real answer — and it makes flaky connections a
non-event, since your session outlives the transport.</p>

<h2 id="9-harden-the-server-side">9. Harden the server side</h2>

<p>A few lines in <code class="language-plaintext highlighter-rouge">/etc/ssh/sshd_config</code> remove most of your exposure:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PermitRootLogin prohibit-password
PasswordAuthentication no
KbdInteractiveAuthentication no
</code></pre></div></div>

<p>Key-only auth eliminates password brute-forcing entirely — which makes
<code class="language-plaintext highlighter-rouge">fail2ban</code> and moving to a non-standard port largely theater. They cut log noise,
not risk.</p>

<p>Generate keys as <code class="language-plaintext highlighter-rouge">ed25519</code>; they’re shorter, faster, and dodge the parameter
selection problems RSA has:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh-keygen <span class="nt">-t</span> ed25519 <span class="nt">-C</span> <span class="s2">"you@machine"</span>
</code></pre></div></div>

<p>Always run <code class="language-plaintext highlighter-rouge">sshd -t</code> before restarting, and keep your existing session open while
you test the new one from a second terminal. Locking yourself out of a remote box
is a rite of passage best skipped.</p>

<h2 id="10-verify-host-keys-actually">10. Verify host keys, actually</h2>

<p>The <code class="language-plaintext highlighter-rouge">Are you sure you want to continue connecting?</code> prompt is a security control
that essentially everyone answers “yes” to without reading. If you’re
provisioning machines, get the fingerprint out-of-band from your cloud provider’s
console output and compare it. If you’re using GitHub, they
<a href="https://docs.github.com/authentication/keeping-your-account-secure/githubs-ssh-key-fingerprints">publish their fingerprints</a> —
compare, don’t assume.</p>

<p>For infrastructure you control at any scale, the real fix is an SSH certificate
authority: sign host keys once, distribute the CA public key via
<code class="language-plaintext highlighter-rouge">@cert-authority</code> in <code class="language-plaintext highlighter-rouge">known_hosts</code>, and the prompt disappears forever because
every host is now verifiable. The same works for user keys, which means you can
issue short-lived credentials instead of managing <code class="language-plaintext highlighter-rouge">authorized_keys</code> files by
hand.</p>

<hr />

<p>The thread running through all of this: SSH is an authenticated, encrypted
transport with a shell bolted on, not the other way around. Once that clicks, a
lot of problems that look like they need a VPN, a tunnel service, or a file
transfer tool turn out to need about forty characters of SSH.</p>]]></content><author><name></name></author><category term="linux" /><category term="ssh" /><summary type="html"><![CDATA[Most people learn ssh user@host, get a shell, and stop there. That’s a shame, because SSH is less a remote-login tool than a general-purpose encrypted transport that happens to also give you a shell. Here are the parts worth knowing, roughly in order of how much time they’ll save you.]]></summary></entry><entry><title type="html">Hello World</title><link href="https://techdelver.com/meta/2026/08/13/hello-world.html" rel="alternate" type="text/html" title="Hello World" /><published>2026-08-13T09:00:00-05:00</published><updated>2026-08-13T09:00:00-05:00</updated><id>https://techdelver.com/meta/2026/08/13/hello-world</id><content type="html" xml:base="https://techdelver.com/meta/2026/08/13/hello-world.html"><![CDATA[<p>First post. This blog is a Jekyll site built by GitHub Pages — Markdown goes in
<code class="language-plaintext highlighter-rouge">_posts/</code>, GitHub does the rest.</p>

<h2 id="adding-a-post">Adding a post</h2>

<p>Create a file in <code class="language-plaintext highlighter-rouge">_posts/</code> named <code class="language-plaintext highlighter-rouge">YYYY-MM-DD-title.md</code> with front matter:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">layout</span><span class="pi">:</span> <span class="s">post</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Your</span><span class="nv"> </span><span class="s">Title"</span>
<span class="na">date</span><span class="pi">:</span> <span class="s">2026-08-13 09:00:00 -0500</span>
<span class="na">categories</span><span class="pi">:</span> <span class="s">whatever</span>
<span class="nn">---</span>
</code></pre></div></div>

<p>Then write Markdown below it. Commit, push, and it’s live in about a minute.</p>

<h2 id="code-blocks-work">Code blocks work</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh user@example.com <span class="s1">'systemctl status nginx'</span>
</code></pre></div></div>

<p>That’s the whole workflow.</p>]]></content><author><name></name></author><category term="meta" /><summary type="html"><![CDATA[First post. This blog is a Jekyll site built by GitHub Pages — Markdown goes in _posts/, GitHub does the rest.]]></summary></entry></feed>