<?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: Schiff Heimlich</title>
    <description>The latest articles on DEV Community by Schiff Heimlich (@schiff_heimlich).</description>
    <link>https://dev.to/schiff_heimlich</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%2F3949704%2F89c08e96-274f-4f09-a299-8ebdabdc7096.jpg</url>
      <title>DEV Community: Schiff Heimlich</title>
      <link>https://dev.to/schiff_heimlich</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/schiff_heimlich"/>
    <language>en</language>
    <item>
      <title>NGINX Control API: Finally, Config Reloads That Tell You What Went Wrong</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Sun, 01 Nov 2026 17:04:37 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/nginx-control-api-finally-config-reloads-that-tell-you-what-went-wrong-51na</link>
      <guid>https://dev.to/schiff_heimlich/nginx-control-api-finally-config-reloads-that-tell-you-what-went-wrong-51na</guid>
      <description>&lt;h1&gt;
  
  
  NGINX Control API: Finally, Config Reloads That Tell You What Went Wrong
&lt;/h1&gt;

&lt;p&gt;If you've run NGINX in production, you know the drill. Change a config, run &lt;code&gt;nginx -s reload&lt;/code&gt;, wait, then tail the error log to see if it actually worked. Or worse - it didn't work, and you're left guessing why.&lt;/p&gt;

&lt;p&gt;NGINX 1.31.5+ ships a Control API that changes this. It's an HTTP endpoint that lets you inspect the in-memory configuration, validate changes before applying them, and reload with actual error feedback. Here's what it looks like in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling It
&lt;/h2&gt;

&lt;p&gt;Add a &lt;code&gt;control&lt;/code&gt; block to your &lt;code&gt;nginx.conf&lt;/code&gt; (inside the &lt;code&gt;main&lt;/code&gt; context, not &lt;code&gt;http&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;control&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="nf"&gt;127.0.0.1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;allow&lt;/span&gt; &lt;span class="mf"&gt;127.0&lt;/span&gt;&lt;span class="s"&gt;.0.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This binds to localhost only. Don't expose this publicly - it lets you modify running configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Can Do
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;View active config:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:8080/config | jq &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns the full in-memory config as JSON. Useful for "what's actually running right now" checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validate a config file before applying:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:8080/config/validate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: text/plain"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @/etc/nginx/nginx.conf.new
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns validation errors without touching the running config. No more "reload and pray."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reload with feedback:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:8080/config/reload &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: text/plain"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @/etc/nginx/nginx.conf.new
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it fails, you get the error in the response body. If it succeeds, you get confirmation. No more tailing error logs hoping to catch the failure message.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Workflow
&lt;/h2&gt;

&lt;p&gt;Here's how this changes a typical config update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Write your new config&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; /etc/nginx/nginx.conf /etc/nginx/nginx.conf.new
vim /etc/nginx/nginx.conf.new

&lt;span class="c"&gt;# 2. Validate it&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:8080/config/validate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: text/plain"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @/etc/nginx/nginx.conf.new

&lt;span class="c"&gt;# If validation passes:&lt;/span&gt;
&lt;span class="c"&gt;# 3. Apply it&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:8080/config/reload &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: text/plain"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @/etc/nginx/nginx.conf.new

&lt;span class="c"&gt;# 4. Verify the running config matches&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:8080/config | jq &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/active-config.json
diff &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;jq &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; /etc/nginx/nginx.conf.new&lt;span class="o"&gt;)&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;jq &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; /tmp/active-config.json&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;The old &lt;code&gt;nginx -s reload&lt;/code&gt; / SIGHUP approach has real problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Silent failures&lt;/strong&gt;: If the new config has a syntax error, the reload fails but the old config keeps running. You only know if you're watching logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No validation step&lt;/strong&gt;: You can't test a config without applying it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No visibility&lt;/strong&gt;: You can't easily query "what's the current value of proxy_read_timeout for upstream X?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Control API fixes all three. It's not a revolutionary feature - it's a long-overdue operational improvement that makes NGINX behave more like a modern manageable service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Only available in NGINX 1.31.5+ (released late 2024). Check your version: &lt;code&gt;nginx -v&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The API is read-only for the running config; you validate and reload by POSTing full config files&lt;/li&gt;
&lt;li&gt;RBAC is basic (IP allow/deny). Put it behind a VPN or SSH tunnel in production&lt;/li&gt;
&lt;li&gt;Doesn't replace &lt;code&gt;nginx -t&lt;/code&gt; for static validation - use both&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;If you're running NGINX at any scale, the Control API removes a whole category of "wait, did that reload work?" uncertainty. It's a small feature that solves a real daily friction point.&lt;/p&gt;

&lt;p&gt;Worth enabling on your next NGINX upgrade.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Schiff Heimlich | Sysadmin who prefers knowing over hoping&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nginx</category>
      <category>devops</category>
      <category>sysadmin</category>
      <category>configuration</category>
    </item>
    <item>
      <title>What top Actually Shows You (And What It Doesnt)</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Thu, 29 Oct 2026 17:09:02 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/what-top-actually-shows-you-and-what-it-doesnt-dn1</link>
      <guid>https://dev.to/schiff_heimlich/what-top-actually-shows-you-and-what-it-doesnt-dn1</guid>
      <description>&lt;p&gt;You run &lt;code&gt;top&lt;/code&gt; or &lt;code&gt;htop&lt;/code&gt;. You see CPU at 80%. You think "my app is using 80% CPU." That is not what that number means.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Breakdown
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;top&lt;/code&gt; shows you four main categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;us&lt;/strong&gt; (user) — your application code executing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sy&lt;/strong&gt; (system) — kernel work on behalf of your process (syscalls, page faults, scheduler)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ni&lt;/strong&gt; (nice) — low-priority user work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;id&lt;/strong&gt; (idle) — CPU doing nothing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are also &lt;strong&gt;wa&lt;/strong&gt; (iowait), &lt;strong&gt;hi&lt;/strong&gt;/&lt;strong&gt;si&lt;/strong&gt; (hardware/software interrupts), &lt;strong&gt;st&lt;/strong&gt; (steal — time the hypervisor gave to another VM).&lt;/p&gt;

&lt;p&gt;The problem: &lt;strong&gt;us + sy&lt;/strong&gt; is not "your application's CPU usage." It is "CPU time accounted to your process context." A process spinning in a futex loop, a process blocked on a slow filesystem, a process constantly context-switching — all show up as CPU time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Matters for Performance
&lt;/h2&gt;

&lt;p&gt;Brendan Gregg has been saying this for years: &lt;strong&gt;CPU utilization is not a performance metric.&lt;/strong&gt; It is a capacity metric.&lt;/p&gt;

&lt;p&gt;What you actually want to know:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;IPC (Instructions Per Cycle)&lt;/strong&gt; — are you getting work done per cycle? Low IPC = memory-bound, cache misses, branch mispredicts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stalled cycles&lt;/strong&gt; — frontend stalls (instruction fetch), backend stalls (data dependencies, cache misses).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run-queue latency&lt;/strong&gt; — how long tasks wait for CPU. This is what &lt;code&gt;pidstat -w&lt;/code&gt; or &lt;code&gt;perf sched&lt;/code&gt; shows you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context switch rate&lt;/strong&gt; — high voluntary switches = locking/contention. High involuntary = time slice exhaustion.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Practical Commands
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Per-process scheduler stats (voluntary/involuntary switches)&lt;/span&gt;
pidstat &lt;span class="nt"&gt;-w&lt;/span&gt; 1

&lt;span class="c"&gt;# System-wide run-queue latency&lt;/span&gt;
perf sched latency

&lt;span class="c"&gt;# CPU-wide IPC and stall cycles (requires PMU access)&lt;/span&gt;
perf &lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; cycles,instructions,cache-references,cache-misses &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;10

&lt;span class="c"&gt;# What top *doesnt* show you: steal time in VMs&lt;/span&gt;
&lt;span class="c"&gt;# If st is high, your hypervisor is overcommitted — your process isnt slow, it is not running&lt;/span&gt;
top &lt;span class="nt"&gt;-1&lt;/span&gt;  &lt;span class="c"&gt;# press 1 to see per-CPU breakdown&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A Real Example
&lt;/h2&gt;

&lt;p&gt;Go service, CPU at 90% in &lt;code&gt;top&lt;/code&gt;. &lt;code&gt;pidstat -w&lt;/code&gt; shows 50k voluntary context switches/sec. &lt;code&gt;perf stat&lt;/code&gt; shows IPC of 0.3 (terrible). The culprit: a mutex hot path in a logging library. Every log call locked a global mutex. Threads spent cycles spinning, not working.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;top&lt;/code&gt; said "90% CPU." The reality: "90% CPU, 10% useful work."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix Is Not In top
&lt;/h2&gt;

&lt;p&gt;You do not fix this by staring at &lt;code&gt;top&lt;/code&gt;. You fix it by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;perf record -g&lt;/code&gt; to find the hot path&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;perf report&lt;/code&gt; to see the call graph&lt;/li&gt;
&lt;li&gt;Actually reading the flame graph&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;top&lt;/code&gt; is for "is the box busy?" Not "why is my app slow?"&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Schiff Heimlich | Sysadmin who checks perf before blaming the scheduler&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>performance</category>
      <category>debugging</category>
      <category>sysadmin</category>
    </item>
    <item>
      <title>When Your Pipeline Goes Silent: The stdin Buffering Trap</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Wed, 28 Oct 2026 17:03:02 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/when-your-pipeline-goes-silent-the-stdin-buffering-trap-149a</link>
      <guid>https://dev.to/schiff_heimlich/when-your-pipeline-goes-silent-the-stdin-buffering-trap-149a</guid>
      <description>&lt;p&gt;You know this one. You wire up &lt;code&gt;tail -f /var/log/app.log | grep error | your-webhook&lt;/code&gt;, it works in your terminal, then you deploy it and... nothing. The errors are happening, you can see them in the log file, but the webhook never fires.&lt;/p&gt;

&lt;p&gt;It's not grep. It's not the webhook. It's stdout buffering.&lt;/p&gt;

&lt;p&gt;When stdout isn't a TTY — which it isn't in a pipe — the C library switches from line-buffered to fully buffered (usually 4 KB). So &lt;code&gt;grep&lt;/code&gt; finds the match, writes it to its stdout buffer, and sits there waiting for that buffer to fill. Your webhook sees nothing until 4 KB of errors accumulate. On a quiet system, that's forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix is one flag:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# GNU grep&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;--line-buffered&lt;/span&gt; error

&lt;span class="c"&gt;# BSD/macOS grep&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;--line-buffered&lt;/span&gt; error  &lt;span class="c"&gt;# same flag works&lt;/span&gt;

&lt;span class="c"&gt;# Or the portable way&lt;/span&gt;
&lt;span class="nb"&gt;stdbuf&lt;/span&gt; &lt;span class="nt"&gt;-oL&lt;/span&gt; &lt;span class="nb"&gt;grep &lt;/span&gt;error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;stdbuf -oL&lt;/code&gt; forces line buffering on stdout for any command. The &lt;code&gt;-oL&lt;/code&gt; means "stdout, line buffered." There's also &lt;code&gt;-o0&lt;/code&gt; for unbuffered (every write is a syscall) and &lt;code&gt;-eL&lt;/code&gt; for stderr.&lt;/p&gt;

&lt;p&gt;If you're on a system without &lt;code&gt;stdbuf&lt;/code&gt; (some minimal containers), &lt;code&gt;unbuffer&lt;/code&gt; from the &lt;code&gt;expect&lt;/code&gt; package does the same thing by faking a TTY:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unbuffer &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/app.log | &lt;span class="nb"&gt;grep &lt;/span&gt;error | your-webhook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The rule of thumb:&lt;/strong&gt; any time you pipe into something that consumes line-by-line (grep, awk, sed, your own script reading stdin), and the producer isn't interactive, check buffering. Add &lt;code&gt;--line-buffered&lt;/code&gt; or wrap the consumer in &lt;code&gt;stdbuf -oL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I've lost hours to this. Now it's the first thing I check when a pipeline "works locally but not in prod."&lt;/p&gt;

</description>
      <category>linux</category>
      <category>bash</category>
      <category>debugging</category>
      <category>sysadmin</category>
    </item>
    <item>
      <title>Finding and Replacing setuid Binaries on Linux</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Tue, 27 Oct 2026 17:02:22 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/finding-and-replacing-setuid-binaries-on-linux-5736</link>
      <guid>https://dev.to/schiff_heimlich/finding-and-replacing-setuid-binaries-on-linux-5736</guid>
      <description>&lt;p&gt;SUSE is working to disable setuid binaries system-wide. The idea: setuid/setgid binaries are a perennial source of Linux vulnerabilities, so why not eliminate the attack surface entirely?&lt;/p&gt;

&lt;p&gt;They already forbid setuid in packages unless explicitly reviewed. Now Thorsten Kukuk is pushing to disable them at the kernel/distribution level via capabilities instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The practical part&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First, audit what you have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find / &lt;span class="nt"&gt;-perm&lt;/span&gt; &lt;span class="nt"&gt;-4000&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see things like &lt;code&gt;/usr/bin/sudo&lt;/code&gt;, &lt;code&gt;/usr/bin/passwd&lt;/code&gt;, &lt;code&gt;/usr/bin/su&lt;/code&gt;, &lt;code&gt;/bin/ping&lt;/code&gt;. Some of these have capability-based replacements now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ping&lt;/code&gt; → &lt;code&gt;cap_net_raw&lt;/code&gt; (already done in most distros)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;su&lt;/code&gt; / &lt;code&gt;sudo&lt;/code&gt; → PAM + capabilities&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;passwd&lt;/code&gt; → PAM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What to do&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the find command above&lt;/li&gt;
&lt;li&gt;For each binary, check if your distro has a capability-based alternative&lt;/li&gt;
&lt;li&gt;Where possible, replace the setuid binary with a capability wrapper&lt;/li&gt;
&lt;li&gt;Watch your distro's efforts — SUSE, Fedora, and others are moving this direction&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal isn't to break your system. It's to shrink the setuid attack surface one binary at a time. Start with the easy wins like &lt;code&gt;ping&lt;/code&gt; (already capability-based on modern systems) and work from there.&lt;/p&gt;

&lt;p&gt;If you run containers, this matters even more — fewer setuid binaries in your base image means less privilege escalation risk if something breaks out.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Small hardening steps compound. This is one of them.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>sysadmin</category>
      <category>hardening</category>
    </item>
    <item>
      <title>chattr +i: The Immutable Flag That Actually Stops root</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Mon, 26 Oct 2026 17:05:50 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/chattr-i-the-immutable-flag-that-actually-stops-root-405g</link>
      <guid>https://dev.to/schiff_heimlich/chattr-i-the-immutable-flag-that-actually-stops-root-405g</guid>
      <description>&lt;p&gt;You know how &lt;code&gt;chmod 000&lt;/code&gt; feels like it should protect a file? It doesn't. Root ignores it. &lt;code&gt;sudo rm&lt;/code&gt; still works.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;chattr +i&lt;/code&gt; is different. It sets the immutable flag at the filesystem level. Even root can't delete, rename, or write to the file without explicitly removing the attribute first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Make a file immutable&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;chattr +i /etc/ssh/sshd_config

&lt;span class="c"&gt;# Verify&lt;/span&gt;
lsattr /etc/ssh/sshd_config
&lt;span class="c"&gt;# ----i---------e----- /etc/ssh/sshd_config&lt;/span&gt;

&lt;span class="c"&gt;# Try to modify (fails)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Port 2222"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/ssh/sshd_config
&lt;span class="c"&gt;# tee: /etc/ssh/sshd_config: Operation not permitted&lt;/span&gt;

&lt;span class="c"&gt;# Remove the flag when you actually need to change it&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;chattr &lt;span class="nt"&gt;-i&lt;/span&gt; /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also &lt;code&gt;+a&lt;/code&gt; for append-only — useful for log files you want protected from rotation bugs or accidental truncation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;chattr +a /var/log/myapp.log
&lt;span class="c"&gt;# Now you can only append, not overwrite or delete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things that bite people:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Filesystem support varies.&lt;/strong&gt; Works on ext4, XFS, Btrfs. Doesn't work on tmpfs, NFS mounts, or some FUSE filesystems. Check with &lt;code&gt;lsattr&lt;/code&gt; first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recursive is &lt;code&gt;-R&lt;/code&gt;, not &lt;code&gt;-r&lt;/code&gt;.&lt;/strong&gt; Easy to mix up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;cp&lt;/code&gt; doesn't preserve attributes.&lt;/strong&gt; If you copy an immutable file, the copy is mutable. Use &lt;code&gt;cp --attributes-only&lt;/code&gt; if you need that behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker volumes.&lt;/strong&gt; If you bind-mount a file with &lt;code&gt;+i&lt;/code&gt; into a container, the container can't modify it — which is either a feature or a headache depending on what you're doing.&lt;/p&gt;

&lt;p&gt;Practical uses I've actually reached for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Locking &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; and &lt;code&gt;/etc/sudoers&lt;/code&gt; after hardening&lt;/li&gt;
&lt;li&gt;Protecting critical cron files from "quick edits" that survive a reboot&lt;/li&gt;
&lt;li&gt;Append-only on audit logs where compliance matters&lt;/li&gt;
&lt;li&gt;Preventing logrotate from accidentally truncating a file the app holds open&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not a security boundary — a determined attacker with root can just &lt;code&gt;chattr -i&lt;/code&gt; — but it's a solid guard against accidents and automation gone wrong. The kind of thing that saves you at 2 AM when a script runs &lt;code&gt;rm -rf&lt;/code&gt; on &lt;code&gt;/etc&lt;/code&gt; because a variable expanded to empty.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>sysadmin</category>
      <category>filesystems</category>
      <category>security</category>
    </item>
    <item>
      <title>jq streams: when your JSON is too nested for regular paths</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Sun, 25 Oct 2026 17:02:06 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/jq-streams-when-your-json-is-too-nested-for-regular-paths-3ffm</link>
      <guid>https://dev.to/schiff_heimlich/jq-streams-when-your-json-is-too-nested-for-regular-paths-3ffm</guid>
      <description>&lt;p&gt;You know the problem. You've got a 500KB OpenAPI spec and need to strip every &lt;code&gt;description&lt;/code&gt; field at any depth. Regular jq paths like &lt;code&gt;.. | .description?&lt;/code&gt; work for reading, but try deleting them and you're stuck — the path doesn't tell you &lt;em&gt;where&lt;/em&gt; to put the result back.&lt;/p&gt;

&lt;p&gt;That's what &lt;code&gt;tostream&lt;/code&gt; and &lt;code&gt;fromstream&lt;/code&gt; are for.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tostream&lt;/code&gt; flattens the entire document into a stream of &lt;code&gt;[path, value]&lt;/code&gt; pairs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="s1"&gt;'tostream'&lt;/span&gt; spec.json | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;
&lt;span class="c"&gt;# [["openapi"], "3.0.1"]&lt;/span&gt;
&lt;span class="c"&gt;# [["info", "title"], "Petstore"]&lt;/span&gt;
&lt;span class="c"&gt;# [["info", "description"], "This is a sample server..."]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can filter with normal tools — &lt;code&gt;grep -v&lt;/code&gt;, &lt;code&gt;jq 'select(.[1] | type != "string" or . != "description")'&lt;/code&gt;, whatever — then pipe to &lt;code&gt;fromstream&lt;/code&gt; to reconstruct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="s1"&gt;'tostream | select(.[0][-1] != "description") | fromstream'&lt;/span&gt; spec.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; spec-clean.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one-liner drops every &lt;code&gt;description&lt;/code&gt; key at any depth. The stream format is &lt;code&gt;[path_array, leaf_value]&lt;/code&gt; — so &lt;code&gt;.[0][-1]&lt;/code&gt; gives you the last key in the path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveats&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Loses key order (JSON objects are unordered anyway)&lt;/li&gt;
&lt;li&gt;Streams the whole doc in memory; fine for a few MB, not for GB&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fromstream&lt;/code&gt; expects valid stream format — if your filter breaks the structure, it errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used this last week on a 2MB Kubernetes CRD manifest to remove all &lt;code&gt;x-kubernetes-*&lt;/code&gt; vendor extensions. Took 3 seconds. The alternative was writing a Python script with recursive dict walking.&lt;/p&gt;

&lt;p&gt;jq's streaming filters have been there since 1.6 (2019). Not new, just underused. Next time you're fighting nested JSON, try the stream approach before reaching for a general-purpose language.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Reference: Lars Kellogg-Stedman's writeup on jq streams (blog.oddbit.com, 2023)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cli</category>
      <category>json</category>
      <category>programming</category>
      <category>tools</category>
    </item>
    <item>
      <title>Small log-reading tricks that save hours</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Sat, 24 Oct 2026 17:02:43 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/small-log-reading-tricks-that-save-hours-8ij</link>
      <guid>https://dev.to/schiff_heimlich/small-log-reading-tricks-that-save-hours-8ij</guid>
      <description>&lt;p&gt;You know &lt;code&gt;tail -f&lt;/code&gt;. You've used &lt;code&gt;grep&lt;/code&gt;. But there are a handful of flags that turn "scrolling through logs" into "finding the problem."&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;&lt;code&gt;tail --follow=name&lt;/code&gt;&lt;/strong&gt; — survives log rotation&lt;/p&gt;

&lt;p&gt;Standard &lt;code&gt;tail -f&lt;/code&gt; follows the file &lt;em&gt;descriptor&lt;/em&gt;. When logrotate moves the file and creates a new one, you're still watching the old (now deleted) inode. &lt;code&gt;--follow=name&lt;/code&gt; reopens the file by name, so you keep seeing new lines after rotation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# instead of&lt;/span&gt;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/app.log
&lt;span class="c"&gt;# use&lt;/span&gt;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;--follow&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;name /var/log/app.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;head -n -N&lt;/code&gt;&lt;/strong&gt; — skip the last N lines&lt;/p&gt;

&lt;p&gt;Useful when logs have a footer (summary line, timestamp, ASCII art banner) you don't care about.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# show everything except the last 3 lines&lt;/span&gt;
&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt; /var/log/app.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;cat -A&lt;/code&gt;&lt;/strong&gt; — reveal the invisible&lt;/p&gt;

&lt;p&gt;Tabs become &lt;code&gt;^I&lt;/code&gt;, line endings become &lt;code&gt;$&lt;/code&gt;, non-printing chars get visible. Catches the "why does this config work on my machine but not prod?" class of bugs — usually a tab vs spaces issue or CRLF vs LF.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; /etc/nginx/nginx.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;tac&lt;/code&gt;&lt;/strong&gt; — read backwards&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cat&lt;/code&gt; spelled backwards. Reads a file from the last line to the first. Faster than &lt;code&gt;tail -n 100 | tac&lt;/code&gt; when you're hunting for the &lt;em&gt;most recent&lt;/em&gt; occurrence of something.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tac&lt;/span&gt; /var/log/app.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-m1&lt;/span&gt; &lt;span class="s2"&gt;"ERROR"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;journalctl -u service -f&lt;/code&gt;&lt;/strong&gt; — systemd journal follow&lt;/p&gt;

&lt;p&gt;If your service logs to journald (most systemd services do), this is cleaner than chasing log files. No rotation issues, includes metadata, and &lt;code&gt;-f&lt;/code&gt; works like &lt;code&gt;tail -f&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; nginx &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;span class="c"&gt;# or with time bounds&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; nginx &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"1 hour ago"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;None of these are fancy. They're just the flags that keep you from writing &lt;code&gt;awk&lt;/code&gt; one-liners at 2 AM. Keep them in muscle memory.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>sysadmin</category>
      <category>linux</category>
      <category>logging</category>
    </item>
    <item>
      <title>Kernel Stack Memory: The Hidden Consumer Eating Your RAM</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Fri, 23 Oct 2026 17:08:07 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/kernel-stack-memory-the-hidden-consumer-eating-your-ram-1cah</link>
      <guid>https://dev.to/schiff_heimlich/kernel-stack-memory-the-hidden-consumer-eating-your-ram-1cah</guid>
      <description>&lt;p&gt;Here's something I ran into recently that might save you a debugging session.&lt;/p&gt;

&lt;p&gt;You're running a high-concurrency server. Memory usage is creeping up, but your application heap looks fine. &lt;code&gt;top&lt;/code&gt; shows the process RSS growing, but &lt;code&gt;pmap&lt;/code&gt; or &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/smaps&lt;/code&gt; doesn't reveal an obvious leak in the heap, stacks, or mapped files. You're left wondering where the memory went.&lt;/p&gt;

&lt;p&gt;The answer might be kernel stacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Every thread in your process has a kernel stack. On x86_64 Linux, that's typically 16KB per thread (two 8KB pages, or one 16KB page depending on config). If you're running a Go service with 10,000 goroutines, or a Java app with a large thread pool, or nginx with many worker connections — that's 160MB just in kernel stacks. And it's not accounted for in your application's memory metrics.&lt;/p&gt;

&lt;p&gt;This came up at the 2026 Linux Plumbers Conference. The discussion was about reducing kernel stack usage because, at scale, it adds up. A thread doing a simple system call might only use a few hundred bytes of its kernel stack, but deep call chains (filesystem, network, security modules, BPF) can consume most of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Shows Up
&lt;/h2&gt;

&lt;p&gt;You'll see it in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RSS growth&lt;/strong&gt; that doesn't correlate with your application's allocations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/proc/&amp;lt;pid&amp;gt;/status&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;VmRSS&lt;/code&gt; includes kernel stacks, &lt;code&gt;VmSize&lt;/code&gt; doesn't always make it obvious&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cgroup memory stats&lt;/strong&gt; — &lt;code&gt;memory.current&lt;/code&gt; in cgroup v2 includes kernel stacks charged to the cgroup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tricky part: standard profilers (pprof, async-profiler, Java Flight Recorder) don't show kernel stack memory. It's outside your process's userspace view.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Measure It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Per-process:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Rough estimate: thread count * 16KB (x86_64 default)&lt;/span&gt;
ps &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;nlwp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &amp;lt;pid&amp;gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1 * 16 " KB"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;System-wide:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Total kernel stack memory across all processes&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"^Threads:"&lt;/span&gt; /proc/&lt;span class="k"&gt;*&lt;/span&gt;/status 2&amp;gt;/dev/null | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{sum += $2} END {print sum * 16 " KB"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;With &lt;code&gt;vmstat&lt;/code&gt; / &lt;code&gt;slabtop&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;slabtop &lt;span class="nt"&gt;-o&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; thread
&lt;span class="c"&gt;# Look for thread_struct, task_struct, kmalloc-16k (or kmalloc-8k x2)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When It Matters
&lt;/h2&gt;

&lt;p&gt;This isn't a problem for most workloads. But it becomes relevant when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High thread counts&lt;/strong&gt;: Go services with thousands of goroutines, Java apps with large thread pools, event-driven servers with many connections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory-constrained environments&lt;/strong&gt;: Containers with tight memory limits where 100-200MB of "invisible" memory pushes you into OOM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Density optimization&lt;/strong&gt;: Running many services on a single node where kernel stack overhead compounds&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mitigations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Reduce thread count&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The most direct fix. Use connection pooling, async I/O, or worker patterns that don't require one thread per connection. Go's goroutines map to OS threads only when blocked in syscalls, but a busy server can still have thousands of threads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;CONFIG_VMAP_STACK&lt;/code&gt;&lt;/strong&gt; (kernel config)&lt;/p&gt;

&lt;p&gt;Modern kernels (5.8+) support &lt;code&gt;CONFIG_VMAP_STACK&lt;/code&gt; which allocates kernel stacks from virtually mapped memory with guard pages. This doesn't reduce memory, but it catches stack overflows reliably. Most distro kernels have this enabled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;THREAD_STACK_SIZE&lt;/code&gt; tuning&lt;/strong&gt; (Go)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Go 1.19+&lt;/span&gt;
&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetThreadStackSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// 8KB instead of default (OS-dependent)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only do this if you're certain your Cgo calls and deep call chains fit. The default exists for a reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Monitor it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add kernel stack estimation to your memory dashboards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Approximate kernel stack memory per cgroup
sum by (container) (container_threads * 16384)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Practical Takeaway
&lt;/h2&gt;

&lt;p&gt;If your container memory usage is higher than your application metrics explain, and you're running high-concurrency workloads, check your thread count. Kernel stacks are the hidden tax on thread-heavy architectures.&lt;/p&gt;

&lt;p&gt;It's not a leak. It's not a bug. It's just memory that lives in the kernel, charged to your process, invisible to your profiler. Know it exists, measure it, and factor it into your capacity planning.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Schiff Heimlich | Sysadmin who checks /proc before blaming the app&lt;/em&gt;&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>infrastructure</category>
      <category>linux</category>
      <category>performance</category>
    </item>
    <item>
      <title>Bash Parameter Expansion Forms You Will Actually Use</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Thu, 22 Oct 2026 17:05:53 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/bash-parameter-expansion-forms-you-will-actually-use-1i12</link>
      <guid>https://dev.to/schiff_heimlich/bash-parameter-expansion-forms-you-will-actually-use-1i12</guid>
      <description>&lt;p&gt;You know the basics: &lt;code&gt;${var:-default}&lt;/code&gt; gives you a fallback. &lt;code&gt;${var:=default}&lt;/code&gt; assigns it. Fine. But there are a handful of forms that show up in real scripts and save you from writing extra conditionals.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ones Worth Remembering
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;${var@Q}&lt;/code&gt; — shell-quote the value&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the one I reach for when passing arguments through SSH or building command strings that need to survive parsing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"docker run --rm alpine echo &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;@Q&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
ssh host &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;@Q&lt;/code&gt;, a message like &lt;code&gt;hello world&lt;/code&gt; becomes two arguments. With it, you get &lt;code&gt;'hello world'&lt;/code&gt; — single argument, preserved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;${arr[@]@Q}&lt;/code&gt; — quote every array element&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same idea, but for arrays. Invaluable when you're forwarding a command with its arguments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;run_remote&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    ssh host &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;[@]@Q&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

run_remote docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; alpine &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"hello world"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each element gets quoted individually. The remote shell sees exactly what you passed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;${var@P}&lt;/code&gt; — prompt expansion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Expands prompt escape sequences. Useful if you're generating PS1-style strings programmatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="se"&gt;\h&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\w\$&lt;/span&gt;&lt;span class="s2"&gt; "&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;@P&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# saiki@server:/home/saiki$ &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not something you need daily, but handy when you're building a dynamic prompt or logging with context.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Glob Replacement Form
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;${var//pattern/repl}&lt;/code&gt;&lt;/strong&gt; — global replace (there is also &lt;code&gt;${var/pattern/repl}&lt;/code&gt; for first match only)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/usr/local/bin:/usr/bin:/bin"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="p"&gt;//&lt;/span&gt;:/&lt;span class="p"&gt; &lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# /usr/local/bin /usr/bin /bin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cleaner than &lt;code&gt;tr&lt;/code&gt; or &lt;code&gt;sed&lt;/code&gt; for simple delimiters. Works with extended globs too if you &lt;code&gt;shopt -s extglob&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Error Form
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;${var:?error message}&lt;/code&gt;&lt;/strong&gt; — fail with message if unset or empty&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEPLOY_TARGET&lt;/span&gt;:?DEPLOY_TARGET&lt;span class="p"&gt; not set&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fails fast with your message. Better than a cryptic "unbound variable" later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;These aren't clever tricks. They're standard Bash (since 4.4 for &lt;code&gt;@Q&lt;/code&gt;/&lt;code&gt;@P&lt;/code&gt;, earlier for the rest). Using them means fewer &lt;code&gt;if&lt;/code&gt; blocks, fewer &lt;code&gt;printf '%q'&lt;/code&gt; calls, fewer bugs from word-splitting surprises.&lt;/p&gt;

&lt;p&gt;Next time you reach for a conditional to handle a default or quote a string, check if there's a parameter expansion form that does it inline. Often there is.&lt;/p&gt;

</description>
      <category>bash</category>
      <category>cli</category>
      <category>devops</category>
      <category>linux</category>
    </item>
    <item>
      <title>Bash Parameter Expansion: Two Quirks That Slip Through Code Reviews</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Wed, 21 Oct 2026 17:03:25 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/bash-parameter-expansion-two-quirks-that-slip-through-code-reviews-5e25</link>
      <guid>https://dev.to/schiff_heimlich/bash-parameter-expansion-two-quirks-that-slip-through-code-reviews-5e25</guid>
      <description>&lt;p&gt;Here's a pair of bash parameter expansion behaviors that I've seen slip through code reviews and cause intermittent failures in production scripts. Small stuff, but worth knowing.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;${var:-default}&lt;/code&gt; vs &lt;code&gt;${var-default}&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This trips people up constantly. The colon matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;  &lt;span class="c"&gt;# var is set, but empty&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;var&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# prints: default&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;var&lt;/span&gt;&lt;span class="p"&gt;-default&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;     &lt;span class="c"&gt;# prints: (empty)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the colon, bash checks if the variable is &lt;em&gt;unset OR empty&lt;/em&gt;. Without the colon, it only checks if the variable is unset. So an empty string gets treated as "use the default" with &lt;code&gt;:-&lt;/code&gt;, but as "use the empty value" without.&lt;/p&gt;

&lt;p&gt;In practice, this matters most when you're handling environment variables or config values that might legitimately be empty:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Safe: use default only when truly absent&lt;/span&gt;
&lt;span class="nv"&gt;HOME_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;/root&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Unsafe: empty HOME would still use /root&lt;/span&gt;
&lt;span class="nv"&gt;HOME_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="p"&gt;-/root&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The colon form is almost always what you want when setting defaults for potentially-empty values.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;[[ -v var ]]&lt;/code&gt; vs &lt;code&gt;[[ -n $var ]]&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Another subtle one. &lt;code&gt;-v&lt;/code&gt; checks if a variable is &lt;em&gt;declared&lt;/em&gt; (and non-empty in bash 4.2+). &lt;code&gt;-n&lt;/code&gt; checks if the &lt;em&gt;value&lt;/em&gt; is non-empty.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;  &lt;span class="c"&gt;# declared but empty&lt;/span&gt;

&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; var &lt;span class="o"&gt;]]&lt;/span&gt;    &lt;span class="c"&gt;# true (var exists)&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$var&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;   &lt;span class="c"&gt;# false ($var expands to empty string)&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$var&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="c"&gt;# false (same, quoted or not)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-n "$var"&lt;/code&gt; version is actually checking if the &lt;em&gt;quoted&lt;/em&gt; string is non-empty, which is always false for an empty variable. But &lt;code&gt;-v&lt;/code&gt; correctly identifies that the variable exists, regardless of its value.&lt;/p&gt;

&lt;p&gt;This shows up when checking if optional flags were passed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;VERBOSE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;  &lt;span class="c"&gt;# passed but empty&lt;/span&gt;

&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; VERBOSE &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"verbose mode"&lt;/span&gt;     &lt;span class="c"&gt;# correctly matches&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$VERBOSE&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"verbose mode"&lt;/span&gt;     &lt;span class="c"&gt;# misses it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For declared/empty check: use &lt;code&gt;-v&lt;/code&gt;. For "has non-empty value" check: use &lt;code&gt;-z&lt;/code&gt; or &lt;code&gt;-n "$var"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Takeaway
&lt;/h2&gt;

&lt;p&gt;When writing scripts that handle config or environment variables, prefer &lt;code&gt;${var:-default}&lt;/code&gt; and &lt;code&gt;[[ -v var ]]&lt;/code&gt;. They're the more defensive choices and behave consistently across bash versions.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Schiff Heimlich | Sysadmin who checks the obvious things&lt;/em&gt;&lt;/p&gt;

</description>
      <category>bash</category>
      <category>debugging</category>
      <category>programming</category>
    </item>
    <item>
      <title>SSH Connection Multiplexing: The Config That Saves You From "Too Many Authentication Failures"</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Tue, 20 Oct 2026 17:03:17 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/ssh-connection-multiplexing-the-config-that-saves-you-from-too-many-authentication-failures-1082</link>
      <guid>https://dev.to/schiff_heimlich/ssh-connection-multiplexing-the-config-that-saves-you-from-too-many-authentication-failures-1082</guid>
      <description>&lt;h1&gt;
  
  
  SSH Connection Multiplexing: The Config That Saves You From "Too Many Authentication Failures"
&lt;/h1&gt;

&lt;p&gt;You've been there. You're SSH'ing through a jump host to reach a fleet of servers, and suddenly every connection starts failing with "Too many authentication failures." You check the logs, nothing obvious. You restart sshd, same problem. You start questioning your life choices.&lt;/p&gt;

&lt;p&gt;It's your SSH config. Specifically, how &lt;code&gt;ControlMaster&lt;/code&gt; interacts with &lt;code&gt;ProxyJump&lt;/code&gt; chains and the identity ordering that quietly overrides your explicit settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Multiplexing Basics
&lt;/h2&gt;

&lt;p&gt;If you're not using connection multiplexing, you should be. It reuses a single TCP connection for multiple SSH sessions to the same host. First connection authenticates, subsequent ones are nearly instant.&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;# ~/.ssh/config&lt;/span&gt;
&lt;span class="k"&gt;Host&lt;/span&gt; *
  &lt;span class="k"&gt;ControlMaster&lt;/span&gt; &lt;span class="no"&gt;auto&lt;/span&gt;
  &lt;span class="k"&gt;ControlPath&lt;/span&gt; ~/.ssh/cm-%r@%h:%p
  &lt;span class="k"&gt;ControlPersist&lt;/span&gt; &lt;span class="m"&gt;600&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a Unix socket at &lt;code&gt;~/.ssh/cm-user@host:22&lt;/code&gt; that stays open for 10 minutes after the last session closes. Your second, third, tenth connection to that host? Instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Breaks With ProxyJump
&lt;/h2&gt;

&lt;p&gt;Here's the catch. When you chain &lt;code&gt;ProxyJump&lt;/code&gt;, each hop needs its own control socket. But the &lt;code&gt;Host *&lt;/code&gt; pattern above creates one socket pattern for everything, and the jump host and target host can end up fighting over the same socket path.&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;# This looks fine but causes problems&lt;/span&gt;
&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; deploy

&lt;span class="k"&gt;Host&lt;/span&gt; app-*
  &lt;span class="k"&gt;HostName&lt;/span&gt; %h.internal.example.com
  &lt;span class="k"&gt;User&lt;/span&gt; deploy
  &lt;span class="k"&gt;ProxyJump&lt;/span&gt; bastion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you &lt;code&gt;ssh app-01&lt;/code&gt;, the multiplexing socket for &lt;code&gt;bastion&lt;/code&gt; and &lt;code&gt;app-01&lt;/code&gt; can collide if your &lt;code&gt;ControlPath&lt;/code&gt; doesn't distinguish them properly. The result: "Control socket connect failed" or the authentication failure cascade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix: Unique ControlPath Per Hop
&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; bastion
  &lt;span class="k"&gt;HostName&lt;/span&gt; bastion.example.com
  &lt;span class="k"&gt;User&lt;/span&gt; deploy
  &lt;span class="k"&gt;ControlPath&lt;/span&gt; ~/.ssh/cm-bastion-%r@%h:%p

&lt;span class="k"&gt;Host&lt;/span&gt; app-*
  &lt;span class="k"&gt;HostName&lt;/span&gt; %h.internal.example.com
  &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;ControlPath&lt;/span&gt; ~/.ssh/cm-%h-%r@%h:%p
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;bastion&lt;/code&gt; prefix on the jump host's socket. Now they're completely separate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The IdentityFile Trap
&lt;/h2&gt;

&lt;p&gt;Another silent killer: &lt;code&gt;ssh-agent&lt;/code&gt; forwarding with multiple keys. You have three keys in your agent. The target server only accepts one. SSH tries them in order, hits the limit, and you get "Too many authentication failures" before it ever tries the right key.&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;IdentitiesOnly&lt;/span&gt; &lt;span class="no"&gt;yes&lt;/span&gt;
  &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/id_ed25519
  &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/id_rsa_backup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;IdentitiesOnly yes&lt;/code&gt; tells SSH: only use the keys I explicitly list here, ignore whatever's in the agent. Combined with explicit &lt;code&gt;IdentityFile&lt;/code&gt; ordering, you control exactly which key gets tried first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Host * Catch-All That Overrides Everything
&lt;/h2&gt;

&lt;p&gt;This one bites people constantly. You have a specific &lt;code&gt;Host app-01&lt;/code&gt; block, but somewhere earlier in your config (or in a included file) there's a &lt;code&gt;Host *&lt;/code&gt; that sets &lt;code&gt;User root&lt;/code&gt; or &lt;code&gt;Port 2222&lt;/code&gt;. SSH processes config top-to-bottom, first match wins for each option. Your specific block never gets a chance to override.&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;# At the TOP of your config, or in an included file&lt;/span&gt;
&lt;span class="k"&gt;Host&lt;/span&gt; *
  &lt;span class="k"&gt;User&lt;/span&gt; deploy
  &lt;span class="k"&gt;ControlMaster&lt;/span&gt; &lt;span class="no"&gt;auto&lt;/span&gt;
  &lt;span class="k"&gt;ControlPath&lt;/span&gt; ~/.ssh/cm-%r@%h:%p
  &lt;span class="k"&gt;ControlPersist&lt;/span&gt; &lt;span class="m"&gt;600&lt;/span&gt;

&lt;span class="c1"&gt;# Later, your specific config&lt;/span&gt;
&lt;span class="k"&gt;Host&lt;/span&gt; app-01
  &lt;span class="k"&gt;HostName&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;.0.1.5
  &lt;span class="k"&gt;User&lt;/span&gt; admin        &lt;span class="c1"&gt;# This WILL apply (not set in Host *)&lt;/span&gt;
  &lt;span class="k"&gt;Port&lt;/span&gt; &lt;span class="m"&gt;2222&lt;/span&gt;         &lt;span class="c1"&gt;# This WILL apply&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if &lt;code&gt;Host *&lt;/code&gt; sets &lt;code&gt;IdentityFile&lt;/code&gt;, your specific block's &lt;code&gt;IdentityFile&lt;/code&gt; gets ignored. The fix: put your &lt;code&gt;Host *&lt;/code&gt; defaults at the bottom, or use &lt;code&gt;Include&lt;/code&gt; to separate concerns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Include for Team vs Personal
&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;# ~/.ssh/config&lt;/span&gt;
&lt;span class="k"&gt;Include&lt;/span&gt; config.d/team/*
&lt;span class="k"&gt;Include&lt;/span&gt; config.d/personal/*
&lt;span class="k"&gt;Host&lt;/span&gt; *
  &lt;span class="c1"&gt;# Your personal fallbacks here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now team configs (shared, version-controlled) load first, personal overrides load second, and your &lt;code&gt;Host *&lt;/code&gt; catch-all is truly last.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging Commands You'll Actually Use
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check if a control socket exists and is alive&lt;/span&gt;
ssh &lt;span class="nt"&gt;-O&lt;/span&gt; check app-01

&lt;span class="c"&gt;# List all active multiplexed connections&lt;/span&gt;
ssh &lt;span class="nt"&gt;-O&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;app-01  &lt;span class="c"&gt;# closes the master connection&lt;/span&gt;

&lt;span class="c"&gt;# Verbose multiplexing debug&lt;/span&gt;
ssh &lt;span class="nt"&gt;-vvv&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; check app-01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-O check&lt;/code&gt; tells you if the master connection is up. &lt;code&gt;-O exit&lt;/code&gt; tears it down cleanly. The &lt;code&gt;-vvv&lt;/code&gt; shows you exactly which config options are being applied and in what order.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Practical Pattern
&lt;/h2&gt;

&lt;p&gt;Here's a working template I use:&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;# Team-shared jump hosts and defaults&lt;/span&gt;
&lt;span class="k"&gt;Include&lt;/span&gt; ~/.ssh/config.d/team/*

&lt;span class="c1"&gt;# Personal overrides&lt;/span&gt;
&lt;span class="k"&gt;Include&lt;/span&gt; ~/.ssh/config.d/personal/*

&lt;span class="c1"&gt;# Global fallback - LAST&lt;/span&gt;
&lt;span class="k"&gt;Host&lt;/span&gt; *
  &lt;span class="k"&gt;ControlMaster&lt;/span&gt; &lt;span class="no"&gt;auto&lt;/span&gt;
  &lt;span class="k"&gt;ControlPath&lt;/span&gt; ~/.ssh/cm-%r@%h:%p
  &lt;span class="k"&gt;ControlPersist&lt;/span&gt; &lt;span class="m"&gt;600&lt;/span&gt;
  &lt;span class="k"&gt;IdentitiesOnly&lt;/span&gt; &lt;span class="no"&gt;yes&lt;/span&gt;
  &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/id_ed25519
  &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;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Team configs define the jump hosts with explicit &lt;code&gt;ControlPath&lt;/code&gt; prefixes. Personal configs add per-host tweaks. The global &lt;code&gt;Host *&lt;/code&gt; only applies where nothing else did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;SSH multiplexing saves enormous amounts of time, but the default examples assume simple single-hop connections. Once you add &lt;code&gt;ProxyJump&lt;/code&gt; chains and multiple keys, the config needs to be explicit about socket separation and identity selection. The debugging tools (&lt;code&gt;ssh -O check/exit&lt;/code&gt;) are built in—use them before you blame the network.&lt;/p&gt;

&lt;p&gt;Next time "too many authentication failures" shows up, check your control sockets and your identity ordering. It's usually one of those two.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>sysadmin</category>
      <category>ssh</category>
      <category>linux</category>
    </item>
    <item>
      <title>tcpdump and dig Examples You'll Actually Paste Into a Runbook</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Mon, 19 Oct 2026 17:02:39 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/tcpdump-and-dig-examples-youll-actually-paste-into-a-runbook-bjg</link>
      <guid>https://dev.to/schiff_heimlich/tcpdump-and-dig-examples-youll-actually-paste-into-a-runbook-bjg</guid>
      <description>&lt;p&gt;The man pages for &lt;code&gt;tcpdump&lt;/code&gt; and &lt;code&gt;dig&lt;/code&gt; are thorough. They are also useless when you are staring at a broken TLS handshake at 2 AM and need the exact filter syntax &lt;em&gt;now&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Julia Evans put together the practical examples the man pages leave out. Here are the ones that have earned a permanent spot in my runbook.&lt;/p&gt;

&lt;h2&gt;
  
  
  tcpdump
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Filter by TCP flags (SYN, FIN, RST)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tcpdump &lt;span class="nt"&gt;-i&lt;/span&gt; eth0 &lt;span class="s2"&gt;"tcp[tcpflags] &amp;amp; (tcp-syn|tcp-fin|tcp-rst) != 0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this when you need to see connection setup/teardown without the data packets drowning you out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extract SNI from TLS ClientHello
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tcpdump &lt;span class="nt"&gt;-i&lt;/span&gt; eth0 &lt;span class="nt"&gt;-s&lt;/span&gt; 0 &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s2"&gt;"tcp port 443 and (tcp[((tcp[12] &amp;amp; 0xf0) &amp;gt;&amp;gt; 2):1] = 0x16)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SNI is in the ClientHello (record type 0x16). This prints the raw packet so you can grep for the hostname. Not pretty, but it works without &lt;code&gt;tshark&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Capture full conversation to a file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tcpdump &lt;span class="nt"&gt;-i&lt;/span&gt; eth0 &lt;span class="nt"&gt;-s&lt;/span&gt; 0 &lt;span class="nt"&gt;-w&lt;/span&gt; capture.pcap host 10.0.1.5 and port 443
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-s 0&lt;/code&gt; captures full packets (default truncates). Open &lt;code&gt;capture.pcap&lt;/code&gt; in Wireshark later when you have coffee.&lt;/p&gt;

&lt;h2&gt;
  
  
  dig
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Debug DNSSEC validation failures
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +dnssec +multi +noall +answer +authority +additional example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;+multi&lt;/code&gt; formats records in human-readable multiline. You will see RRSIG, DNSKEY, and DS records inline. If validation fails, compare the DS in the parent zone with the DNSKEY in the child.&lt;/p&gt;

&lt;h3&gt;
  
  
  YAML output for automation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +yaml example.com A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Outputs structured YAML instead of the default text format. Parses cleanly in scripts. No more &lt;code&gt;awk&lt;/code&gt; wrestling with column alignment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trace the full delegation chain
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +trace +additional example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Walks from root down to the authoritative servers. Shows every delegation, every glue record, every CNAME. When DNS "works everywhere but here," this tells you where the chain breaks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query a specific nameserver directly
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig @ns1.example.com example.com A +noall +answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bypasses your resolver. Essential for verifying what an authoritative server actually returns vs. what your recursive resolver cached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why These Stick
&lt;/h2&gt;

&lt;p&gt;None of these are clever one-liners. They are the commands you reach for when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A TLS handshake fails and you need to see &lt;em&gt;what the client actually sent&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;DNSSEC validation breaks and you need to compare DS vs. DNSKEY&lt;/li&gt;
&lt;li&gt;You need machine-readable &lt;code&gt;dig&lt;/code&gt; output for a script that runs in CI&lt;/li&gt;
&lt;li&gt;Someone says "it resolves fine for me" and you need to prove the authoritative server disagrees&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bookmark the ones that match your stack. Delete the rest. The man pages are still there if you need the full flag list — but these are the ones you will actually type.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What tcpdump or dig invocations live in your runbook? Drop them in the comments — I am always looking for the one I forgot.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>sysadmin</category>
      <category>networking</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
