<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sidharth C P</title>
    <description>The latest articles on DEV Community by Sidharth C P (@sidharth_cp).</description>
    <link>https://dev.to/sidharth_cp</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4117298%2F41526b34-5be0-4044-8597-3c1e0cee6172.png</url>
      <title>DEV Community: Sidharth C P</title>
      <link>https://dev.to/sidharth_cp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sidharth_cp"/>
    <language>en</language>
    <item>
      <title>Upgrading a Locked-Down Linux Fleet with Nothing but kubectl</title>
      <dc:creator>Sidharth C P</dc:creator>
      <pubDate>Mon, 14 Sep 2026 18:39:04 +0000</pubDate>
      <link>https://dev.to/sidharth_cp/upgrading-a-locked-down-linux-fleet-with-nothing-but-kubectl-3d7k</link>
      <guid>https://dev.to/sidharth_cp/upgrading-a-locked-down-linux-fleet-with-nothing-but-kubectl-3d7k</guid>
      <description>&lt;p&gt;Say you're running a fleet of machines on an &lt;em&gt;immutable&lt;/em&gt; operating system — root filesystem read-only, nothing drifts, and you upgrade the whole OS as a single artifact instead of patching it in place. Great for reliability.&lt;/p&gt;

&lt;p&gt;But now you need to do something mundane: rotate a login password across every node, or roll out a new OS version.&lt;/p&gt;

&lt;p&gt;The obvious move is to SSH into each box and do it by hand. Except that's exactly the kind of ad-hoc, drift-inducing change the immutable model exists to prevent — and on a real fleet, "SSH into each box" doesn't scale past your patience.&lt;/p&gt;

&lt;p&gt;So here's the setup I landed on, and the thing I actually want to show you: &lt;strong&gt;I can change a password or push a full OS upgrade to the whole fleet by applying a single Kubernetes ConfigMap. No SSH, no per-node scripts, no drift.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Rotate the password everywhere:&lt;/span&gt;
kubectl get configmap kairos-config &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system &lt;span class="nt"&gt;-o&lt;/span&gt; yaml | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/passwd: kairos/passwd: something-better/'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Roll out a new OS image to the whole fleet:&lt;/span&gt;
kubectl create configmap kairos-upgrade &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker.io/you/kairos-rhel96:v2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v2.0.0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system &lt;span class="nt"&gt;--dry-run&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;client &lt;span class="nt"&gt;-o&lt;/span&gt; yaml | kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire operator experience. Below is how it works — and, more interestingly, how I made it do that &lt;em&gt;without&lt;/em&gt; handing a compromised container the keys to every machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-paragraph background
&lt;/h2&gt;

&lt;p&gt;The OS here is &lt;a href="https://kairos.io/" rel="noopener noreferrer"&gt;Kairos&lt;/a&gt;, an open-source immutable Linux distribution built for running Kubernetes at the edge. The mental model: you build the OS like a container image, ship it as an artifact, and &lt;em&gt;replace&lt;/em&gt; it wholesale to upgrade.&lt;/p&gt;

&lt;p&gt;Upgrades write the new image to an inactive A/B partition and flip the bootloader to it — so a bad upgrade rolls back instead of leaving you with a half-patched box. My image is built on RHEL 9.6 with k3s baked in.&lt;/p&gt;

&lt;p&gt;If you've never touched Kairos, that's all you need to follow along. The interesting part isn't Kairos itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interesting part: how a &lt;code&gt;kubectl apply&lt;/code&gt; becomes a change on the host
&lt;/h2&gt;

&lt;p&gt;The gap I had to bridge is this: editing a ConfigMap happens &lt;em&gt;inside&lt;/em&gt; Kubernetes, but flipping a GRUB partition, running an OS upgrade, or rebooting a node happens &lt;em&gt;on the host&lt;/em&gt;, as root, outside the cluster's world. Something has to cross that boundary.&lt;/p&gt;

&lt;p&gt;The naive way to cross it is a &lt;strong&gt;privileged DaemonSet&lt;/strong&gt; — a pod with the host's PID namespace, the host root filesystem mounted in, and all Linux capabilities intact. It works, and it's a trap.&lt;/p&gt;

&lt;p&gt;A pod with that much power is one of the cleanest paths on a whole cluster from "attacker gets into a container" to "attacker has root on every node." A bad base image, one poisoned dependency, a sloppy RBAC rule — any of those, and a privileged DaemonSet hands the whole fleet over.&lt;/p&gt;

&lt;p&gt;So I split the job in two, along the privilege boundary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;In the cluster:&lt;/strong&gt; an &lt;em&gt;unprivileged&lt;/em&gt; DaemonSet pod. All capabilities dropped, &lt;code&gt;allowPrivilegeEscalation: false&lt;/code&gt;, and its only reach into the host is two narrow &lt;code&gt;hostPath&lt;/code&gt; mounts — &lt;code&gt;/oem&lt;/code&gt; and &lt;code&gt;/usr/local&lt;/code&gt;. It can &lt;em&gt;write two files&lt;/em&gt;. That's the entire blast radius. It reads the ConfigMaps you apply and drops the requested change into those files.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;On the host:&lt;/strong&gt; a plain root-owned &lt;code&gt;systemd&lt;/code&gt; service (&lt;code&gt;kairos-agent-watcher&lt;/code&gt;) that loops every 15 seconds, reads only those same two files, and does the actual privileged work — applying config with &lt;code&gt;kairos-agent run-stage&lt;/code&gt;, flipping the A/B slot, running an upgrade, rebooting.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The payoff is the security property: the pod that faces the cluster has almost no power, and the process that has all the power never faces the cluster.&lt;/p&gt;

&lt;p&gt;If someone fully owns the DaemonSet pod, the worst they can do is write a config change or point at an upgrade image. They still can't run an arbitrary command as root on the node, because nothing in the pod ever &lt;em&gt;executes&lt;/em&gt; privileged work — the privilege lives in a service that only ever reads two files.&lt;/p&gt;

&lt;p&gt;That's the idea I'd take to any host-level-ops-from-Kubernetes problem, Kairos or not: don't grant the network-facing thing the privilege; let it express &lt;em&gt;intent&lt;/em&gt; into a narrow channel, and keep the privilege on the other side of that channel.&lt;/p&gt;

&lt;p&gt;The flow, end to end:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9mwx9qiv8wlp1o7ju7nm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9mwx9qiv8wlp1o7ju7nm.png" alt="Flow diagram: a kubectl apply goes to an unprivileged Kubernetes DaemonSet pod (all capabilities dropped, allowPrivilegeEscalation false) which can only write two files — /oem/90_custom.yaml and a /usr/local upgrade trigger. Those files sit on a privilege boundary. On the host side, a root systemd service (kairos-agent-watcher) reads only those two files every 15 seconds and does the privileged work: applying config, flipping the A/B slot, running the upgrade, and rebooting. A compromised pod can write intent but never run a root command." width="800" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A scar worth sharing: the disk that installed itself across two disks
&lt;/h2&gt;

&lt;p&gt;One more piece runs earlier, at install time — a small service that picks &lt;em&gt;which disk&lt;/em&gt; the OS lands on. On a multi-disk box you don't want the OS installing over your data disk or your install media, so the script picks the smallest attached disk over 20GB that isn't the boot media.&lt;/p&gt;

&lt;p&gt;That sounds simple. It was not simple, and I want to be honest about how I learned that, because it's the kind of thing you won't find in the docs.&lt;/p&gt;

&lt;p&gt;The one that actually hurt showed up &lt;em&gt;after&lt;/em&gt; we shipped it to a client. On their hardware, the install would sometimes end up with the OS partitions &lt;strong&gt;split across two different disks&lt;/strong&gt; — some on one, some on another.&lt;/p&gt;

&lt;p&gt;It took a bunch of back-and-forth with my team to work out why: those disks had been used for a previous Kairos install, and the leftover Kairos partition labels (&lt;code&gt;COS_GRUB&lt;/code&gt;, &lt;code&gt;COS_OEM&lt;/code&gt;, &lt;code&gt;COS_STATE&lt;/code&gt;, and friends) were still on them.&lt;/p&gt;

&lt;p&gt;Kairos finds its partitions &lt;em&gt;by label&lt;/em&gt;. With stale labels lying around on a second disk, the installer happily adopted them, and the install smeared itself across both.&lt;/p&gt;

&lt;p&gt;The fix is unglamorous and now lives in the script: before install, scrub any stale Kairos labels off the candidate disks (&lt;code&gt;wipefs&lt;/code&gt;, &lt;code&gt;sgdisk --zap-all&lt;/code&gt;) so nothing gets adopted by accident.&lt;/p&gt;

&lt;p&gt;But the lesson is the transferable bit: &lt;em&gt;label-based partition discovery trusts labels, so an install onto previously-used hardware has to assume those labels are lying.&lt;/em&gt; That's a sentence I wish I'd read before shipping, not after.&lt;/p&gt;

&lt;p&gt;(That scrub step is destructive by design — it's meant for lab and disposable-disk hardware. Read it before you point it at anything you care about.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole thing is on GitHub
&lt;/h2&gt;

&lt;p&gt;The full working setup — the Dockerfile, both systemd services, the DaemonSet and ConfigMaps, the disk-select script, and step-by-step commands to build the image, produce an ISO, spin up a demo VM, and test both a password rotation and a live OS upgrade end to end — is here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://github.com/sidharth48-hub/kairos" rel="noopener noreferrer"&gt;https://github.com/sidharth48-hub/kairos&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The README walks through it in order and calls out what's demo-only (default passwords, the destructive disk scrub) versus what's safe to reuse.&lt;/p&gt;

&lt;p&gt;If you're running immutable Linux under Kubernetes and you've been SSHing into nodes for day-2 changes, I think the pod-writes-intent / host-holds-privilege split is worth stealing — even if you never touch Kairos.&lt;/p&gt;

&lt;p&gt;Happy to answer questions in the comments — especially if you've solved the host-ops-from-a-cluster problem a different way. I'd like to hear it.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>linux</category>
      <category>devops</category>
      <category>security</category>
    </item>
  </channel>
</rss>
