<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Blog on VAHID.DEV</title><link>https://vahid.dev/posts/</link><description>Recent content in Blog on VAHID.DEV</description><generator>Hugo -- gohugo.io</generator><language>en-US</language><managingEditor>contact@vahid.dev (Vahid)</managingEditor><webMaster>contact@vahid.dev (Vahid)</webMaster><copyright>© 2024</copyright><lastBuildDate>Wed, 19 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://vahid.dev/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>Passing a Wi-Fi dongle into an Incus LXC container</title><link>https://vahid.dev/posts/incus-wifi-dongle-passthrough/</link><pubDate>Wed, 19 Aug 2026 00:00:00 +0000</pubDate><author>contact@vahid.dev (Vahid)</author><guid>https://vahid.dev/posts/incus-wifi-dongle-passthrough/</guid><description>&lt;p>Every now and then I want a container to be a real client on a Wi-Fi network, not just another
thing hidden behind the host&amp;rsquo;s bridge. That means the container needs the actual wireless device,
not a virtual NIC with someone else&amp;rsquo;s IP stacked on top. Incus can do this, but it&amp;rsquo;s one of those
jobs where the &amp;ldquo;how&amp;rdquo; is short and the &amp;ldquo;why it works this way&amp;rdquo; is the part worth understanding.&lt;/p></description><content:encoded><![CDATA[<p>Every now and then I want a container to be a real client on a Wi-Fi network, not just another
thing hidden behind the host&rsquo;s bridge. That means the container needs the actual wireless device,
not a virtual NIC with someone else&rsquo;s IP stacked on top. Incus can do this, but it&rsquo;s one of those
jobs where the &ldquo;how&rdquo; is short and the &ldquo;why it works this way&rdquo; is the part worth understanding.</p>
<h2 id="why-a-bridge-wont-do-it">Why a bridge won&rsquo;t do it</h2>
<p>Incus puts containers behind a virtual bridge (usually <code>incusbr0</code>) that NATs them onto the host&rsquo;s
network. Great for wired access. What it can&rsquo;t do is make the container <em>authenticate</em> to a Wi-Fi
access point as its own client, because Wi-Fi association is a property of the radio itself. A
bridge is layer 2 and 3; Wi-Fi client auth is layer 1 with a handshake. So if you want the container
to show up on the AP as its own device, you hand it the physical dongle and let it do the talking.</p>
<p><img src="/images/wifi-passthrough-drawio.png" alt="Incus Wi-Fi dongle passthrough (draw.io)"></p>
<p>Once passed through, the dongle vanishes from the host&rsquo;s network stack and reappears inside the
container as <code>wlan0</code>. The host can&rsquo;t use that radio while it&rsquo;s gone, which is exactly the point:
the container owns it.</p>
<h2 id="the-trade-off-you-cant-skip">The trade-off you can&rsquo;t skip</h2>
<p>Attaching a physical device requires the container to run <strong>privileged</strong>
(<code>security.privileged=true</code>). That loosens how much the container shares with the host&rsquo;s kernel
namespaces, and it visibly weakens the isolation that makes unprivileged containers worth using in
the first place. Incus runs unprivileged by default for a reason. I&rsquo;m fine with this on a box I
own, running workloads I trust. I would not do it for anything I didn&rsquo;t. If the code inside that
container is untrusted, this is the wrong tool and you should stop here.</p>
<h2 id="what-actually-happens-step-by-step">What actually happens, step by step</h2>
<p>The mechanics are simple once you see the shape of it. Plug the dongle in, then find what the
kernel called it:</p>





<pre tabindex="0"><code>ip link show        # look for a wlan*/wl* entry that appeared when you plugged it in
iw dev              # note the phy# (e.g. phy0) it&#39;s attached to</code></pre><p>The thing people get wrong is the next line. You attach the device with <code>nictype=physical</code>, and
the <code>parent</code> is the <strong>phy index</strong> from <code>iw dev</code> (phy0, phy1, …), not the <code>wlan</code> name. Mix those up
and the device simply never shows up in the container, which is the most common way this whole
exercise fails:</p>





<pre tabindex="0"><code>incus config set &lt;container&gt; security.privileged=true
incus config device add &lt;container&gt; wlan0 nic nictype=physical parent=phy0 name=wlan0</code></pre><p>Inside, the device is a Wi-Fi radio, so I name it <code>wlan0</code> to match what <code>wpa_supplicant</code> expects
rather than leaving it as a generic <code>eth0</code>.</p>
<p>There&rsquo;s a chicken-and-egg moment: before the dongle can authenticate, the container needs internet
to install the supplicant tools. I borrow the host bridge just long enough to get them, then pull it
back out:</p>





<pre tabindex="0"><code>incus config device add &lt;container&gt; tempnet nic network=incusbr0
incus exec &lt;container&gt; bash
  dhclient eth0
  apt update &amp;&amp; apt install -y wpasupplicant wireless-tools iw
incus config device remove &lt;container&gt; tempnet</code></pre><p>Then it&rsquo;s just standard Wi-Fi from inside. Write the network config, bring the supplicant up on the
passed-through interface, and ask the router for a lease:</p>





<pre tabindex="0"><code>wpa_passphrase &#34;Your_WiFi_Name&#34; &#34;Your_WiFi_Password&#34; &gt; /etc/wpa_supplicant/wpa_supplicant.conf
wpa_supplicant -B -i wlan0 -c /etc/wpa_supplicant/wpa_supplicant.conf
ip addr flush dev wlan0
dhclient -v wlan0
iw dev wlan0 link      # confirm you&#39;re associated</code></pre><p>Moving to another network later is the same dance with a fresh <code>wpa_passphrase</code> and a
<code>pkill wpa_supplicant</code> before you restart it.</p>
<h2 id="things-that-will-bite-you">Things that will bite you</h2>
<ul>
<li><code>parent=</code> is the <strong>phy</strong> index, not the interface name. This is the number one reason the device
&ldquo;doesn&rsquo;t appear&rdquo; in the container.</li>
<li>A privileged container holding a physical device is, for that radio, effectively standing on the
host&rsquo;s side of the network. Keep it on networks you trust.</li>
<li>None of this survives a host reboot on its own. After a restart you re-run the supplicant steps,
or you wire them into the container&rsquo;s own init if you want it to come back automatically.</li>
</ul>
<p>It&rsquo;s a small amount of config for a genuinely useful capability: a container that is a first-class
Wi-Fi citizen instead of a guest behind the host. Just remember you paid for it with isolation, and
spend that privilege where it&rsquo;s actually safe.</p>
]]></content:encoded></item><item><title>Incus in my home lab: why I dropped Proxmox (and then clustering)</title><link>https://vahid.dev/posts/incus-home-lab-experience/</link><pubDate>Tue, 18 Aug 2026 00:00:00 +0000</pubDate><author>contact@vahid.dev (Vahid)</author><guid>https://vahid.dev/posts/incus-home-lab-experience/</guid><description>&lt;p>For most of my home-lab life the default answer was Proxmox. It&amp;rsquo;s a good product. But over the last
year I moved my containers and VMs to &lt;strong>Incus&lt;/strong>, and I want to write down why, including the one place
it didn&amp;rsquo;t work out the way I hoped.&lt;/p>
&lt;h2 id="why-incus-over-proxmox">Why Incus over Proxmox&lt;/h2>
&lt;p>Incus is what came out of LXD, kept alive by the original LXC team at Linux Containers. Same mental
model: one daemon (&lt;code>incusd&lt;/code>) you talk to with a client (&lt;code>incus&lt;/code>), running both system containers (LXC,
sharing the host kernel, very light) and virtual machines (QEMU/KVM, full guests, stronger isolation).
If you&amp;rsquo;ve touched LXD you already know most of it; the commands even match (&lt;code>incus launch&lt;/code>, &lt;code>incus exec&lt;/code>, &lt;code>incus config device add&lt;/code>).&lt;/p></description><content:encoded><![CDATA[<p>For most of my home-lab life the default answer was Proxmox. It&rsquo;s a good product. But over the last
year I moved my containers and VMs to <strong>Incus</strong>, and I want to write down why, including the one place
it didn&rsquo;t work out the way I hoped.</p>
<h2 id="why-incus-over-proxmox">Why Incus over Proxmox</h2>
<p>Incus is what came out of LXD, kept alive by the original LXC team at Linux Containers. Same mental
model: one daemon (<code>incusd</code>) you talk to with a client (<code>incus</code>), running both system containers (LXC,
sharing the host kernel, very light) and virtual machines (QEMU/KVM, full guests, stronger isolation).
If you&rsquo;ve touched LXD you already know most of it; the commands even match (<code>incus launch</code>, <code>incus exec</code>, <code>incus config device add</code>).</p>
<p>What pulled me over was that Incus doesn&rsquo;t insist on being the whole OS. You drop the package onto the
distro you already run, run <code>incus admin init</code>, and you&rsquo;re set. Proxmox wants to own the machine. Incus
is happy being a layer on top of one. That difference matters more in practice than it sounds on paper.</p>
<h2 id="the-web-ui-caught-me-off-guard">The Web UI caught me off guard</h2>
<p>Proxmox ships a polished web UI and that&rsquo;s a real reason people like it for day-to-day clicking. Incus
doesn&rsquo;t include one by default, but the Incus Web UI (the <code>incus-ui-canonical</code> frontend) is genuinely
good once you switch it on:</p>





<pre tabindex="0"><code>incus config set core.https_address :8443
incus webui</code></pre><p>Point a browser at the host on <code>:8443</code>, accept the cert, and you get a clean console for launching,
shelling into, snapshotting, and networking instances, VMs and LXCs alike. I do most routine management
there now instead of the CLI, which surprised me because I expected to live in the terminal. For a home
lab, that UI is the thing that makes Incus feel as turnkey as Proxmox.</p>
<h2 id="running-on-two-architectures">Running on two architectures</h2>
<p>My lab isn&rsquo;t a single box. I&rsquo;ve got amd64 and arm64 hosts, and the arm64 ones are quiet and cheap, so
they carry the always-on services. Incus handles both natively, and the image server tags architectures
cleanly (<code>images:debian/12/arm64</code>, <code>images:debian/12/amd64</code>), so picking the right image per host is
trivial. That mixed fleet is where Proxmox has historically lagged, and it&rsquo;s a real reason Incus fits
my setup better.</p>
<h2 id="where-it-broke-down-clustering-across-architectures">Where it broke down: clustering across architectures</h2>
<p>I started the way you&rsquo;d expect: stand up an <strong>Incus cluster</strong>, join the nodes, get one unified view,
live-migrate VMs between hosts. The clustering story is arguably nicer than Proxmox&rsquo;s Corosync-based
one, with no hard node limit and the ability to rebalance VMs off a busy node.</p>
<p>Then I ran into the architecture wall, and the detail here changed between Incus versions so it&rsquo;s worth
getting right. Copying or moving an instance across architectures used to be refused outright, because
Incus did an architecture check on <code>incus copy</code>/<code>incus move</code>. That check was removed in <strong>Incus 0.6</strong>
(early 2024), so today you can copy an arm64 container onto an amd64 host and vice versa. What still
fails is <em>running</em> it there: a container&rsquo;s binaries are built for one ISA, so an amd64 container won&rsquo;t
start on an arm64 host, and <code>incus start</code> throws an architecture mismatch. VMs under KVM are the same
story, the guest OS has to target the host&rsquo;s architecture to boot, even though the migration itself
succeeds.</p>
<p>So the cluster gave me a tidy console and the ability to relocate instances across arch, which is
handy for storage and backups or for starting them later on a same-arch node. What it did not give me
was &ldquo;run anything anywhere regardless of CPU.&rdquo; After a while the operational overhead, keeping members
healthy, wrangling certificates and trust, rebalancing that only helped VMs, stopped paying for itself
against that hard limit. If you&rsquo;re on an older Incus and hit <code>Requested architecture isn't supported by this host</code> on a copy, that&rsquo;s the pre-0.6 behaviour; upgrade and the copy goes through, just don&rsquo;t
expect to start it on the wrong arch.</p>
<h2 id="two-more-reasons-it-beat-proxmox">Two more reasons it beat Proxmox</h2>
<p>Both come from the same root: Incus is a daemon on a normal distro, not an OS you boot into.</p>
<p>It runs OCI images directly. Since <strong>Incus 6.3</strong> you can register Docker Hub as an OCI remote and
launch application containers natively:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">incus remote add docker https://docker.io --protocol<span class="o">=</span>oci
</span></span><span class="line"><span class="ln">2</span><span class="cl">incus launch docker:nginx my-nginx</span></span></code></pre></div><p>No Docker engine required, Incus runs the image itself. Be clear-eyed about it, though. Incus&rsquo;s main
model is the system container, a long-lived, VM-like full OS, while OCI/Docker images are application
containers, ephemeral and task-focused. The Incus team added OCI support but treat it as a secondary
path, not the headliner, and it&rsquo;s newer, so rough edges remain (GPU passthrough into OCI, local-image
import, that sort of thing). For a throwaway <code>nginx</code> or a quick experiment it&rsquo;s slick. For a stack of
interdependent services I still reach for Docker Compose.</p>
<p>It also sits alongside Docker and Podman on the same host without complaint. Because <code>incusd</code> is just
another service on the distro, I can <code>apt install docker.io</code> or <code>podman</code> next to it and run OCI
containers as a peer of Incus, not nested inside it. That&rsquo;s the clean way to do it: Incus owns the
system containers and VMs, Docker/Podman owns the ephemeral app workloads, both sharing the host kernel
through different mechanisms. What you don&rsquo;t want is the Docker engine running inside an unprivileged
Incus container, nested containers need privilege and cgroup fiddling and tend to surprise you on
network isolation. Keep them side by side on the host and they get along.</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># On the Incus host, as a peer, not inside a container:</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">apt install -y docker.io
</span></span><span class="line"><span class="ln">3</span><span class="cl">docker run -d -p 8080:80 nginx</span></span></code></pre></div><h2 id="what-i-run-today">What I run today</h2>
<p>I dropped clustering and now run each host as a standalone Incus server, split by architecture:</p>
<p><img src="/images/homelab-drawio.png" alt="Incus home-lab: amd64 and arm64 standalone hosts on the trusted LAN"></p>
<p>The amd64 host carries the heavier services and the VMs that need x86. The arm64 host carries
lightweight always-on LXC containers, DNS, small Go services, the odds and ends. I&rsquo;ve made peace with a
workload living on one architecture and staying there. When I need it elsewhere I rebuild the image for
that arch, or run a multi-arch container image, rather than migrating. That&rsquo;s a small, predictable tax
instead of a cluster I maintain for a benefit I couldn&rsquo;t fully use.</p>
<p>In hindsight the lesson is simple: clustering is worth it when your nodes are the same architecture, or
when your VMs really are portable. With a mixed amd64 and arm64 fleet, running standalone per arch is
just simpler, and for a home lab it&rsquo;s honestly as effective.</p>
<h2 id="what-id-tell-myself-at-the-start">What I&rsquo;d tell myself at the start</h2>
<p>Unprivileged is the default for a reason. Flip <code>security.privileged=true</code> only when you have a concrete
need, like device passthrough for a Wi-Fi dongle (there&rsquo;s a whole post on that). Most workloads never
need it. And don&rsquo;t suffer the CLI out of pride, the Web UI is one command away.</p>
<p>Plan around architectures from day one. Pick an image tag per host, <code>/amd64</code> or <code>/arm64</code>, and decide
where a service lives before you build it. You can copy or move a container to a different-arch host
now that Incus 0.6 dropped that block, but you can&rsquo;t start it there, so design workloads per ISA
instead of promising yourself you&rsquo;ll migrate later. The real productivity win, though, is snapshots and
profiles: a decent default profile carrying storage, the bridge, and your SSH key turns a useful
container into a one-liner, and <code>incus snapshot</code> has bailed me out more than once.</p>
<h2 id="bottom-line">Bottom line</h2>
<p>Incus replaced Proxmox in my home lab because it&rsquo;s lighter, distro-agnostic, and native on arm64, and
the Web UI closes the everyday UX gap. Clustering was a fun experiment I retired once I hit the
cross-architecture wall. If your nodes share an architecture, keep the cluster. If they don&rsquo;t, don&rsquo;t.</p>
]]></content:encoded></item><item><title>Segmenting IoT from Trusted with Zero New Hardware</title><link>https://vahid.dev/posts/one-way-trusted-iot-network/</link><pubDate>Fri, 07 Aug 2026 00:00:00 +0000</pubDate><author>contact@vahid.dev (Vahid)</author><guid>https://vahid.dev/posts/one-way-trusted-iot-network/</guid><description>&lt;p>Like a lot of people, I collected smart gadgets faster than I built any hygiene around them. Cameras,
plugs, an ESPHome setup, a couple of cloud-only gizmos, all sitting on the same flat network as my
laptop, my phone, and the homelab. The day one of those ships with a known CVE and gets popped, it&amp;rsquo;s
a straight line into everything I actually care about.&lt;/p>
&lt;p>So I wanted one rule, stated simply and annoying to enforce:&lt;/p></description><content:encoded><![CDATA[<p>Like a lot of people, I collected smart gadgets faster than I built any hygiene around them. Cameras,
plugs, an ESPHome setup, a couple of cloud-only gizmos, all sitting on the same flat network as my
laptop, my phone, and the homelab. The day one of those ships with a known CVE and gets popped, it&rsquo;s
a straight line into everything I actually care about.</p>
<p>So I wanted one rule, stated simply and annoying to enforce:</p>
<blockquote>
<p>Trusted devices can reach IoT devices. IoT devices cannot reach trusted devices.</p></blockquote>
<p>And I added a constraint that made it interesting: use only the hardware I already own. No new access
point, no paid firewall, no subscription. This is the story of how I got there, including the dead ends,
because the dead ends are where I actually learned something.</p>
<h2 id="the-starting-point">The starting point</h2>
<p>My edge is a Calix Gigaspire from the ISP. It gives me a trusted Wi-Fi on <code>10.10.1.0/24</code> and a guest
Wi-Fi on <code>192.168.0.0/24</code>, isolated from trusted. What it does not give me is the ability to add static
routes. The two LANs are fully isolated at layer 3, and the gateway (<code>10.10.1.1</code>) lives inside the
trusted subnet.</p>
<p>On the IoT side I had an OpenWrt box acting as the IoT gateway and AP: its WAN on the guest LAN
(<code>192.168.0.x</code>), its LAN on <code>172.16.1.0/24</code> with its own IoT Wi-Fi. The homelab runs on an Incus host
on the trusted network.</p>
<p>That&rsquo;s the whole inventory. The puzzle was to make trusted reach IoT one way, with exactly that pile.</p>
<h2 id="dead-end-opnsense-as-the-trusted-gateway">Dead end: OPNsense as the trusted gateway</h2>
<p>My first instinct was the textbook one. Stand up OPNsense in a VM, make it the router between trusted
and IoT, write a zone firewall: trusted to IoT allow, IoT to trusted deny.</p>
<p>It failed, and the reasons are worth sitting with:</p>
<ol>
<li>The ISP router won&rsquo;t take a static route. Trusted devices use the Calix as their gateway, and the
Calix has no route to <code>172.16.1.0/24</code>, nor will it accept one. So trusted traffic to IoT dies at the
Calix.</li>
<li>The subnet topology fights you. The Calix gateway (<code>10.10.1.1</code>) sits <em>inside</em> the trusted LAN. For
OPNsense to be the gateway, its WAN and LAN would both have to be <code>10.10.1.0/24</code>, the same subnet on
two interfaces, which OPNsense simply won&rsquo;t allow.</li>
<li>Kea DHCP won&rsquo;t serve a WAN interface. Even when I forced the trusted network onto OPNsense&rsquo;s WAN, the
DHCP server refused to run there.</li>
</ol>
<p>The lesson stuck with me: when the ISP router is the gateway and won&rsquo;t do static routes, you can&rsquo;t make
a downstream box the trusted gateway without fighting the subnet topology the whole way down.</p>
<h2 id="the-pivot-stop-routing-start-tunneling">The pivot: stop routing, start tunneling</h2>
<p>The thing that broke the logjam was realizing I didn&rsquo;t need to <em>route</em> trusted to IoT at layer 3. I
needed trusted devices to <em>reach</em> IoT. A tunnel does that without touching the Calix&rsquo;s routing table.</p>
<p>Raw WireGuard would work, but I&rsquo;d be hand-managing keys per device, and anything off-LAN means port
forwarding. Tailscale is WireGuard underneath with automatic key exchange, NAT traversal, and magic DNS,
so it won. Then the elegant bit: OpenWrt can run Tailscale. I turned the OpenWrt IoT box into a Tailscale
subnet router for <code>172.16.1.0/24</code>.</p>
<h2 id="the-architecture-that-worked">The architecture that worked</h2>
<p><img src="/images/iot-drawio.png" alt="One-way trusted→IoT network: Calix, trusted devices, OpenWrt subnet router, IoT"></p>
<p>Trusted devices join my Tailscale tailnet and get a route to <code>172.16.1.0/24</code> through the OpenWrt subnet
router. IoT devices stay behind OpenWrt, which itself sits on the isolated guest LAN.</p>
<h2 id="how-the-one-way-rule-actually-holds">How the one-way rule actually holds</h2>
<p>This is the subtle part, and it&rsquo;s easy to get wrong by assuming one control point does all the work.
There are two, and both matter:</p>
<table>
  <thead>
      <tr>
          <th>Flow</th>
          <th>Allowed?</th>
          <th>Enforced by</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Trusted → IoT</td>
          <td>yes</td>
          <td>Tailscale ACL (<code>tag:trusted</code> → <code>172.16.1.0/24</code>) + OpenWrt <code>tailscale → lan</code> forward</td>
      </tr>
      <tr>
          <td>IoT → Trusted</td>
          <td>no</td>
          <td>No route + Tailscale default-deny</td>
      </tr>
      <tr>
          <td>IoT → Internet</td>
          <td>yes</td>
          <td>OpenWrt <code>lan → wan</code> + masquerade</td>
      </tr>
      <tr>
          <td>IoT → Guest</td>
          <td>no</td>
          <td>OpenWrt explicit REJECT</td>
      </tr>
      <tr>
          <td>Guest → OpenWrt mgmt</td>
          <td>no</td>
          <td>OpenWrt WAN input REJECT + port block</td>
      </tr>
  </tbody>
</table>
<p>The real insight is that IoT to trusted is already blocked by the topology (IoT lives on the isolated
guest LAN behind OpenWrt, with no route to <code>10.10.1.0/24</code>). The Tailscale ACL is a second layer.
Defense in depth, not a single gate.</p>
<h2 id="what-it-costs-you">What it costs you</h2>
<p>This is good enough for home, not enterprise, and I&rsquo;ll be honest about the trade-offs:</p>
<ol>
<li>Third-party metadata. Tailscale sees your node graph and relay metadata. The traffic itself is
end-to-end WireGuard. If that bugs you, raw WireGuard is the alternative.</li>
<li>Control-plane dependency. If Tailscale&rsquo;s coordination server is down, new connections can&rsquo;t be
negotiated. Tunnels already up keep running.</li>
<li>OpenWrt storage. The Tailscale package needs flash and RAM. On a tiny router it may not fit, in
which case run the subnet router as a container on the homelab host instead.</li>
<li>No layer 2 segmentation. This is layer 3 plus a tunnel. Turn on client isolation on the IoT SSID
for real device-to-device isolation.</li>
<li>mDNS doesn&rsquo;t cross. HomeKit, AirPlay, Chromecast from trusted to IoT won&rsquo;t work without an mDNS
reflector.</li>
<li>The Calix is still the perimeter. Its guest/trusted isolation is doing real work. Verify it every
so often.</li>
</ol>
<h2 id="what-id-do-differently">What I&rsquo;d do differently</h2>
<p>If I were building from scratch I&rsquo;d buy a VLAN-capable router, UniFi or Omada or OPNsense on a mini-PC,
and do this with real VLANs and a zone firewall. But given the &ldquo;use what I have&rdquo; constraint, the
Tailscale-on-OpenWrt approach got me the security property I wanted with zero new hardware and an
afternoon of tinkering.</p>
<p>The takeaway is less &ldquo;buy a fancy firewall&rdquo; and more &ldquo;know what your ISP router will and won&rsquo;t let you
do, then tunnel around the parts it won&rsquo;t.&rdquo;</p>
]]></content:encoded></item><item><title>Optimizing Cloud Costs</title><link>https://vahid.dev/posts/optimizing-cloud-costs/</link><pubDate>Sat, 14 Feb 2026 00:00:00 +0000</pubDate><author>contact@vahid.dev (Vahid)</author><guid>https://vahid.dev/posts/optimizing-cloud-costs/</guid><description>&lt;p>Cloud bills surprise people. Not because the provider is opaque, the invoices are painfully detailed, but
because cost is an output of design, and most teams only look at it after the design is locked. This is the
approach I use to keep cloud spend honest: find the real levers, automate the boring savings, and make cost
a normal part of the architecture review.&lt;/p>
&lt;h2 id="first-see-the-bill">First, see the bill&lt;/h2>
&lt;p>You can&amp;rsquo;t optimize what you can&amp;rsquo;t attribute. Before touching anything:&lt;/p></description><content:encoded><![CDATA[<p>Cloud bills surprise people. Not because the provider is opaque, the invoices are painfully detailed, but
because cost is an output of design, and most teams only look at it after the design is locked. This is the
approach I use to keep cloud spend honest: find the real levers, automate the boring savings, and make cost
a normal part of the architecture review.</p>
<h2 id="first-see-the-bill">First, see the bill</h2>
<p>You can&rsquo;t optimize what you can&rsquo;t attribute. Before touching anything:</p>
<ul>
<li>Turn on tagging and enforce it. Every resource needs owner, env, service, cost-center. No tag, no deploy.</li>
<li>Use cost allocation reports per team or service so each team sees its own number.</li>
<li>Set a budget and an anomaly alert. A weekly &ldquo;this service cost $X&rdquo; email beats a monthly shock.</li>
</ul>
<p>Visibility is most of the win. Most teams find 10 to 20 percent of spend on idle or orphaned resources in the
first week of actually looking.</p>
<h2 id="the-levers-that-actually-move-the-number">The levers that actually move the number</h2>
<p>Not all optimizations are equal, so rank by impact.</p>
<p>Compute shape and utilization is the biggest leak. Oversized instances are the number one culprit. Right-size
from real CPU and memory metrics, not guesses. A service averaging 12 percent CPU on a 4-vCPU box is paying
for headroom it never uses.</p>
<p>Stop paying for idle time. Dev and test environments don&rsquo;t need to run at 2am or on weekends. Scheduled
shutdown and start saves around 65 percent on non-prod.</p>
<p>Commitment discounts, reserved instances, savings plans, matter if a baseline workload runs 24/7. A one to
three year commitment typically cuts 30 to 60 percent versus on-demand. Buy commitments for the stable
baseline only and keep the spiky part on-demand.</p>
<p>Storage lifecycle. Move cold data down tiers, standard to infrequent to archive. Delete orphaned volumes and
snapshots. Egress is the sneaky one, so watch cross-region and NAT gateway traffic.</p>
<p>Networking egress. Data out is expensive and easy to ignore. Cache at the edge, keep chatter in-region, and
scrutinize any architecture that moves bytes between regions for no reason.</p>
<h2 id="a-workflow-not-a-one-off">A workflow, not a one-off</h2>
<p>Cost optimization decays. The team that ran a &ldquo;cost cleanup sprint&rdquo; last quarter is over budget again next
quarter unless the discipline sticks. Make it continuous.</p>
<p>Architecture review includes a cost estimate. New service? Estimate the monthly run cost before approval.
&ldquo;Cheap to build, four grand a month to run&rdquo; should be a conscious decision.</p>
<p>Right-size on a schedule, a quarterly utilization review per service.</p>
<p>Automate the easy wins. Auto-stop non-prod on a schedule, auto-delete orphaned volumes after N days, alert on
untagged resources.</p>
<p>Track cost per transaction or per user, not just total. A total can hide a service that got three times more
expensive per request.</p>
<h2 id="common-mistakes">Common mistakes</h2>
<p>Over-buying commitments to hit a discount percentage, then not using them. Commit the stable baseline and let
the rest float.</p>
<p>Chasing pennies, missing dollars. Tuning a Lambda to save five bucks a month while a forgotten oversized
database costs nine hundred. Fix the biggest line item first.</p>
<p>No ownership. When cost is nobody&rsquo;s job, it only improves during crises. Give each team its number.</p>
<p>Ignoring data transfer. Compute gets the attention, while egress and inter-AZ traffic quietly dominate.</p>
<h2 id="the-point">The point</h2>
<p>Cloud cost engineering isn&rsquo;t about being cheap, it&rsquo;s about spending deliberately. The goal is that every
dollar on the bill corresponds to a decision someone made, not a default someone forgot. Build the
visibility, pull the big levers, and bake a cost estimate into the review gate. The savings compound, and the
discipline is what makes them stick.</p>
]]></content:encoded></item><item><title>Understanding Kubernetes</title><link>https://vahid.dev/posts/understanding-kubernetes/</link><pubDate>Sat, 10 Jan 2026 00:00:00 +0000</pubDate><author>contact@vahid.dev (Vahid)</author><guid>https://vahid.dev/posts/understanding-kubernetes/</guid><description>&lt;p>Kubernetes has a reputation for being complicated, and some of that reputation is earned. But most of the
intimidation comes from meeting it at the wrong layer. This is a mental model: what Kubernetes is, what
each major piece does, and how to reason about your workloads without drowning in YAML.&lt;/p>
&lt;h2 id="what-problem-does-it-solve">What problem does it solve?&lt;/h2>
&lt;p>At its core, Kubernetes is a scheduler and reconciler for containers. You hand it a desired state (&amp;ldquo;run 3
copies of this image, expose port 8080&amp;rdquo;), and a control loop keeps reality matched to that state. A node
dies, it reschedules. Traffic spikes, it scales, if you asked it to. A config changes, it rolls out
without downtime.&lt;/p></description><content:encoded><![CDATA[<p>Kubernetes has a reputation for being complicated, and some of that reputation is earned. But most of the
intimidation comes from meeting it at the wrong layer. This is a mental model: what Kubernetes is, what
each major piece does, and how to reason about your workloads without drowning in YAML.</p>
<h2 id="what-problem-does-it-solve">What problem does it solve?</h2>
<p>At its core, Kubernetes is a scheduler and reconciler for containers. You hand it a desired state (&ldquo;run 3
copies of this image, expose port 8080&rdquo;), and a control loop keeps reality matched to that state. A node
dies, it reschedules. Traffic spikes, it scales, if you asked it to. A config changes, it rolls out
without downtime.</p>
<p>That reconciliation loop, observe current state, compare to desired, act to close the gap, is the one idea
underneath everything else.</p>
<h2 id="the-control-plane">The control plane</h2>
<p>The control plane is the brain, and these are its parts:</p>
<ul>
<li>API server is the front door. Every <code>kubectl</code> command is an HTTP call to it, and it&rsquo;s the only component
that talks to etcd.</li>
<li>etcd is the strongly-consistent datastore. The entire cluster state lives here. Back it up, seriously.</li>
<li>Scheduler watches for unscheduled pods and picks a node for each, based on resources, affinity, and
taints or tolerations.</li>
<li>Controller managers are the reconcilers: deployment controller, node controller, endpoint controller,
and friends. Each owns one resource type&rsquo;s &ldquo;make reality match spec&rdquo; loop.</li>
<li>kubelet is the agent on every node. It talks to the API server and runs pods through the container
runtime.</li>
<li>kube-proxy keeps node networking rules in place so Services route to the right pods.</li>
</ul>
<h2 id="the-objects-youll-actually-use">The objects you&rsquo;ll actually use</h2>
<p>You don&rsquo;t need to memorize the whole API. The daily drivers:</p>
<ul>
<li>Pod is one or more containers that share network and storage. It&rsquo;s the smallest thing you deploy, but
you rarely create pods directly. You create something that creates pods.</li>
<li>Deployment declares how many replicas of a pod and which image. Rolling updates and rollback come free
with it.</li>
<li>Service gives stable networking and load balancing to a set of pods, by label selector. ClusterIP is
internal, NodePort and LoadBalancer are external.</li>
<li>ConfigMap and Secret hold config and credentials, mounted as files or env vars. Don&rsquo;t bake secrets into
images.</li>
<li>Ingress is HTTP routing: path <code>/api</code> goes to Service X, host <code>app.example</code> goes to Service Y. It sits in
front of Services.</li>
<li>Namespace is a scoping boundary for names and RBAC. It is not a network boundary; that&rsquo;s NetworkPolicy.</li>
</ul>
<h2 id="a-useful-mental-model">A useful mental model</h2>
<p><img src="/images/kubernetes-drawio.png" alt="Kubernetes flow: Ingress → Service → Deployment, controlled by the control plane"></p>
<p>Requests flow down, Ingress to Service to Pod. Desired state flows up: you write a Deployment, the
controller makes pods, the scheduler places them, the kubelet runs them.</p>
<h2 id="where-people-get-stuck">Where people get stuck</h2>
<p>Confusing Deployment and Pod. Edit the Deployment&rsquo;s image, don&rsquo;t <code>kubectl edit pod</code>, it&rsquo;ll get replaced.</p>
<p>Forgetting probes. Liveness and readiness probes are how Kubernetes knows a pod is actually serving, not
just that it started. Without them, traffic lands on broken pods.</p>
<p>Secret hygiene. Turn on encryption at rest for etcd, and use an external secret store, Sealed Secrets,
External Secrets Operator, or your cloud&rsquo;s KMS, rather than committing YAML.</p>
<p>Resource requests and limits. Set requests so the scheduler places pods sanely, and set limits so one bad
pod can&rsquo;t eat the node.</p>
<h2 id="when-not-to-use-it">When not to use it</h2>
<p>Kubernetes is a platform, not a product. If you have one small service with steady traffic, a single
container on a managed service, or even a PaaS, is simpler and cheaper. Reaching for Kubernetes because
it&rsquo;s fashionable is how teams end up operating a distributed system they never needed. Adopt it when you
have multiple services, multiple teams, or real scaling and orchestration needs.</p>
<h2 id="closing-thought">Closing thought</h2>
<p>Learn it from the reconciliation loop outward. Once &ldquo;observe, compare, act&rdquo; clicks, every other object,
HorizontalPodAutoscaler, CronJob, Operator, is just another specialized reconciler. The rest really is
YAML.</p>
]]></content:encoded></item></channel></rss>