<?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>Why logrotate silently does nothing and how to actually debug it</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Sat, 15 Aug 2026 17:04:58 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/why-logrotate-silently-does-nothing-and-how-to-actually-debug-it-2gd</link>
      <guid>https://dev.to/schiff_heimlich/why-logrotate-silently-does-nothing-and-how-to-actually-debug-it-2gd</guid>
      <description>&lt;p&gt;A config that doesn't error is not a config that works.&lt;/p&gt;

&lt;p&gt;logrotate misconfigurations don't fail loudly. They fail silently. You only find out when /var/log is 100% full and something crashed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The copytruncate race condition
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;copytruncate&lt;/code&gt; directive exists because some processes don't respond to SIGHUP. Instead of telling the process to reopen its log, logrotate copies the file and truncates it in place.&lt;/p&gt;

&lt;p&gt;Sounds reasonable. The problem is the gap between "copy" and "truncate." On a busy nginx server writing hundreds of lines per second, you're going to drop log entries. On a quieter system you might silently duplicate them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;/&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;log&lt;/span&gt;/&lt;span class="n"&gt;nginx&lt;/span&gt;/&lt;span class="n"&gt;access&lt;/span&gt;.&lt;span class="n"&gt;log&lt;/span&gt; {
    &lt;span class="n"&gt;daily&lt;/span&gt;
    &lt;span class="n"&gt;rotate&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
    &lt;span class="n"&gt;compress&lt;/span&gt;
    &lt;span class="n"&gt;copytruncate&lt;/span&gt;   &lt;span class="c"&gt;# this is the problem on high-throughput services
&lt;/span&gt;}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is the postrotate approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;/&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;log&lt;/span&gt;/&lt;span class="n"&gt;nginx&lt;/span&gt;/&lt;span class="n"&gt;access&lt;/span&gt;.&lt;span class="n"&gt;log&lt;/span&gt; {
    &lt;span class="n"&gt;daily&lt;/span&gt;
    &lt;span class="n"&gt;rotate&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
    &lt;span class="n"&gt;compress&lt;/span&gt;
    &lt;span class="n"&gt;postrotate&lt;/span&gt;
        &lt;span class="n"&gt;nginx&lt;/span&gt; -&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;reload&lt;/span&gt;
    &lt;span class="n"&gt;endscript&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your process handles the log reopen cleanly. No race, no dropped lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The silent-failure trap
&lt;/h2&gt;

&lt;p&gt;logrotate returning 0 doesn't mean anything rotated. A missing file, wrong path, permission issue, or a config directive that doesn't apply — all of these produce a clean exit code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/usr/sbin/logrotate &lt;span class="nt"&gt;-d&lt;/span&gt; /etc/logrotate.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-d&lt;/code&gt; flag runs logrotate in debug mode. It prints what it &lt;em&gt;would&lt;/em&gt; do without doing it. Run this before shipping any config change.&lt;/p&gt;

&lt;p&gt;One thing to know: &lt;code&gt;-d&lt;/code&gt; still reads your actual state. If a file was already rotated today, the debug run won't show you what happens on a fresh invocation. Run it right after touching the config, not after the daily run already fired.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick sanity check for any config
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/usr/sbin/logrotate &lt;span class="nt"&gt;-d&lt;/span&gt; /etc/logrotate.d/your-app 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"renaming" lines — confirms it found the target files&lt;/li&gt;
&lt;li&gt;"empty log" warnings — the source file is missing or zero-size&lt;/li&gt;
&lt;li&gt;"error" strings — permission or path issues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you see "log needs rotating" but no "renaming", you hit the maxsize/rotations-per-day guard and the file was skipped intentionally.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually fills disks in practice
&lt;/h2&gt;

&lt;p&gt;The most common real-world cause isn't a clever attack or misbehaving application. It's a logrotate config that never had &lt;code&gt;compress&lt;/code&gt; on a high-volume file, and nobody noticed for three months because the disk monitor didn't alert on /var.&lt;/p&gt;

&lt;p&gt;The second most common: a process that held a file descriptor open after logrotate moved the file. The rotated file stays on disk until the process restarts. &lt;code&gt;lsof +L1&lt;/code&gt; catches this quickly.&lt;/p&gt;

&lt;p&gt;That's it. No magic. Check your postrotate hooks, run &lt;code&gt;logrotate -d&lt;/code&gt; before shipping configs, and add &lt;code&gt;lsof +L1&lt;/code&gt; to your disk-full checklist.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>devops</category>
      <category>infrastructure</category>
      <category>linux</category>
    </item>
    <item>
      <title>One-Command VPS Reinstall: Two Things Worth Knowing</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Fri, 14 Aug 2026 17:04:30 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/one-command-vps-reinstall-two-things-worth-knowing-2f8j</link>
      <guid>https://dev.to/schiff_heimlich/one-command-vps-reinstall-two-things-worth-knowing-2f8j</guid>
      <description>&lt;h1&gt;
  
  
  One-Command VPS Reinstall: Two Things Worth Knowing
&lt;/h1&gt;

&lt;p&gt;There's a tool called restage that reinstalls a Linux VPS over the network with one command. Saw it mentioned last week and finally tried it. Two things I found actually useful:&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Dry-Run Is Real
&lt;/h2&gt;

&lt;p&gt;Most "idempotency" claims in shell scripts are aspirational. With restage, &lt;code&gt;--dry-run&lt;/code&gt; actually shows you the entire action plan before touching anything:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;restage linux debian 12 --ssh-key "ssh-ed25519 AAAA..." --dry-run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;It prints: target disk (by PTUUID, not device name), OS image it's pulling, boot entry it's writing, SSH settings. If the plan looks wrong, you Ctrl-C and nothing happened.&lt;/p&gt;

&lt;p&gt;The paranoia here is warranted. A mistyped device name in a reinstall script can ruin your day.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Disk Selection by Partition Table UUID
&lt;/h2&gt;

&lt;p&gt;Device names drift between boots on some providers. &lt;code&gt;/dev/sda&lt;/code&gt; becomes &lt;code&gt;/dev/vda&lt;/code&gt;, or worse, &lt;code&gt;/dev/sdc&lt;/code&gt;. Restage selects the target disk using the partition-table UUID (PTUUID), which is stable and queryable:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;blkid -s PTTYPE -o value /dev/sda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The tool captures this during planning and references it in the installer, so device renaming between the dry-run and the actual reinstall doesn't break anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Catch
&lt;/h2&gt;

&lt;p&gt;This still erases the disk. The dry-run protects against typos, not against choosing the wrong machine. Know which node you're on before you run it.&lt;/p&gt;

&lt;p&gt;If you manage a handful of VPSes and keep putting off that Ubuntu-to-Debian migration because the control panel workflow is annoying, this is worth a look. The MIT-licensed toolkit is at github.com/codebyjawad/restage.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>cli</category>
      <category>devops</category>
      <category>linux</category>
    </item>
    <item>
      <title>Why BusyBox in Your Alpine Containers Is a Bigger Problem Than You Think</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Thu, 13 Aug 2026 17:06:34 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/why-busybox-in-your-alpine-containers-is-a-bigger-problem-than-you-think-ggo</link>
      <guid>https://dev.to/schiff_heimlich/why-busybox-in-your-alpine-containers-is-a-bigger-problem-than-you-think-ggo</guid>
      <description>&lt;p&gt;When you pull an Alpine image, you are probably not thinking about what is actually running inside. Most teams see minimal and move on.&lt;/p&gt;

&lt;p&gt;Here is the thing: Alpine Linux is built on BusyBox. One binary. Everything else links to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; alpine:latest &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /bin/ | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-10&lt;/span&gt;
&lt;span class="c"&gt;# lrwxrwxrwx 1 root root 12 /bin/ls -&amp;gt; /bin/busybox&lt;/span&gt;
&lt;span class="c"&gt;# lrwxrwxrwx 1 root root 12 /bin/cat -&amp;gt; /bin/busybox&lt;/span&gt;
&lt;span class="c"&gt;# lrwxrwxrwx 1 root root 12 /bin/cp -&amp;gt; /bin/busybox&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every utility you see is the same 1.3MB binary. That is your entire operating system.&lt;/p&gt;

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

&lt;p&gt;When BusyBox has a vulnerability, your entire userspace has it. There is no compartmentalization.&lt;/p&gt;

&lt;p&gt;Take CVE-2022-28391. It affected BusyBox is DHCP client. Any Alpine container acting as a DHCP client was vulnerable to remote code execution via a malicious DHCP response. This covered Kubernetes pods, CI runners, and network sidecars across the industry.&lt;/p&gt;

&lt;p&gt;The fix most teams did: docker pull alpine:latest. But that does not guarantee you are on a patched version.&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;# Check BusyBox version&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; alpine:3.19 busybox | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;

&lt;span class="c"&gt;# Verify against security.alpinelinux.org&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;&lt;strong&gt;1. Audit what you are actually running&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Find Alpine images in your cluster and check for BusyBox presence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Consider distroless for application containers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For Go or Rust services that compile statically, use distroless images. No shell, no package manager, no BusyBox. The image is slightly larger but your attack surface is smaller.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. If you must use Alpine, pin and automate rebuilds&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use specific version tags like alpine:3.19 instead of :latest. Set up Renovate or Dependabot to watch Alpine release tags and rebuild when CVEs drop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Tradeoff
&lt;/h2&gt;

&lt;p&gt;Alpine is convenient. Distroless requires more build work. But if you are running internet-facing workloads or anything with elevated privileges, the convenience is not worth the invisible attack surface.&lt;/p&gt;

&lt;p&gt;Go check your base images. See how many are Alpine. Then decide if you want to keep playing that game.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The logrotate copytruncate race condition that silently drops logs</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Wed, 12 Aug 2026 17:03:29 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/the-logrotate-copytruncate-race-condition-that-silently-drops-logs-nn4</link>
      <guid>https://dev.to/schiff_heimlich/the-logrotate-copytruncate-race-condition-that-silently-drops-logs-nn4</guid>
      <description>&lt;h2&gt;
  
  
  The problem with copytruncate
&lt;/h2&gt;

&lt;p&gt;If you've ever configured logrotate for a high-throughput service like nginx or syslog, you might have used &lt;code&gt;copytruncate&lt;/code&gt;. It copies the log file then truncates it in place, avoiding the need to signal the process to reopen its file descriptors.&lt;/p&gt;

&lt;p&gt;The issue: there's a window between the copy and the truncate. Any log lines written during that window get either dropped or duplicated when rotation runs on a busy system.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like in practice
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/logrotate.d/nginx
&lt;/span&gt;/&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;log&lt;/span&gt;/&lt;span class="n"&gt;nginx&lt;/span&gt;/&lt;span class="n"&gt;access&lt;/span&gt;.&lt;span class="n"&gt;log&lt;/span&gt; {
    &lt;span class="n"&gt;daily&lt;/span&gt;
    &lt;span class="n"&gt;rotate&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
    &lt;span class="n"&gt;copytruncate&lt;/span&gt;
    &lt;span class="n"&gt;compress&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a server doing high request volume, that copy-then-truncate gap can silently eat log lines per rotation. If you're doing incident response and wondering why your access logs don't match your application logs, this is one of the places to check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The more reliable approach
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;postrotate&lt;/code&gt; to signal the service to reopen its logs, then let logrotate do an atomic rename:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;/&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;log&lt;/span&gt;/&lt;span class="n"&gt;nginx&lt;/span&gt;/&lt;span class="n"&gt;access&lt;/span&gt;.&lt;span class="n"&gt;log&lt;/span&gt; {
    &lt;span class="n"&gt;daily&lt;/span&gt;
    &lt;span class="n"&gt;rotate&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
    &lt;span class="n"&gt;compress&lt;/span&gt;
    &lt;span class="n"&gt;postrotate&lt;/span&gt;
        [ -&lt;span class="n"&gt;f&lt;/span&gt; /&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;run&lt;/span&gt;/&lt;span class="n"&gt;nginx&lt;/span&gt;.&lt;span class="n"&gt;pid&lt;/span&gt; ] &amp;amp;&amp;amp; &lt;span class="n"&gt;kill&lt;/span&gt; -&lt;span class="n"&gt;USR1&lt;/span&gt; $(&lt;span class="n"&gt;cat&lt;/span&gt; /&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;run&lt;/span&gt;/&lt;span class="n"&gt;nginx&lt;/span&gt;.&lt;span class="n"&gt;pid&lt;/span&gt;)
    &lt;span class="n"&gt;endscript&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;nginx (and most well-behaved services) will finish writing the current line, then reopen to the new file on SIGUSR1. No gap, no dupes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Another gotcha: dateext collisions
&lt;/h2&gt;

&lt;p&gt;If you're using &lt;code&gt;dateext&lt;/code&gt; with &lt;code&gt;dateformat&lt;/code&gt;, watch out for rotation runs that trigger twice in the same second (can happen with noisy cron or manual runs). The second rotation will fail to create a unique file. Set &lt;code&gt;dateformat&lt;/code&gt; with enough precision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dateformat -%Y%m%d-%s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The %s gives you Unix timestamp seconds, which keeps it unique even if cron fires twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;copytruncate&lt;/code&gt; is a workaround for services that don't handle SIGUSR1 gracefully. If your service supports log reopening, use &lt;code&gt;postrotate&lt;/code&gt;. If you're stuck with &lt;code&gt;copytruncate&lt;/code&gt;, be aware it has a silent data loss window on busy systems.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>devops</category>
      <category>linux</category>
    </item>
    <item>
      <title>Bare-Metal Kubernetes: What NKP Metal Gets Right</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Wed, 12 Aug 2026 06:25:44 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/bare-metal-kubernetes-what-nkp-metal-gets-right-3840</link>
      <guid>https://dev.to/schiff_heimlich/bare-metal-kubernetes-what-nkp-metal-gets-right-3840</guid>
      <description>&lt;h1&gt;
  
  
  Bare-Metal Kubernetes: What NKP Metal Gets Right
&lt;/h1&gt;

&lt;p&gt;Here's something I ran into that might be worth a second look.&lt;/p&gt;

&lt;p&gt;Nutanix launched NKP Metal recently - basically their Kubernetes Platform extended to run directly on physical servers. I think the interesting part isn't the bare-metal capability itself, but what it means for operational consistency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical bit
&lt;/h2&gt;

&lt;p&gt;If you're running AI/ML workloads or dealing with edge deployments, bare-metal matters for one reason: you avoid the hypervisor layer. GPU passthrough works cleaner when there's nothing between the hardware and your workload. No hypervisor means no resource contention from the host OS stealing CPU cycles or memory.&lt;/p&gt;

&lt;p&gt;The other thing worth noting is the unified management angle. Same control plane whether you're running on VMs or physical hardware. That means one less management interface to switch between, one less place to check when something goes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this actually helps
&lt;/h2&gt;

&lt;p&gt;This isn't a "bare-metal is always better" take. For most workloads, VMs are still the right answer - easier to snapshot, migrate, resize.&lt;/p&gt;

&lt;p&gt;But if you're doing GPU-heavy inference at the edge, or you've got hardware constraints that make virtualization impractical, bare-metal Kubernetes becomes relevant. And if you're already in the Nutanix ecosystem, having both under the same console means less context-switching.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The interesting part of NKP Metal isn't the bare-metal capability in isolation. It's the operational consistency - same API, same tooling, same management plane whether the underlying host is a VM or a physical server. That's the practical value for teams already running Nutanix.&lt;/p&gt;

&lt;p&gt;If you're evaluating Kubernetes infrastructure options and have GPU workloads that need direct hardware access, it's worth a look. If your workloads fit comfortably in VMs, this doesn't change much.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Schiff Heimlich | Sysadmin who pays attention to operational simplicity&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>infrastructure</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Your Internal Developer Platform Is Probably Ignoring Your Java Services</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Tue, 04 Aug 2026 17:04:16 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/your-internal-developer-platform-is-probably-ignoring-your-java-services-b28</link>
      <guid>https://dev.to/schiff_heimlich/your-internal-developer-platform-is-probably-ignoring-your-java-services-b28</guid>
      <description>&lt;h1&gt;
  
  
  Your Internal Developer Platform Is Probably Ignoring Your Java Services
&lt;/h1&gt;

&lt;p&gt;Here's something I keep seeing in platform engineering work.&lt;/p&gt;

&lt;p&gt;You build an Internal Developer Platform. You want self-service for everyone. Kubernetes, CI templates, deployment pipelines - all unified under one platform.&lt;/p&gt;

&lt;p&gt;Then your Java teams start filing tickets. Something's off. Deploys take longer than expected. Memory usage is higher than it should be. The platform team says "it works for everyone else."&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Most IDPs are built by people who primarily work with Go, Python, or Node. Those are straightforward - compile or interpret, run as a process, resource requests are basically "give me some RAM."&lt;/p&gt;

&lt;p&gt;Java doesn't work that way. A Spring Boot microservice has JVM heap tuning, garbage collector settings, classpath management, Maven or Gradle build quirks. It needs a sidecar for metrics because the JVM exposes different telemetry than a native process. When you treat a 512MB JVM pod the same as a 50MB Go binary, you're already wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What generic IDPs get wrong
&lt;/h2&gt;

&lt;p&gt;The platform gives you a deployment template. You fill in your image, your CPU request, your memory limit. Done.&lt;/p&gt;

&lt;p&gt;But for a JVM service, the memory limit isn't just "how much RAM does the process use." It's "what does &lt;code&gt;-Xmx&lt;/code&gt; need to be relative to the container limit." If you set container memory to 1GB and don't tune the JVM, the runtime might only use 256MB heap and spend the rest in metaspace, native memory, and GC overhead.&lt;/p&gt;

&lt;p&gt;Same with sidecars. Java services with Spring Boot typically need specific sidecar configurations - actuator endpoints for health, Micrometer for metrics, language-specific logging patterns. Generic sidecar injection assumes a process that behaves like everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  What helps
&lt;/h2&gt;

&lt;p&gt;Java-aware IDP templates account for this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JVM flags tied to container limits (e.g. &lt;code&gt;-XX:MaxRAMPercentage&lt;/code&gt; instead of fixed &lt;code&gt;-Xmx&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Health endpoints that actually work with Java (not just "does the process exist")&lt;/li&gt;
&lt;li&gt;Build tool awareness - knowing whether you're using Maven wrapper or Gradle, how the artifact gets built&lt;/li&gt;
&lt;li&gt;GC tuning profiles that match the workload type&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're running Spring Boot on Kubernetes and your platform team doesn't know what &lt;code&gt;-XX:+UseG1GC&lt;/code&gt; does, that's a gap worth filling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical point
&lt;/h2&gt;

&lt;p&gt;IDPs work best when they know what they're deploying. A platform that treats all services equally is a platform that works equally poorly for anything with specific runtime requirements. Java's got those. So does anything with native dependencies, or GPU workloads, or stateful services.&lt;/p&gt;

&lt;p&gt;The answer isn't to build a separate platform for Java. It's to make sure whoever owns the IDP understands the actual workloads running on it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Schiff Heimlich | Sysadmin who reads the JVM flags&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>devops</category>
      <category>java</category>
    </item>
    <item>
      <title>SSH Key Permissions: The 5-Minute Fix That Actually Works</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Mon, 03 Aug 2026 17:07:01 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/ssh-key-permissions-the-5-minute-fix-that-actually-works-26b1</link>
      <guid>https://dev.to/schiff_heimlich/ssh-key-permissions-the-5-minute-fix-that-actually-works-26b1</guid>
      <description>&lt;h1&gt;
  
  
  SSH Key Permissions: The 5-Minute Fix That Actually Works
&lt;/h1&gt;

&lt;p&gt;Here's a fun one that bites you at 2am.&lt;/p&gt;

&lt;p&gt;User calls: "I added a new key to authorized_keys but it still won't work." You check the file — it's there, permissions look fine. You &lt;code&gt;cat ~/.ssh/authorized_keys&lt;/code&gt; and the key looks correct. You &lt;code&gt;ssh -v&lt;/code&gt; and... nothing useful.&lt;/p&gt;

&lt;p&gt;Nine times out of ten, the problem isn't the key. It's the permissions on something upstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Exact Permission Matrix
&lt;/h2&gt;

&lt;p&gt;OpenSSH is picky about permissions. Not politely suggestive — actively hostile. It will silently ignore your authorized_keys file if any of these are wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Home directory:      755 or stricter (no 777, no 775 group-writable)
.ssh directory:     700 (only the owner can access it)
authorized_keys:    600 (only the owner can write it)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Those are the three you need to check.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Verify Quickly
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;sshd -T&lt;/code&gt; command does a dry-run of your sshd config and prints out the effective settings. But more useful: you can test a specific user's key authentication with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sshd &lt;span class="nt"&gt;-T&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;username &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="nv"&gt;laddr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.0.0.0 &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="nv"&gt;lport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;22
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But for a quick local check on the permission issues themselves, just use &lt;code&gt;namei&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;namei &lt;span class="nt"&gt;-l&lt;/span&gt; ~/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows you the permission bits on every component of the path. If you see anything like &lt;code&gt;drwxrwxr-x&lt;/code&gt; instead of &lt;code&gt;drwxr-x---&lt;/code&gt;, that's your culprit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The chmod Commands (When You're Fixing It)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;700 ~/.ssh
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 ~/.ssh/authorized_keys
&lt;span class="nb"&gt;chmod &lt;/span&gt;500 ~  &lt;span class="c"&gt;# if needed, though 755 usually works&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't do &lt;code&gt;chmod 777&lt;/code&gt; on anything. I know it's tempting when you're tired. Don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Gotcha Worth Knowing
&lt;/h2&gt;

&lt;p&gt;If the user's home directory itself is group-writable or world-writable, OpenSSH will still refuse to trust the .ssh directory. So even if your &lt;code&gt;.ssh&lt;/code&gt; and &lt;code&gt;authorized_keys&lt;/code&gt; are perfect, a sloppy home directory breaks everything.&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;# Check this too&lt;/span&gt;
&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"%a %n"&lt;/span&gt; ~
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it returns anything other than something like &lt;code&gt;755&lt;/code&gt; or &lt;code&gt;700&lt;/code&gt;, fix it first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Payoff
&lt;/h2&gt;

&lt;p&gt;Once permissions are right, the key works. No daemon restart needed. No config change. Just permissions.&lt;/p&gt;

&lt;p&gt;It's one of those things that's obvious in retrospect but easy to overlook when you're staring at a correct-looking authorized_keys file wondering why SSH keeps asking for a password.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: sysadmin, ssh, security, devops&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>When Your Script Dies with EPIPE: The Bug Is Two Functions Upstream</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Sun, 02 Aug 2026 17:05:03 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/when-your-script-dies-with-epipe-the-bug-is-two-functions-upstream-4ke2</link>
      <guid>https://dev.to/schiff_heimlich/when-your-script-dies-with-epipe-the-bug-is-two-functions-upstream-4ke2</guid>
      <description>&lt;p&gt;You're staring at a Python script that pipes output to &lt;code&gt;grep&lt;/code&gt; or &lt;code&gt;awk&lt;/code&gt; or any filter. It works fine in your shell. It fails in CI with a traceback pointing to a write call, something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OSError: [Errno 32] Broken pipe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The instinct is to look at line 47 where the write happens. The instinct is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Happens
&lt;/h2&gt;

&lt;p&gt;The EPIPE error fires at the writer because the &lt;strong&gt;reader closed its end of the pipe first&lt;/strong&gt;. The reader in your pipeline — &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;head&lt;/code&gt;, &lt;code&gt;awk&lt;/code&gt;, whatever — reached its input limit and exited. Your script kept writing. The kernel said "no more readers, stop."&lt;/p&gt;

&lt;p&gt;The bug isn't at the write. The bug is upstream in two ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The reader has a finite consumption rate.&lt;/strong&gt; &lt;code&gt;head -n 100&lt;/code&gt; closes its end after 100 lines. &lt;code&gt;grep&lt;/code&gt; exits on first match with &lt;code&gt;-m 1&lt;/code&gt;. &lt;code&gt;awk&lt;/code&gt; might finish a pattern and close. Your writer doesn't know this is coming.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Buffering lies to you.&lt;/strong&gt; Pipes have a kernel buffer — typically 64KB on Linux. Your writer fills that buffer, then blocks. Meanwhile the reader is already gone. When the buffer drains, the writer wakes up to a closed pipe and gets EPIPE.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Fix: Handle the Reader's Lifecycle
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="c1"&gt;# Option 1: Ignore EPIPE and let the reader drive
&lt;/span&gt;&lt;span class="n"&gt;proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Popen&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;awk&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;NR&amp;gt;1{print}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data.csv&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; 
                        &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PIPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PIPE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Your script's logic here
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;  &lt;span class="c1"&gt;# reader exited, stop writing
&lt;/span&gt;    &lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Option 2: in bash, handle it explicitly&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; pipefail
&lt;span class="nb"&gt;grep &lt;/span&gt;pattern large-file.txt | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;ret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$ret&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 141 &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="c"&gt;# 141 = 128 + 13 (SIGPIPE)&lt;/span&gt;
        &lt;span class="c"&gt;# Reader exited before writer finished - this is OK&lt;/span&gt;
        :
    &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;set -o pipefail&lt;/code&gt; approach isn't enough by itself — you still need to catch the case where the writer gets EPIPE before the pipeline overall exits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Debugging Rule
&lt;/h2&gt;

&lt;p&gt;When you get a broken pipe error, look at the &lt;strong&gt;downstream&lt;/strong&gt; command in your pipeline first. Ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does it have an input limit? (&lt;code&gt;head -n&lt;/code&gt;, &lt;code&gt;grep -m&lt;/code&gt;, &lt;code&gt;tail -n&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Does it exit early on some condition?&lt;/li&gt;
&lt;li&gt;What happens if input is shorter than expected?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The traceback points at where the symptom is, not where the cause is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Check
&lt;/h2&gt;

&lt;p&gt;If you're building pipelines in scripts, add this before you blame the writer:&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;# Check if the downstream tool exits early&lt;/span&gt;
&lt;span class="nb"&gt;timeout &lt;/span&gt;5 &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 1 pattern file.txt &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;"grep exited cleanly"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it exits before your writer is done, you found your culprit.&lt;/p&gt;




&lt;p&gt;This comes up often enough that it's worth building the habit: EPIPE is a reader problem, not a writer problem.&lt;/p&gt;

</description>
      <category>cli</category>
      <category>debugging</category>
      <category>linux</category>
      <category>python</category>
    </item>
    <item>
      <title>Your Alpine Base Image Has BusyBox. That's Probably Fine Until It's Not.</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Sat, 01 Aug 2026 17:05:36 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/your-alpine-base-image-has-busybox-thats-probably-fine-until-its-not-2aep</link>
      <guid>https://dev.to/schiff_heimlich/your-alpine-base-image-has-busybox-thats-probably-fine-until-its-not-2aep</guid>
      <description>&lt;p&gt;Let's talk about something that shows up in most container Dockerfiles without anyone really thinking about it: &lt;strong&gt;BusyBox&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you're using Alpine as your base image, you're pulling in BusyBox. It's in BusyBox-based images, MiniCoI, and various other minimal Linux distros designed for containers. It's small, it's handy, and it's been there forever.&lt;/p&gt;

&lt;p&gt;Here's the thing though — BusyBox was never designed for cloud production environments. It was designed for embedded systems. That's a meaningful difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  What BusyBox actually is
&lt;/h2&gt;

&lt;p&gt;BusyBox is a single binary that bundles implementations of dozens of Unix utilities: &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;cp&lt;/code&gt;, &lt;code&gt;sh&lt;/code&gt;, and so on. The idea is you get a working userspace in a tiny package. For Alpine, it's what makes &lt;code&gt;ash&lt;/code&gt; (the shell) and basic utilities work.&lt;/p&gt;

&lt;p&gt;The problem isn't that BusyBox is bad software. It's that it's a single point of exposure. When BusyBox has a vulnerability — and it does get them, like any software — your entire userspace is affected. Runtime scanners catch some of this, but the damage is already baked into your image layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The layered problem
&lt;/h2&gt;

&lt;p&gt;When you build from Alpine, you're not just getting your app. You're getting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Alpine base (BusyBox)&lt;/li&gt;
&lt;li&gt;Your package manager additions&lt;/li&gt;
&lt;li&gt;Your application layers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the base layer has a vulnerability, rebuilding your app layer doesn't fix it. You have to rebuild from a patched base, which means your whole image stack changes.&lt;/p&gt;

&lt;p&gt;This is the "compatibility" problem that CleanStart and similar approaches are trying to solve at the build level rather than the runtime level. Make the base composition a policy decision, not an accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can actually do today
&lt;/h2&gt;

&lt;p&gt;If you're using Alpine images:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pin your base image versions explicitly, don't use &lt;code&gt;latest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Track CVEs for BusyBox specifically, not just your app dependencies&lt;/li&gt;
&lt;li&gt;Consider whether your production containers actually need a full shell and utilities — distroless or scratch images may be more appropriate if you're just running a single process&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This isn't a crisis. Most teams are running fine with Alpine. But it's worth knowing what's actually in your base image and making that an intentional choice rather than a default you inherited.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Just a practical observation, not a scare piece. BusyBox has a good security track record — but so did a lot of things until they didn't.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>linux</category>
    </item>
    <item>
      <title>set -e Will Kill Your Script For No Good Reason</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Thu, 30 Jul 2026 17:06:15 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/set-e-will-kill-your-script-for-no-good-reason-175h</link>
      <guid>https://dev.to/schiff_heimlich/set-e-will-kill-your-script-for-no-good-reason-175h</guid>
      <description>&lt;h1&gt;
  
  
  &lt;code&gt;set -e&lt;/code&gt; Will Kill Your Script For No Good Reason
&lt;/h1&gt;

&lt;p&gt;You add &lt;code&gt;set -e&lt;/code&gt; to your bash script because you want it to stop on errors. Reasonable expectation. What could go wrong?&lt;/p&gt;

&lt;p&gt;Plenty.&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;set -e&lt;/code&gt; makes your script exit immediately when any command returns non-zero. Sounds fine until you remember how many things legitimately return non-zero:&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;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"pattern"&lt;/span&gt; file    &lt;span class="c"&gt;# returns 1 when no match found&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com  &lt;span class="c"&gt;# returns non-zero on 4xx/5xx&lt;/span&gt;
&lt;span class="o"&gt;((&lt;/span&gt; counter &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 5 &lt;span class="o"&gt;))&lt;/span&gt;          &lt;span class="c"&gt;# returns 1 when false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of those are errors. But &lt;code&gt;set -e&lt;/code&gt; doesn't care.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Example
&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;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="c"&gt;# This fails silently when the string isn't found&lt;/span&gt;
&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"^username="&lt;/span&gt; /etc/app/config | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-f2&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# This fails when count is zero  &lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt; count &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 0 &lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;process_items
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;grep&lt;/code&gt; returns 1 when there's no match. &lt;code&gt;set -e&lt;/code&gt; kills the script. Same with arithmetic that's false.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Workarounds
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Option 1: Disable locally&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="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"pattern"&lt;/span&gt; file &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;   &lt;span class="c"&gt;# always succeeds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option 2: Toggle around suspect code&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="nb"&gt;set&lt;/span&gt; +e
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"pattern"&lt;/span&gt; file
&lt;span class="nv"&gt;result&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option 3: Check explicitly&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="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"pattern"&lt;/span&gt; file&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"^username="&lt;/span&gt; /etc/app/config | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-f2&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Smarter Default
&lt;/h2&gt;

&lt;p&gt;Skip &lt;code&gt;set -e&lt;/code&gt; entirely. Check your commands explicitly:&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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"pattern"&lt;/span&gt; file&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"pattern not found"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More verbose, but you control exactly what fails and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Bites You At 3am
&lt;/h2&gt;

&lt;p&gt;Because the script worked for months, then one day &lt;code&gt;curl&lt;/code&gt; starts returning 404 for a URL that moved, and your whole deploy stops. The actual failure is trivial, but &lt;code&gt;set -e&lt;/code&gt; treats it like the world ended.&lt;/p&gt;

&lt;p&gt;It's not your logic that's broken. It's &lt;code&gt;set -e&lt;/code&gt; being overprotective.&lt;/p&gt;




&lt;p&gt;This isn't a reason to never use &lt;code&gt;set -e&lt;/code&gt;. Just don't use it without understanding what it actually checks. Most shell script bugs I debug come down to this flag surprising someone at the worst moment.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>cli</category>
      <category>linux</category>
      <category>programming</category>
    </item>
    <item>
      <title>The One-Line Audit That Might Save You a Container Incident</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Wed, 29 Jul 2026 17:05:43 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/the-one-line-audit-that-might-save-you-a-container-incident-570f</link>
      <guid>https://dev.to/schiff_heimlich/the-one-line-audit-that-might-save-you-a-container-incident-570f</guid>
      <description>&lt;h1&gt;
  
  
  The One-Line Audit That Might Save You a Container Incident
&lt;/h1&gt;

&lt;p&gt;Here's something I check whenever I'm reviewing a new container image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;image] which busybox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it returns a path, you're running BusyBox. And when BusyBox has a vulnerability, your entire userspace has that vulnerability.&lt;/p&gt;

&lt;p&gt;Alpine Linux is the most common case. It's the default base for a lot of Helm charts and CI templates. The selling point is "minimal" and "secure." What you're actually running is BusyBox with a package manager attached.&lt;/p&gt;

&lt;p&gt;The issue isn't that Alpine is bad. It's that BusyBox was designed for embedded systems - single-purpose devices that run one thing forever. When you put it in a multi-tenant cloud environment, you're trusting that the same binary handles &lt;code&gt;sh&lt;/code&gt;, &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;wget&lt;/code&gt;, and everything else in your container correctly.&lt;/p&gt;

&lt;p&gt;One CVE, your whole userspace is exposed.&lt;/p&gt;

&lt;p&gt;The fix isn't to stop using Alpine. It's to know what you're running and factor that into your vulnerability scanning. Most scanners catch CVEs in Alpine packages. They don't always catch CVEs in BusyBox itself.&lt;/p&gt;

&lt;p&gt;Check your base images. Know what's inside them. Two minutes of auditing might save you an incident.&lt;/p&gt;




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

</description>
    </item>
    <item>
      <title>Why Your rsyslog Config Is Quietly Breaking (And How to Fix It)</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Tue, 28 Jul 2026 17:04:55 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/why-your-rsyslog-config-is-quietly-breaking-and-how-to-fix-it-1m49</link>
      <guid>https://dev.to/schiff_heimlich/why-your-rsyslog-config-is-quietly-breaking-and-how-to-fix-it-1m49</guid>
      <description>&lt;h1&gt;
  
  
  Why Your rsyslog Config Is Quietly Breaking (And How to Fix It)
&lt;/h1&gt;

&lt;p&gt;RFC 6587 has been sitting there for years. Most of us didn't notice.&lt;/p&gt;

&lt;p&gt;Here's what actually happened: you have rsyslog configs from 2012 that work fine on UDP port 514. Then one day someone asks "can you send these logs to our SIEM over TCP?" and you spend three hours wondering why connections keep timing out.&lt;/p&gt;

&lt;p&gt;The answer is usually buried in how rsyslog parses messages.&lt;/p&gt;

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

&lt;p&gt;Old rsyslog configs often use &lt;code&gt;TraditionalForward&lt;/code&gt; mode, which expects a BSD syslog-style timestamp at a fixed offset in the message. RFC 6587 changed how syslog over TCP works — it uses octet-counting framing (each message is prefixed with its byte count) rather than newline-delimited messages.&lt;/p&gt;

&lt;p&gt;If you do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;action&lt;/span&gt;(&lt;span class="n"&gt;type&lt;/span&gt;=&lt;span class="s2"&gt;"omfwd"&lt;/span&gt; &lt;span class="n"&gt;protocol&lt;/span&gt;=&lt;span class="s2"&gt;"tcp"&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;=&lt;span class="s2"&gt;"siem.example.com"&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;=&lt;span class="s2"&gt;"514"&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...and your config still has &lt;code&gt;%TIMESTAMP%&lt;/code&gt; at a fixed column position, the receiving SIEM (especially one expecting RFC 6587 compliant input) will read garbage.&lt;/p&gt;

&lt;p&gt;The fix in rsyslog is straightforward — enable RFC 6587 mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;action&lt;/span&gt;(&lt;span class="n"&gt;type&lt;/span&gt;=&lt;span class="s2"&gt;"omfwd"&lt;/span&gt; &lt;span class="n"&gt;protocol&lt;/span&gt;=&lt;span class="s2"&gt;"tcp"&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;=&lt;span class="s2"&gt;"siem.example.com"&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;=&lt;span class="s2"&gt;"514"&lt;/span&gt;
       &lt;span class="n"&gt;streaming&lt;/span&gt;=&lt;span class="s2"&gt;"on"&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;streaming="on"&lt;/code&gt; switches to octet-counting. But that's only half the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured Logging and the Legacy Template Problem
&lt;/h2&gt;

&lt;p&gt;Compliance auditors are now asking about RFC 5848 (signed log streams) and structured logging (JSON-formatted messages). If your rsyslog templates still use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;$&lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="n"&gt;TraditionalFormat&lt;/span&gt;,&lt;span class="s2"&gt;"%TIMESTAMP% %HOSTNAME% %syslogtag%%msg%"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...you're not sending structured data. You're sending text that a human can read but a SIEM has to parse with regex.&lt;/p&gt;

&lt;p&gt;The actual migration path most teams end up on is: keep rsyslog running for the legacy stuff, but bridge it to journald which handles structured logging natively.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bridge Setup
&lt;/h2&gt;

&lt;p&gt;On a modern RHEL/Fedora box, &lt;code&gt;syslog-ng&lt;/code&gt; sits between rsyslog and journald nicely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# syslog-ng.conf — receive from legacy rsyslog
&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="n"&gt;s_net&lt;/span&gt; { &lt;span class="n"&gt;tcp&lt;/span&gt;(&lt;span class="n"&gt;port&lt;/span&gt;(&lt;span class="m"&gt;514&lt;/span&gt;) &lt;span class="n"&gt;flags&lt;/span&gt;(&lt;span class="n"&gt;expect&lt;/span&gt;-&lt;span class="n"&gt;octet&lt;/span&gt;-&lt;span class="n"&gt;counting&lt;/span&gt;())); };

&lt;span class="c"&gt;# Forward to journald in structured form
&lt;/span&gt;&lt;span class="n"&gt;destination&lt;/span&gt; &lt;span class="n"&gt;d_journal&lt;/span&gt; { &lt;span class="n"&gt;pipe&lt;/span&gt;(&lt;span class="s2"&gt;"/run/systemd/journal/syslog"&lt;/span&gt;); };

&lt;span class="n"&gt;log&lt;/span&gt; { &lt;span class="n"&gt;source&lt;/span&gt;(&lt;span class="n"&gt;s_net&lt;/span&gt;); &lt;span class="n"&gt;destination&lt;/span&gt;(&lt;span class="n"&gt;d_journal&lt;/span&gt;); };
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then journald handles the structured part — you get JSON via &lt;code&gt;journalctl --output=json&lt;/code&gt;, and you can sign streams with &lt;code&gt;journalctl --setup-keys&lt;/code&gt; if you need RFC 5848 compliance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What About Just Replacing rsyslog?
&lt;/h2&gt;

&lt;p&gt;You can. &lt;code&gt;systemd-journald&lt;/code&gt; alone is fine for local logging. But most enterprise environments have network devices and appliances that only know how to send traditional BSD syslog. You need something listening on UDP 514 for those.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;syslog-ng&lt;/code&gt; Premium Edition handles the RFC 6587 stuff properly and has good template support. The community edition works too, just with slightly different syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Lesson
&lt;/h2&gt;

&lt;p&gt;The configs from 2012 worked because the world was simpler — UDP, single SIEM, no compliance requirements. That world is gone. The fix isn't dramatic, but you need to understand what RFC 6587 actually changed about message framing before you can debug why your SIEM is getting half-formed messages at 2am.&lt;/p&gt;

&lt;p&gt;Check your rsyslog version (&lt;code&gt;rsyslogd -v&lt;/code&gt;) and look at whether your templates assume fixed-width fields. That's usually where things break.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
