<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged sysadmin</title>
<link>https://coderlegion.com/tag/sysadmin</link>
<description>Powered by Question2Answer</description>
<item>
<title>Finding the world-writable files on your server before someone else does</title>
<link>https://coderlegion.com/28028/finding-the-world-writable-files-on-your-server-before-someone-else-does</link>
<description>&lt;p&gt;A world-writable file can be rewritten by every account on the machine — including the service account your web server runs as. Code executing as &lt;code&gt;www-data&lt;/code&gt; through a compromised plugin doesn't need root: any &lt;code&gt;chmod 777&lt;/code&gt; you left behind during past debugging is a file it can overwrite or replace outright.&lt;/p&gt;
&lt;p&gt;Two details break most homegrown audits. First, &lt;code&gt;find -perm 777&lt;/code&gt; (no dash) matches mode 777 &lt;em&gt;exactly&lt;/em&gt; — a &lt;code&gt;666&lt;/code&gt; file is equally world-writable and slips through. The dash form uses mask semantics and catches every file with the other-write bit set. Second, never fix findings with &lt;code&gt;chmod -R 644&lt;/code&gt;: on directories, the execute bit means &quot;may be entered,&quot; and stripping it recursively seals off the whole tree. Split the fix by type instead:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;find /var/www -type f -perm -o+w 2&amp;gt;/dev/null
find /var/www -type f -exec chmod 644 {} \;
find /var/www -type d -exec chmod 755 {} \;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SSH keys and secrets go tighter, to &lt;code&gt;600&lt;/code&gt; — OpenSSH refuses group- or world-readable private keys outright.&lt;/p&gt;
&lt;p&gt;I keep the longer version as a script with a saved report file, a SUID scan, and a recommended-modes reference: &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/file-permissions-security&quot;&gt;full file-permissions security audit script&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28028/finding-the-world-writable-files-on-your-server-before-someone-else-does</guid>
<pubDate>Thu, 29 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>The log was deleted. Its 48M was still allocated.</title>
<link>https://coderlegion.com/30657/the-log-was-deleted-its-48m-was-still-allocated</link>
<description>&lt;p&gt;A 64M scratch tmpfs, a worker process holding its log open on file descriptor 3, and the usual reflex when space runs low: &lt;code&gt;rm&lt;/code&gt; the log. &lt;code&gt;du&lt;/code&gt; on the directory dropped from 48M to 0. &lt;code&gt;df&lt;/code&gt; stayed at 48M used. The next 32M write failed with &lt;code&gt;No space left on device&lt;/code&gt;, and &lt;code&gt;df&lt;/code&gt; read 100% while &lt;code&gt;du&lt;/code&gt; found only the 16M that had been written.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rm&lt;/code&gt; removes a name. The kernel frees the blocks only when no names are left and no process has the file open, and the worker still had it open. &lt;code&gt;lsof +L1&lt;/code&gt; lists exactly those files:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ lsof -w +L1 /srv
COMMAND    PID USER FD   TYPE DEVICE SIZE/OFF NLINK NODE NAME
sleep   608657 root 3w   REG   0,78 50331648     0    3 /srv/app/worker.log (deleted)
exit=0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;NLINK 0&lt;/code&gt;, &lt;code&gt;(deleted)&lt;/code&gt;, 48M held for writing by PID 608657. Truncating through the process's own descriptor, &lt;code&gt;: &amp;gt; /proc/608657/fd/3&lt;/code&gt;, took &lt;code&gt;df&lt;/code&gt; back to 25% while the process kept running.&lt;/p&gt;
&lt;p&gt;Next time, empty a live log with &lt;code&gt;: &amp;gt; file&lt;/code&gt; instead of deleting it. With an append-mode writer (&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;) that is clean. A writer that opened with &lt;code&gt;&amp;gt;&lt;/code&gt; keeps its old offset: in the same test, its log came back as a 17M sparse file with 1.0M on disk.&lt;/p&gt;
&lt;p&gt;Finding which process holds a port or a busy mount, plus a script that groups deleted-but-open files and totals them, is on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/lsof-command-examples&quot;&gt;lsof command examples page&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30657/the-log-was-deleted-its-48m-was-still-allocated</guid>
<pubDate>Wed, 28 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>Patch These Four Linux Kernel Local Root Vulns Now: DirtyAH6, PPPoEject, TUNderflow, and DiagSpill</title>
<link>https://coderlegion.com/32434/patch-these-four-linux-kernel-local-root-vulns-dirtyah6-pppoeject-tunderflow-diagspill</link>
<description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://codewithromi.com/patch-these-four-linux-kernel-local-root-vulns-now-dirtyah6-pppoeject-tunderflow-and-diagspill/&quot;&gt;codewithromi.com&lt;/a&gt;.&lt;/em&gt;&lt;br&gt;
Four Linux kernel vulnerabilities went public on 18 September 2026, and they are the kind you should not sit on. DirtyAH6 (CVE-2026-80844), PPPoEject (CVE-2026-68121), TUNderflow (CVE-2026-81000) and DiagSpill (CVE-2026-74469) are all local privilege escalation bugs, meaning an attacker or malicious process with a foothold on your machine can escalate to root. Working proof of concept exploits for all four are public. If you are running Proxmox, a Raspberry Pi, or any self-hosted Linux node, this is a patching weekend.&lt;/p&gt;
&lt;p&gt;Security researcher Asim Manizada reported the four bugs in mid July 2026 and published a technical write-up plus working exploits on 18 September 2026, after a coordinated hold so distributions could ship fixes first. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://seclists.org/oss-sec/2026/q3/822&quot;&gt;oss-security post&lt;/a&gt; and the accompanying &lt;a rel=&quot;nofollow&quot; href=&quot;https://heyitsas.im/posts/lpe-quartet/&quot;&gt;write-up&lt;/a&gt; carry the per-CVE detail. Upstream, the earliest stable releases that contain all four fixes are 5.10.270, 5.15.221, 6.1.188, 6.6.157, 6.12.109, 6.18.50 and 7.2.4. Note that the 7.0 series reached end of life on 27 June 2026, so a 7.0 kernel only carries these fixes if your vendor backported them.&lt;/p&gt;
&lt;h3 id=&quot;-why-this-matters-for-homelabbers&quot;&gt;💡 Why This Matters for Homelabbers&lt;/h3&gt;
&lt;p&gt;Local root exploits are often dismissed as &quot;low risk&quot; because they require existing access. That framing misses the threat model for homelabs and self-hosted infrastructure. If any service you run gets compromised, a web app, an LXC container with a shared kernel, or even a rogue package, a local root bug turns that into full node ownership.&lt;/p&gt;
&lt;p&gt;Proxmox LXC containers share the host kernel. A container breakout combined with a local priv-esc is a realistic attack chain. Raspberry Pi nodes running exposed services are in the same boat. Patch the kernel, then verify.&lt;/p&gt;
&lt;h3 id=&quot;-prerequisites&quot;&gt;🧰 Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;SSH or console access to each node&lt;/li&gt;
&lt;li&gt;sudo or root privileges&lt;/li&gt;
&lt;li&gt;A maintenance window, since patching requires a reboot&lt;/li&gt;
&lt;li&gt;Proxmox nodes: access to the Proxmox web UI or CLI for VM/container shutdown sequencing&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-step-by-step-patching-your-nodes&quot;&gt;🔧 Step-by-Step: Patching Your Nodes&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Check your current kernel version on every node so you know what you are starting from.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uname -r
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note this output. You will compare it after the update to confirm the new kernel is running.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Update your package index and apply all available security updates. On Debian and Ubuntu based systems, including Proxmox and Raspberry Pi OS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo apt update &amp;amp;&amp;amp; sudo apt full-upgrade -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On Proxmox specifically, you also want to pull from the Proxmox repositories. If you are on the no-subscription repo, this is already covered by the command above. Run &lt;code&gt;pveversion -v&lt;/code&gt; afterward to confirm the Proxmox packages are current too.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; If you are running Raspberry Pi OS, check the kernel package by its current name. On Bullseye and earlier the kernel shipped in the &lt;code&gt;raspberrypi-kernel&lt;/code&gt; package, but Raspberry Pi OS moved to Debian style &lt;code&gt;linux-image-rpi-*&lt;/code&gt; packages with Bookworm, so on a current install you want &lt;code&gt;linux-image-rpi-v8&lt;/code&gt; or &lt;code&gt;linux-image-rpi-2712&lt;/code&gt; depending on the board. The &lt;code&gt;apt full-upgrade&lt;/code&gt; command above will still catch it, but confirm with:&lt;/p&gt;
&lt;p&gt;On Bookworm and newer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;apt-cache policy linux-image-rpi-v8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On Bullseye and older:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;apt-cache policy raspberrypi-kernel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &quot;Installed&quot; and &quot;Candidate&quot; versions should match after your upgrade. If they do not, run the upgrade again.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4:&lt;/strong&gt; Reboot to load the new kernel. This is not optional. The running kernel in memory is still the old one until you reboot.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo reboot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For Proxmox hosts, shut down or migrate your VMs and containers before rebooting the host. Use the Proxmox web UI, or list what is running:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;qm list
pct list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then shut down each VM or container by its ID:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;qm shutdown [vmid]
pct shutdown [ctid]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 5:&lt;/strong&gt; After the reboot, confirm the new kernel is loaded:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;uname -r
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The version string should be newer than what you recorded in Step 1. Cross-reference it against your distro's security advisory to confirm the patched version is in place. For Debian, check &lt;a rel=&quot;nofollow&quot; href=&quot;https://security-tracker.debian.org/tracker/&quot;&gt;security-tracker.debian.org&lt;/a&gt;. For Ubuntu, check &lt;a rel=&quot;nofollow&quot; href=&quot;https://ubuntu.com/security/notices&quot;&gt;ubuntu.com/security/notices&lt;/a&gt;. For Raspberry Pi OS, check the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.raspberrypi.com/news/&quot;&gt;Raspberry Pi blog and release notes&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;-patch-status-checked-29-september-2026&quot;&gt;🛡️ Patch Status, Checked 29 September 2026&lt;/h3&gt;
&lt;p&gt;This is the part of the post that goes stale fastest, so here is a dated snapshot. Everything below was checked against the Debian security tracker, the Ubuntu CVE tracker and the Proxmox kernel changelog on 29 September 2026. Re-check before you decide you are covered.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Debian 13 trixie:&lt;/strong&gt; all four first fixed in linux 6.12.111-1, shipped as DSA-6528-1.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debian 12 bookworm:&lt;/strong&gt; DirtyAH6, PPPoEject and DiagSpill first fixed in linux 6.1.187-1 from bookworm-security. TUNderflow is the exception. The Debian tracker still lists bookworm as vulnerable, 6.1.187-1 included, so there is no fix for CVE-2026-81000 on bookworm yet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ubuntu 22.04, 24.04 and 26.04:&lt;/strong&gt; at the time of this check the Ubuntu CVE tracker showed no released kernel for any of the four on the main &lt;code&gt;linux&lt;/code&gt; packages. They sit at needed or pending. Watch &lt;a rel=&quot;nofollow&quot; href=&quot;https://ubuntu.com/security/notices&quot;&gt;ubuntu.com/security/notices&lt;/a&gt; instead of assuming an upgrade fixed it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Proxmox VE:&lt;/strong&gt; the Proxmox kernel changelog names only TUNderflow, first fixed in proxmox-kernel-7.0 version 7.0.14-19, released 18 September 2026. The newest build at the time of checking was 7.0.14-20 from 24 September 2026. The other three CVEs are not named anywhere in that changelog, and the Proxmox 7.0 kernel follows the Ubuntu kernel, which has not released them, so do not assume DirtyAH6, PPPoEject and DiagSpill are closed on a Proxmox host. Run &lt;code&gt;pveversion -v&lt;/code&gt; and compare against the changelog.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Raspberry Pi OS:&lt;/strong&gt; no Raspberry Pi OS kernel build carrying all four fixes could be confirmed at the time of this check. On the 6.12 branch you need 6.12.109 or newer for the full set, so check the installed package version rather than assuming.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-what-each-vulnerability-affects&quot;&gt;🛡️ What Each Vulnerability Affects&lt;/h3&gt;
&lt;p&gt;Here is what each bug actually is, taken from the disclosure and the researcher's write-up. Three of the four need &lt;code&gt;CAP_NET_ADMIN&lt;/code&gt; in a network namespace the attacker controls, which an ordinary local user obtains by creating an unprivileged user namespace first. DiagSpill is the exception and needs no special privileges at all.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DirtyAH6 (CVE-2026-80844):&lt;/strong&gt; The IPv6 Authentication Header code in the IPsec and XFRM path does not check that a routing header's &lt;code&gt;segments_left&lt;/code&gt; field is no larger than the number of addresses actually present, so pointer arithmetic in &lt;code&gt;ipv6_rearrange_rthdr()&lt;/code&gt; walks out of bounds and an oversized &lt;code&gt;memmove()&lt;/code&gt; corrupts kernel memory. It needs AH6 and XFRM support plus &lt;code&gt;CAP_NET_ADMIN&lt;/code&gt; and &lt;code&gt;CAP_NET_RAW&lt;/code&gt; in a namespace the attacker controls. The researcher also reports a remote crash, and in his own lab remote root, against a host acting as an IPv6 router that applies AH in transport mode.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PPPoEject (CVE-2026-68121):&lt;/strong&gt; A use after free in &lt;code&gt;pppoe_sendmsg()&lt;/code&gt;. The function holds a pointer into the socket buffer head across a &lt;code&gt;dev_hard_header()&lt;/code&gt; callback that can reallocate that head, then writes six bytes through the stale pointer. It needs a lower device whose header callback expands the buffer head, not merely a loadable PPPoE module, which makes it the narrowest of the four in practice.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TUNderflow (CVE-2026-81000):&lt;/strong&gt; The TUN/TAP driver stores a receive headroom value without bounding it. A device path that propagates oversized headroom (the proof of concept chains a netkit device through Open vSwitch to a raw TUN port) underflows a size calculation and leaves packet data 64 bytes past the end of the allocation. Simply having a TUN or TAP interface is not enough, you need that headroom propagation path. Worth correcting a common assumption here: Docker's default bridge networking uses veth pairs rather than TUN/TAP, and the in kernel WireGuard module is its own device type, so the usual homelab exposure is QEMU tap interfaces and OpenVPN style TUN devices, not containers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DiagSpill (CVE-2026-74469):&lt;/strong&gt; An SCTP association can hold up to 65,536 peer transports but the counter is 16 bits wide, so it wraps to zero at the limit. &lt;code&gt;sctp_diag&lt;/code&gt; then reserves buffer space based on the wrapped count and copies the full transport list anyway, writing roughly 8 MiB past the end of the response buffer. This one needs no capabilities and no user namespace, only SCTP and &lt;code&gt;sctp_diag&lt;/code&gt; support, which is what makes it the priority of the four.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;DiagSpill is the one to move fastest on if you are triaging. It is the only one of the four that an ordinary local user can reach without creating a user namespace, which also means the usual advice to restrict unprivileged user namespaces does nothing for it. If you do not need SCTP, keep the module out of the kernel.&lt;/p&gt;
&lt;h3 id=&quot;-hardening-beyond-the-patch&quot;&gt;🛡️ Hardening Beyond the Patch&lt;/h3&gt;
&lt;p&gt;Patching is the fix, but a few additional controls reduce your exposure, which matters more than usual here because some distributions have not shipped all four yet. Restricting unprivileged user namespaces closes the ordinary user path to three of the four. It does nothing for DiagSpill.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Restrict unprivileged user namespaces:&lt;/strong&gt; Three of these four bugs need a user namespace for an ordinary local user to reach them. Ubuntu introduced AppArmor based restrictions in 23.10 and enabled them by default from 24.04, controlled by &lt;code&gt;kernel.apparmor_restrict_unprivileged_userns&lt;/code&gt;, so 24.04 and 26.04 are covered out of the box. Debian is not. The Debian and Ubuntu specific &lt;code&gt;kernel.unprivileged_userns_clone&lt;/code&gt; knob has been discussed for deprecation, so confirm it exists on your kernel with &lt;code&gt;sysctl -a | grep userns&lt;/code&gt; before relying on it; the portable upstream equivalent is &lt;code&gt;sysctl -w user.max_user_namespaces=0&lt;/code&gt;. Either way you will break things that legitimately use user namespaces, including Flatpak, rootless containers and browser sandboxes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use AppArmor or SELinux profiles:&lt;/strong&gt; Proxmox ships with AppArmor. Make sure it is enforcing for your LXC containers. Unprivileged containers with AppArmor confinement are significantly harder to break out of.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limit local access:&lt;/strong&gt; Do not run untrusted code or expose shell access to untrusted users on these nodes. Local privesc only matters if someone or something local can trigger it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unload unused kernel modules:&lt;/strong&gt; If you are not using PPPoE or SCTP, keep both out. Add &lt;code&gt;blacklist pppoe&lt;/code&gt; and &lt;code&gt;blacklist sctp&lt;/code&gt; to a file in &lt;code&gt;/etc/modprobe.d/&lt;/code&gt; and run &lt;code&gt;update-initramfs -u&lt;/code&gt;. Blacklisting stops the on demand autoload that a socket call would trigger, and for DiagSpill that is the only mitigation short of patching, since restricting user namespaces does not touch it. Verify with &lt;code&gt;lsmod | grep -E 'pppoe|sctp'&lt;/code&gt; afterwards.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-troubleshooting&quot;&gt;🔧 Troubleshooting&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Kernel version did not change after reboot:&lt;/strong&gt; Your package manager may have the patched package available but it was not installed. Run &lt;code&gt;apt list --upgradable | grep linux-image&lt;/code&gt; to check, then install explicitly with &lt;code&gt;sudo apt install linux-image-[version]&lt;/code&gt; matching your architecture.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Proxmox node is stuck on an older kernel:&lt;/strong&gt; Proxmox sometimes pins the kernel for stability. Check &lt;code&gt;/etc/default/grub&lt;/code&gt; and the GRUB menu to ensure the new kernel is selected as default. Run &lt;code&gt;update-grub&lt;/code&gt; after confirming the package is installed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Raspberry Pi will not boot after upgrade:&lt;/strong&gt; This is rare but can happen if the bootloader and kernel fall out of sync. Boot from a backup SD card or use &lt;code&gt;rpi-update&lt;/code&gt; with caution, and check the Raspberry Pi OS forums for your specific board and OS version before using it.&lt;/p&gt;
&lt;h3 id=&quot;-wrap-up&quot;&gt;✅ Wrap Up&lt;/h3&gt;
&lt;p&gt;Four local root bugs in one disclosure batch is a bad day for Linux security. Updating and rebooting is the right move, but as of 29 September 2026 it is not the whole answer: Debian trixie has all four, Debian bookworm is still missing the TUNderflow fix, and Ubuntu has not released kernels for any of them. Prioritize DiagSpill, since it is the one an unprivileged local user can reach without a user namespace, and check the tracker for your distribution instead of assuming a fresh upgrade covered you.&lt;/p&gt;
&lt;p&gt;After you patch, take five minutes to check your other nodes. It is easy to forget a Raspberry Pi tucked behind the TV or a secondary Proxmox node you set up months ago. Cross-reference the patched kernel version against your distro's security tracker to make sure you are actually covered, not just up to date on packages.&lt;/p&gt;
&lt;p&gt;Stay subscribed to &lt;a rel=&quot;nofollow&quot; href=&quot;https://oss-security.openwall.org/wiki/mailing-lists/oss-security&quot;&gt;oss-sec&lt;/a&gt; if you want these disclosures in your inbox before they make the rounds on aggregators.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;related-from-the-lab&quot;&gt;Related from the lab&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://codewithromi.com/linux-kernel-security-gaps-why-your-homelab-isnt-safe-until-you-know-this/&quot;&gt;Linux Kernel Security Gaps: Why Your Homelab Isn't Safe Until You Know This&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://codewithromi.com/patching-dnsmasq-cves-securing-your-pi-hole-homelab-dns/&quot;&gt;Patching Dnsmasq CVEs: Securing Your Pi-hole &amp;amp; Homelab DNS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://codewithromi.com/crowdsec-community-powered-security/&quot;&gt;CrowdSec: Community-Powered Security&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32434/patch-these-four-linux-kernel-local-root-vulns-dirtyah6-pppoeject-tunderflow-diagspill</guid>
<pubDate>Sun, 25 Oct 2026 23:46:09 +0000</pubDate>
</item>
<item>
<title>bash ran a path that no longer existed, and exited 127</title>
<link>https://coderlegion.com/30653/bash-ran-a-path-that-no-longer-existed-and-exited-127</link>
<description>&lt;p&gt;A demo script called &lt;code&gt;report&lt;/code&gt;, two directories on &lt;code&gt;PATH&lt;/code&gt;, and one &lt;code&gt;mv&lt;/code&gt; between them, all on a scratch tmpfs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ report
report 2.1: 14 jobs ok
exit=0
$ mv /opt/jobs/bin/report /opt/jobs/newbin/report
$ ls -l /opt/jobs/newbin/report
-rwxrwxr-x 1 root root 42 Oct  3 18:26 /opt/jobs/newbin/report
$ report
session.sh: line 5: /opt/jobs/bin/report: No such file or directory
exit=127
$ type -a report
report is /opt/jobs/newbin/report
$ hash -t report
/opt/jobs/bin/report
$ command -v report
/opt/jobs/bin/report
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The file is there, on &lt;code&gt;PATH&lt;/code&gt;, and bash ran the old path anyway. The first time it finds a command through a &lt;code&gt;PATH&lt;/code&gt; search, it stores the full path in a hash table and skips the search after that. When the file moves, it executes the remembered path and reports &lt;code&gt;No such file or directory&lt;/code&gt; with exit 127, the same code a typo gets.&lt;/p&gt;
&lt;p&gt;The confusing part is the end of that output. &lt;code&gt;type -a&lt;/code&gt; searches &lt;code&gt;PATH&lt;/code&gt; fresh and finds the new location. &lt;code&gt;command -v&lt;/code&gt; reports what bash would run, which is the stale one. &lt;code&gt;hash -r&lt;/code&gt; clears the table, a new terminal starts empty, and &lt;code&gt;shopt -s checkhash&lt;/code&gt; makes bash re-search when a hashed path has gone: with it set, the same sequence printed the tool's output and exit 0.&lt;/p&gt;
&lt;p&gt;The stale hash is one of six causes behind &lt;code&gt;command not found&lt;/code&gt;. The others, from a cron &lt;code&gt;PATH&lt;/code&gt; to a CRLF shebang, each reproduced with a script that names the cause, are in &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-command-not-found&quot;&gt;command not found in bash, six causes&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30653/bash-ran-a-path-that-no-longer-existed-and-exited-127</guid>
<pubDate>Wed, 21 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>Every disk-alert script I had piped into a command this machine does not have</title>
<link>https://coderlegion.com/28024/every-disk-alert-script-i-had-piped-into-a-command-this-machine-does-not-have</link>
<description>&lt;p&gt;The standard bash alerting pattern ends the same way everywhere you read it: pipe the finding into &lt;code&gt;mail&lt;/code&gt;. So I checked what my own box would actually do with that line.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ type -a mail mailx sendmail
mail not found
mailx not found
sendmail not found
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing. No MTA, no &lt;code&gt;mailutils&lt;/code&gt;, no fallback. Any alert script written the usual way would have run its check correctly, found the problem correctly, piped the message into a command that does not exist, and told me nothing.&lt;/p&gt;
&lt;p&gt;This fails quieter than it sounds. Under &lt;code&gt;set -euo pipefail&lt;/code&gt; you at least get a non-zero exit somewhere a cron wrapper might notice. Without it, the pipeline's exit status is the &lt;em&gt;last&lt;/em&gt; command's, cron mails you nothing because cron's mail also needs an MTA, and the script looks like it ran fine.&lt;/p&gt;
&lt;p&gt;Check for the binary before you rely on it, not after an incident. And on a box that &lt;em&gt;does&lt;/em&gt; have &lt;code&gt;mail&lt;/code&gt;, exit 0 still only means the message reached a local MTA — on a host with no outbound relay it lands in a spool file under &lt;code&gt;/var/mail&lt;/code&gt; that nobody reads. Confirm real delivery once by tailing &lt;code&gt;/var/log/mail.log&lt;/code&gt;. Cron's minimal &lt;code&gt;PATH&lt;/code&gt; is a second reason to test from inside cron rather than your login shell.&lt;/p&gt;
&lt;p&gt;The msmtp relay config and the rate-limiting guard are in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-send-email-alert&quot;&gt;bash email alert walkthrough&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28024/every-disk-alert-script-i-had-piped-into-a-command-this-machine-does-not-have</guid>
<pubDate>Thu, 15 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>One empty variable, one error, one wrong answer, exit 0</title>
<link>https://coderlegion.com/29670/one-empty-variable-one-error-one-wrong-answer-exit-0</link>
<description>&lt;p&gt;I wrote four lines to capture a bash error for a guide. One empty variable, tested twice:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ cat queue-check.sh
#!/bin/bash
count=&quot;&quot;
if [ $count -eq 0 ]; then echo &quot;queue empty&quot;; fi
if [[ $count -eq 0 ]]; then echo &quot;queue empty ([[ ]])&quot;; fi

$ bash queue-check.sh
queue-check.sh: line 3: [: -eq: unary operator expected
queue empty ([[ ]])
exit=0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The message blames &lt;code&gt;-eq&lt;/code&gt;. The bug is the unquoted &lt;code&gt;$count&lt;/code&gt; before it: word splitting removed the empty value, so &lt;code&gt;[&lt;/code&gt; received &lt;code&gt;-eq 0&lt;/code&gt; and failed with status 2, which an &lt;code&gt;if&lt;/code&gt; treats as &quot;not true&quot;. The &lt;code&gt;[[ ]]&lt;/code&gt; line does no splitting, read the empty string as 0 in an arithmetic test, and printed a confident wrong answer. Then the script exited 0.&lt;/p&gt;
&lt;p&gt;Strict mode does not help here. &lt;code&gt;count=&quot;&quot;&lt;/code&gt; is set, so &lt;code&gt;set -u&lt;/code&gt; stays quiet, and &lt;code&gt;set -e&lt;/code&gt; ignores failures inside an &lt;code&gt;if&lt;/code&gt; condition. What stopped it was validating the value first: &lt;code&gt;[[ $count =~ ^[0-9]+$ ]] || exit 1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Most bash errors work this way. The message names the check that failed, not the cause: a syntax error reported on line 4 for a missing semicolon on line 2, &lt;code&gt;Permission denied&lt;/code&gt; after &lt;code&gt;chmod +x&lt;/code&gt; on a &lt;code&gt;noexec&lt;/code&gt; mount. Nine of them, each reproduced with the check that names the cause, are in &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/guides/bash-error-messages&quot;&gt;Bash Error Messages, Decoded&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29670/one-empty-variable-one-error-one-wrong-answer-exit-0</guid>
<pubDate>Wed, 14 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>My 30-day log cleanup kept a file that was 30 and a half days old</title>
<link>https://coderlegion.com/28022/my-30-day-log-cleanup-kept-a-file-that-was-30-and-a-half-days-old</link>
<description>&lt;p&gt;I set three log files to known ages with &lt;code&gt;touch -d&lt;/code&gt; — exactly 30 days, 30 days and 12 hours, and 31 days — then ran what I thought was a 30-day retention policy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;find . -name '*.log' -mtime +30
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It returned one file: the 31-day-old one. The 30.5-day file stayed. So did the 30-day file.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;-mtime&lt;/code&gt; counts &lt;em&gt;whole&lt;/em&gt; 24-hour periods and throws away the remainder, and &lt;code&gt;+30&lt;/code&gt; means strictly greater than 30 of them. A file aged 30 days and 12 hours counts as 30, and 30 is not greater than 30. A real 30-day policy is &lt;code&gt;-mtime +29&lt;/code&gt;. Re-running with &lt;code&gt;+29&lt;/code&gt; returned all three, which is what I wanted in the first place.&lt;/p&gt;
&lt;p&gt;Two more things that bit during the same session. Piping to &lt;code&gt;xargs rm&lt;/code&gt; split &lt;code&gt;app v2.log&lt;/code&gt; on the space — &lt;code&gt;rm&lt;/code&gt; reported &lt;code&gt;cannot remove 'ws/app'&lt;/code&gt; and &lt;code&gt;cannot remove 'v2.log'&lt;/code&gt;, and the file survived. And &lt;code&gt;-delete&lt;/code&gt; is an action that returns true, so putting it before your filter is catastrophic: &lt;code&gt;find ord -delete -name '*.log'&lt;/code&gt; removed a file called &lt;code&gt;keep.txt&lt;/code&gt; and then the directory itself. Preview with &lt;code&gt;-print&lt;/code&gt;, keep &lt;code&gt;-delete&lt;/code&gt; last.&lt;/p&gt;
&lt;p&gt;The multi-directory loop, the &lt;code&gt;.gz&lt;/code&gt; variant, and the cron lines are in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/delete-old-log-files&quot;&gt;find -mtime log retention walkthrough&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28022/my-30-day-log-cleanup-kept-a-file-that-was-30-and-a-half-days-old</guid>
<pubDate>Thu, 08 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>Catch a filling disk weeks before it becomes an outage</title>
<link>https://coderlegion.com/28021/catch-a-filling-disk-weeks-before-it-becomes-an-outage</link>
<description>&lt;p&gt;A disk almost never fails loudly. It fills a percent at a time until writes stop, and at 100% nothing reports &quot;disk full&quot; — the database throws write errors about its own files, apps crash-loop, and logging (a write itself) stops recording at the exact moment you need evidence. You end up debugging five fake problems instead of one real one.&lt;/p&gt;
&lt;p&gt;The prevention costs one line on a daily cron:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;df -h | awk 'NR&amp;gt;1 &amp;amp;&amp;amp; $5+0 &amp;gt;= 80 {print $0}'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;NR&amp;gt;1&lt;/code&gt; skips the header. &lt;code&gt;$5+0&lt;/code&gt; is the trick: df's Use% column is a string like &lt;code&gt;82%&lt;/code&gt;, and awk's numeric coercion keeps the leading digits and drops the &lt;code&gt;%&lt;/code&gt;, so the threshold comparison works without cut or sed. Pipe any output to &lt;code&gt;mail -s&lt;/code&gt; and you get an email the day a filesystem crosses 80.&lt;/p&gt;
&lt;p&gt;Why 80? At 80% a disk growing one percent a day leaves you weeks to investigate with &lt;code&gt;du -ah | sort -rh | head -20&lt;/code&gt;. At 95% you're deleting files under pressure; at 100% every service on the box misbehaves at once.&lt;/p&gt;
&lt;p&gt;This check is #1 in a set of &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/guides/bash-scripts-every-sysadmin-needs&quot;&gt;25 cron-ready bash scripts for sysadmins covering disk monitoring, backups, service watchdogs, SSL expiry, and port audits&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28021/catch-a-filling-disk-weeks-before-it-becomes-an-outage</guid>
<pubDate>Tue, 06 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>sed 's/port/listen_port/g' rewrote four lines of a four-line config</title>
<link>https://coderlegion.com/28020/sed-s-port-listenport-g-rewrote-four-lines-of-a-four-line-config</link>
<description>&lt;p&gt;A four-line YAML file (&lt;code&gt;port: 8080&lt;/code&gt;, &lt;code&gt;support_email&lt;/code&gt;, &lt;code&gt;transport: tcp&lt;/code&gt;, &lt;code&gt;report_dir: /var/reports&lt;/code&gt;) and the rename most people type first: &lt;code&gt;sed -i 's/port/listen_port/g' app.yml&lt;/code&gt;. GNU sed 4.9 exited 0 and printed nothing, because &lt;code&gt;-i&lt;/code&gt; never does. Afterwards the file held &lt;code&gt;suplisten_port_email&lt;/code&gt;, &lt;code&gt;translisten_port&lt;/code&gt; and &lt;code&gt;/var/relisten_ports&lt;/code&gt;. One line was meant to change. All four did.&lt;/p&gt;
&lt;p&gt;sed matches characters, not words. The letters p-o-r-t sit inside &lt;code&gt;support&lt;/code&gt;, &lt;code&gt;transport&lt;/code&gt; and &lt;code&gt;report&lt;/code&gt;, and &lt;code&gt;g&lt;/code&gt; replaced every occurrence. GNU sed's &lt;code&gt;\b&lt;/code&gt; word boundary fixes the match, and one pipe shows the damage before anything is written:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sed 's/\bport\b/listen_port/g' app.yml | diff app.yml -
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without &lt;code&gt;-i&lt;/code&gt; the file on disk stays intact, and &lt;code&gt;diff&lt;/code&gt; prints the lines that would change. It exits 1 when it finds differences, so the same check works inside a script. Only after the diff reads right does &lt;code&gt;-i&lt;/code&gt; go back on.&lt;/p&gt;
&lt;p&gt;Two caveats before trusting &lt;code&gt;\b&lt;/code&gt;. Underscore counts as a word character, so &lt;code&gt;db_port&lt;/code&gt; stays untouched. And &lt;code&gt;\b&lt;/code&gt; is a GNU extension: BSD sed on macOS spells word boundaries &lt;code&gt;[[:&amp;lt;:]]&lt;/code&gt; and &lt;code&gt;[[:&amp;gt;:]]&lt;/code&gt;, and its &lt;code&gt;-i&lt;/code&gt; requires a suffix argument (&lt;code&gt;sed -i ''&lt;/code&gt;) where GNU's is optional.&lt;/p&gt;
&lt;p&gt;The replacement side bites too. An unescaped &lt;code&gt;&amp;amp;&lt;/code&gt; means the whole match, so &lt;code&gt;s/CEO/Smith &amp;amp; Sons/&lt;/code&gt; produces &lt;code&gt;Smith CEO Sons&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The bulk-replace script, with a &lt;code&gt;grep -rl&lt;/code&gt; file list, per-file diff and an &lt;code&gt;--apply&lt;/code&gt; gate that fails toward reading, is on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-sed-find-replace&quot;&gt;sed find-and-replace snippet page&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28020/sed-s-port-listenport-g-rewrote-four-lines-of-a-four-line-config</guid>
<pubDate>Thu, 01 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>What a Linux Sysadmin Actually Does All Day (Spoiler: It's Not Just `sudo apt update`)</title>
<link>https://coderlegion.com/28624/what-a-linux-sysadmin-actually-does-all-day-spoiler-its-not-just-sudo-apt-update</link>
<description>&lt;p&gt;Somewhere out there is a job posting that describes a &quot;Linux System Administrator&quot; as someone who &quot;maintains servers.&quot; That description is technically true the way &quot;doctors deal with bodies&quot; is technically true. It tells you nothing, it undersells the job by an order of magnitude, and it's why half the people who land in this role are shocked, three weeks in, to discover they're now responsible for network routing, certificate rotation, backup verification, and being the calm voice at 3 a.m. when a disk fills up and takes production with it.&lt;/p&gt;
&lt;p&gt;Let's actually map the territory, without the job-posting euphemisms.&lt;/p&gt;
&lt;h3 id=&quot;everything-except-bending-the-metal&quot;&gt;Everything Except Bending the Metal&lt;/h3&gt;
&lt;p&gt;If it isn't literally designing server hardware, it's probably yours. The realistic scope of the role covers the entire operating system lifecycle — installation, configuration, patching, the accounts and permissions layered on top of all of it — plus every service that depends on that OS staying upright: web servers, databases, file storage, whatever the business actually makes money from. Add the network layer on top: interfaces, firewalls, routing at the host level, VPNs. Add backups, and not just &quot;having&quot; them — configuring them correctly, testing restores on a schedule instead of assuming they'll work when it actually matters, and snapshotting filesystems and databases before you're the reason someone needs them. Add security on top of all of that: access rights, certificates, vulnerability monitoring, log auditing, and standing between the box and whatever's currently trying to get into it.&lt;/p&gt;
&lt;p&gt;That's not a job description. That's five job descriptions wearing a trench coat, and the fact that it's usually billed as one role is either a testament to how capable good sysadmins are or a quiet admission that most companies would rather understaff this than budget for it properly. I'll let you guess which one it usually is.&lt;/p&gt;
&lt;h3 id=&quot;prevention-beats-heroics-every-single-time&quot;&gt;Prevention Beats Heroics, Every Single Time&lt;/h3&gt;
&lt;p&gt;The single most underrated skill in this job isn't fixing things fast. It's noticing them slowly enough in advance that &quot;fixing&quot; was never required.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Monitor before it's an emergency.&lt;/strong&gt; Zabbix and Prometheus exist to tell you a disk is filling up while you still have options, not after the pager goes off and options have narrowed to &quot;explain this in the incident channel.&quot; Prometheus's pull-based, time-series model has become the default assumption for anything running in Kubernetes, while Zabbix still earns its keep in more traditional, heterogeneous fleets where you need broad out-of-the-box coverage without writing your own exporters for everything. Neither is objectively &quot;better&quot; — they're solving slightly different shapes of the same problem, and plenty of shops run both without embarrassment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Visualize, don't just collect.&lt;/strong&gt; Raw metrics are where good intentions go to die unread. Grafana turns a wall of numbers nobody checks into a dashboard people actually glance at on the way to their coffee, which is the entire difference between &quot;we had the data&quot; and &quot;we noticed the problem.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Automate the repetitive stuff before it automates a mistake into your fleet instead.&lt;/strong&gt; This is where the tooling landscape has genuinely shifted, and it's worth being current about it instead of reciting the same three names out of habit. Puppet and Chef both still exist — Chef now lives under Progress Software, Puppet under Perforce — and both remain entirely legitimate choices in shops that already standardized on their agent-based, declarative model years ago. But the gravitational centre of new automation work has moved decisively toward &lt;strong&gt;Ansible's&lt;/strong&gt; agentless, SSH-based approach, precisely because it doesn't require you to pre-install anything on a target before you can manage it. Centralizing configuration this way isn't about looking modern. It's about making sure the fifty servers behind you were actually configured the way you meant, instead of the way you remembered meaning it three deploys ago.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Document like someone else — possibly a more tired version of you — will have to clean up after this change at 2 a.m.&lt;/strong&gt; What changed, why, and what actually happened as a result. A wiki without an update discipline is just an elaborate way of lying to your future self, and version-controlling your configuration alongside that documentation is the difference between &quot;we can see exactly what changed&quot; and &quot;we're diffing memories.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Treat backups as a product, not a checkbox.&lt;/strong&gt; A backup job that's never been test-restored isn't a backup. It's a belief system, and belief systems have a terrible track record during actual outages.&lt;/p&gt;
&lt;h3 id=&quot;the-os-is-the-whole-foundation-so-know-what-you-re-standing-on&quot;&gt;The OS Is the Whole Foundation, So Know What You're Standing On&lt;/h3&gt;
&lt;p&gt;Everything above assumes a server operating system doing its actual job underneath: managing hardware resources, running services, handling networking, enforcing security boundaries and access separation. Get comfortable with the fact that which OS family you're standing on changes almost everything else about how you'll do this job.&lt;/p&gt;
&lt;h4 id=&quot;the-linux-families-and-what-actually-distinguishes-them&quot;&gt;The Linux Families, and What Actually Distinguishes Them&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Debian-based&lt;/strong&gt; — Debian itself, Ubuntu, Mint. The reputation for stability and thorough documentation is earned, not marketing. Ubuntu Server in particular remains the default on-ramp for small and mid-sized shops precisely because the learning curve is forgiving and the community answer to almost any problem already exists somewhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RHEL-based&lt;/strong&gt; — Red Hat Enterprise Linux, AlmaLinux, Rocky Linux, and CentOS, which deserves a specific note because its story isn't finished the way older tutorials imply. CentOS as the free, bit-for-bit downstream rebuild of RHEL is functionally gone — Red Hat shifted it to CentOS Stream, a rolling preview that sits &lt;em&gt;upstream&lt;/em&gt; of RHEL rather than mirroring it, and then tightened access to RHEL's own source in a way that made several downstream projects' lives noticeably harder. AlmaLinux and Rocky Linux exist specifically because that gap needed filling, and both have become the practical default wherever a shop needs RHEL-compatible stability without a RHEL support contract. This is still the corner of the Linux world enterprises reach for when &quot;boring and well-supported for a decade&quot; beats &quot;exciting.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Arch-based&lt;/strong&gt; — Arch Linux, Manjaro. The pitch here isn't convenience, it's comprehension: a rolling-release model and a famously minimal starting point that force you to understand what you're installing instead of inheriting someone else's defaults. Fewer production servers run it than you'd guess from how often it comes up in conversation, which tells you something honest about the gap between &quot;educational&quot; and &quot;operationally boring in the best way.&quot;&lt;/p&gt;
&lt;h4 id=&quot;unix-is-the-ancestor-not-a-competitor-wearing-a-costume&quot;&gt;Unix Is the Ancestor, Not a Competitor Wearing a Costume&lt;/h4&gt;
&lt;p&gt;FreeBSD and OpenBSD carry the Unix lineage forward as genuinely open, still-maintained systems — FreeBSD as a long-standing web-serving workhorse, OpenBSD as the option people reach for specifically when &quot;we obsess over security defaults&quot; needs to be true rather than aspirational, which is why it shows up disproportionately often inside routers and firewalls.&lt;/p&gt;
&lt;p&gt;The distinction that actually matters, though, isn't &quot;which is better.&quot; It's understanding that Linux isn't a Unix clone — it's an independent implementation built in the spirit of Unix's ideas and conventions: the filesystem model, process management, the permissions structure, the whole shape of working in a terminal. When someone says &quot;Linux and other Unix-like systems,&quot; that's not sloppy phrasing. That's the actual, accurate relationship.&lt;/p&gt;
&lt;h4 id=&quot;where-the-proprietary-options-still-fit&quot;&gt;Where the Proprietary Options Still Fit&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Windows Server&lt;/strong&gt; remains the correct call the moment Active Directory, Exchange, or deep integration with the Microsoft stack is the actual requirement rather than a default nobody questioned. &lt;strong&gt;macOS&lt;/strong&gt;, meanwhile, quietly stopped being a distinct server product entirely — Apple folded macOS Server's functionality back into the base OS years ago, and what's left shows up almost exclusively inside small, Apple-native shops rather than anything resembling general-purpose server infrastructure. It also remains the one entry on this list you can't actually buy on its own — it comes bundled with the hardware, whether you wanted the bundle or not.&lt;/p&gt;
&lt;h3 id=&quot;why-this-is-still-worth-learning-bluntly&quot;&gt;Why This Is Still Worth Learning, Bluntly&lt;/h3&gt;
&lt;p&gt;If you're deciding whether Linux administration deserves real time investment, the honest case doesn't need embellishment. Depending on which measurement you trust, Linux runs somewhere between roughly half and the overwhelming majority of the world's servers, and among sites where the underlying OS can actually be identified, the Unix-family total — Linux very much included — sits comfortably above nine in ten. Virtually every top-1-million web server and every single one of the world's fastest supercomputers has been running it for years now, and every major public cloud treats Linux as the load-bearing default rather than a supported alternative.&lt;/p&gt;
&lt;p&gt;Beyond the numbers, the actual reasons to learn it hold up on their own merits: it scales cleanly from a five-dollar VPS to an entire data centre without switching mental models, the multi-user permissions architecture was security-conscious from its foundations rather than bolted on afterward, and — the reason I'd actually put first — it's the rare piece of foundational infrastructure you're genuinely allowed to open up and understand completely. You can read the source, modify the kernel, rebuild a component that annoys you, and nobody's licensing department is going to stop you. That's not a minor philosophical footnote. It's the entire reason this operating system family ended up running the internet instead of just participating in it.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28624/what-a-linux-sysadmin-actually-does-all-day-spoiler-its-not-just-sudo-apt-update</guid>
<pubDate>Fri, 25 Sep 2026 15:27:27 +0000</pubDate>
</item>
<item>
<title>Why I prefer systemd timers over cronjobs</title>
<link>https://coderlegion.com/28485/why-i-prefer-systemd-timers-over-cronjobs</link>
<description>&lt;p&gt;Most users would schedule their tasks using a cronjob, and that's perfectly reasonable&lt;/p&gt;
&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;I will &lt;em&gt;assume&lt;/em&gt; you are running on a linux machine with systemd (which you should be if you're running on a server), other init systems such as runit or openRC are not taken into consideration&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;The content is mainly targeted for servers, however you can apply these concepts to your personal machines &lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;what-is-a-cronjob&quot;&gt;What is a cronjob&lt;/h4&gt;
&lt;p&gt;Cronjobs are essentially used for scheduling tasks at certain intervals or exact time(s)&lt;/p&gt;
&lt;p&gt;The task could be a shell script, a command, a call, anything you may need&lt;/p&gt;
&lt;h5 id=&quot;example-usage&quot;&gt;Example Usage&lt;/h5&gt;
&lt;p&gt;Creating a cronjob is very easy&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;crontab -e&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Paste this: &lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;*/1 * * * * echo &quot;$(date -Iminutes) Running&quot; &amp;gt;&amp;gt; /tmp/cronjob_running.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This would &lt;code&gt;echo&lt;/code&gt; the current datetime in ISO format to file &lt;code&gt;/tmp/cronjob_running.txt&lt;/code&gt; every minute&lt;/p&gt;
&lt;p&gt;Simple, easy and straightforward&lt;/p&gt;
&lt;h3 id=&quot;problem&quot;&gt;Problem&lt;/h3&gt;
&lt;p&gt;This works fine and well for personal, simple or non important tasks.&lt;/p&gt;
&lt;p&gt;But let's take this job:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;0 0 * * * /usr/local/sbin/postgresql_backup.sh &amp;gt;&amp;gt; /var/log/postgres/backups/backup.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This job would run the &lt;code&gt;postgresql_backup.sh&lt;/code&gt; script every day at &lt;code&gt;00:00&lt;/code&gt; and output the logs AND errors to &lt;code&gt;/var/log/postgres/backups/backup.log&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;What happens when the machine goes down at &lt;code&gt;00:00&lt;/code&gt;? or something even more probable, cron's systemd unit (like cronie) stops or does not start because someone forgot to (re)enable it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Answer: The job &lt;em&gt;never&lt;/em&gt; runs&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;systemd-timers&quot;&gt;Systemd timers&lt;/h3&gt;
&lt;h3 id=&quot;what-is-systemd&quot;&gt;What is systemd&lt;/h3&gt;
&lt;p&gt;It's the core init unit of your linux system, when your machine boots up, the kernel will load it with a &lt;code&gt;pid&lt;/code&gt; of 1&lt;/p&gt;
&lt;p&gt;A systemd timer is, as the name suggests, a timer that is managed by &lt;code&gt;systemd&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;So basically, the solution is already integrated into your Operating System, you're just not using it!&lt;/p&gt;
&lt;p&gt;What's more, they have more precision than cronjobs do and they support much finer scheduling granularity.&lt;/p&gt;
&lt;h3 id=&quot;how-to-create-these-timers&quot;&gt;How to create these timers&lt;/h3&gt;
&lt;p&gt;Now I'll be honest, creating them is a bit of a pain and not as quick as cronjobs, but we'll talk about the differences at the end!&lt;/p&gt;
&lt;p&gt;For the sake of simplicity I'll keep the example very straightforward, but note you can really get a bit more complex with these&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;First, you create a user systemd unit by running:&lt;/p&gt;
&lt;p&gt; &lt;code&gt;systemctl --user edit --force --full example-unit.service&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;We're creating a &lt;code&gt;user&lt;/code&gt; scoped timer for this example, but for more serious work like a database backup, use sudo and omit user, like this: &lt;code&gt;sudo systemctl edit --force --full example-unit.service&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;The &lt;code&gt;--force --full&lt;/code&gt; flags are used to create the unit if it doesn't exist, I use the &lt;code&gt;systemctl edit&lt;/code&gt; command so that if we exit with an empty file, we won't have issues, plus it does create a &lt;em&gt;backup&lt;/em&gt; for us which is handy&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Then put this into the content&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-systemd&quot;&gt;[Unit]
Description=Our Example systemd Unit

[Service]
ExecStart=/usr/bin/echo &quot;Hello There!&quot;

[Install]
WantedBy=default.target
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Save and exit&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reload your systemd daemon:&lt;/p&gt;
&lt;p&gt; &lt;code&gt;systemctl --user daemon-reload&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run the unit&lt;/p&gt;
&lt;p&gt; &lt;code&gt;systemctl --user start example-unit.service&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Nothing happens, why? where's my output?&lt;/p&gt;
&lt;p&gt;You inspect your service like any other systemd unit.. with &lt;code&gt;status&lt;/code&gt; subcommand&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Run this&lt;/p&gt;
&lt;p&gt; &lt;code&gt;systemctl --user status example-unit.service&lt;/code&gt;&lt;/p&gt;
&lt;p&gt; You get something similar to this:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://media.blog.nazih.top/systemd-timers/systemd-service-run.png&quot; alt=&quot;Result of our Example Unit output&quot;&gt;&lt;/p&gt;
&lt;p&gt;But where's the schedule here?&lt;br&gt;
Well, this is why I said&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;creating them is a bit of a pain and not as quick as cronjobs&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;the-timer&quot;&gt;The Timer&lt;/h3&gt;
&lt;p&gt;Finally, it's time we create the actual timer unit&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Run this command:&lt;/p&gt;
&lt;p&gt; &lt;code&gt;systemctl --user edit --force --full example-unit.timer&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;The same goes here, omit the &lt;code&gt;--user&lt;/code&gt; flag and add &lt;code&gt;sudo&lt;/code&gt; if doing a system wide timer!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Add this&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-systemd&quot;&gt;[Unit]
Description=Running Our Example Unit regularly

[Timer]
OnCalendar=*:0/1
# This here is our magic flag
Persistent=true
Unit=example-unit.service

[Install]
WantedBy=timers.target
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Save and exit&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run this command&lt;/p&gt;
&lt;p&gt; &lt;code&gt;systemctl --user start example-unit.timer&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;NOTE: We're only starting the timer here, if we reboot the machine the timer will not persist&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;For the timer to remain enabled, you do &lt;code&gt;systemctl --user enable --now example-unit.timer&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Here's what you get:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://media.blog.nazih.top/systemd-timers/systemd-timer-run.png&quot; alt=&quot;timer-run&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://media.blog.nazih.top/systemd-timers/systemd-timer-unit.png&quot; alt=&quot;timer-unit&quot;&gt;&lt;/p&gt;
&lt;p&gt;This will &lt;code&gt;run&lt;/code&gt; our unit every minute, and with our magic &lt;code&gt;Persistent=true&lt;/code&gt; even if your machine goes down, or for whatever reason it gets missed due to some random bit flipping, your timer WILL run when it gets the chance&lt;/p&gt;
&lt;p&gt;It stores the last execution timestamp on disk. When the timer becomes active, systemd checks this file, and if the target trigger time passed while the system was powered off it immediately triggers the service.&lt;/p&gt;
&lt;h3 id=&quot;comparing-systemd-timers-to-other-solutions&quot;&gt;Comparing systemd timers to other solutions&lt;/h3&gt;
&lt;p&gt;Now for more informed readers, you may know of &lt;code&gt;anacron&lt;/code&gt; which is a perfectly reasonable solution however there are some differences you need to be aware of.&lt;/p&gt;
&lt;p&gt;Here's a comparison to systemd timers, cronjobs and anacron&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Solution &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Simple to create &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Flexible Scheduling &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Runs on missed tasks&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Systemd  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;❌               &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✅                  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✅&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Anacron  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✅               &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;❌                  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✅&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Cronjob  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✅               &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✅                  &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;❌&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;anacron doesn't have that much flexibility, they have daily basis checkup, which you may or may not want, so beware of that&lt;/p&gt;
&lt;h3 id=&quot;systemd-timers-scheduling&quot;&gt;Systemd timers Scheduling&lt;/h3&gt;
&lt;p&gt;You may have noticed that we used the &lt;code&gt;OnCalendar&lt;/code&gt; keyword in our timer, it's not the only one! there are more things you can configure&lt;/p&gt;
&lt;h4 id=&quot;monotonic-timers&quot;&gt;Monotonic Timers&lt;/h4&gt;
&lt;p&gt;These basically run relative to &lt;em&gt;something&lt;/em&gt;, it can be relative to system boot, it can be time since the unit was activated!&lt;/p&gt;
&lt;p&gt;Here's a small table of keywords and schedules you can have that I &lt;em&gt;borrowed&lt;/em&gt; from this great &lt;a rel=&quot;nofollow&quot; href=&quot;https://linuxconfig.org/how-to-schedule-tasks-with-systemd-timers-in-linux&quot;&gt;Article&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Keyword           &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Meaning&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;OnActiveSec       &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Schedule the task relative to the time when the timer unit itself is activated&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;OnBootSec         &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Schedule task relative to the system boot time&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;OnStartupSec      &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Schedule the task relative to the time when systemd started&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;OnUnitActiveSec   &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Schedule the task relative to the last time the service unit was active&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;OnUnitInactiveSec &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Schedule the task relative to the last time the service unit was inactive&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Seconds are used as the default unit of time. We can specify a different unit after the value (e.g. 15m for fifteen minutes)&lt;/p&gt;
&lt;h4 id=&quot;realtime-timers&quot;&gt;Realtime Timers&lt;/h4&gt;
&lt;p&gt;These are the ones that use the &lt;code&gt;OnCalendar&lt;/code&gt; keyword&lt;/p&gt;
&lt;p&gt;The basic syntax is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;OnCalendar=Mon *-*-* 12:30:00
           │   │ │ │ │  │  │
           │   │ │ │ │  │  └── SECOND
           │   │ │ │ │  └───── MINUTE
           │   │ │ │ └──────── HOUR
           │   │ │ └────────── DAY OF MONTH
           │   │ └──────────── MONTH
           │   └────────────── YEAR
           └────────────────── WEEKDAY
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Systemd Timers are great for scheduling important tasks or interacting with a daemon that you already installed.&lt;br&gt;
So are they the solution for everything? No, they're cumbersome to create and honestly I don't like the way it's handled.&lt;/p&gt;
&lt;p&gt;Use the right tool for the job, if it's a business critical backup that runs twice a day? a very important data collection task? Use systemd timers, if it's simply another task that would not harm if it was missed once, use cronjobs, they're much easier to create &lt;/p&gt;
&lt;p&gt;There are many things that I have not mentioned in this article, and things I didn't go into depth into but I hope this article helps future engineers with their journey in automating and scheduling&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28485/why-i-prefer-systemd-timers-over-cronjobs</guid>
<pubDate>Thu, 24 Sep 2026 13:39:29 +0000</pubDate>
</item>
<item>
<title>I sent my report script SIGTERM mid-write. Yesterday's output survived intact.</title>
<link>https://coderlegion.com/28018/i-sent-my-report-script-sigterm-mid-write-yesterdays-output-survived-intact</link>
<description>&lt;p&gt;A script that writes straight to the path a consumer reads can die halfway and leave a torn file that everything downstream loads as complete. No error appears anywhere, because the only process that failed is gone. I tested the fix the unfriendly way on bash 5.3.9: a CSV generator with a &lt;code&gt;sleep 30&lt;/code&gt; planted between rows, killed with SIGTERM one second in.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;TMP_FILE=$(mktemp)
cleanup() { local code=$?; rm -f &quot;$TMP_FILE&quot;; exit &quot;$code&quot;; }
trap cleanup EXIT
generate_rows &amp;gt; &quot;$TMP_FILE&quot;
mv &quot;$TMP_FILE&quot; &quot;$FINAL_PATH&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Exit 143, which is 128 plus signal 15. The temp file was gone, and &lt;code&gt;out.csv&lt;/code&gt; still held the previous run's three complete lines. Stale is something monitoring can see. Torn is not.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;EXIT&lt;/code&gt; fires on normal completion, an explicit &lt;code&gt;exit 1&lt;/code&gt;, a &lt;code&gt;set -e&lt;/code&gt; abort and after a handled signal, which is why this is five lines instead of twenty. And because &lt;code&gt;mv&lt;/code&gt; on one filesystem is atomic, being half-written and being at the published path are never true at once. Even &lt;code&gt;kill -9&lt;/code&gt;, which no trap catches, cannot publish a torn file.&lt;/p&gt;
&lt;p&gt;The ordering is the part people move. I ran four handler variants against a script ending in &lt;code&gt;exit 3&lt;/code&gt;, and only one exited 0: &lt;code&gt;rm -f&lt;/code&gt; first, then &lt;code&gt;local code=$?&lt;/code&gt;. It captured rm's success and laundered the failure. Capture first, clean second, exit last.&lt;/p&gt;
&lt;p&gt;The complete script, subshell scoping rules and a line-by-line breakdown are on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/snippets/bash-trap-cleanup&quot;&gt;trap cleanup page for temp files and atomic writes&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28018/i-sent-my-report-script-sigterm-mid-write-yesterdays-output-survived-intact</guid>
<pubDate>Thu, 24 Sep 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>Who owns that port? One ss flag answers it without sudo</title>
<link>https://coderlegion.com/27426/who-owns-that-port-one-ss-flag-answers-it-without-sudo</link>
<description>&lt;h2 id=&quot;who-owns-that-port-one-ss-flag-answers-it-without-sudo&quot;&gt;Who owns that port? One ss flag answers it without sudo&lt;/h2&gt;
&lt;p&gt;For months my dev server started on 3001 because something already held 3000, and I kept stepping around it. When I finally asked, &lt;code&gt;ss -ltn 'sport = :3000'&lt;/code&gt; showed the port listening on every interface — with an empty Process column, because &lt;code&gt;ss -p&lt;/code&gt; only names sockets owned by your own user. Without root, it looked like nobody owned it.&lt;/p&gt;
&lt;p&gt;One flag closes most of that gap:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ss -ltnpe 'sport = :3000'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-e&lt;/code&gt; prints the socket's cgroup, and on a systemd machine the end of that path is the unit that opened it. Mine ended in &lt;code&gt;docker.service&lt;/code&gt;. &lt;code&gt;docker ps&lt;/code&gt; then named it: a &lt;code&gt;docker-proxy&lt;/code&gt; publishing a container I had started in the spring and forgotten, reachable from the whole network.&lt;/p&gt;
&lt;p&gt;Two things worth carrying away. The host only ever sees Docker's proxy, never the application behind it, so a host-side port list tells you what is exposed, not what is running. And &quot;list open ports&quot; is really three questions: what is listening, who owns it, and whether it is reachable. They need three different commands.&lt;/p&gt;
&lt;p&gt;The full write-up covers all three — with and without root, without netstat, inside containers, and an audit script that diffs the listener list between runs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/guides/open-ports-linux&quot;&gt;how to list open ports on Linux with the owning process&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27426/who-owns-that-port-one-ss-flag-answers-it-without-sudo</guid>
<pubDate>Wed, 16 Sep 2026 17:29:08 +0000</pubDate>
</item>
<item>
<title>Why I run my home lab like enterprise infrastructure</title>
<link>https://coderlegion.com/25155/why-i-run-my-home-lab-like-enterprise-infrastructure</link>
<description>&lt;p&gt;&lt;em&gt;Originally published on DEV Community in July 2026. This version is updated with the current state of the lab and a new section on what happened when I finally tested my backups.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I'm an IT Systems Administrator / Service Desk Team Lead L2 in Torino, Italy. During the day I manage endpoints and infrastructure for a large public-sector IT provider. At night I run &lt;strong&gt;PolarisCore&lt;/strong&gt;, a home lab built with the same standards I use at work: VLAN segmentation, a real reverse proxy, internal DNS, ZFS storage with redundancy, and monitoring. Not a pile of Docker containers on a single box.&lt;/p&gt;
&lt;p&gt;Here's the current state, pulled live via SSH an hour before publishing.&lt;/p&gt;
&lt;h3 id=&quot;the-compute-layer-proxmox&quot;&gt;The compute layer — Proxmox&lt;/h3&gt;
&lt;p&gt;When I wrote the original version of this article, the lab ran on a single Proxmox node with 7.5 GiB of RAM. I had assigned about 10 GiB across the containers. Linux lets you over-commit memory, so it worked until it didn't, and the OOM killer took a container roughly once a month.&lt;/p&gt;
&lt;p&gt;A mainboard swap fixed that this week:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;pve-manager/9.2.11 (kernel 7.0.14-12-pve)
Host: HP EliteDesk 800 G5, i7-8700T (12 threads)
Uptime: 4 hours (fresh install)
RAM: 12 / 31 GiB used
ZFS rpool (NVMe mirror): 74.3 GB / 236 GB (31%)
Guests: 14 LXC running / 17 total, plus 1 VM
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Those containers handle a Caddy reverse proxy, internal DNS (PowerDNS + Pi-hole), a self-built deployment tool, a Next.js search frontend, a job scheduler, an asset inventory, and a handful of smaller services. VLANs separate management, services, IoT, gaming, guest and VPN traffic, with the services VLAN carrying most of what faces the internet.&lt;/p&gt;
&lt;p&gt;One detail worth stealing: the node has a single physical NIC. Every VLAN reaches it through one VLAN-aware Linux bridge with 802.1Q tagging, and the switch port is a trunk.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;iface nic0 inet manual

auto vmbr0
    bridge-ports nic0
    bridge-vlan-aware yes
    bridge-vids 2-4094
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I spent a day convinced I needed a second network card before I checked how the old node was actually wired and found it had been doing exactly this all along. Read your own configs before you buy hardware.&lt;/p&gt;
&lt;h3 id=&quot;the-storage-layer-truenas-scale&quot;&gt;The storage layer — TrueNAS SCALE&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;TrueNAS 25.10.6
Uptime: 9 hours
Pool DATA (RAIDZ1, 4x8TB): 14.1 TB / 21.8 TB used (64%)
Pool APPS (mirror): 93.8 GB / 460 GB used (20%)
Pool boot: 10.2 GB / 236 GB used (4%)
All pools: ONLINE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;22 TB isn't there for the sake of it. It holds a media library (Jellyfin and the *arr stack, running as native TrueNAS apps rather than VMs), project data, and a dedicated encrypted share. RAIDZ1 across four drives gives me a single-drive failure buffer, which for a setup holding real data matters more to me than raw speed.&lt;/p&gt;
&lt;h3 id=&quot;the-part-i-got-wrong-backups&quot;&gt;The part I got wrong: backups&lt;/h3&gt;
&lt;p&gt;This is the section that wasn't in the original article, and it's the one worth reading.&lt;/p&gt;
&lt;p&gt;While preparing the container migration to the new node, I opened the backup storage to confirm I had a rollback path. The most recent archive was dated 19 April. It covered 4 containers out of 14.&lt;/p&gt;
&lt;p&gt;For four months I believed I had a safety net. It covered less than a third of the machines and was four months stale. Nothing had alerted me. No email, no red icon in the interface, no failing job. The backup job was configured against an explicit list of container IDs, that list had been written once, and the lab had grown past it. A job that backs up four containers successfully reports success.&lt;/p&gt;
&lt;p&gt;So I ran a full backup of everything. It failed on all seventeen guests with the same error:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;tar: failed to return to initial working directory: Permission denied
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The cause is a two-layer UID translation. Unprivileged LXC containers map their internal users to a host range starting at 100000. The NFS server the backups were written to applies its own remapping on top. The result is a &lt;code&gt;tar&lt;/code&gt; process whose working directory becomes unreachable to it partway through, so it can't &lt;code&gt;chdir&lt;/code&gt; back when it finishes.&lt;/p&gt;
&lt;p&gt;The fix is one flag:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;vzdump --all 1 --storage &amp;lt;target&amp;gt; --mode snapshot --tmpdir /var/tmp/vzdump
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--tmpdir&lt;/code&gt; keeps the working directory on local disk instead of the network share. The archive still lands on the share; only the scratch path moves.&lt;/p&gt;
&lt;p&gt;Seventeen archives, then seventeen restores onto the new node. Sixteen of sixteen running containers came back on the first attempt, and the seventeenth was already powered off by design. That was the first time those archives had ever been read by anyone.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The lesson isn't &quot;make backups.&quot; Everyone makes backups.&lt;/strong&gt; The lesson is that a backup job reports on what you told it to do, not on what you needed it to do, and the only way to learn the difference is to restore.&lt;/p&gt;
&lt;h3 id=&quot;the-backup-layer-proxmox-backup-server&quot;&gt;The backup layer — Proxmox Backup Server&lt;/h3&gt;
&lt;p&gt;The old node didn't get retired. It became the backup target:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Proxmox Backup Server 4.2.5
Host: HP EliteDesk 800 G4, i5-8500T (6 cores), 7.53 GiB RAM
Datastore backup-ct: 40.20 GB / 245.55 GB used (16.37%)
Backup count: 17 CT groups, 1 VM group — 18 snapshots
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first full run finished in ten minutes for eighteen guests. It now runs nightly at 02:30 against every guest, with deduplication, a weekly integrity verification job, and a retention policy that keeps 7 daily, 4 weekly and 6 monthly snapshots.&lt;/p&gt;
&lt;p&gt;Three properties make this better than what I had before:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;It backs up &lt;code&gt;--all&lt;/code&gt;, not a list.&lt;/strong&gt; A new container is protected the night it's created, with no config change. That single flag is the direct fix for the failure above.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deduplication is content-addressed at the chunk level.&lt;/strong&gt; Seventeen Debian containers share most of their filesystem, so they cost far less than seventeen times one container.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verification is a separate scheduled job.&lt;/strong&gt; Restoring is still the only real test, but a weekly checksum pass catches silent corruption without me remembering to look.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;One thing I have not solved: the backup server sits in the same cabinet as the machine it backs up. That protects me against disk failure and my own mistakes. It does nothing against fire or theft. An off-site copy of the critical subset is the next job on the list, and I'd rather say that out loud than let the diagram imply I'm covered.&lt;/p&gt;
&lt;h4 id=&quot;the-alerting-trap-again&quot;&gt;The alerting trap, again&lt;/h4&gt;
&lt;p&gt;I configured email notifications and sent a test. It bounced:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;550-5.7.1 Messages missing a valid Message-ID header are not accepted
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Gmail doesn't accept mail sent directly from a residential IP address. The fix was routing through an authenticated SMTP relay on port 587 with an app password instead of delivering directly.&lt;/p&gt;
&lt;p&gt;Notice the shape of this. If I hadn't sent that test, the only channel that tells me a backup failed would have been silently discarding every message. I would have rebuilt exactly the situation I had just spent a day fixing: a system that looks healthy because nothing is reporting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test the alerting path, not only the thing being alerted on.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-network-layer&quot;&gt;The network layer&lt;/h3&gt;
&lt;p&gt;FTTH 10 Gbps from TIM into a UniFi Cloud Gateway Max, distributed over a 2.5 GbE backbone to switches and clients. The 10 Gbps WAN isn't about single-stream throughput to the internet. It's headroom, so the internal backbone never bottlenecks against the uplink when backups, streaming and syncs run in parallel. TLS is a wildcard certificate issued via DNS-01, auto-renewing, with zero inbound ports opened for issuance.&lt;/p&gt;
&lt;p&gt;A debugging note from this week that cost me an hour. The new node negotiated its link at 100 Mb/s while everything else ran at 1000 or 2500. My first theory was a bad cable, and I was wrong: the cable was a perfectly good Cat 7. &lt;code&gt;ethtool&lt;/code&gt; showed the real answer, that the machine advertised 1000baseT/Full while the link partner advertised only up to 100baseT/Full. Autonegotiation had settled on 100 during one of the many power cycles that day and never retried, because it doesn't. Forcing the speed once and then re-enabling autoneg fixed it, and a small boot service now re-negotiates if it ever finds itself at 100 again.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ethtool &amp;lt;iface&amp;gt; | grep -A2 &quot;Link partner advertised link modes&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That one command separates &quot;the cable is broken&quot; from &quot;the link is stuck&quot;, and they have completely different fixes.&lt;/p&gt;
&lt;h3 id=&quot;the-lightweight-cloud-piece-oracle-always-free&quot;&gt;The lightweight cloud piece — Oracle Always Free&lt;/h3&gt;
&lt;p&gt;Not everything lives on-prem. Site analytics run on Umami, self-hosted, replacing GA4 across five sites, on an Oracle Cloud Always Free VM:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Ubuntu 20.04.6 LTS
VM.Standard.A1.Flex — 1 OCPU / 6 GB RAM
RAM used: 694 MiB (11%)
Uptime: 6 weeks
Stack: Umami + Postgres 15 + Caddy, Docker Compose
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Six weeks of uptime and it barely registers on CPU. For a low-traffic analytics backend the free tier is more than enough, and there's no reason to pay for a VPS to do this.&lt;/p&gt;
&lt;h3 id=&quot;a-side-project-that-came-out-of-daily-frustration&quot;&gt;A side project that came out of daily frustration&lt;/h3&gt;
&lt;p&gt;The one piece of software here that isn't infrastructure glue is &lt;strong&gt;NovaSCM&lt;/strong&gt;, an open-source self-hosted alternative to Microsoft SCCM for zero-touch Windows deployment: PXE boot, autounattend generation, network scanning, WiFi 802.1X certificate management, and workflow-based software deployment. It's MIT-licensed, lives on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ClaudioBecchis/NovaSCM&quot;&gt;GitHub&lt;/a&gt;, and runs as one of the containers above.&lt;/p&gt;
&lt;h3 id=&quot;why-bother-with-all-this-at-home&quot;&gt;Why bother with all this at home&lt;/h3&gt;
&lt;p&gt;Building it the enterprise way, with segmentation, real certificate automation, redundant storage and a reverse proxy instead of exposed ports, is the best way I know to understand those patterns rather than read about them.&lt;/p&gt;
&lt;p&gt;The backup story above is the argument in miniature. I have written backup procedures at work. I have reviewed them. I still had a four-month gap in my own lab, because reviewing a procedure and restoring from it are different activities, and only one of them tells you the truth. When something breaks at 11 pm on my own network, nobody is going to open a ticket about it, and there's no second line to escalate to. That's a different kind of pressure, and it teaches differently.&lt;/p&gt;
&lt;p&gt;More on the project and the services I haven't written up yet at &lt;a rel=&quot;nofollow&quot; href=&quot;https://polariscore.it&quot;&gt;polariscore.it&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25155/why-i-run-my-home-lab-like-enterprise-infrastructure</guid>
<pubDate>Sat, 22 Aug 2026 20:19:51 +0000</pubDate>
</item>
<item>
<title>The 4 things every cronjob needs that most tutorials skip</title>
<link>https://coderlegion.com/16899/the-4-things-every-cronjob-needs-that-most-tutorials-skip</link>
<description>&lt;h3 id=&quot;cron-job-tutorials-have-a-problem-they-teach-you-the-five-field-syntax-show-you-a-couple-of-examples-like-0-2-and-send-you-on-your-way-what-they-don-t-cover-is-everything-that-needs-to-go-around-the-expression-for-the-job-to-actually-work-reliably-in-production-&quot;&gt;Cron job tutorials have a problem. They teach you the five-field syntax, show you a couple of examples like &lt;code&gt;0 2 * * *&lt;/code&gt;, and send you on your way. What they don't cover is everything that needs to go &lt;em&gt;around&lt;/em&gt; the expression for the job to actually work reliably in production.&lt;/h3&gt;
&lt;h4 id=&quot;here-s-what-i-mean-&quot;&gt;Here's what I mean.&lt;/h4&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Problem 1: Cron's PATH is not your PATH&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;when-you-run-a-command-in-your-terminal-your-shell-has-a-full-path-all-the-binary-directories-your-distro-configured-plus-whatever-you-ve-added-when-cron-runs-it-uses-a-stripped-down-default-path-binaries-that-are-available-to-you-interactively-may-not-be-visible-to-cron-&quot;&gt;When you run a command in your terminal, your shell has a full PATH — all the binary directories your distro configured, plus whatever you've added. When cron runs, it uses a stripped-down default PATH. Binaries that are available to you interactively may not be visible to cron.&lt;/h4&gt;
&lt;h4 id=&quot;the-fix-is-to-set-path-explicitly-in-your-crontab-or-at-the-top-of-your-wrapper-script-&quot;&gt;The fix is to set PATH explicitly in your crontab or at the top of your wrapper script:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;PATH=/usr/local/bin:/usr/bin:/bin
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;this-single-line-prevents-the-most-common-class-of-cron-job-runs-but-does-nothing-failures-&quot;&gt;This single line prevents the most common class of &quot;cron job runs but does nothing&quot; failures.&lt;/h4&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Problem 2: Cron's default shell is sh, not bash&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;if-your-script-uses-any-bash-specific-syntax-arrays-conditionals-substitution-styles-and-cron-is-running-it-with-sh-you-ll-get-silent-failures-or-unexpected-behavior-&quot;&gt;If your script uses any bash-specific syntax — arrays, &lt;code&gt;[[ ]]&lt;/code&gt; conditionals, &lt;code&gt;$()&lt;/code&gt; substitution styles — and cron is running it with sh, you'll get silent failures or unexpected behavior.&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;SHELL=/bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;add-this-to-the-top-of-your-crontab-one-line-&quot;&gt;Add this to the top of your crontab. One line.&lt;/h4&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Problem 3: No logging means no debugging&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;if-a-cron-job-fails-where-does-the-error-go-by-default-nowhere-it-vanishes-adding-log-redirection-to-every-crontab-entry-is-the-fix-&quot;&gt;If a cron job fails, where does the error go? By default: nowhere. It vanishes. Adding log redirection to every crontab entry is the fix:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;0 2 * * * /path/to/script.sh &amp;gt;&amp;gt; /var/log/mybackup.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-2-gt-amp-1-captures-stderr-alongside-stdout-so-you-see-command-errors-not-just-your-script-s-output-&quot;&gt;The &lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt; captures stderr alongside stdout so you see command errors, not just your script's output.&lt;/h4&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Problem 4: Long-running jobs can stack on top of themselves&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;if-you-have-a-job-that-runs-every-5-minutes-but-sometimes-takes-8-minutes-a-second-instance-starts-before-the-first-one-finishes-depending-on-what-the-script-is-doing-writing-to-a-file-syncing-a-directory-running-a-database-query-this-can-corrupt-output-&quot;&gt;If you have a job that runs every 5 minutes but sometimes takes 8 minutes, a second instance starts before the first one finishes. Depending on what the script is doing (writing to a file, syncing a directory, running a database query), this can corrupt output.&lt;/h4&gt;
&lt;h4 id=&quot;lock-files-via-flock-prevent-this-&quot;&gt;Lock files via &lt;code&gt;flock&lt;/code&gt; prevent this:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/usr/bin/flock -n /tmp/myjob.lock /path/to/script.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-second-instance-sees-the-lock-is-held-and-exits-immediately-&quot;&gt;The second instance sees the lock is held and exits immediately.&lt;/h4&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;All of this is in a free tool&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;i-built-cron-job-builder-to-handle-all-four-of-these-automatically-you-pick-your-schedule-from-dropdowns-check-the-options-you-want-logging-path-shell-mailto-lock-file-and-it-generates-the-complete-crontab-entry-expression-wrappers-bash-header-ready-to-paste-into-crontab--e-&quot;&gt;I built &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/tools/cron-job-builder.html&quot;&gt;Cron Job Builder&lt;/a&gt; to handle all four of these automatically. You pick your schedule from dropdowns, check the options you want (logging, PATH, SHELL, MAILTO, lock file), and it generates the complete crontab entry — expression + wrappers + bash header — ready to paste into &lt;code&gt;crontab -e&lt;/code&gt;.&lt;/h4&gt;
&lt;h4 id=&quot;it-also-shows-you-the-next-10-scheduled-run-times-so-you-can-verify-your-expression-is-actually-doing-what-you-intend-before-you-deploy-it-&quot;&gt;It also shows you the next 10 scheduled run times so you can verify your expression is actually doing what you intend before you deploy it.&lt;/h4&gt;
&lt;h4 id=&quot;there-are-quick-presets-for-the-schedules-you-use-constantly-daily-2am-every-6-hours-weekdays-9am-monthly-on-the-1st-every-15-minutes-&quot;&gt;There are quick presets for the schedules you use constantly: daily 2am, every 6 hours, weekdays 9am, monthly on the 1st, every 15 minutes.&lt;/h4&gt;
&lt;h4 id=&quot;no-login-no-account-runs-entirely-in-the-browser-free-&quot;&gt;No login, no account, runs entirely in the browser. Free.&lt;/h4&gt;
&lt;p&gt;→ &lt;a rel=&quot;nofollow&quot; href=&quot;https://bashsnippets.xyz/tools/cron-job-builder.html&quot;&gt;https://bashsnippets.xyz/tools/cron-job-builder.html&lt;/a&gt;&lt;/p&gt;
&lt;h5 id=&quot;the-rest-of-the-library-free-bash-scripts-for-backup-log-cleanup-disk-monitoring-uptime-checking-lives-at-https-bashsnippets-xyz-&quot;&gt;The rest of the library — free bash scripts for backup, log cleanup, disk monitoring, uptime checking — lives at &lt;a rel=&quot;nofollow&quot; href=&quot;https://https://bashsnippets.xyz&quot;&gt;https://bashsnippets.xyz&lt;/a&gt;.&lt;/h5&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/16899/the-4-things-every-cronjob-needs-that-most-tutorials-skip</guid>
<pubDate>Wed, 06 May 2026 21:23:10 +0000</pubDate>
</item>
<item>
<title>Linux Fundamentals For DevOps: Linux core mechanism</title>
<link>https://coderlegion.com/16002/linux-fundamentals-for-devops-linux-core-mechanism</link>
<description>&lt;p&gt;Knowing where files live is only half the battle. To be an effective DevOps engineer, you need to know how Linux actually executes programs, manages their lifecycles, and secures them.&lt;br&gt;
The crucial concepts you need to troubleshoot effectively.&lt;/p&gt;
&lt;h3 id=&quot;the-path-variable&quot;&gt;The &lt;code&gt;$PATH&lt;/code&gt; Variable&lt;/h3&gt;
&lt;p&gt;Linux does not search your entire hard drive when you type a command. It only checks a specific, prioritized list of folders. This list is saved in an environment variable called &lt;code&gt;$PATH&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You download a new binary tool (like &lt;code&gt;kubectl&lt;/code&gt; or &lt;code&gt;terraform&lt;/code&gt;). You type its name in the terminal, but Linux replies with the dreaded &lt;code&gt;command not found&lt;/code&gt;. To fix this, you must either move the binary into a folder that is already in your &lt;code&gt;$PATH&lt;/code&gt;, or add your custom folder to the &lt;code&gt;$PATH&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# Command: View the list of directories Linux currently checks for commands

echo $PATH

# Output Proof (Directories are separated by colons):
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# Command: Add your custom scripts folder to the PATH for the current session

export PATH=&quot;/home/saeed/custom-scripts:$PATH&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;how-binaries-work-shared-libraries-&quot;&gt;How Binaries Work (Shared Libraries)&lt;/h3&gt;
&lt;p&gt;When you run a command like &lt;code&gt;nginx&lt;/code&gt; or &lt;code&gt;python&lt;/code&gt;, you are executing a binary file. In Linux, executables are typically formatted as ELF (Executable and Linkable Format) files. However, most binaries are not standalone; they rely on external pieces of code called shared libraries (&lt;code&gt;.so&lt;/code&gt; files, similar to &lt;code&gt;.dll&lt;/code&gt; files in Windows).&lt;/p&gt;
&lt;p&gt;You compile a Go application or migrate a tool into a minimal Docker container (like Alpine Linux). When you run it, it crashes silently or throws a confusing &lt;code&gt;no such file or directory&lt;/code&gt; error—even though you can see the binary sitting right there. Usually, the binary isn't missing; &lt;strong&gt;its required &lt;em&gt;libraries&lt;/em&gt; are missing.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can use the &lt;code&gt;ldd&lt;/code&gt; command to print the shared libraries required by any program.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# Command: Verify exactly which shared libraries Nginx needs to run
ldd /usr/sbin/nginx

# Output Proof:
        linux-vdso.so.1 (0x00007ffe3b1f4000)
        libcrypt.so.1 =&amp;gt; /lib/x86_64-linux-gnu/libcrypt.so.1 (0x00007f3a8b411000)
        libpcre.so.3 =&amp;gt; /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007f3a8b39c000)
        libssl.so.3 =&amp;gt; /lib/x86_64-linux-gnu/libssl.so.3 (0x00007f3a8b2f6000)
        libc.so.6 =&amp;gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3a8b0cd000)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;file-permission&quot;&gt;File Permission&lt;/h3&gt;
&lt;p&gt;Linux strict permissions dictate exactly who can &lt;strong&gt;R&lt;/strong&gt;ead, &lt;strong&gt;W&lt;/strong&gt;rite, or e&lt;strong&gt;X&lt;/strong&gt;ecute a file. This is often represented by numbers (like 777 or 644).&lt;/p&gt;
&lt;p&gt;You are trying to connect to a cloud server via SSH using a private key (&lt;code&gt;.pem&lt;/code&gt; file), but the connection is instantly rejected. If you set a &lt;code&gt;.pem&lt;/code&gt; file to &lt;code&gt;777&lt;/code&gt; (meaning everyone on the system can &lt;strong&gt;read&lt;/strong&gt;, &lt;strong&gt;write&lt;/strong&gt;, and &lt;strong&gt;execute&lt;/strong&gt; it), SSH will intentionally reject it for being too exposed. Private keys must be set to &lt;code&gt;400&lt;/code&gt;—meaning absolutely no one except the owner is allowed to read it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# Command: Attempting to SSH with a bad permission key
ssh -i my-key.pem ubuntu@10.0.0.5

# Output Proof:
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@         WARNING: UNPROTECTED PRIVATE KEY FILE!          @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0777 for 'my-key.pem' are too open.

# Command: Secure the key so only the owner can read it (4), and no one can write (0) or execute (0)

chmod 400 my-key.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;processes-and-signals&quot;&gt;Processes and Signals&lt;/h3&gt;
&lt;p&gt;Every running program is a process assigned a unique Process ID (PID). The operating system communicates with and controls these processes using &quot;Signals.&quot;&lt;/p&gt;
&lt;p&gt;An application is hogging 100% of the CPU and needs to be stopped, or Kubernetes is trying to scale down a pod without dropping active user connections. You can send a &lt;code&gt;SIGTERM&lt;/code&gt; (Signal 15), which politely asks the program to save its data and exit cleanly. Alternatively, a &lt;code&gt;SIGKILL&lt;/code&gt; (Signal 9) forcefully terminates it at the hardware level, bypassing cleanup and potentially causing data corruption.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# Command: Find the Process ID of an application
pgrep -la nginx

# Output Proof:
1234 nginx: master process
1235 nginx: worker process

# Command: Politely ask the master process to shut down and clean up
kill -15 1234
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;file-descriptors&quot;&gt;File Descriptors&lt;/h3&gt;
&lt;p&gt;Remember concept , &lt;strong&gt;&quot;Everything is a File&quot;&lt;/strong&gt;? That rule has a dark side.&lt;/p&gt;
&lt;p&gt;Because Linux treats &lt;strong&gt;&lt;em&gt;network connections&lt;/em&gt;&lt;/strong&gt; as files, every time a user connects to your web server, the operating system opens a &quot;File Descriptor&quot; to track that connection. To protect itself from crashing, the Linux kernel sets strict limits on how many file descriptors a single process can open at once.&lt;/p&gt;
&lt;p&gt;Your Nginx web server or Node.js application is suddenly dropping user connections during a high-traffic spike. You check the CPU and RAM, and both are perfectly fine. You check the Nginx error logs and see the dreaded &lt;code&gt;worker_connections are not enough&lt;/code&gt; or &lt;code&gt;Too many open files&lt;/code&gt; error. The operating system has forcibly stopped your app from accepting new traffic.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# Command: Show the maximum number of open file descriptors allowed for this user
ulimit -n

# Output Proof:
1024

# Command: Check how many file descriptors the entire system is currently using
cat /proc/sys/fs/file-nr

# Output Proof:
1205   0   9223372036854775807
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;blog-summary&quot;&gt;Blog Summary&lt;/h4&gt;
&lt;p&gt;Understanding Linux goes beyond knowing where files live; it’s about understanding how the operating system executes code, secures data, and manages its own limits. This guide breaks down the invisible mechanics of Linux that every DevOps engineer must master to troubleshoot effectively.&lt;/p&gt;
&lt;p&gt;You'll learn why binaries silently fail due to missing shared libraries, how to quickly fix &quot;command not found&quot; errors using the &lt;code&gt;$PATH&lt;/code&gt; variable, and the strict permission rules required to secure SSH keys. We also cover how to safely manage and terminate running processes using signals, and finally, uncover the hidden trap of File Descriptors—explaining why servers suddenly drop connections with &quot;Too many open files&quot; errors and how to raise those limits.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16002/linux-fundamentals-for-devops-linux-core-mechanism</guid>
<pubDate>Mon, 27 Apr 2026 16:45:13 +0000</pubDate>
</item>
</channel>
</rss>