<?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>Cilium on bismigel</title><link>https://bismigel.dev/tags/cilium/</link><description>Recent content in Cilium on bismigel</description><generator>Hugo</generator><language>en-us</language><copyright>bismigel</copyright><lastBuildDate>Mon, 19 Oct 2026 09:30:00 -0400</lastBuildDate><atom:link href="https://bismigel.dev/tags/cilium/index.xml" rel="self" type="application/rss+xml"/><item><title>Cilium VXLAN troubleshooting: port 8472 already in use</title><link>https://bismigel.dev/posts/2026-10-19-cilium-vxlan-troubleshooting/</link><pubDate>Mon, 19 Oct 2026 09:30:00 -0400</pubDate><guid>https://bismigel.dev/posts/2026-10-19-cilium-vxlan-troubleshooting/</guid><description/><content:encoded><![CDATA[<p>Cilium was healthy in native routing. After switching to VXLAN, new pods stayed in <code>ContainerCreating</code> and Cilium health was unreachable. The cause was not the kernel or the firewall.</p>
<h1 id="symptoms">Symptoms</h1>
<ul>
<li><code>cilium-dbg status</code> showed <code>Routing: Tunnel [vxlan]</code>.</li>
<li>New pods stayed in <code>ContainerCreating</code>.</li>
<li>Cilium health warned that the health daemon was unreachable.</li>
<li>Existing pods could still serve traffic.</li>
<li>Cilium agent logs showed:</li>
</ul>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">text</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">failed to setup vxlan tunnel device:
</span></span><span class="line"><span class="cl">  address already in use</span></span></code></pre></div>
</div>
<h1 id="checks">Checks</h1>
<p>Run these on the node:</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">bash</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ip -d link show <span class="nb">type</span> vxlan
</span></span><span class="line"><span class="cl">ss -lunp <span class="p">|</span> grep <span class="m">8472</span>
</span></span><span class="line"><span class="cl">ip -d link show cilium_vxlan
</span></span><span class="line"><span class="cl">ip -4 route
</span></span><span class="line"><span class="cl">ip -4 rule
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Check the Cilium agent log:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#+begin_src bash</span>
</span></span><span class="line"><span class="cl">kubectl -n kube-system logs ds/cilium --tail<span class="o">=</span><span class="m">100</span> <span class="p">|</span>
</span></span><span class="line"><span class="cl">  grep -Ei <span class="s1">&#39;vxlan|8472|address already|datapath&#39;</span></span></span></code></pre></div>
</div>
<p>In this lab the output showed two VXLAN devices:</p>
<ul>
<li><code>flannel.1</code>, still UP and bound to UDP 8472;</li>
<li><code>cilium_vxlan</code>, DOWN because Cilium could not bind the same port.</li>
</ul>
<p>Flannel was disabled in k3s, but the old interface survived the CNI migration.</p>
<h1 id="fix">Fix</h1>
<p>Remove the stale interface, then restart Cilium:</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">bash</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ip link del flannel.1
</span></span><span class="line"><span class="cl">kubectl -n kube-system rollout restart ds/cilium</span></span></code></pre></div>
</div>
<p>On NixOS, make it survive a reboot:</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">nix</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="n">systemd</span><span class="o">.</span><span class="n">services</span><span class="o">.</span><span class="n">cleanup-flannel-vxlan</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="o">=</span> <span class="s2">&#34;Remove a stale Flannel VXLAN interface&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="n">before</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">&#34;k3s.service&#34;</span> <span class="p">];</span>
</span></span><span class="line"><span class="cl">  <span class="n">wantedBy</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">&#34;multi-user.target&#34;</span> <span class="p">];</span>
</span></span><span class="line"><span class="cl">  <span class="n">serviceConfig</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">Type</span> <span class="o">=</span> <span class="s2">&#34;oneshot&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">RemainAfterExit</span> <span class="o">=</span> <span class="no">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl">  <span class="n">script</span> <span class="o">=</span> <span class="s2">&#34;ip link del flannel.1 2&gt;/dev/null || true&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span></span></span></code></pre></div>
</div>
<h1 id="verify">Verify</h1>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">bash</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ip -d link show cilium_vxlan
</span></span><span class="line"><span class="cl">ss -lunp <span class="p">|</span> grep <span class="m">8472</span>
</span></span><span class="line"><span class="cl">kubectl -n kube-system <span class="nb">exec</span> ds/cilium -- cilium-dbg status
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl run nettest --image<span class="o">=</span>curlimages/curl:8.10.1 --restart<span class="o">=</span>Never --command -- <span class="se">\
</span></span></span><span class="line"><span class="cl">  sh -c <span class="s1">&#39;curl -s -o /dev/null -w &#34;%{http_code}\n&#34; https://example.com&#39;</span></span></span></code></pre></div>
</div>
<p>Expected:</p>
<ul>
<li><code>cilium_vxlan</code> is UP;</li>
<li>UDP 8472 is owned by Cilium;</li>
<li>new pods reach <code>Running</code>;</li>
<li>DNS, external egress and ClusterIP work;</li>
<li><code>cilium-dbg status</code> health is 3/3.</li>
</ul>
<h1 id="why-native-routing-hid-it">Why native routing hid it</h1>
<p>Native routing never creates a VXLAN device, so a stale <code>flannel.1</code> interface did not matter. The tunnel failure looked like a kernel difference because native worked on the same hosts.</p>
<p>Two other settings still matter:</p>
<ul>
<li><code>networking.firewall.checkReversePath = false</code> on NixOS;</li>
<li>the fwmark overlap case when another VPN uses policy routing.</li>
</ul>
<p>References:</p>
<ul>
<li>Cilium routing: <a href="https://docs.cilium.io/en/stable/network/concepts/routing/" target="_blank" rel="noopener noreferrer">https://docs.cilium.io/en/stable/network/concepts/routing/</a>
</li>
<li>NixOS rpfilter issue: <a href="https://github.com/lucas-albers-lz4/rke2nixos/issues/10" target="_blank" rel="noopener noreferrer">https://github.com/lucas-albers-lz4/rke2nixos/issues/10</a>
</li>
<li>fwmark collision: <a href="https://pokgak.xyz/notes/cilium-vxlan-tailscale-fwmark-collision/" target="_blank" rel="noopener noreferrer">https://pokgak.xyz/notes/cilium-vxlan-tailscale-fwmark-collision/</a>
</li>
<li>Native vs encapsulation: <a href="/posts/2026-10-19-native-vs-encapsulation/">Native vs encapsulated routing for a small homelab</a>
</li>
</ul>
]]></content:encoded></item><item><title>Native vs encapsulated routing for a small homelab</title><link>https://bismigel.dev/posts/2026-10-19-native-vs-encapsulation/</link><pubDate>Mon, 19 Oct 2026 09:30:00 -0400</pubDate><guid>https://bismigel.dev/posts/2026-10-19-native-vs-encapsulation/</guid><description/><content:encoded><![CDATA[<p>Cilium gives you two main ways to move packets between nodes. The choice is small, but it decides what your network and firewall must do.</p>
<h1 id="the-short-version">The short version</h1>
<table>
	<thead>
			<tr>
					<th>Mode</th>
					<th>What moves packets</th>
					<th>Main requirement</th>
					<th>Best fit</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Native</td>
					<td>Linux routing</td>
					<td>PodCIDRs must be routable between nodes</td>
					<td>A network you control, often one L2 segment</td>
			</tr>
			<tr>
					<td>Encapsulation</td>
					<td>VXLAN inside UDP 8472</td>
					<td>Nodes only need IP reachability</td>
					<td>A simple lab, or nodes without PodCIDR routes</td>
			</tr>
	</tbody>
</table>
<p>Both keep kube-proxy in this lab. Cilium handles the CNI, policy and visibility; kube-proxy still handles ClusterIP services.</p>
<h1 id="native-routing">Native routing</h1>
<p>Native routing tells Cilium to hand non-local packets to the Linux routing table. With <code>autoDirectNodeRoutes</code>, each node learns a route to every other node&rsquo;s PodCIDR:</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">yaml</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">routingMode</span><span class="p">:</span><span class="w"> </span><span class="l">native</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">autoDirectNodeRoutes</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">ipv4NativeRoutingCIDR</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;10.42.0.0/16&#34;</span></span></span></code></pre></div>
</div>
<p>Good:</p>
<ul>
<li>no encapsulation overhead;</li>
<li>full MTU;</li>
<li>simple packet capture and routing inspection.</li>
</ul>
<p>Cost:</p>
<ul>
<li>the underlay must route PodCIDRs;</li>
<li>every node needs to learn the other nodes&rsquo; pod routes;</li>
<li>spanning multiple L2 or L3 domains usually means BGP or static routes.</li>
</ul>
<h1 id="encapsulation">Encapsulation</h1>
<p>VXLAN wraps the pod packet in a UDP packet between nodes. The underlay only needs normal node-to-node IP reachability:</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">yaml</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">routingMode</span><span class="p">:</span><span class="w"> </span><span class="l">tunnel</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">tunnelProtocol</span><span class="p">:</span><span class="w"> </span><span class="l">vxlan</span></span></span></code></pre></div>
</div>
<p>Good:</p>
<ul>
<li>the simplest underlay requirement;</li>
<li>no PodCIDR routes on the physical network;</li>
<li>new nodes join without changing a router;</li>
<li>works well on a flat home LAN.</li>
</ul>
<p>Cost:</p>
<ul>
<li>about 50 bytes of overhead, so pod MTU drops to 1450 on a 1500 MTU LAN;</li>
<li>extra encapsulation and decapsulation work;</li>
<li>an extra failure surface: VXLAN device, UDP port and firewall rules.</li>
</ul>
<h1 id="what-this-lab-uses">What this lab uses</h1>
<p>VXLAN. All three nodes are on one LAN, but the goal is to keep the network configuration small. Native routing worked, but encapsulation lets the nodes join with only IP reachability and no PodCIDR routes outside the cluster.</p>
<p>The relevant settings are in <code>bakuran-gitops</code>:</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">yaml</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">routingMode</span><span class="p">:</span><span class="w"> </span><span class="l">tunnel</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">tunnelProtocol</span><span class="p">:</span><span class="w"> </span><span class="l">vxlan</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kubeProxyReplacement</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;false&#34;</span></span></span></code></pre></div>
</div>
<p>The NixOS hosts keep their firewall and trust the Cilium interfaces:</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">nix</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nix" data-lang="nix"><span class="line"><span class="cl"><span class="n">networking</span><span class="o">.</span><span class="n">firewall</span><span class="o">.</span><span class="n">checkReversePath</span> <span class="o">=</span> <span class="no">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">networking</span><span class="o">.</span><span class="n">firewall</span><span class="o">.</span><span class="n">trustedInterfaces</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">&#34;cni+&#34;</span> <span class="p">];</span></span></span></code></pre></div>
</div>
<p><code>checkReversePath=false</code> removes the strict NixOS reverse-path chain that can drop VXLAN return traffic. It is necessary here, but it is not enough by itself.</p>
<h1 id="the-failure-that-cost-the-most-time">The failure that cost the most time</h1>
<p>VXLAN would not start. Cilium agents logged:</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">text</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">failed to setup vxlan tunnel device:
</span></span><span class="line"><span class="cl">  address already in use</span></span></code></pre></div>
</div>
<p>The old Flannel <code>flannel.1</code> interface was still up and owned UDP 8472. Removing it and restarting Cilium made VXLAN work immediately:</p>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">bash</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ip link del flannel.1
</span></span><span class="line"><span class="cl">kubectl -n kube-system rollout restart ds/cilium</span></span></code></pre></div>
</div>
<p>Native routing had hidden the problem because it does not use VXLAN. A oneshot service now removes a stale <code>flannel.1</code> before k3s starts.</p>
<h1 id="what-to-test-after-switching">What to test after switching</h1>
<div class="code-block">
  <div class="code-block-header">
    <span class="code-block-lang">bash</span>
    <button type="button" class="code-copy" aria-label="Copy code">
      <span class="code-copy-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
</span>
      <span class="code-copy-icon code-copy-icon-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
      <span class="code-copy-label">Copy</span>
    </button>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ip -d link show <span class="nb">type</span> vxlan
</span></span><span class="line"><span class="cl">ss -lunp <span class="p">|</span> grep <span class="m">8472</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl run nettest --image<span class="o">=</span>curlimages/curl:8.10.1 --restart<span class="o">=</span>Never --command -- <span class="se">\
</span></span></span><span class="line"><span class="cl">  sh -c <span class="s1">&#39;curl -s -o /dev/null -w &#34;%{http_code}\n&#34; https://example.com&#39;</span></span></span></code></pre></div>
</div>
<p>Then check:</p>
<ul>
<li>new pods reach <code>Running</code>;</li>
<li>pod DNS works;</li>
<li>external egress works;</li>
<li>ClusterIP services work;</li>
<li>public ingress and LAN hostPort still work;</li>
<li><code>cilium-dbg status</code> shows <code>Tunnel [vxlan]</code> and health is 3/3.</li>
</ul>
<p>The full failure path is in <a href="/posts/2026-10-19-cilium-vxlan-troubleshooting/">Cilium VXLAN troubleshooting: port 8472 already in use</a>
.</p>
<p>The earlier migration story is in <a href="/posts/2026-10-18-homelab-network-migration/">Homelab network migration</a>
.</p>
<h1 id="sources">Sources</h1>
<ul>
<li>Cilium routing concepts: <a href="https://docs.cilium.io/en/stable/network/concepts/routing/" target="_blank" rel="noopener noreferrer">https://docs.cilium.io/en/stable/network/concepts/routing/</a>
</li>
<li>Cilium masquerading: <a href="https://docs.cilium.io/en/stable/network/concepts/masquerading/" target="_blank" rel="noopener noreferrer">https://docs.cilium.io/en/stable/network/concepts/masquerading/</a>
</li>
<li>NixOS rpfilter issue: <a href="https://github.com/lucas-albers-lz4/rke2nixos/issues/10" target="_blank" rel="noopener noreferrer">https://github.com/lucas-albers-lz4/rke2nixos/issues/10</a>
</li>
<li>fwmark collision case: <a href="https://pokgak.xyz/notes/cilium-vxlan-tailscale-fwmark-collision/" target="_blank" rel="noopener noreferrer">https://pokgak.xyz/notes/cilium-vxlan-tailscale-fwmark-collision/</a>
</li>
</ul>
]]></content:encoded></item><item><title>Homelab network migration: NetworkPolicies, egress and Cilium</title><link>https://bismigel.dev/posts/2026-10-18-homelab-network-migration/</link><pubDate>Sun, 18 Oct 2026 18:00:00 -0400</pubDate><guid>https://bismigel.dev/posts/2026-10-18-homelab-network-migration/</guid><description/><content:encoded><![CDATA[<blockquote>
<p><strong>Building the lab: afterword.</strong> (Not part of the numbered series.) ← <a href="/posts/2026-10-18-homelab-epilogue/">Homelab epilogue: exposing services and publishing the repo</a>
</p>
</blockquote>
<p>The epilogue was about deciding what to show. This post is about changing the network underneath it.</p>
<p>I wanted to take the useful ideas from a smaller reference homelab and apply them to Bakuran: isolate application namespaces, close egress where possible, split the admin path from the public path, and add SLOs. I also wanted Cilium and Hubble for flow visibility.</p>
<p>Most of that is live. Cilium was the hard part, and it is live now. The failure and the fix are the most useful part of the story.</p>
<h1 id="what-the-network-looked-like-before">What the network looked like before</h1>
<pre class="mermaid" id="mermaid-0"></pre>
<script>(window.__mermaidSources ||= {})["mermaid-0"] = "flowchart LR\n  U[\"Internet\"] --\u003e CF[\"Cloudflare edge\u003cbr/\u003eTLS, Access, Tunnel\"]\n  CF --\u003e T[\"Tunnel\u003cbr/\u003e2 connectors\"]\n  T --\u003e I[\"ingress-nginx\"]\n  I --\u003e A[\"apps\"]\n  I --\u003e B[\"books\"]\n  I --\u003e D[\"default / Jellyfin\"]\n  I --\u003e DL[\"downloads\"]\n  I --\u003e H[\"homepage\"]\n  I --\u003e IM[\"immich\"]\n  I --\u003e K[\"karakeep\"]\n  I --\u003e M[\"media\"]\n  I --\u003e Z[\"agent\"]\n  A \u003c--\u003e B\n  A \u003c--\u003e M\n  M \u003c--\u003e DL\n  Z \u003c--\u003e M\n  LAN[\"LAN TV\"] --\u003e D";</script>

<p>The only boundary was the edge. Inside the cluster, every pod could reach every other pod. Jellyfin was reachable on the LAN through hostPort 8096.</p>
<h1 id="what-it-looks-like-now">What it looks like now</h1>
<pre class="mermaid" id="mermaid-1"></pre>
<script>(window.__mermaidSources ||= {})["mermaid-1"] = "flowchart LR\n  U[\"Internet\"] --\u003e CF[\"Cloudflare edge\u003cbr/\u003eTLS, Access, Tunnel\"]\n  CF --\u003e T[\"Tunnel\u003cbr/\u003e2 connectors\"]\n  T --\u003e I[\"ingress-nginx\"]\n  I --\u003e A[\"apps\"]\n  I --\u003e B[\"books\"]\n  I --\u003e D[\"default / Jellyfin\"]\n  I --\u003e DL[\"downloads\"]\n  I --\u003e H[\"homepage\"]\n  I --\u003e IM[\"immich\"]\n  I --\u003e K[\"karakeep\"]\n  I --\u003e M[\"media\"]\n  I --\u003e Z[\"agent\"]\n  M --\u003e B\n  M --\u003e D\n  M --\u003e DL\n  MON[\"monitoring\"] --\u003e D\n  MON --\u003e IM\n  Z --\u003e M\n  LAN[\"LAN 192.168.2.0/24\"] --\u003e|TCP 8096| D\n  X[\"other pods\"] -.-\u003e|denied| A";</script>

<p>Every application namespace now has an ingress-only NetworkPolicy. Same namespace and ingress-nginx are allowed. Cross-namespace rules are explicit. Egress is now default-deny as well, with DNS, local dependencies and public HTTP(S) allowed only where needed.</p>
<h1 id="the-one-shot-rollout">The one-shot rollout</h1>
<p>I did not roll this out one namespace at a time. I took a restore point first:</p>
<ul>
<li>git tags for the main repositories;</li>
<li>an etcd snapshot;</li>
<li>fresh credentials and appdata backups in R2.</li>
</ul>
<p>Then I applied the policies to the application namespaces in one pull request. The restore point was the permission to move quickly.</p>
<p>The first pilot was Karakeep. It had no Prometheus scrape annotations, no cross-namespace dependencies and no probes that could be blocked. The policy allowed same-namespace traffic and ingress-nginx only. The public page returned 200, Meilisearch worked, and a pod outside the namespace was refused.</p>
<p>Then the rest: apps, books, default, downloads, homepage, immich, karakeep, media and the agent.</p>
<h1 id="what-broke-and-what-it-taught-me">What broke, and what it taught me</h1>
<h2 id="jellyfin-on-the-lan">Jellyfin on the LAN</h2>
<p>The TV uses <code>http://192.168.2.81:8096</code>, not the public hostname. The first policy blocked the hostPort path. The fix was a narrow exception: the LAN CIDR, TCP 8096 only, in the <code>default</code> namespace. The public path still goes through the tunnel.</p>
<h2 id="login-watchers-and-nat">Login watchers and NAT</h2>
<p>The Immich and Jellyfin login watchers started failing after the rollout. Two things were happening:</p>
<ol>
<li>When a watcher landed on the media node, NAT masked the pod source IP. The destination policy saw a node IP, not the monitoring namespace.</li>
<li>A new pod could connect before kube-router had programmed the policy.</li>
</ol>
<p>The fix was to pin both watchers to the platform node and wait five seconds before the first query. They now complete on schedule.</p>
<h2 id="the-policy-programming-race">The policy programming race</h2>
<p>The first egress test from the <code>apps</code> namespace showed a false success. The pod started before the policy was programmed. A second test, after the policy had settled, was blocked. The lesson is simple: wait and test again before believing a policy is live.</p>
<h1 id="egress-not-just-ingress">Egress, not just ingress</h1>
<p>Ingress was the easy half. Egress has more destinations:</p>
<ul>
<li>DNS</li>
<li>the Kubernetes API</li>
<li>local services</li>
<li>public metadata and indexers</li>
<li>Ollama on the LAN</li>
<li>GitHub, DeepSeek, Telegram and Cloudflare</li>
</ul>
<p>I started with the static and API apps. They need DNS and same-namespace traffic, and nothing else. Then I moved to Karakeep and Immich. Karakeep kept Ollama and public HTTP(S); Immich kept public HTTP(S) for metadata and models. The media stack kept its local dependencies and public indexers. The downloaders kept public internet for trackers and Usenet.</p>
<p>Private ranges are denied except named dependencies. Egress is still open for the infrastructure namespaces; that is deliberate.</p>
<h1 id="slos">SLOs</h1>
<p>The monitoring stack now has seven public probes. The blackbox exporter runs inside the cluster and probes the public URL through Cloudflare and back through the tunnel. That measures the path a user takes, not just a pod.</p>
<p>The target is 99.5% availability over 7 days, with a 0.5% error budget. The recording rules track 5m, 1h, 6h and 7d ratios, and the alerting rules cover fast and slow burn. The Homelab SLOs dashboard shows the budget, per-target availability and probe duration.</p>
<p>The SLOs also caught a real bug: <code>alerts.yml</code> had been invalid since the Cloudflare alert group was added. Prometheus rejected the file, kept the old rules, and never loaded the edge or SLO alerts. The reload endpoint returned 500, but I had not checked it. Fixing the YAML indentation fixed both the edge alerts and the SLO rules.</p>
<h1 id="cilium-finally">Cilium, finally</h1>
<p>The Cilium failures were not a kernel bug. The old Flannel <code>flannel.1</code> interface was still up on every node and held UDP 8472. Cilium could not start its VXLAN device: <code>address already in use</code>. Native routing avoided the collision, so it looked like a kernel difference. After deleting the stale interface, VXLAN worked on the same kernel.</p>
<p>Production now uses VXLAN encapsulation with kube-proxy. The NixOS firewall trusts <code>cni+</code> and sets <code>checkReversePath=false</code>. A oneshot removes a stale <code>flannel.1</code> before k3s starts.</p>
<p>The full comparison is in <a href="/posts/2026-10-19-native-vs-encapsulation/">Native vs encapsulation</a>
. The failure path is in <a href="/posts/2026-10-19-cilium-vxlan-troubleshooting/">the VXLAN troubleshooting guide</a>
.</p>
<h1 id="lessons">Lessons</h1>
<ol>
<li><strong><strong>A CNI swap needs a disposable cluster.</strong></strong> Single-node validation is useful, but it does not prove a multi-node path.</li>
<li><strong><strong>Pod-to-pod working is not enough.</strong></strong> Test ClusterIP, NodePort, the API, external egress and hostPort.</li>
<li><strong><strong>CNI uninstall is not clean.</strong></strong> Check iptables, ip rules and BPF state before restoring the old CNI.</li>
<li><strong><strong>Egress is harder than ingress.</strong></strong> Ingress has a closed source set; egress has a long destination list.</li>
<li><strong><strong>HostPort needs explicit CNI support.</strong></strong> Cilium&rsquo;s CNI config needed <code>portmap</code> for the TV path.</li>
<li><strong><strong>Reboot order matters for NFS.</strong></strong> If <code>.81</code> boots before <code>.82</code>, the media branches fail to mount and the books pods stay in <code>ContainerCreating</code>.</li>
<li><strong><strong>A false-positive test is worse than no test.</strong></strong> Wait for policy programming before concluding that a rule works.</li>
<li><strong><strong>Validate alert rules before reloading.</strong></strong> A YAML error kept Prometheus on the old rule set and hid the new alerts.</li>
</ol>
<h1 id="what-is-next">What is next</h1>
<p>Cilium now runs with VXLAN, and the trust-zone edge is live. The remaining work is less about the packet path:</p>
<ul>
<li>finish the full DR rehearsal;</li>
<li>close the image pinning gap;</li>
<li>rotate the overprivileged Cloudflare token;</li>
<li>keep the backup and alert checks honest.</li>
</ul>
<p>The lab is no longer avoiding the network. It is running it.</p>
<blockquote>
<p><strong>Building the lab: afterword.</strong> Previous: <a href="/posts/2026-10-18-homelab-epilogue/">Homelab epilogue: exposing services and publishing the repo</a>
.</p>
</blockquote>
]]></content:encoded></item></channel></rss>