<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>OS on Zwindler's Reflection</title><link>https://blog.zwindler.fr/en/categories/systeme/</link><description>Recent content in OS on Zwindler's Reflection</description><generator>Hugo -- gohugo.io</generator><language>en</language><copyright>Licensed under CC BY-SA 4.0</copyright><lastBuildDate>Tue, 28 Apr 2026 10:00:00 +0200</lastBuildDate><atom:link href="https://blog.zwindler.fr/en/categories/systeme/index.xml" rel="self" type="application/rss+xml"/><item><title>Kubernetes UserNamespaces: the overhyped GA feature</title><link>https://blog.zwindler.fr/en/2026/04/28/kubernetes-usernamespaces-the-overhyped-ga-feature/</link><pubDate>Tue, 28 Apr 2026 10:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/en/2026/04/28/kubernetes-usernamespaces-the-overhyped-ga-feature/</guid><description>&lt;img src="https://blog.zwindler.fr/2026/04/usernamespaces.webp" alt="Featured image of post Kubernetes UserNamespaces: the overhyped GA feature" /&gt;&lt;h2 id="the-infographic-that-triggered-me"&gt;The infographic that triggered me
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Note: I stumbled upon this GenAI infographic and it was so bad I wrote a post about it. I didn&amp;rsquo;t generate this thing.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Over the past few days, LinkedIn has been flooded with the same kind of infographic. Kubernetes 1.36 is out, and one of the most talked-about features is the GA release of &lt;strong&gt;UserNamespaces&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a topic I&amp;rsquo;ve been following since 2018 (talk &lt;a class="link" href="https://blog.zwindler.fr/2018/05/03/recap-du-premier-jour-de-kubecon-europe-2018/" target="_blank" rel="noopener"
&gt;The Route to rootless containers&lt;/a&gt; at KubeCon EU 2018), so I&amp;rsquo;m genuinely glad to see this long journey finally reach the finish line. That said, I&amp;rsquo;m appalled by the way it&amp;rsquo;s being marketed on LinkedIn, apparently by people who have no idea how it actually works — and frankly don&amp;rsquo;t care.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;Kubernetes just made root safer. Just add &lt;code&gt;hostUsers: false&lt;/code&gt; to your Pod spec.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The visual: an all-powerful king &amp;ldquo;inside the container&amp;rdquo; and a helpless beggar &amp;ldquo;outside on the host&amp;rdquo;. The promise: &amp;ldquo;No Host Access. No Privilege Escalation. No Lateral Movement. No Node Takeover.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Catchy.&lt;/p&gt;
&lt;p&gt;But presenting it this way is genuinely dangerous, because it obscures entire areas of application and operational security. Selling &lt;code&gt;hostUsers: false&lt;/code&gt; as the universal fix for the &amp;ldquo;root in containers&amp;rdquo; problem is a dramatic oversimplification that will push teams to ignore the real security &lt;strong&gt;priorities&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="what-usernamespaces-actually-do"&gt;What UserNamespaces actually do
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;The threat model: container escape&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;First, what are we actually talking about? A &lt;strong&gt;container escape&lt;/strong&gt; is when an attacker manages to break out of their container and directly access the host&amp;rsquo;s kernel or filesystem — completely bypassing the normal isolation mechanisms.&lt;/p&gt;
&lt;p&gt;This type of vulnerability is &lt;strong&gt;rare&lt;/strong&gt;, but real-world examples exist:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a class="link" href="https://nvd.nist.gov/vuln/detail/CVE-2019-5736" target="_blank" rel="noopener"
&gt;CVE-2019-5736&lt;/a&gt;&lt;/strong&gt; (runc): write to &lt;code&gt;/proc/self/exe&lt;/code&gt; of the host process from inside the container&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a class="link" href="https://nvd.nist.gov/vuln/detail/CVE-2022-0492" target="_blank" rel="noopener"
&gt;CVE-2022-0492&lt;/a&gt;&lt;/strong&gt; (cgroups v1): escape via &lt;code&gt;unshare&lt;/code&gt; in certain configurations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a class="link" href="https://nvd.nist.gov/vuln/detail/CVE-2024-21626" target="_blank" rel="noopener"
&gt;CVE-2024-21626&lt;/a&gt;&lt;/strong&gt; (runc, &amp;ldquo;Leaky Vessels&amp;rdquo;): file descriptor leak to the host working directory&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If there&amp;rsquo;s a vulnerability of this type on your node AND a process is compromised AND it runs as root in the container AND it doesn&amp;rsquo;t use UserNamespaces, the attacker gets &lt;strong&gt;root on the host&lt;/strong&gt; — &lt;strong&gt;game over&lt;/strong&gt;. Full access to every file on the node, every secret mounted by other pods, ability to install a rootkit or exfiltrate data from all tenants running on that node.&lt;/p&gt;
&lt;p&gt;It remains possible, but that&amp;rsquo;s a lot of &amp;ldquo;ifs&amp;rdquo;. Either way, this is exactly the scenario UserNamespaces address. They introduce &lt;strong&gt;UID mapping&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UID 0 inside the container is mapped to an unprivileged UID on the host (e.g. 100000, unique per pod)&lt;/li&gt;
&lt;li&gt;If an attacker successfully escapes the container via a kernel exploit, they land as &lt;strong&gt;&lt;code&gt;nobody&lt;/code&gt;&lt;/strong&gt; on the node — the escape succeeds, but the post-escape impact is dramatically reduced&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the &amp;ldquo;Breakouts Lose Impact&amp;rdquo; scenario from the infographic, and on that point, &lt;strong&gt;the infographic is right&lt;/strong&gt;. That&amp;rsquo;s the real value of the feature.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Edge case: multi-tenancy even with non-root containers&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Even without root containers, UserNamespaces provide something extra in a &lt;strong&gt;truly multi-tenant&lt;/strong&gt; context (multiple customers on the same cluster). Without UserNamespaces, if two pods from different customers both run with &lt;code&gt;runAsUser: 1000&lt;/code&gt;, they share the same UID 1000 on the node. If one escapes, the attacker can access files from the other pod with the same owner. UserNamespaces, by assigning a unique UID offset per pod, isolates UIDs between pods even when they use the same value inside the container.&lt;/p&gt;
&lt;p&gt;For internal clusters where you control all workloads, this scenario is theoretical. For a multi-tenant SaaS platform or a public build service, it&amp;rsquo;s a real line of defense.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Technical requirements&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;There are a few prerequisites, but most up-to-date clusters should qualify.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linux kernel ≥ 5.19&lt;/li&gt;
&lt;li&gt;Compatible runtime (containerd ≥ 1.7, CRI-O ≥ 1.25)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Idmapped mounts&lt;/em&gt; support for persistent volumes (XFS, ext4 — not NFS in all cases)&lt;/li&gt;
&lt;li&gt;Kubernetes ≥ 1.33 (Beta), ≥ 1.36 (GA)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="what-the-infographic-exaggerates-and-leaves-out"&gt;What the infographic exaggerates (and leaves out)
&lt;/h2&gt;&lt;p&gt;The infographic is right about one specific thing: UserNamespaces reduces the impact of a successful container escape. That&amp;rsquo;s real. The problem is it sells the feature as a universal solution to &amp;ldquo;root in containers&amp;rdquo; — and that&amp;rsquo;s just wrong.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. UID isolation is not application privilege isolation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The infographic promises &amp;ldquo;No Lateral Movement&amp;rdquo;. That&amp;rsquo;s false — completely false.&lt;/p&gt;
&lt;p&gt;A root container with &lt;code&gt;hostUsers: false&lt;/code&gt; can still read the &lt;strong&gt;ServiceAccount Token&lt;/strong&gt; mounted at &lt;code&gt;/var/run/secrets/kubernetes.io/serviceaccount/token&lt;/code&gt;. If that token has broad RBAC permissions (which happens — we may cover this in a future post), the attacker can call the API Server, enumerate cluster resources, and move laterally — all without ever touching the host node.&lt;/p&gt;
&lt;p&gt;UID mapping protects the host. It does not protect the cluster.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. A root container is still root inside the container&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;ldquo;Install Anything ✅&amp;rdquo; — it&amp;rsquo;s literally written in the infographic, presented as a feature 😖.&lt;/p&gt;
&lt;p&gt;In a root container (even with UserNS), an attacker who gains control can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Install &lt;code&gt;nmap&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;nc&lt;/code&gt; to scan the internal network&lt;/li&gt;
&lt;li&gt;Modify application files, binaries, configurations&lt;/li&gt;
&lt;li&gt;Read all files mounted as volumes&lt;/li&gt;
&lt;li&gt;Persist in the container across restarts if the filesystem is writable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;UserNamespaces removes none of these attack vectors. The ability to install software is a fast track to lateral movement.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. It&amp;rsquo;s not that easy, especially for storage&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Enabling &lt;code&gt;hostUsers: false&lt;/code&gt; breaks existing storage in most cases.&lt;/p&gt;
&lt;p&gt;Container UID 0 is mapped to UID 100000+ on the host (each container has its own offset). If a persistent volume (NFS, EBS, Ceph RBD) is owned by UID 1000, the root container can&amp;rsquo;t read or write it. The result: counterintuitive &lt;code&gt;Permission Denied&lt;/code&gt; errors that are potentially hard to diagnose, since the application was likely never designed to be root yet have no access to its own files.&lt;/p&gt;
&lt;p&gt;The technical solution exists (&lt;em&gt;idmapped mounts&lt;/em&gt;), but it requires a recent kernel and a compatible filesystem. See the &lt;a class="link" href="https://www.kernel.org/doc/html/latest/filesystems/idmappings.html" target="_blank" rel="noopener"
&gt;official idmapped mounts documentation&lt;/a&gt; for details.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Same story, but for networking&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;hostUsers: false&lt;/code&gt; is incompatible with &lt;code&gt;hostNetwork: true&lt;/code&gt;. It&amp;rsquo;s a corner case, but it catches networking workloads (monitoring agents, CNI plugins, etc.).&lt;/p&gt;
&lt;p&gt;Note: that said, running containers with &lt;code&gt;hostNetwork&lt;/code&gt; is &lt;strong&gt;its own security problem&lt;/strong&gt;, so&amp;hellip;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="honest-comparison-userns-vs-the-real-alternatives"&gt;Honest comparison: UserNS vs the real alternatives
&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: left"&gt;Attack vector&lt;/th&gt;
&lt;th style="text-align: center"&gt;UserNS (root inside)&lt;/th&gt;
&lt;th style="text-align: center"&gt;Non-root (UID 1000)&lt;/th&gt;
&lt;th style="text-align: center"&gt;Distroless / Scratch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;Post-escape impact after successful container escape&lt;/td&gt;
&lt;td style="text-align: center"&gt;✅ Nobody on host&lt;/td&gt;
&lt;td style="text-align: center"&gt;⚠️ UID 1000 on host&lt;/td&gt;
&lt;td style="text-align: center"&gt;⚠️ UID 1000 on host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;UID isolation between pods (multi-tenant)&lt;/td&gt;
&lt;td style="text-align: center"&gt;✅ Unique offset per pod&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ Shared UID on node&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ Shared UID on node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;Malware installation inside the container&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ Trivial&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ Possible&lt;/td&gt;
&lt;td style="text-align: center"&gt;✅ Near impossible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;Write scope in ephemeral container FS&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ Full filesystem&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ App directory only&lt;/td&gt;
&lt;td style="text-align: center"&gt;✅ Near impossible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;Lateral movement via SA Token&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ Possible&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ Possible&lt;/td&gt;
&lt;td style="text-align: center"&gt;⚠️ Potentially difficult&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;Operational complexity&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ Sometimes high&lt;/td&gt;
&lt;td style="text-align: center"&gt;✅ Often near zero&lt;/td&gt;
&lt;td style="text-align: center"&gt;✅ Often low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;Compatibility with existing storage&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ Sometimes problematic&lt;/td&gt;
&lt;td style="text-align: center"&gt;✅ Standard&lt;/td&gt;
&lt;td style="text-align: center"&gt;✅ Standard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Reading the table reveals the true nature of UserNamespaces: it excels on &lt;strong&gt;exactly two rows&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;post-escape impact&lt;/li&gt;
&lt;li&gt;UID isolation in multi-tenant environments&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On everything else, non-root + distroless does better, or just as well, without the operational complexity. And that &amp;ldquo;everything else&amp;rdquo; — write scope in ephemeral FS, malware installation, lateral movement via SA Token — represents the vast majority of real-world attack vectors, far more common than a container escape. We&amp;rsquo;ll come back to this in the &lt;a class="link" href="#where-to-invest-your-security-budget" &gt;Where to invest your security budget&lt;/a&gt; section.&lt;/p&gt;
&lt;h2 id="real-use-cases"&gt;Real use cases
&lt;/h2&gt;&lt;p&gt;It would be dishonest to dismiss the feature entirely. There are three scenarios where UserNamespaces aren&amp;rsquo;t a lazy option but a genuine technical necessity (with caveats).&lt;/p&gt;
&lt;h3 id="1-build-as-a-service-buildah-rootless-podman"&gt;1. Build-as-a-Service (Buildah, rootless Podman)
&lt;/h3&gt;&lt;p&gt;To build a Docker image, the build engine needs to perform &lt;code&gt;chown&lt;/code&gt;, &lt;code&gt;chmod&lt;/code&gt; and &lt;code&gt;mknod&lt;/code&gt;. These operations require &lt;code&gt;CAP_CHOWN&lt;/code&gt; and &lt;code&gt;CAP_FOWNER&lt;/code&gt;. Before UserNamespaces, the solution was to run the pod as &lt;code&gt;--privileged&lt;/code&gt; — an obvious open door to the host.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;hostUsers: false&lt;/code&gt;, the build engine believes it&amp;rsquo;s root for its own file manipulation, but it can&amp;rsquo;t touch the host. This is the only case where &amp;ldquo;root inside&amp;rdquo; is a technical constraint rather than technical debt.&lt;/p&gt;
&lt;p&gt;Note: &lt;a class="link" href="https://github.com/GoogleContainerTools/kaniko" target="_blank" rel="noopener"
&gt;Kaniko&lt;/a&gt;, long the go-to for in-cluster builds, has been archived since October 2025 and no longer receives security updates. Buildah or rootless Podman are the active alternatives.&lt;/p&gt;
&lt;p&gt;My take: it can be useful for shared CI/CD platforms (GitLab Runners, Tekton) that refuse privileged pods. But if isolation is critical (public platform, aggressive multi-tenancy), microVMs (Kata Containers, Firecracker) offer far stronger guarantees for an overhead that has become quite manageable.&lt;/p&gt;
&lt;h3 id="2-hostile-multi-tenancy-user-code-platforms"&gt;2. Hostile multi-tenancy (user code platforms)
&lt;/h3&gt;&lt;p&gt;If your business is running code provided by strangers (PaaS, online code editors, public CI/CD), you know upfront that users &lt;em&gt;will&lt;/em&gt; try to escalate their privileges. In this context, UserNS is an extra barrier against kernel 0-days.&lt;/p&gt;
&lt;p&gt;My take: honestly, if the environment is truly &lt;strong&gt;hostile&lt;/strong&gt;, UserNS alone isn&amp;rsquo;t enough. MicroVMs (Kata Containers, Firecracker) provide real hardware isolation and are the right choice here. UserNS can be a complement, not a substitute.&lt;/p&gt;
&lt;h3 id="3-hard-coded-legacy-postfix-dovecot-bind"&gt;3. Hard-coded legacy (Postfix, Dovecot, BIND)
&lt;/h3&gt;&lt;p&gt;Some old UNIX daemons start as root to open a privileged port (&amp;lt; 1024) or read sensitive config files, then drop privileges via &lt;code&gt;setuid()&lt;/code&gt;. This mechanism fails in a classic non-root container.&lt;/p&gt;
&lt;p&gt;UserNamespaces let these processes believe they can make their identity management syscalls, because they are root inside their namespace.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a concrete example written by a colleague (thanks Louis 😘):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;v1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Pod&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;postfix&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;hostUsers: false # UID mapping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;root in container → nobody on host&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;securityContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runAsNonRoot: false # allowed under PSS Restricted *only* because of hostUsers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;fsGroup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;103&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# postfix GID&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;containers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;postfix&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;postfix:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;securityContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runAsNonRoot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# same — cf. https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/#integration-with-pod-security-admission-checks&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;allowPrivilegeEscalation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;readOnlyRootFilesystem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;seccompProfile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;RuntimeDefault&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;ALL&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# drop everything first&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;add&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;SETUID &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# privilege drop via setuid() done by postfix itself at startup&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;SETGID &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# same for groups&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;CHOWN &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# chown on mail queues at startup&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;FOWNER &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# file operations without being the owner&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;FSETID &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# preserve setuid bit after write&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;DAC_OVERRIDE # MANDATORY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;root in UserNS is not &amp;#34;real&amp;#34; root —&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# DAC checks are not automatically bypassed&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This manifest illustrates several important things.&lt;/p&gt;
&lt;p&gt;First, making a legacy application actually secure with UserNS is painful and requires compromises — especially around capabilities. This is a far cry from the &amp;ldquo;magic feature that secures root apps&amp;rdquo; the LinkedIn wanna-be influencers imply.&lt;/p&gt;
&lt;p&gt;Then there are some interesting surprises. Normally, the &lt;code&gt;Restricted&lt;/code&gt; Pod Security Standard forbids &lt;code&gt;runAsNonRoot: false&lt;/code&gt;. Kubernetes makes an exception when &lt;code&gt;hostUsers: false&lt;/code&gt; is present. This is documented &lt;a class="link" href="https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/#integration-with-pod-security-admission-checks" target="_blank" rel="noopener"
&gt;here&lt;/a&gt;. Without UserNamespaces, this pod would be rejected by the admission controller.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also the &lt;strong&gt;&lt;code&gt;DAC_OVERRIDE&lt;/code&gt; capability&lt;/strong&gt;, which is counterintuitive. Root in a UserNS is not real root from the kernel&amp;rsquo;s perspective for DAC (Discretionary Access Control) checks. When Postfix runs &lt;code&gt;set-permissions&lt;/code&gt; to &lt;code&gt;chown&lt;/code&gt; its queues, the kernel still verifies permissions — and denies them if &lt;code&gt;DAC_OVERRIDE&lt;/code&gt; isn&amp;rsquo;t present. This is exactly the kind of operational surprise that stays invisible until the first production deployment.&lt;/p&gt;
&lt;p&gt;Worth noting: we were still able to keep &lt;code&gt;readOnlyRootFilesystem: true&lt;/code&gt; and &lt;code&gt;allowPrivilegeEscalation: false&lt;/code&gt; — legacy doesn&amp;rsquo;t justify throwing everything overboard.&lt;/p&gt;
&lt;p&gt;My take: this is the only use case where UserNS is genuinely acceptable. No untrusted third-party code, no hostile platform — just well-identified legacy with a &lt;strong&gt;migration plan&lt;/strong&gt;. The other two cases are &amp;ldquo;acceptable under conditions&amp;rdquo;; the legacy case is the cleanest of the three.&lt;/p&gt;
&lt;h2 id="some-counterarguments"&gt;Some counterarguments
&lt;/h2&gt;&lt;p&gt;I see you coming with objections, so let&amp;rsquo;s save everyone some time with a quick Q&amp;amp;A:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;It&amp;rsquo;s defense in depth.&amp;rdquo;&lt;/strong&gt;
True — but defense in depth assumes the foundational layers are already in place. If you haven&amp;rsquo;t migrated your images to non-root yet, investing energy in UserNS is putting the cart before the horse. And once you&amp;rsquo;re non-root, the marginal gain of UserNS is negligible compared to the complexity it introduces.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;We don&amp;rsquo;t control third-party images.&amp;rdquo;&lt;/strong&gt;
Somewhat weak, in my opinion: if a proprietary vendor&amp;rsquo;s black-box image is hardcoded to run as root, there&amp;rsquo;s a good chance it either genuinely needs it (as is the case for some proprietary security tooling) or it will break with UID mapping (see the storage problem above). UserNS is not a magic wand that makes any third-party image compatible and secure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;It&amp;rsquo;s a centralized safeguard against human error.&amp;rdquo;&lt;/strong&gt;
It&amp;rsquo;s just as easy to forget &lt;code&gt;hostUsers: false&lt;/code&gt; as it is to forget &lt;code&gt;runAsNonRoot: true&lt;/code&gt;. The real centralized solution is &lt;strong&gt;Pod Security Standards&lt;/strong&gt; or an Admission Controller (Kyverno, OPA) that outright rejects root pods. Simpler, more reliable, and it doesn&amp;rsquo;t break storage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;We need it for SOC2/PCI-DSS/&amp;hellip; compliance.&amp;rdquo;&lt;/strong&gt;
If your compliance requires strict tenant isolation, UserNS will likely be deemed insufficient by your auditors. VMs or microVMs remain the gold standard. Using UserNS for compliance means choosing the most complex tool to maintain for a result that remains debatable.&lt;/p&gt;
&lt;h2 id="where-to-invest-your-security-budget"&gt;Where to invest your security budget
&lt;/h2&gt;&lt;p&gt;Setting aside the marketing, here&amp;rsquo;s where effort actually pays off — from highest impact to most niche:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Priority 1 — Non-root images + &lt;code&gt;nobody&lt;/code&gt; (UID 65534)&lt;/strong&gt;
Move images to non-root, ideally using the &lt;code&gt;nobody&lt;/code&gt; user (the least privileged on the system). If an application is compromised under &lt;code&gt;nobody&lt;/code&gt;, the attacker can do almost nothing, even on the container filesystem. Combine with &lt;code&gt;readOnlyRootFilesystem: true&lt;/code&gt; and &lt;code&gt;capabilities: drop: [&amp;quot;ALL&amp;quot;]&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;securityContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runAsNonRoot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runAsUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;65534&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# nobody&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;seccompProfile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;RuntimeDefault&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;containers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;app&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;my-app:distroless&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;securityContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;allowPrivilegeEscalation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;ALL&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;readOnlyRootFilesystem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Priority 2 — Pod Security Standards (PSS) at &lt;code&gt;Baseline&lt;/code&gt; or &lt;code&gt;Restricted&lt;/code&gt;&lt;/strong&gt;
Block root and privileges without breaking anything at the infra level. It requires having done Priority 1 first, but it&amp;rsquo;s free, standard, and applies cluster-wide via a namespace label (with per-namespace overrides when needed). No more risk of forgetting. Already enabled by default on several Kubernetes distributions (Talos being one, but not the only one).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Priority 3 — MicroVMs (Kata Containers, Firecracker)&lt;/strong&gt;
For truly untrusted workloads. Real hardware isolation, overhead now quite reasonable on recent generations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Priority 4 — UserNamespaces&lt;/strong&gt;
When all else fails. Only for the legitimate cases identified above (builds, legacy, hostile multi-tenancy). This is genuinely the &lt;strong&gt;last&lt;/strong&gt; thing to do.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;Kubernetes 1.36 UserNamespaces are the result of a project that officially took five years (KEP-127 dates back to 2021) and has been discussed since nearly the dawn of Kubernetes. For shared build platforms and multi-tenant SaaS running user-provided code, it&amp;rsquo;s a potentially useful building block — particularly to prevent one customer&amp;rsquo;s app from reading another&amp;rsquo;s in the event of a container escape without privilege escalation.&lt;/p&gt;
&lt;p&gt;For everything else — that is to say, 99% of production clusters — that&amp;rsquo;s not where container security starts. And that&amp;rsquo;s precisely the problem with this kind of infographic.&lt;/p&gt;
&lt;p&gt;LinkedIn infographics selling effortless security are dangerous: &amp;ldquo;keep your 800MB root image full of tools, just add &lt;code&gt;hostUsers: false&lt;/code&gt;, and you&amp;rsquo;re protected.&amp;rdquo; That&amp;rsquo;s exactly &lt;strong&gt;the wrong approach&lt;/strong&gt;. Real container security is built in the Dockerfile, not in the PodSpec.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you&amp;rsquo;re enabling UserNamespaces to secure an application whose source code you own, you&amp;rsquo;ve probably missed a step in your secure software development lifecycle.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="references"&gt;References
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/127-user-namespaces/README.md" target="_blank" rel="noopener"
&gt;KEP-127: Support for User Namespaces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/" target="_blank" rel="noopener"
&gt;Kubernetes docs — User Namespaces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://kubernetes.io/docs/concepts/security/pod-security-standards/" target="_blank" rel="noopener"
&gt;Pod Security Standards&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://nvd.nist.gov/vuln/detail/CVE-2019-5736" target="_blank" rel="noopener"
&gt;CVE-2019-5736&lt;/a&gt; — runc: write to host&amp;rsquo;s &lt;code&gt;/proc/self/exe&lt;/code&gt; from inside the container&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://nvd.nist.gov/vuln/detail/CVE-2022-0492" target="_blank" rel="noopener"
&gt;CVE-2022-0492&lt;/a&gt; — cgroups v1: escape via &lt;code&gt;unshare&lt;/code&gt;, UserNS helps but &lt;code&gt;runAsNonRoot&lt;/code&gt; does too&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://nvd.nist.gov/vuln/detail/CVE-2024-21626" target="_blank" rel="noopener"
&gt;CVE-2024-21626&lt;/a&gt; — runc &amp;ldquo;Leaky Vessels&amp;rdquo;: file descriptor leak to host working directory&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/GoogleContainerTools/distroless" target="_blank" rel="noopener"
&gt;Distroless containers — GoogleContainerTools&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Installing Alpine Linux in Headless Mode on Raspberry Pi</title><link>https://blog.zwindler.fr/en/2025/12/23/installing-alpine-linux-in-headless-mode-on-raspberry-pi/</link><pubDate>Tue, 23 Dec 2025 08:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/en/2025/12/23/installing-alpine-linux-in-headless-mode-on-raspberry-pi/</guid><description>&lt;img src="https://blog.zwindler.fr/2025/12/rpizero.webp" alt="Featured image of post Installing Alpine Linux in Headless Mode on Raspberry Pi" /&gt;&lt;h2 id="introduction"&gt;Introduction
&lt;/h2&gt;&lt;p&gt;For an article that might be published in print (fingers crossed), I needed to test different OS types to install on a Raspberry Pi Zero 2 W.&lt;/p&gt;
&lt;p&gt;Since I was unfamiliar with both Alpine and the specifics of the Pi Zero 2 W, I came across quite a few things I didn&amp;rsquo;t know.&lt;/p&gt;
&lt;p&gt;Even though the print article ultimately doesn&amp;rsquo;t cover Alpine Linux installation (or even use it), I thought it would be a shame not to publish this Alpine + RPi Zero experience, so here it is.&lt;/p&gt;
&lt;h2 id="why-alpine-linux"&gt;Why Alpine Linux?
&lt;/h2&gt;&lt;p&gt;Mainly for its lightness and low resource consumption. On a nano-computer with only 512 MB of RAM, every megabyte counts. Alpine Linux is known for being minimalist and also somewhat controversial (especially in the Docker world) for replacing glibc with musl (which sometimes causes surprises).&lt;/p&gt;
&lt;p&gt;The goal of this article is to have a headless installation (without screen or keyboard) accessible via SSH through WiFi, with maximum RAM optimization.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ll see lots of interesting technical details, especially about GPU RAM management and swap configuration with zswap!&lt;/p&gt;
&lt;h2 id="downloading-the-alpine-linux-image-for-raspberry-pi"&gt;Downloading the Alpine Linux Image for Raspberry Pi
&lt;/h2&gt;&lt;p&gt;The first thing to do is download the image before copying it to a microSD card that will be inserted into the RPi before boot.&lt;/p&gt;
&lt;p&gt;Fortunately, Alpine Linux offers specific images for Raspberry Pi, so we won&amp;rsquo;t need to manually add the RPi firmware.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/12/alpine-pi.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ll retrieve the latest available version from the &lt;a class="link" href="https://www.alpinelinux.org/downloads/" target="_blank" rel="noopener"
&gt;official website&lt;/a&gt;. Example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;wget https://dl-cdn.alpinelinux.org/alpine/v3.23/releases/aarch64/alpine-rpi-3.23.2-aarch64.tar.gz
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This image is specially compiled for the ARM64 (aarch64) architecture of recent Raspberry Pis (so my RPi Zero 2 W).&lt;/p&gt;
&lt;h2 id="headless-configuration-with-overlay-file"&gt;Headless Configuration with Overlay File
&lt;/h2&gt;&lt;p&gt;Once the image is flashed onto your microSD card (with Raspberry Pi Imager for example), we&amp;rsquo;ll prepare the system for headless access.&lt;/p&gt;
&lt;p&gt;Alpine Linux supports what are called &amp;ldquo;overlay files&amp;rdquo; that allow you to preconfigure the system before first boot. This is documented in the &lt;a class="link" href="https://wiki.alpinelinux.org/wiki/Installation_on_a_headless_host#Headless_bootstrap_overlay_file" target="_blank" rel="noopener"
&gt;official documentation on headless installation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Mount the contents of your microSD card on your machine and place the overlay file at the root of this microSD card:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;wget https://github.com/macmpi/alpine-linux-headless-bootstrap/raw/469ee440e7d394cca0976c78f357e7a0e1c82cc4/headless.apkovl.tar.gz
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note: it&amp;rsquo;s quite &lt;strong&gt;scary&lt;/strong&gt; to mount such a sensitive file from a third-party GitHub repo (github.com/macmpi), but Alpine Linux is reassuring by telling us:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: The author of above repo macmpi also maintains a number of packages for Alpine Linux.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I&amp;rsquo;m not much more reassured&amp;hellip; but well, it&amp;rsquo;s &amp;ldquo;official&amp;rdquo;&lt;/p&gt;
&lt;p&gt;This &lt;code&gt;headless.apkovl.tar.gz&lt;/code&gt; file configures Alpine to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Boot in DHCP mode&lt;/li&gt;
&lt;li&gt;Enable SSH automatically&lt;/li&gt;
&lt;li&gt;Allow root login without password (be careful not to leave it like that for too long!)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the Raspberry Pi to automatically connect to your WiFi network, create a &lt;code&gt;wpa_supplicant.conf&lt;/code&gt; file at the root of the microSD:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat &amp;gt; wpa_supplicant.conf &lt;span class="s"&gt;&amp;lt;&amp;lt; EOF
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;country=FR
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;network={
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; key_mgmt=WPA-PSK
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; ssid=&amp;#34;mySSID&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; psk=&amp;#34;myPassPhrase&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/12/alpine-root-headless.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Obviously replace &lt;code&gt;mySSID&lt;/code&gt; and &lt;code&gt;myPassPhrase&lt;/code&gt; with your WiFi information.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The alpine-linux-headless-bootstrap project offers other very useful configuration options, such as setting a root password, a static IP address, etc. More info in &lt;a class="link" href="https://github.com/macmpi/alpine-linux-headless-bootstrap/?tab=readme-ov-file#extra-configuration" target="_blank" rel="noopener"
&gt;their README&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="first-boot-and-permanent-installation"&gt;First Boot and Permanent Installation
&lt;/h2&gt;&lt;p&gt;Once the first boot is complete, connect via SSH:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh root@&amp;lt;raspberry_ip&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/12/alpine-headless-boot.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Warning&lt;/strong&gt;: By default, the root password is empty! But we don&amp;rsquo;t need to change that if we perform the following installation &lt;strong&gt;immediately&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;At this stage, Alpine runs entirely in RAM (&amp;ldquo;live&amp;rdquo; image). To install it permanently, we&amp;rsquo;ll use the &lt;code&gt;setup-alpine&lt;/code&gt; utility whose options are described in &lt;a class="link" href="https://wiki.alpinelinux.org/wiki/Installation#Base_configuration" target="_blank" rel="noopener"
&gt;wiki.alpinelinux.org/wiki/Installation&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;setup-alpine
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here&amp;rsquo;s a small excerpt of the requested information (all quite standard for an OS install):&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/12/alpine-setup-1.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/12/alpine-setup-2.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Follow the instructions, then reboot:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;reboot
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="gpu-ram-optimization"&gt;GPU RAM Optimization
&lt;/h2&gt;&lt;p&gt;By default on a Raspberry Pi, a (large) part of the RAM is allocated to the GPU for graphics processing. On a headless server, this is absolutely useless.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s first check the available RAM before optimization:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;free -m
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; total used free shared buff/cache available
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Mem: &lt;span class="m"&gt;417&lt;/span&gt; &lt;span class="m"&gt;37&lt;/span&gt; &lt;span class="m"&gt;325&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;55&lt;/span&gt; &lt;span class="m"&gt;369&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Swap: &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Only 417 MB available out of the theoretical 512 MB. Let&amp;rsquo;s see how to recover those precious megabytes.&lt;/p&gt;
&lt;p&gt;Ideally, we want to set the &lt;code&gt;gpu_mem=16&lt;/code&gt; option in &lt;code&gt;/boot/config.txt&lt;/code&gt; to allocate only 16 MB to the GPU (we can&amp;rsquo;t set less, and it must be multiples of 16).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;But be careful&lt;/strong&gt;, there&amp;rsquo;s a trap!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/12/nouvelle_regle.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;(a french mème, sorry english speakers)&lt;/p&gt;
&lt;p&gt;When you set &lt;code&gt;gpu_mem&lt;/code&gt; to 16 MB, the Raspberry Pi bootloader attempts to load lightweight firmware files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;start_cd.elf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fixup_cd.dat&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Except these files are not present on your boot partition. If we just put &lt;code&gt;gpu_mem=16&lt;/code&gt; in &lt;code&gt;/boot/config.txt&lt;/code&gt; and reboot, the Pi will refuse to start (ACT LED usually blinking 4 times).&lt;/p&gt;
&lt;h3 id="solutions-to-minimize-gpu-ram"&gt;Solutions to Minimize GPU RAM
&lt;/h3&gt;&lt;p&gt;You have two options:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Option 1&lt;/strong&gt;: Set &lt;code&gt;gpu_mem=32&lt;/code&gt; in &lt;code&gt;/boot/config.txt&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This is the easy solution (who said lazy?), there&amp;rsquo;s nothing to do. You still recover 32 MB of RAM.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Option 2&lt;/strong&gt;: Download the lightweight firmware files (the one I prefer)&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re not short on a few MB on the microSD card, which is not the case for RAM, where every MB counts.&lt;/p&gt;
&lt;p&gt;If you really want to go down to 16 MB, retrieve the missing files:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /boot
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;wget https://raw.githubusercontent.com/raspberrypi/firmware/master/boot/start_cd.elf
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;wget https://raw.githubusercontent.com/raspberrypi/firmware/master/boot/fixup_cd.dat
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then modify &lt;code&gt;/boot/config.txt&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[...]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;gpu_mem&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;16&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;start_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;start_cd.elf &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;fixup_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;fixup_cd.dat&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After reboot, check the gain:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;free -m
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; total used free shared buff/cache available
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Mem: &lt;span class="m"&gt;465&lt;/span&gt; &lt;span class="m"&gt;37&lt;/span&gt; &lt;span class="m"&gt;393&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;35&lt;/span&gt; &lt;span class="m"&gt;416&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Swap: &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We go from 417 MB to 465 MB available! That&amp;rsquo;s almost 50 MB recovered, representing about 10% additional RAM. On such a constrained system, this is significant.&lt;/p&gt;
&lt;h2 id="swap-configuration-with-zswap"&gt;Swap Configuration with zswap
&lt;/h2&gt;&lt;p&gt;On a system with only 512 MB of RAM, it&amp;rsquo;s highly advisable to configure swap. However, Alpine Linux doesn&amp;rsquo;t configure it by default. I&amp;rsquo;ll configure one here with &lt;a class="link" href="https://docs.kernel.org/admin-guide/mm/zswap.html" target="_blank" rel="noopener"
&gt;zswap&lt;/a&gt;, which compresses memory pages before writing them to disk (which also limits the size of accesses to the microSD card).&lt;/p&gt;
&lt;h3 id="creating-the-swapfile"&gt;Creating the Swapfile
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Create a 2 GB swapfile&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fallocate -l 2G /swapfile
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;600&lt;/span&gt; /swapfile
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkswap /swapfile
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;swapon /swapfile
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="persistent-configuration"&gt;Persistent Configuration
&lt;/h3&gt;&lt;p&gt;To make the swapfile and zswap persistent across reboots, create &lt;code&gt;/etc/fstab&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/swapfile none swap sw 0 0&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; /etc/fstab
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Create a startup script for Alpine:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat &amp;gt; /etc/local.d/swap.start &lt;span class="s"&gt;&amp;lt;&amp;lt; &amp;#39;EOF&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;#!/bin/sh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;swapon /swapfile
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod +x /etc/local.d/swap.start
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rc-update add &lt;span class="nb"&gt;local&lt;/span&gt; default
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Finally, edit &lt;code&gt;/boot/cmdline.txt&lt;/code&gt; and add at the end (on a single line):&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;zswap.enabled=1 zswap.compressor=lz4 zswap.max_pool_percent=20 zswap.accept_threshold_percent=85 zswap.shrinker_enabled=Y
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Some explanations on these parameters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;compressor=lz4&lt;/code&gt;: uses lz4, fast and efficient for compression&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max_pool_percent=20&lt;/code&gt;: zswap won&amp;rsquo;t use more than 20% of RAM (~93 MB) as compression cache before writing to the disk swapfile&lt;/li&gt;
&lt;li&gt;&lt;code&gt;accept_threshold_percent=85&lt;/code&gt;: only accepts compression if the expected compression rate is at least 85%&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shrinker_enabled=Y&lt;/code&gt;: allows automatic freeing of compressed memory if necessary&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The principle: zswap acts as a fast cache in RAM (compressed) in front of the 2 GB swapfile on disk. Pages are first compressed in zswap, then evacuated to disk if necessary.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;Not really a conclusion to this article, actually.&lt;/p&gt;
&lt;p&gt;I hit quite a few &amp;ldquo;walls&amp;rdquo;, not knowing how to use Alpine (beyond the uses I had made of it with Docker), and I discovered some subtleties related to the Raspberry Pi Zero firmware that I&amp;rsquo;ve been tinkering with for a few weeks now.&lt;/p&gt;
&lt;p&gt;Hence this article ;-)&lt;/p&gt;
&lt;p&gt;Happy hacking! 🚀&lt;/p&gt;</description></item><item><title>Proxmox VE 8 Tutorial - SDN in VXLAN Mode with Machines on the Internet</title><link>https://blog.zwindler.fr/en/2025/03/30/proxmox-ve-8-tutorial-sdn-in-vxlan-mode-with-machines-on-the-internet/</link><pubDate>Sun, 30 Mar 2025 12:30:00 +0200</pubDate><guid>https://blog.zwindler.fr/en/2025/03/30/proxmox-ve-8-tutorial-sdn-in-vxlan-mode-with-machines-on-the-internet/</guid><description>&lt;img src="https://blog.zwindler.fr/2025/03/vxlan2.webp" alt="Featured image of post Proxmox VE 8 Tutorial - SDN in VXLAN Mode with Machines on the Internet" /&gt;&lt;h2 id="preamble"&gt;Preamble
&lt;/h2&gt;&lt;p&gt;This tutorial is a kind of &lt;em&gt;special edition&lt;/em&gt; in my series of articles on Proxmox VE 8.&lt;/p&gt;
&lt;p&gt;In the &lt;a class="link" href="https://blog.zwindler.fr/en/2025/03/25/deploying-a-proxmox-ve-8-cluster-on-dedicated-servers-3/4/" &gt;third part which came out a few days ago&lt;/a&gt;, we stopped at a cluster of remote machines on the Internet.&lt;/p&gt;
&lt;p&gt;They&amp;rsquo;re not in the same LAN, and to be honest, they&amp;rsquo;re even about 100ms apart (and it works very well).&lt;/p&gt;
&lt;p&gt;At the end of the article, it works, we have a single network on our two machines, with a functional IPAM and DHCP on both sides. But still, virtual machines on different hosts can&amp;rsquo;t talk to each other directly and it&amp;rsquo;s still a bit unfortunate not to have gone all the way.&lt;/p&gt;
&lt;p&gt;Rather than rewriting that part, I decided to make a small aside where I finish the work and test the VXLAN function of the Proxmox VE SDN, because YES, it can work all the way.&lt;/p&gt;
&lt;h2 id="prerequisites"&gt;Prerequisites
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;m assuming you already have two (or more) machines within the same Proxmox VE 8 cluster.&lt;/p&gt;
&lt;p&gt;If not, you&amp;rsquo;re in for reading my series of articles (currently being written) on the subject (have fun!):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/en/2025/01/27/deploying-a-proxmox-ve-8-cluster-on-dedicated-servers-1/2/" &gt;Deploying a Proxmox VE 8 Cluster on Dedicated Servers (1/4)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These machines are not in the same LAN (otherwise no need to bother with VXLAN, the &lt;a class="link" href="https://blog.zwindler.fr/en/2025/03/25/deploying-a-proxmox-ve-8-cluster-on-dedicated-servers-3/4/" &gt;Simple SDN can be enough, as we did in part 3&lt;/a&gt;)&lt;/p&gt;
&lt;h2 id="problem"&gt;Problem
&lt;/h2&gt;&lt;p&gt;What we want to do is create an SDN that goes through the Internet, to the point that virtual machines believe they&amp;rsquo;re on the same LAN.&lt;/p&gt;
&lt;p&gt;We can&amp;rsquo;t do this with Simple SDN, VMs on one host can&amp;rsquo;t contact VMs on another distant host on the Internet, even being in the same Simple SDN and with the same addressing plan.&lt;/p&gt;
&lt;p&gt;But if we read a bit further in the &lt;a class="link" href="https://pve.proxmox.com/pve-docs/chapter-pvesdn.html" target="_blank" rel="noopener"
&gt;PVE documentation on SDN&lt;/a&gt;, we see that this is precisely the use case highlighted for using VXLAN type SDNs:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The VXLAN plugin establishes a tunnel (overlay) on top of an existing network (underlay). This encapsulates layer 2 Ethernet frames within layer 4 UDP datagrams&amp;hellip;
[&amp;hellip;]
You can, for example, create a VXLAN overlay network on top of public internet, appearing to the VMs as if they share the same local Layer 2 network&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Small issue however, in reality, you&amp;rsquo;d better not do it, because frames are not encrypted, and on the Internet, it&amp;rsquo;s really not the best idea in the world&amp;hellip;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Warning VXLAN on its own does does not provide any encryption. When joining multiple sites via VXLAN, make sure to establish a secure connection between the site, for example by using a site-to-site VPN.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;As an alternative, we could have gone directly with the most complete/complex version of the Proxmox VE SDN: &lt;strong&gt;EVPN Zones&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The EVPN zone creates a routable Layer 3 network, capable of spanning across multiple clusters. This is achieved by establishing a VPN and utilizing BGP as the routing protocol.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But I don&amp;rsquo;t want to mess with BGP here and I want to keep things simple. However, it&amp;rsquo;s probably the &amp;ldquo;cleanest&amp;rdquo; option for a production setup.&lt;/p&gt;
&lt;h2 id="vxlan-then-but-with-a-vpn"&gt;VXLAN Then, But with a VPN
&lt;/h2&gt;&lt;p&gt;I won&amp;rsquo;t overthink it, we&amp;rsquo;ll set up a small VPN with wireguard manually. There are tons of VPN tools on the market and tons of tools to make life easier. If you want more info on Wireguard, I&amp;rsquo;m sure you&amp;rsquo;ll find plenty, I&amp;rsquo;m just putting the bare minimum here so it &amp;ldquo;just works&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;On both servers, install wireguard, which should be in the default debian repositories:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt install -y wireguard
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Generate a key pair &lt;strong&gt;on each machine&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;wg genkey &lt;span class="p"&gt;|&lt;/span&gt; tee privatekey &lt;span class="p"&gt;|&lt;/span&gt; wg pubkey &amp;gt; publickey
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A little mental gymnastics, we&amp;rsquo;re going to write a configuration file with for each server, the private key of the server (file &lt;code&gt;privatekey&lt;/code&gt;) we&amp;rsquo;re on, but the public key (file &lt;code&gt;publickey&lt;/code&gt;) &lt;strong&gt;of the remote server&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;(When you think about it, it&amp;rsquo;s obvious, but I prefer to insist)&lt;/p&gt;
&lt;p&gt;And you obviously need to change addresses and network interfaces depending on which server you&amp;rsquo;re on so as not to have conflicts.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;vi /etc/wireguard/wg0.conf
[Interface]
Address = 10.10.30.1/24
ListenPort = 51820
PrivateKey = &amp;lt;local server private key&amp;gt;
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o &amp;lt;network interface name&amp;gt; -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o &amp;lt;network interface name&amp;gt; -j MASQUERADE
[Peer]
PublicKey = &amp;lt;remote server public key&amp;gt;
Endpoint = &amp;lt;remote server public IP&amp;gt;:51820
AllowedIPs = 10.10.30.2/32
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note: you can also add more nodes to the VPN, just add more &amp;ldquo;Peer&amp;rdquo; sections.&lt;/p&gt;
&lt;p&gt;Finish by starting the VPN and verifying that nodes talk to each other:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; wg-quick@wg0 --now
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ping 10.10.30.1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ping 10.10.30.2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="sdn-configuration"&gt;SDN Configuration
&lt;/h2&gt;&lt;p&gt;Normally, if you followed the previous articles, you already have the prerequisites, but for those who haven&amp;rsquo;t, I&amp;rsquo;ll repeat that in the /etc/network/interfaces file, it&amp;rsquo;s necessary to add at the end of the file the following line (followed by a small &lt;code&gt;systemctl restart networking&lt;/code&gt;)&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;source /etc/network/interfaces.d/*
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then, in the Datacenter menu, open the SDN submenu.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/sdn1.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;In the SDN/Zones menu, add a &amp;ldquo;VXLan&amp;rdquo; type zone. Two important points here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;we need to fill in the list of all &amp;ldquo;peers&amp;rdquo;, here the IPs of our VPNs so 10.10.30.1 and 10.10.30.2&lt;/li&gt;
&lt;li&gt;we&amp;rsquo;ll need to adjust the MTU. By default, a frame is 1500. Except that wireguard for its encapsulation has reduced the MTU to 1420 and VXLAN encapsulation takes 50. So we need to set 1370 to best avoid fragmentation, at worst, dropped packets&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/vxlan2.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;If you just read the previous article, you&amp;rsquo;ll notice that here, there&amp;rsquo;s no &amp;ldquo;checkbox&amp;rdquo; to enable DHCP on the zone. Unfortunately, that&amp;rsquo;s because it&amp;rsquo;s not available yet.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Currently only Simple Zones have support for automatic DHCP&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Source: &lt;a class="link" href="https://pve.proxmox.com/pve-docs/chapter-pvesdn.html#pvesdn_config_dhcp" target="_blank" rel="noopener"
&gt;pve.proxmox.com/pve-docs/chapter-pvesdn.html#pvesdn_config_dhcp&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/hulk_sad.gif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Once you&amp;rsquo;ve validated, go to the SDN/VNets submenu, click the &amp;ldquo;Create&amp;rdquo; button to create a virtual network, then select it once created to create a subnet.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/vxlan4.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/vxlan5.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Once again, don&amp;rsquo;t forget to check the DHCP section, to declare the range for our future DHCP.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/vxlan6.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Once we&amp;rsquo;ve validated everything, we see that next to all these new things we&amp;rsquo;ve just created, there&amp;rsquo;s an icon with two yellow arrows and a &amp;ldquo;New&amp;rdquo; status.&lt;/p&gt;
&lt;p&gt;To deploy these changes, we need to go back to the SDN menu from the beginning, and click Apply. Two &amp;ldquo;vxlan1&amp;rdquo; lines (one for each server, actually) should appear.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/vxlan7.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="is-that-all"&gt;Is That All?
&lt;/h2&gt;&lt;p&gt;Yes, that&amp;rsquo;s all, honestly, I really should have pushed a bit more before posting my previous article&amp;hellip;&lt;/p&gt;
&lt;p&gt;To convince yourself, just pop two LXC containers (without DHCP 🥲), one on each server, and see if they ping each other.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/vxlan-test.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;It works!&lt;/p&gt;
&lt;p&gt;Well&amp;hellip; all that&amp;rsquo;s left is to migrate all my existing machines to the VXLAN vnet to finally have a cluster where VMs can see each other!!&lt;/p&gt;</description></item><item><title>Deploying a Proxmox VE 8 Cluster on Dedicated Servers (3/4)</title><link>https://blog.zwindler.fr/en/2025/03/25/deploying-a-proxmox-ve-8-cluster-on-dedicated-servers-3/4/</link><pubDate>Tue, 25 Mar 2025 12:30:00 +0200</pubDate><guid>https://blog.zwindler.fr/en/2025/03/25/deploying-a-proxmox-ve-8-cluster-on-dedicated-servers-3/4/</guid><description>&lt;img src="https://blog.zwindler.fr/2025/03/sdn2.webp" alt="Featured image of post Deploying a Proxmox VE 8 Cluster on Dedicated Servers (3/4)" /&gt;&lt;p&gt;Note - this article is part of a series:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/en/2025/01/27/deploying-a-proxmox-ve-8-cluster-on-dedicated-servers-1/2/" &gt;Deploying a Proxmox VE 8 Cluster on Dedicated Servers (1/4)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/en/2025/02/17/deploying-a-proxmox-ve-8-cluster-on-dedicated-servers-2/3/" &gt;Deploying a Proxmox VE 8 Cluster on Dedicated Servers (2/4)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/en/2025/03/25/deploying-a-proxmox-ve-8-cluster-on-dedicated-servers-3/4/" &gt;Deploying a Proxmox VE 8 Cluster on Dedicated Servers (3/4)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/en/2025/03/30/proxmox-ve-8-tutorial-sdn-in-vxlan-mode-with-machines-on-the-internet/" &gt;Special Edition - SDN in VXLAN Mode with Machines on the Internet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;And &lt;em&gt;at least&lt;/em&gt; one more to come&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="picking-up-where-we-left-off-first-steps-in-the-ui"&gt;Picking Up Where We Left Off: First Steps in the UI
&lt;/h2&gt;&lt;p&gt;In the previous tutorial (which I invite you to read, if you haven&amp;rsquo;t already, otherwise this won&amp;rsquo;t be clear), we configured storage, networking, and deployed our first virtual machine to make sure everything works. But there&amp;rsquo;s still quite a bit to explore, including clustering, the Proxmox VE SDN, the built-in firewall, backups, monitoring&amp;hellip;&lt;/p&gt;
&lt;p&gt;So we&amp;rsquo;re picking up where we left off, and we&amp;rsquo;re going to install another server (hard to make a cluster with a single machine).&lt;/p&gt;
&lt;p&gt;This server was reinstalled with the same procedures as the server I&amp;rsquo;m talking about in parts 1 and 2, except that we don&amp;rsquo;t need to create the bridge (vmbr0) because we&amp;rsquo;re going to use the SDN!!&lt;/p&gt;
&lt;p&gt;Ideally, all machines in the cluster should have the same storage pools since I remind you, this configuration is supposed to be common to all servers, as it&amp;rsquo;s managed at the &amp;ldquo;Datacenter&amp;rdquo; level of our cluster (even if you can configure exceptions).&lt;/p&gt;
&lt;h2 id="creating-the-cluster"&gt;Creating the Cluster
&lt;/h2&gt;&lt;p&gt;Clustering Proxmox VE machines is a topic I&amp;rsquo;ve covered extensively on the blog, to the point that some of you know me mainly for these articles ;-):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2019/07/22/proxmox-en-5-min-ansible-tinc/" &gt;A Proxmox VE Cluster in 5 Minutes with Ansible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2019/08/20/cluster-proxmox-ve-v6-cette-fois-ci/" &gt;Proxmox VE Cluster, v6 this time!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2019/10/11/un-cluster-proxmox-ve-avec-seulement-2-machines/" &gt;A Proxmox VE Cluster with Only 2 Machines!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2017/09/19/tutoriel-demonter-proprement-cluster-proxmox-ve/" &gt;[Tutorial] Properly Dismantling a Proxmox VE Cluster&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So I won&amp;rsquo;t spend too much time on it. Just know that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;it&amp;rsquo;s easier to do since version 6 because there&amp;rsquo;s no longer a need to set up a VPN between servers (corosync v3)&lt;/li&gt;
&lt;li&gt;it can be done from the command line&lt;/li&gt;
&lt;li&gt;in any case, you need to log in as root &lt;strong&gt;with password&lt;/strong&gt; when clustering&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Very briefly then, we go to the first server, in the &lt;strong&gt;Server View&lt;/strong&gt; at the top left, we select our &lt;strong&gt;Datacenter&lt;/strong&gt;, then in the &lt;strong&gt;Cluster&lt;/strong&gt; menu.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/create-cluster-1.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Click on the &lt;strong&gt;Create Cluster&lt;/strong&gt; button.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/create-cluster-2.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Note: here, it&amp;rsquo;s simple, I only have one network card, so there&amp;rsquo;s no doubt about which interface to use. However, in the case of a server with more interfaces and more bridges (or VPNs as we had to do until PVE 6), you need to choose carefully which interface is used for clustering.&lt;/p&gt;
&lt;p&gt;We validate the form:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/create-cluster-3.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;From this point on, the server is no longer in &amp;ldquo;standalone&amp;rdquo; mode and has additional systemd services.&lt;/p&gt;
&lt;p&gt;We notably have a /etc/pve folder that is synchronized between all nodes (the famous &lt;a class="link" href="https://pve.proxmox.com/wiki/Proxmox_Cluster_File_System_%28pmxcfs%29" target="_blank" rel="noopener"
&gt;Proxmox Cluster File System&lt;/a&gt;) with the configuration of each node and the configuration files of virtual machines. We also have the corosync software, which will regularly contact all cluster nodes to ensure everyone is alive/reachable.&lt;/p&gt;
&lt;h2 id="joining-the-cluster"&gt;Joining the Cluster
&lt;/h2&gt;&lt;p&gt;If we go back to the &amp;ldquo;Cluster&amp;rdquo; menu, we now have information displayed about our cluster, namely its number of nodes (here just one, normal). Click on the &lt;strong&gt;Join information&lt;/strong&gt; button.&lt;/p&gt;
&lt;p&gt;A popup opens with several pieces of information to copy-paste that will be requested in the menu of the other server, the one that will join the cluster:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/create-cluster-4.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;So we connect to the graphical interface of our second Proxmox VE server (my Atom in the example), we go to the same menu, but instead of creating a new cluster, we click on the &lt;strong&gt;Join Cluster&lt;/strong&gt; button:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/join-cluster.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Normally a little scare, once the node joins the cluster, you&amp;rsquo;ll lose control of the web interface and get a &amp;ldquo;401&amp;rdquo; error message. This is &amp;ldquo;normal&amp;rdquo;, when the server enters the cluster, services are restarted and we switch from standalone mode to clustered mode&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/join-cluster2.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Don&amp;rsquo;t panic, an F5 should fix the issue (or connect to the &amp;ldquo;first&amp;rdquo; server of the cluster).&lt;/p&gt;
&lt;p&gt;We now have a nice cluster :)&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/pve-cluster.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="split-brain"&gt;Split Brain
&lt;/h2&gt;&lt;p&gt;Note however that in my example, we only have 2 nodes, which is more than discouraged when making a Linux cluster (actually, a cluster in general). If the network link ever goes down between our two machines, they&amp;rsquo;ll have no way of knowing if it&amp;rsquo;s a network problem (&lt;em&gt;also known as split brain&lt;/em&gt;) or a node failure.&lt;/p&gt;
&lt;p&gt;To avoid breaking everything by restarting VMs on both sides (the most catastrophic case in a split brain), when quorum is no longer reached, Proxmox VE puts nodes without a majority in (almost) &amp;ldquo;read-only&amp;rdquo; mode.&lt;/p&gt;
&lt;p&gt;And in case it&amp;rsquo;s not clear, quorum is half the nodes +1, so here: 2/2+1 = 2&amp;hellip; so as soon as we lose one of the two nodes, we lose quorum.&lt;/p&gt;
&lt;p&gt;So if we ever cut a node or the communication between the two, your still active VMs will continue to work (I debated this on LinkedIn a few days ago).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;However&lt;/strong&gt;, it will be impossible to modify the cluster configuration, start or create VMs, etc.&lt;/p&gt;
&lt;p&gt;The only thing we can possibly do that isn&amp;rsquo;t &amp;ldquo;read only&amp;rdquo; is stop VMs, because that doesn&amp;rsquo;t induce any risk for data consistency. But you won&amp;rsquo;t be able to turn them back on, you&amp;rsquo;ve been warned&amp;hellip;&lt;/p&gt;
&lt;p&gt;For the record, the LinkedIn debate was &amp;ldquo;does ProxmoxVE cut all VMs in case of split brain&amp;rdquo; and the answer is obviously &lt;strong&gt;no&lt;/strong&gt;. However, most actions are indeed blocked.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/quorum-ko1.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/quorum-ko2.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;In any case&lt;/strong&gt;, to avoid this, the best thing is to have an odd number of nodes so that split brain can never happen, or possibly add a complementary vote using an instance serving only as an arbiter as explained in my old 2019 article or in the official documentation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2019/10/11/un-cluster-proxmox-ve-avec-seulement-2-machines/" target="_blank" rel="noopener"
&gt;A Proxmox VE Cluster with Only 2 Machines!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://pve.proxmox.com/wiki/Cluster_Manager#_corosync_external_vote_support" target="_blank" rel="noopener"
&gt;Proxmox Wiki - Corosync External Vote Support&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="sdn-configuration"&gt;SDN Configuration
&lt;/h2&gt;&lt;p&gt;OK, cool, we have a two-machine cluster. But in the current configuration, the VM we created in the previous part can&amp;rsquo;t easily migrate from one server to another. Indeed, we attached it to a Linux bridge (vmbr0) that has its own network addressing plan.&lt;/p&gt;
&lt;p&gt;In case of migration, we would potentially need to start the machine, change the IP address, review iptables/firewall rules. That&amp;rsquo;s what I was doing until now, and in the case of a DR plan for a lab infra and a personal blog, it&amp;rsquo;s &amp;ldquo;OK&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;A big new feature in version 8.1 is the activation of an &amp;ldquo;SDN&amp;rdquo; (Software Defined Network) module that has been in testing by developers and some early adopters since version 6.X, which will allow us to manage this centrally from the interface. If you want to read the official documentation, it&amp;rsquo;s here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://pve.proxmox.com/pve-docs/chapter-pvesdn.html" target="_blank" rel="noopener"
&gt;pve.proxmox.com/pve-docs/chapter-pvesdn.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The only prerequisite we&amp;rsquo;re missing is dnsmasq for DHCP, and the documentation tells us to run the following commands on our machines:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt install dnsmasq
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# disable default instance&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl disable --now dnsmasq
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And in the /etc/network/interfaces file, it&amp;rsquo;s also necessary to add at the end of the file the line (followed by a small &lt;code&gt;systemctl restart networking&lt;/code&gt;)&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;source /etc/network/interfaces.d/*
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the Datacenter menu, open the SDN submenu. Note that SDNs are already declared, but these are actually the default networks we declared in previous articles:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/sdn1.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;In the SDN/Zones menu, add a &amp;ldquo;Simple&amp;rdquo; type zone. There are more complex zones that will be useful in production contexts with non-trivial LANs (VLAN, VXLan) but in our case, Simple is sufficient.&lt;/p&gt;
&lt;p&gt;Note: don&amp;rsquo;t forget to check the &lt;strong&gt;advanced&lt;/strong&gt; box to enable DHCP. &lt;strong&gt;YES we&amp;rsquo;re finally going to have DHCP for our virtual machines and containers&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/sdn2.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Once you&amp;rsquo;ve validated, go to the SDN/VNets submenu, click the &amp;ldquo;Create&amp;rdquo; button to create a virtual network, then select it once created to create a subnet.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/sdn3.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/sdn4.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Once again, don&amp;rsquo;t forget to check the DHCP section, to declare the range for our future DHCP.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/sdn5.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Once we&amp;rsquo;ve validated everything, we see that next to all these new things we&amp;rsquo;ve just created, there&amp;rsquo;s an icon with two yellow arrows and a &amp;ldquo;New&amp;rdquo; status.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/sdn6.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;To deploy these changes, we need to go back to the SDN menu from the beginning, and click Apply. Two &amp;ldquo;sdn1&amp;rdquo; lines (one for each server, actually) should appear.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/sdn7.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Fun fact, the Proxmox VE SDN will add iptables rules that should look familiar if you&amp;rsquo;ve followed the previous articles:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#version:5
auto vnet1
iface vnet1
address 10.10.20.1/24
post-up iptables -t nat -A POSTROUTING -s &amp;#39;10.10.20.0/24&amp;#39; -o vmbr0 -j SNAT --to-source 203.0.113.1
post-down iptables -t nat -D POSTROUTING -s &amp;#39;10.10.20.0/24&amp;#39; -o vmbr0 -j SNAT --to-source 203.0.113.1
post-up iptables -t raw -I PREROUTING -i fwbr+ -j CT --zone 1
post-down iptables -t raw -D PREROUTING -i fwbr+ -j CT --zone 1
bridge_ports none
bridge_stp off
bridge_fd 0
ip-forward on
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And if now, I recreate another virtual machine, but this time instead of &lt;strong&gt;vmbr0&lt;/strong&gt;, I choose &lt;strong&gt;vnet1&lt;/strong&gt;, I don&amp;rsquo;t need to enter the IP address (since I have the SDN serving DHCP) and it has Internet access without additional configuration.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/sdn8.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/03/sdn9.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;With this configuration however (SDN in &lt;strong&gt;Simple Zone&lt;/strong&gt; mode), it still won&amp;rsquo;t be possible to have virtual machines communicate with each other. That will be possible with more complex configurations:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You can, for example, create a VXLAN overlay network on top of public internet, appearing to the VMs as if they share the same local Layer 2 network&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://pve.proxmox.com/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_vxlan" target="_blank" rel="noopener"
&gt;VXLan Zones&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://pve.proxmox.com/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_evpn" target="_blank" rel="noopener"
&gt;EVPN Zones&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note: I&amp;rsquo;ve explored VXLANs since then, and &lt;a class="link" href="https://blog.zwindler.fr/en/2025/03/30/proxmox-ve-8-tutorial-sdn-in-vxlan-mode-with-machines-on-the-internet/" &gt;I wrote a complementary article here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;I still haven&amp;rsquo;t finished this series, because I still have things to say about SDN replication and Proxmox VE firewalling, backups, monitoring, but once again, I&amp;rsquo;m already exceeding 12,000 characters. So it&amp;rsquo;s time to publish the article and start writing the next one.&lt;/p&gt;
&lt;p&gt;And in the meantime, have fun :)&lt;/p&gt;</description></item><item><title>Deploying a Proxmox VE 8 Cluster on Dedicated Servers (2/3)</title><link>https://blog.zwindler.fr/en/2025/02/17/deploying-a-proxmox-ve-8-cluster-on-dedicated-servers-2/3/</link><pubDate>Mon, 17 Feb 2025 10:30:00 +0200</pubDate><guid>https://blog.zwindler.fr/en/2025/02/17/deploying-a-proxmox-ve-8-cluster-on-dedicated-servers-2/3/</guid><description>&lt;img src="https://blog.zwindler.fr/2025/02/proxmox8.webp" alt="Featured image of post Deploying a Proxmox VE 8 Cluster on Dedicated Servers (2/3)" /&gt;&lt;p&gt;Note - this article is part of a series:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/en/2025/01/27/deploying-a-proxmox-ve-8-cluster-on-dedicated-servers-1/2/" &gt;Deploying a Proxmox VE 8 Cluster on Dedicated Servers (1/4)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/en/2025/02/17/deploying-a-proxmox-ve-8-cluster-on-dedicated-servers-2/3/" &gt;Deploying a Proxmox VE 8 Cluster on Dedicated Servers (2/4)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/en/2025/03/25/deploying-a-proxmox-ve-8-cluster-on-dedicated-servers-3/4/" &gt;Deploying a Proxmox VE 8 Cluster on Dedicated Servers (3/4)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/en/2025/03/30/proxmox-ve-8-tutorial-sdn-in-vxlan-mode-with-machines-on-the-internet/" &gt;Special Edition - SDN in VXLAN Mode with Machines on the Internet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;And &lt;em&gt;at least&lt;/em&gt; one more to come&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="picking-up-where-we-left-off-first-steps-in-the-ui"&gt;Picking Up Where We Left Off: First Steps in the UI
&lt;/h2&gt;&lt;p&gt;In the previous tutorial (which I invite you to read, if you haven&amp;rsquo;t already, otherwise this won&amp;rsquo;t be clear), we chose a dedicated physical server from a provider like OVHCloud or Hetzner. We then installed Proxmox VE in the latest version (8.x), configured some basic security options, added users and groups.&lt;/p&gt;
&lt;p&gt;Now we can try connecting to the UI. The first thing that should strike you (💥 ouch!) is that the administration page is in HTTPS with a self-signed certificate. We&amp;rsquo;ll start by fixing that.&lt;/p&gt;
&lt;p&gt;Something quite frustrating with Proxmox VE (but it&amp;rsquo;s probably for &amp;ldquo;good reasons&amp;rdquo; that I don&amp;rsquo;t know) is that some operations can&amp;rsquo;t be done in the UI if you connect with the admin account zwindler@pve that we created in the previous blog post.&lt;/p&gt;
&lt;p&gt;This is typically true for updates, clustering, and configuration and&amp;hellip; for &lt;strong&gt;certificate configuration&lt;/strong&gt; (non-exhaustive list). So here, we&amp;rsquo;ll have to connect as root@pam for now and not as zwindler@pve&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/auth-pam.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Note that there are (for now) 2 &amp;ldquo;realms&amp;rdquo;, Linux PAM (for root) and Proxmox VE Auth (for admin).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/auth-pve.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;In the right bar, we find our server, which is for now alone in its datacenter (we&amp;rsquo;ll come back to this later). There are several views in the Proxmox VE UI, some things are hard to find if you don&amp;rsquo;t have this in mind. Here, I&amp;rsquo;m in the &amp;ldquo;server view&amp;rdquo;, probably the most classic if you come from the VMware world. In this menu, you&amp;rsquo;ll find your hypervisors and we can interact with each of them, how they are configured.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/pve-first-login.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Once our server is selected, we have the main menu displaying many menus. Select &lt;strong&gt;System / Certificates&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/pve-certs-01.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re in luck, Proxmox VE comes with a module to automatically order &lt;a class="link" href="https://letsencrypt.org/" target="_blank" rel="noopener"
&gt;Let&amp;rsquo;s Encrypt&lt;/a&gt; certificates. If you have your own certificate that works too (button &lt;strong&gt;Upload Custom Certificate&lt;/strong&gt;), of course, but I&amp;rsquo;ll assume you don&amp;rsquo;t have one, like me.&lt;/p&gt;
&lt;p&gt;In the ACME section, click on &lt;strong&gt;Add ACME Account&lt;/strong&gt; to register your email and accept the Let&amp;rsquo;s Encrypt Terms Of Service (TOS).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/pve-certs-02.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;From there, add our server&amp;rsquo;s FQDN by clicking the &lt;strong&gt;Add&lt;/strong&gt; button under ACME. The button was grayed out until we added an account.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/pve-certs-03.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Now we can finally order the certificate by clicking on &lt;strong&gt;Order Certificates Now&lt;/strong&gt;. If everything goes well, the certificate will be correctly delivered by Let&amp;rsquo;s Encrypt, and the pve-proxy server (which serves the Proxmox VE UI frontend) should restart, and your web page refresh, with a proper certificate this time :).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/pve-certs-04.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="storage--zfs"&gt;Storage / ZFS
&lt;/h2&gt;&lt;p&gt;If you read my blog often and I tell you that I&amp;rsquo;m an unconditional fan of ZFS, you won&amp;rsquo;t be surprised.&lt;/p&gt;
&lt;p&gt;And if you read the previous article carefully, you&amp;rsquo;ll remember that we reduced the default partition of the OVHcloud template in LVM to be able to create a nice ZFS partition. But if we go to the storage view, it&amp;rsquo;s unfortunately not visible.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/hulk_sad.gif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Small UI subtlety, the menu to add storage is found at the Datacenter level and not in the server configuration itself. In theory, storage management must be identical on all servers in the same DC in Proxmox VE, even if we can override this principle and set up exceptions for many versions already.&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;Datacenter / Storage&lt;/strong&gt;, click &lt;strong&gt;Add&lt;/strong&gt;, then ZFS in the dropdown menu:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/storage-01.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/storage-02.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;On ZFS type storage, we can only store &amp;ldquo;Disk images&amp;rdquo; and &amp;ldquo;Containers&amp;rdquo;, which won&amp;rsquo;t help us if we also want to store ISOs, templates, or backups.&lt;/p&gt;
&lt;p&gt;We can settle for storing them on the default &amp;ldquo;local&amp;rdquo; storage.&lt;/p&gt;
&lt;p&gt;But we can also &amp;ldquo;cheat&amp;rdquo; by connecting to the server via SSH, creating a folder (/tank/data in my example), then creating a new Directory type storage that has no restriction on data types.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/storage-03.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="lets-talk-about-zfs"&gt;Let&amp;rsquo;s Talk About ZFS
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve banged my head countless times with people who persist in saying nonsense about ZFS, even after explaining it to them.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;no, you don&amp;rsquo;t need ECC RAM to store things on ZFS. First, it&amp;rsquo;s a recommendation (probably a bit zealous) made by people who designed an extremely robust filesystem. Many production systems run with ZFS without ECC RAM. Second, it&amp;rsquo;s especially true if you do deduplication, because losing a block can have catastrophic consequences in such cases&amp;hellip;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;no, ZFS doesn&amp;rsquo;t require 1 GB of RAM per TB of disk, again, that&amp;rsquo;s only if you enable deduplication (to store the correspondence tables between hashes and blocks).&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the other hand, what is accurate is that by default, ZFS will try to use 50% of the available RAM on your server and use it as cache. In many administration software, including Proxmox VE, this value will be way too high for us, because RAM is a precious resource on a hypervisor.&lt;/p&gt;
&lt;p&gt;That doesn&amp;rsquo;t mean this feature is useless. Having cache when you have unused RAM is always good. So, we&amp;rsquo;ll reduce this value to avoid resource conflicts between optimizing our storage performance and the number of VMs we can host.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Restrict to 512MB&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="m"&gt;536870912&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo tee -a /sys/module/zfs/parameters/zfs_arc_max
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Restrict to 4GB&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="m"&gt;4294967296&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo tee -a /sys/module/zfs/parameters/zfs_arc_max
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note: this command will need to be persisted one way or another&lt;/p&gt;
&lt;h2 id="networking"&gt;Networking
&lt;/h2&gt;&lt;p&gt;Well&amp;hellip; I tried to postpone as long as possible when we get to networking (because I hate it) but at some point, we&amp;rsquo;ll have to get to it&amp;hellip;&lt;/p&gt;
&lt;p&gt;By default, our Proxmox VE server is installed with a Linux bridge that will allow us to share network between our server&amp;rsquo;s physical interface and our virtual machines.&lt;/p&gt;
&lt;p&gt;This setup works well on your local network with DHCP and your own local IPv4 LAN or if you have an IP range (whether v6 or v4) and something to assign them to virtual machines.&lt;/p&gt;
&lt;p&gt;Unfortunately in my case, I only have one IPv4 (I could enable IPv6 but I&amp;rsquo;d have other issues and I prefer to keep it simple here). If I create VMs and assign them to the bridge, they won&amp;rsquo;t get an IP and won&amp;rsquo;t have Internet access.&lt;/p&gt;
&lt;p&gt;There are several ways to connect virtual machines to the external network. If you have time, I recommend reading carefully the Proxmox VE Wiki which has a very good page listing the different possibilities.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://pve.proxmox.com/wiki/Network_Configuration" target="_blank" rel="noopener"
&gt;pve.proxmox.com/wiki/Network_Configuration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the previous articles on &lt;a class="link" href="https://blog.zwindler.fr/2017/07/11/deploiment-de-proxmox-ve-5-sur-un-serveur-dedie-part-1" &gt;Proxmox VE 5&lt;/a&gt; then &lt;a class="link" href="https://blog.zwindler.fr/2020/03/02/deploiement-de-proxmox-ve-6-pfsense-sur-un-serveur-dedie/" &gt;Proxmox VE 6&lt;/a&gt;, we did complicated stuff with multiple bridges, DMZ, pfSense firewalling, and a hellish iptables script.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2017/07/proxmox-install_simple-infra-map.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Just thinking about it, I&amp;rsquo;m already exhausted.&lt;/p&gt;
&lt;p&gt;Fortunately, there has been a built-in firewall in Proxmox VE for several versions now, as well as an SDN that I&amp;rsquo;ve never taken the time to explore and I think this is the right tutorial to do it :).&lt;/p&gt;
&lt;p&gt;So we&amp;rsquo;ll refer to the official documentation, and choose the &amp;ldquo;Masquerading (NAT) with iptables&amp;rdquo; mode, but with a simpler setup compared to before.&lt;/p&gt;
&lt;h2 id="danger-zone"&gt;Danger Zone
&lt;/h2&gt;&lt;p&gt;Network interface configuration can theoretically be done directly from the UI rather than modifying configuration files (/etc/network/interfaces). In the server view, go to the &lt;strong&gt;System / Network&lt;/strong&gt; menu:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/pve-network-1.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Here, you can see I wasn&amp;rsquo;t making things up, and we do have a bridge with our physical interface (here enp1s0).&lt;/p&gt;
&lt;p&gt;(We even have an IPv6 range, oh my 🙈 I have no excuse&amp;hellip; ANYWAY, let&amp;rsquo;s pretend we didn&amp;rsquo;t see that.)&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re now at the fun part where we can easily cut ourselves off. I&amp;rsquo;ve done it often enough and had to go into rescue mode&amp;hellip;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;fun fun fun fun&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The idea here is to remove the interface from the bridge, configure the network directly on it, and give a local network for our VMs on the bridge, while adding iptables rules for masquerading.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/pve-network-2.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/pve-network-3.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;As long as we don&amp;rsquo;t click the &lt;strong&gt;Apply Configuration&lt;/strong&gt; button, we&amp;rsquo;re not &amp;ldquo;at risk&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;The problem with the UI is that we&amp;rsquo;ll only be able to modify the IPs, gateways, and interfaces on the bridge or not. But to do the masquerading and have our virtual machines access the Internet, we need to add the famous iptables rules I&amp;rsquo;m talking about just before and we can&amp;rsquo;t do it from the UI.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ll need to add the last five lines in the vmbr0 configuration&amp;hellip;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /etc/network/interfaces
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;...&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;auto vmbr0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;iface vmbr0 inet static
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; address 10.10.10.1/24
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; bridge-ports none
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; bridge-stp off
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; bridge-fd &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# add for masquerading to work&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; post-up &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &amp;gt; /proc/sys/net/ipv4/ip_forward
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; post-up iptables -t nat -A POSTROUTING -s &lt;span class="s1"&gt;&amp;#39;10.10.10.0/24&amp;#39;&lt;/span&gt; -o enp1s0 -j MASQUERADE
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; post-down iptables -t nat -D POSTROUTING -s &lt;span class="s1"&gt;&amp;#39;10.10.10.0/24&amp;#39;&lt;/span&gt; -o enp1s0 -j MASQUERADE
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; post-up iptables -t raw -I PREROUTING -i fwbr+ -j CT --zone &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; post-down iptables -t raw -D PREROUTING -i fwbr+ -j CT --zone &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Warning&lt;/strong&gt;: the most important point is to verify that the output interface (-o) has the correct name. Here, my physical interface connected to the Internet is &lt;strong&gt;enp1s0&lt;/strong&gt;, but if yours has a different name, you need to adapt.&lt;/p&gt;
&lt;p&gt;From my point of view, the UI is still not completely useless, because it allows us to make the most delicate change (IP swap) with the visual diff and the &amp;ldquo;apply config&amp;rdquo; button. The post-up scripts are unlikely to cut off our access.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/pve-network-4.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="first-test"&gt;First Test
&lt;/h2&gt;&lt;p&gt;We&amp;rsquo;re far from done (it won&amp;rsquo;t fit in this article anyway, so might as well rush through&amp;hellip;). But I&amp;rsquo;m sure you&amp;rsquo;re eager to launch a VM and we should have something functional, though incomplete.&lt;/p&gt;
&lt;p&gt;So we&amp;rsquo;ll do a quick test of our setup to verify that we have a minimally functional hypervisor (meaning without clustering, without SDN, without firewall, without monitoring, without backup&amp;hellip; without anything, really).&lt;/p&gt;
&lt;p&gt;I started playing around with &lt;a class="link" href="https://blog.zwindler.fr/recherche/?keyword=talos" target="_blank" rel="noopener"
&gt;Talos Linux&lt;/a&gt; as a Linux distribution for Kubernetes. To keep it simple, I&amp;rsquo;ll launch the install of a Talos Node. If it manages to enroll in the Omni interface, it means I have a functional hypervisor, network included.&lt;/p&gt;
&lt;p&gt;Once the ISO is generated on my Omni site (the SaaS control plane from Sidero Labs, the publisher of Talos Linux), we can push it to our Proxmox VE.&lt;/p&gt;
&lt;p&gt;To do this, from our server (so not at the Datacenter level), we select a storage pool that accepts ISO images (if you remember correctly, our ZFS pool doesn&amp;rsquo;t support it by default), choose &lt;strong&gt;ISO Images&lt;/strong&gt;, and click &lt;strong&gt;Upload&lt;/strong&gt; to upload the ISO.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/iso.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Once uploaded, we can now click the blue button at the top right &lt;strong&gt;Create VM&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/proxmox-create-01.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;From there, the VM creation wizard should take over. It&amp;rsquo;s going to be a bit verbose, but all menus are useful when you start to know Proxmox VE well. I&amp;rsquo;ll give you the minimum though:&lt;/p&gt;
&lt;p&gt;In the first menu, we won&amp;rsquo;t choose which node to install the VM on since for now we only have one&amp;hellip; However, we&amp;rsquo;ll have to give it a unique ID (100 by default) for the entire cluster. Ideally, we also give it a little name. I opted for talos02 (because I already have a talos01 on another server, see how original I am?).&lt;/p&gt;
&lt;p&gt;Finally, I checked the &amp;ldquo;Start at boot&amp;rdquo; box, that&amp;rsquo;s the kind of annoying thing when you forget to set it and the hypervisor reboots (it happens)&amp;hellip;&lt;/p&gt;
&lt;p&gt;You can give tags to your VMs, they&amp;rsquo;ll have nice colored badges to distinguish them :)&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/create-vm-1.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;In the second menu (OS), we need to indicate two things. First, that we want to use the ISO we just retrieved/uploaded to boot our machine (you need to choose the right storage pool, by default &amp;ldquo;local&amp;rdquo; is selected). We also need to give the OS type for driver compatibility reasons for virtual devices. It&amp;rsquo;s been a long time since we really needed that, &amp;ldquo;6.X - 2.6&amp;rdquo; works for all recent Linux (kernel 2.6 release was in 2003!).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/create-vm-2.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;I skip System, in Disks, we add a disk on the right pool (so not local, but the one on ZFS). For my 32 GB Talos VM, that&amp;rsquo;s more than enough&amp;hellip; Note that there are many important options here for everything related to read/write &amp;ldquo;optimization&amp;rdquo;, but we&amp;rsquo;re well beyond the scope of this article.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/create-vm-3.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;I skip CPU and memory, the important thing is to give it at least 2 cores and 2 GB of RAM (at least). You can also optimize CPU performance by enabling the right flags, but again, we&amp;rsquo;re beyond the purpose of the article so I won&amp;rsquo;t insist.&lt;/p&gt;
&lt;p&gt;In the network section, we just make sure we&amp;rsquo;re connected to the right bridge (normally if you followed the article, we only have one, &lt;strong&gt;vmbr0&lt;/strong&gt;, so we can&amp;rsquo;t go wrong).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/create-vm-4.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;And we verify everything is OK. Protip, to save five seconds in your life, click &amp;ldquo;Start after created&amp;rdquo;, which will start the VM as soon as possible after creation.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/create-vm-5.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="its-alive"&gt;It&amp;rsquo;s Alive?
&lt;/h2&gt;&lt;p&gt;At this point, the VM should automatically boot from the Talos CD and the OS should install on the disk. Once ready, the machine will try to enroll on my Omni control plane.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;And that&amp;rsquo;s when disaster strikes&amp;hellip;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Because yes, I haven&amp;rsquo;t configured DHCP or SDN. So we have to manually configure the virtual network card (which isn&amp;rsquo;t great), by pressing F3 in the console.&lt;/p&gt;
&lt;p&gt;(And suffering a bit of QWERTY, but that&amp;rsquo;s how you recognize a good sysadmin normally: they can do QWERTY in consoles)&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/talos02-config.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Once the network is configured, the machine should almost instantly start configuring itself&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/talos02-booted.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&amp;hellip; and enroll in Omni:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/omni.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Victory!!&lt;/p&gt;
&lt;h2 id="well"&gt;Well&amp;hellip;
&lt;/h2&gt;&lt;p&gt;Well&amp;hellip; yes, we have a functional VM. But I aspire to go a bit further before really declaring victory.&lt;/p&gt;
&lt;p&gt;As I said above, we had to configure our VM&amp;rsquo;s network by hand, and we don&amp;rsquo;t yet have clustering, firewalling, SDN, monitoring, or backup&amp;hellip; But we&amp;rsquo;re comfortably scratching 16,000 characters for this blog post, so it&amp;rsquo;s time to hang up and say &amp;ldquo;see you next time&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;And in the meantime, have fun :)&lt;/p&gt;</description></item><item><title>Deploying a Proxmox VE 8 Cluster on Dedicated Servers (1/2)</title><link>https://blog.zwindler.fr/en/2025/01/27/deploying-a-proxmox-ve-8-cluster-on-dedicated-servers-1/2/</link><pubDate>Mon, 27 Jan 2025 20:00:00 +0000</pubDate><guid>https://blog.zwindler.fr/en/2025/01/27/deploying-a-proxmox-ve-8-cluster-on-dedicated-servers-1/2/</guid><description>&lt;img src="https://blog.zwindler.fr/2025/02/proxmox8.webp" alt="Featured image of post Deploying a Proxmox VE 8 Cluster on Dedicated Servers (1/2)" /&gt;&lt;p&gt;Note - this article is part of a series:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/en/2025/01/27/deploying-a-proxmox-ve-8-cluster-on-dedicated-servers-1/2/" &gt;Deploying a Proxmox VE 8 Cluster on Dedicated Servers (1/4)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/en/2025/02/17/deploying-a-proxmox-ve-8-cluster-on-dedicated-servers-2/3/" &gt;Deploying a Proxmox VE 8 Cluster on Dedicated Servers (2/4)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/en/2025/03/25/deploying-a-proxmox-ve-8-cluster-on-dedicated-servers-3/4/" &gt;Deploying a Proxmox VE 8 Cluster on Dedicated Servers (3/4)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/en/2025/03/30/proxmox-ve-8-tutorial-sdn-in-vxlan-mode-with-machines-on-the-internet/" &gt;Special Edition - SDN in VXLAN Mode with Machines on the Internet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;And &lt;em&gt;at least&lt;/em&gt; one more to come&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="introduction"&gt;Introduction
&lt;/h2&gt;&lt;p&gt;Those who follow the &amp;ldquo;lore&amp;rdquo; of this blog know that I&amp;rsquo;m somewhat known in the Sunday tinkerer community for having written (or co-written) several tutorials on Proxmox VE. I&amp;rsquo;m thinking in particular of two article series on &lt;a class="link" href="https://blog.zwindler.fr/2017/07/11/deploiment-de-proxmox-ve-5-sur-un-serveur-dedie-part-1/" target="_blank" rel="noopener"
&gt;Proxmox VE 5&lt;/a&gt;, then &lt;a class="link" href="https://blog.zwindler.fr/2020/03/02/deploiement-de-proxmox-ve-6-pfsense-sur-un-serveur-dedie/" target="_blank" rel="noopener"
&gt;Proxmox VE 6&lt;/a&gt;, to create a virtualization cluster from scratch with Proxmox VE (and pfSense at the time).&lt;/p&gt;
&lt;p&gt;This is a new iteration of these tutorials, this time based on Proxmox VE 8, with quite a few things changing and new approaches.&lt;/p&gt;
&lt;p&gt;The ultimate goal of this article series is to have a virtualization platform with Proxmox VE. This platform will be composed of several machines (ideally 3, but 2 works too), secured, monitored, highly available, and backed up.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ll need to follow several articles to achieve this.&lt;/p&gt;
&lt;h2 id="starting-assumptions"&gt;Starting Assumptions
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;1-&lt;/strong&gt; I&amp;rsquo;m assuming you don&amp;rsquo;t have machines to do this at home and don&amp;rsquo;t want or can&amp;rsquo;t afford to invest in a homelab (&lt;a class="link" href="https://blog.zwindler.fr/recherche/?keyword=homelab" &gt;even though it can be a lot of fun, it requires investment&lt;/a&gt;). However, if you have machines at home, a large part of this article remains valid, once the machines are installed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2-&lt;/strong&gt; Since Proxmox VE is a virtualization platform, I&amp;rsquo;m also assuming you want &lt;strong&gt;dedicated&lt;/strong&gt; (physical) machines and not virtual machines, precisely to be able to virtualize entire OSes without too many problems (even though we always have the &amp;ldquo;nested virt&amp;rdquo; option, it&amp;rsquo;s less ideal).&lt;/p&gt;
&lt;p&gt;Note: if you don&amp;rsquo;t need virtualization (example: no Windows or special OS), Proxmox VE is a great containerization platform, particularly thanks to LXC, which allows you to create Linux containers with a full OS, that you can manage exactly like a VM but with much better performance (and some limitations due to kernel sharing, but it&amp;rsquo;s often sufficient). You can read &lt;a class="link" href="https://blog.zwindler.fr/recherche/?keyword=lxc" &gt;my articles on LXC&lt;/a&gt; if you&amp;rsquo;re interested.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3-&lt;/strong&gt; I&amp;rsquo;d like to draw your attention to the fact that many actions could have been scripted or managed via &amp;ldquo;infrastructure as code&amp;rdquo;. It&amp;rsquo;s more in tune with the times, because it&amp;rsquo;s more robust and reliable. However, the point of this article is to do it &amp;ldquo;by hand&amp;rdquo; to understand what we&amp;rsquo;re doing, step by step, not just provide you with a &amp;ldquo;turnkey&amp;rdquo; infrastructure that you won&amp;rsquo;t know how to manage at the first hiccup.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re looking for more industrial methods to deploy virtualization clusters, you&amp;rsquo;ll surely find plenty on the Internet, from other bloggers (some French, some are even friends).&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve actually done the exercise myself several times with Ansible or &lt;a class="link" href="https://blog.zwindler.fr/recherche/?keyword=rudder" &gt;Rudder&lt;/a&gt; in the past, for example here (note: this code is obsolete):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.zwindler.fr/2019/07/22/proxmox-en-5-min-ansible-tinc/" &gt;A Proxmox VE cluster in 5 minutes with Ansible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/zwindler/ansible-proxmoxve/tree/master" target="_blank" rel="noopener"
&gt;github.com/zwindler/ansible-proxmoxve&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now that the objective and context is clear for everyone, we can begin :)&lt;/p&gt;
&lt;h2 id="choosing-the-machines"&gt;Choosing the Machines
&lt;/h2&gt;&lt;p&gt;Since we said we&amp;rsquo;re not self-hosting, we need to find a host that offers physical machines. Ideally, something not too expensive would be nice to start with.&lt;/p&gt;
&lt;p&gt;So we&amp;rsquo;re going to look for inexpensive machines BUT with support for VT-x / AMD-v instructions, which is pretty much all CPUs that still work today EXCEPT Atoms and ARM machines like Raspberry.&lt;/p&gt;
&lt;p&gt;Basically, that leaves us with Kimsufi at OVHCloud, Dedibox at Scaleway, and machines at Hetzner.&lt;/p&gt;
&lt;p&gt;At Hetzner for €45, you get an i5 gen13, 64 GB of RAM and 2 NVMe SSDs.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/hetzner.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;At OVHcloud, with a bit of luck, you can find machines around €15-20 per month with older hardware, but more than sufficient for what we&amp;rsquo;re going to do.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/ovhcloud.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;At Scaleway, the entry-level offer is centered on Atom Avoton or very old Xeons. The Avoton C2750 remains a valid option, as it&amp;rsquo;s an octa-core that supports VT-x (unlike the C2350, beware!). But well, it&amp;rsquo;s 2013 hardware 💀&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/scaleway.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;For the purposes of this article, I optimized costs and went with the OVHcloud server I present just above (KS-GAME-LE), unfortunately only available in Canada :(. But 8 threads and 16 GB of RAM for €12 including tax per month, that&amp;rsquo;s really not expensive.&lt;/p&gt;
&lt;p&gt;Two downsides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ridiculously small storage for virtualization (only 240 GB, a single disk)&lt;/li&gt;
&lt;li&gt;latency - almost 100 ms from my place, compared to 11 ms for my other machine at Scaleway&lt;/li&gt;
&lt;/ul&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;64 bytes from proxmox.example.org (203.0.113.78): icmp_seq=1 ttl=53 time=98.0 ms
64 bytes from proxmox.example.org (203.0.113.78): icmp_seq=2 ttl=53 time=97.5 ms
64 bytes from proxmox.example.org (203.0.113.78): icmp_seq=3 ttl=53 time=97.6 ms
64 bytes from proxmox.example.org (203.0.113.78): icmp_seq=4 ttl=53 time=97.7 ms
64 bytes from proxmox.example.org (203.0.113.78): icmp_seq=5 ttl=53 time=97.7 ms
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="os-installation"&gt;OS Installation
&lt;/h2&gt;&lt;p&gt;Once ordered, the server is relatively quickly available in your &amp;ldquo;OVH manager&amp;rdquo;. We have a fairly intuitive menu to directly install Proxmox VE 8, almost up to date (the provided images are rebuilt very regularly with all updates).&lt;/p&gt;
&lt;p&gt;To start the installation, click on the &amp;ldquo;&amp;hellip;&amp;rdquo; in the Operating System (OS) section, then select install from an OVH template&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/install01.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/install02.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Scroll through the list of OS types, select Virtualization, then Proxmox VE 8 (while we&amp;rsquo;re at it). I also checked the &amp;ldquo;Customize partition configuration&amp;rdquo; box, because by default, OVH&amp;rsquo;s installation uses &amp;ldquo;LVM thick&amp;rdquo; type storage for most of the disk space, while we&amp;rsquo;ll use advanced ZFS features later:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/install03.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/install04.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Before validating the installation, don&amp;rsquo;t forget to change its name (Custom hostname) to avoid having to do it post-install, then add a public SSH key:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/install05.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/install06.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;h2 id="first-post-installation-tasks-upgrade-and-reboot"&gt;First Post-Installation Tasks: Upgrade and Reboot
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s a whole series of things to do when you&amp;rsquo;ve just installed a machine, especially on Proxmox VE, and we&amp;rsquo;ll try to resist the urge to &amp;ldquo;quickly&amp;rdquo; connect to the graphical interface.&lt;/p&gt;
&lt;p&gt;Anyway, we won&amp;rsquo;t be able to connect to the Proxmox VE graphical interface&amp;hellip;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Really???&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Yes, because while we can normally connect with the &lt;code&gt;root&lt;/code&gt; user of our Linux, we won&amp;rsquo;t be able to in the case of OVHcloud, simply because we DON&amp;rsquo;T have the root password. Good thing we added an SSH key ;)&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ll come back to it later, we have more urgent things to do.&lt;/p&gt;
&lt;p&gt;The first thing I did was immediately add a type A DNS record so that my machine&amp;rsquo;s IP corresponds to an FQDN. For this article, I&amp;rsquo;ll refer to it as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;hostname: myPVEhost&lt;/li&gt;
&lt;li&gt;FQDN: proxmox.example.org&lt;/li&gt;
&lt;li&gt;IP: 203.0.113.159&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/dns_ovh.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;So we connect to our freshly installed machine&amp;rsquo;s IP via SSH (root@IPmachine) and we&amp;rsquo;ll take the opportunity to do some cleanup.&lt;/p&gt;
&lt;p&gt;The first thing to do is update the server, ideally followed by a reboot:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt upgrade -y &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; reboot
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once the server is back to life, we reconnect and enable automatic updates in one way or another. The simplest is to use &lt;code&gt;unattended-upgrades&lt;/code&gt; if it&amp;rsquo;s not installed by default (it may already be there) but there are more sophisticated tools.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt install unattended-upgrades
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="its-for-security"&gt;It&amp;rsquo;s for Security
&lt;/h2&gt;&lt;p&gt;Once that&amp;rsquo;s done, I recommend installing CrowdSec right away. Here, I simply based myself on the official crowdsec documentation, to add the detection engine, the remediation component (here via &lt;code&gt;iptables&lt;/code&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://doc.crowdsec.net/u/getting_started/installation/linux" target="_blank" rel="noopener"
&gt;CrowdSec.net - Linux Installation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -s https://install.crowdsec.net &lt;span class="p"&gt;|&lt;/span&gt; sudo sh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt install crowdsec
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# verify that the crowdsec service is working&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl status crowdsec
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;● crowdsec.service - Crowdsec agent
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Loaded: loaded &lt;span class="o"&gt;(&lt;/span&gt;/lib/systemd/system/crowdsec.service&lt;span class="p"&gt;;&lt;/span&gt; enabled&lt;span class="p"&gt;;&lt;/span&gt; preset: enabled&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Active: active &lt;span class="o"&gt;(&lt;/span&gt;running&lt;span class="o"&gt;)&lt;/span&gt; since Sun 2025-01-26 21:04:26 UTC&lt;span class="p"&gt;;&lt;/span&gt; 1min 56s ago
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Process: &lt;span class="m"&gt;73830&lt;/span&gt; &lt;span class="nv"&gt;ExecStartPre&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/bin/crowdsec -c /etc/crowdsec/config.yaml -t -error &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;exited, &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0/SUCCESS&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Main PID: &lt;span class="m"&gt;73862&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;crowdsec&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Tasks: &lt;span class="m"&gt;13&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;limit: 19059&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Memory: 33.1M
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; CPU: 2.571s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; CGroup: /system.slice/crowdsec.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─73862 /usr/bin/crowdsec -c /etc/crowdsec/config.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─73877 journalctl --follow -n &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="nv"&gt;_SYSTEMD_UNIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ssh.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Jan &lt;span class="m"&gt;26&lt;/span&gt; 21:04:23 myPVEhost systemd&lt;span class="o"&gt;[&lt;/span&gt;1&lt;span class="o"&gt;]&lt;/span&gt;: Starting crowdsec.service - Crowdsec agent...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Jan &lt;span class="m"&gt;26&lt;/span&gt; 21:04:26 myPVEhost systemd&lt;span class="o"&gt;[&lt;/span&gt;1&lt;span class="o"&gt;]&lt;/span&gt;: Started crowdsec.service - Crowdsec agent.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# install the remediation engine&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt install crowdsec-firewall-bouncer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once Crowdsec is operational, we can add the &amp;ldquo;fulljackz/proxmox&amp;rdquo; collection, which we&amp;rsquo;ll need to modify (thanks to Julien Louis&amp;rsquo;s great article on his slash-root.fr blog), which will listen to the webUI logs to detect any brute force and ban those who try:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://app.crowdsec.net/hub/author/fulljackz/collections/proxmox" target="_blank" rel="noopener"
&gt;https://app.crowdsec.net/hub/author/fulljackz/collections/proxmox&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://slash-root.fr/crowdsec-proteger-lauthentification-proxmox/" target="_blank" rel="noopener"
&gt;slash-root.fr - CrowdSec: Protect Proxmox authentication&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cscli collections install fulljackz/proxmox
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;...&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Enabled fulljackz/proxmox
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Run &lt;span class="s1"&gt;&amp;#39;sudo systemctl reload crowdsec&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; the new configuration to be effective.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The pattern listened to by the proxmox parser is no longer correct with new versions&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;Jan 26 21:23:14 myPVEhost pvedaemon[1250]: authentication failure; rhost=::ffff:203.0.113.159.78 user=coucou@pve msg=no such user (&amp;#39;coucou@pve&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We see here that Julien added the &amp;ldquo;::ffff:&amp;rdquo; prefix in the &lt;strong&gt;PVE_AUTH_FAIL&lt;/strong&gt; variable of the &lt;code&gt;/etc/crowdsec/parsers/s01-parse/proxmox-logs.yaml&lt;/code&gt; file, but when I look at the test cases provided in the collection, it seems like it&amp;rsquo;s not necessary&amp;hellip;&lt;/p&gt;
&lt;p&gt;In any case, we&amp;rsquo;ll create a &lt;code&gt;/etc/crowdsec/acquis.d/proxmox.yaml&lt;/code&gt; file so that CrowdSec can start analyzing our logs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p /etc/crowdsec/acquis.d/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat &amp;gt; /etc/crowdsec/acquis.d/proxmox.yaml &lt;span class="s"&gt;&amp;lt;&amp;lt;EOF
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;journalctl_filter:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; - _SYSTEMD_UNIT=pvedaemon.service
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;labels:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; type: syslog
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We finish by reloading crowdsec and watching &lt;del&gt;the world burn&lt;/del&gt; script kiddies get blocked.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl reload crowdsec
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cscli alerts list
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;╭────┬────────────────────┬───────────────────────────┬─────────┬──────────────────────────────────────────────────────────────┬───────────┬─────────────────────────────────────────╮
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ID │ value │ reason │ country │ as │ decisions │ created_at │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├────┼────────────────────┼───────────────────────────┼─────────┼──────────────────────────────────────────────────────────────┼───────────┼─────────────────────────────────────────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ &lt;span class="m"&gt;2&lt;/span&gt; │ Ip:203.0.113.222 │ crowdsecurity/ssh-slow-bf │ IR │ &lt;span class="m"&gt;202468&lt;/span&gt; Gloubi Bo ulgua Co. &lt;span class="o"&gt;(&lt;/span&gt; Private Joint Stock&lt;span class="o"&gt;)&lt;/span&gt; │ ban:1 │ 2025-01-26 22:31:15.366187524 +0000 UTC │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;╰────┴────────────────────┴───────────────────────────┴─────────┴──────────────────────────────────────────────────────────────┴───────────┴─────────────────────────────────────────╯
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="nagging-popup"&gt;Nagging Popup
&lt;/h2&gt;&lt;p&gt;Among the little annoying things shipped by default with Proxmox VE, there used to be the fact that Proxmox VE had enterprise apt repositories on all installations, and it was necessary to manually disable them before being able to do updates.&lt;/p&gt;
&lt;p&gt;It was super annoying and the Proxmox VE developers heard (a little) their community and removed this requirement&amp;hellip; to replace it with a popup.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/nagging_popup.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Now, a popup, I&amp;rsquo;m fine with that! It&amp;rsquo;s important to reward developers&amp;rsquo; work and support them. But is it really necessary to do it &lt;strong&gt;at every login&lt;/strong&gt; AND &lt;strong&gt;every time we refresh the list of packages to update&lt;/strong&gt;?&lt;/p&gt;
&lt;p&gt;This is obviously a rhetorical question.&lt;/p&gt;
&lt;p&gt;Obviously, many people have found several methods to disable the responsible JS code. Big up to &lt;a class="link" href="https://blog.zwindler.fr/authors/fabio/" target="_blank" rel="noopener"
&gt;fabio&lt;/a&gt; for his version that works well.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sed -Ezi.bak &lt;span class="s2"&gt;&amp;#34;s/(function\(orig_cmd\) \{)/\1\n\torig_cmd\(\);\n\treturn;/g&amp;#34;&lt;/span&gt; /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; systemctl restart pveproxy.service
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note: if like me you&amp;rsquo;ve already connected once, you&amp;rsquo;ll also need to clear the cache otherwise the popup will remain&amp;hellip;&lt;/p&gt;
&lt;h2 id="creating-groups-and-users-for-the-graphical-interface"&gt;Creating Groups and Users for the Graphical Interface
&lt;/h2&gt;&lt;blockquote&gt;
&lt;p&gt;Ok ok, we&amp;rsquo;ve done a lot of stuff, can we connect now???&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Yes yes, we can. Anyway, I&amp;rsquo;m already at almost 15,000 characters, this article is way too long, we need to wrap up.&lt;/p&gt;
&lt;p&gt;Last step in this article, we&amp;rsquo;ll create an administrator to connect to the UI and a user that we&amp;rsquo;ll use later for monitoring.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# you&amp;#39;re not **really** obligated to call the admin &amp;#34;zwindler&amp;#34; you know?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pveum group add admin -comment &lt;span class="s2"&gt;&amp;#34;System Administrators&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pveum acl modify / -group admin -role Administrator
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pveum useradd zwindler@pve
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pveum usermod zwindler@pve -group admin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pveum passwd zwindler@pve
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# our monitoring user&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pveum groupadd monitoring -comment &lt;span class="s1"&gt;&amp;#39;Monitoring group&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pveum aclmod / -group monitoring -role PVEAuditor
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pveum useradd pve_exporter@pve
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pveum usermod pve_exporter@pve -group monitoring
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pveum passwd pve_exporter@pve
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Alright, you&amp;rsquo;ve waited long enough, you can connect to your Proxmox VE! Go to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://@IPmachine:8006
or&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://proxmox.example.org:8006" target="_blank" rel="noopener"
&gt;https://proxmox.example.org:8006&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note: for now, the certificate is self-signed and the browser will show an error. We can accept to ignore the problem for now.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/pve_login.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Don&amp;rsquo;t forget to change the &amp;ldquo;realm&amp;rdquo; from PAM (unix users, which we&amp;rsquo;re not using here) to &amp;ldquo;Proxmox&amp;rdquo; to be able to use the administrator we just created.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2025/02/proxmox8.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;See you soon for the next part. And in the meantime, have fun!&lt;/p&gt;</description></item><item><title>Migrating Cilium routing from iptables to eBPF... hot!</title><link>https://blog.zwindler.fr/en/2023/10/20/migrating-cilium-routing-from-iptables-to-ebpf...-hot/</link><pubDate>Fri, 20 Oct 2023 06:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/en/2023/10/20/migrating-cilium-routing-from-iptables-to-ebpf...-hot/</guid><description>&lt;img src="https://blog.zwindler.fr/2023/10/cilium-logo.webp" alt="Featured image of post Migrating Cilium routing from iptables to eBPF... hot!" /&gt;&lt;h2 id="context"&gt;Context
&lt;/h2&gt;&lt;p&gt;It could be that I configured production Kubernetes clusters with &lt;a class="link" href="https://cilium.io/" target="_blank" rel="noopener"
&gt;cilium&lt;/a&gt; in &lt;code&gt;iptables&lt;/code&gt; mode and not &lt;code&gt;eBPF&lt;/code&gt; mode. But you have no proof&amp;hellip;&lt;/p&gt;
&lt;p&gt;However, in the highly unlikely hypothesis that I could have made such a blunder, here&amp;rsquo;s how I would have gone about solving the problem.&lt;/p&gt;
&lt;p&gt;#trollface&lt;/p&gt;
&lt;p&gt;So let&amp;rsquo;s imagine that when checking the Cilium configuration, here&amp;rsquo;s what you found:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;kubectl -n cilium exec -it cilium-aaaaa -- cilium status&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;...]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;Host Routing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Legacy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;Masquerading: IPTables [IPv4: Enabled, IPv6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Disabled]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;...]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Damn!&lt;/p&gt;
&lt;p&gt;Looking more closely, the cilium-agent containers in your cilium pods are using a lot of CPU and RAM and are starting to clog up your workers&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is a disaster&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="why"&gt;Why?
&lt;/h2&gt;&lt;p&gt;The first implementations of the Kubernetes &lt;em&gt;imaginary network&lt;/em&gt; (the famous CNI plugins) used &lt;code&gt;iptables&lt;/code&gt;. However, we&amp;rsquo;ve known for a very long time, especially in the Kubernetes use case, that &lt;code&gt;iptables&lt;/code&gt; is pretty bad at handling large quantities of rules.&lt;/p&gt;
&lt;p&gt;In the particular case of Kubernetes, the number of rules tends to increase exponentially with cluster size, and the CPU consumed to route network packets with it&amp;hellip;&lt;/p&gt;
&lt;p&gt;At some point, traversing the rules list takes all the CPU of a given node and makes it unresponsive.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hello boss? We&amp;rsquo;re in trouble.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is one of the reasons why I really like cilium as a CNI plugin - the developers were among the first to bet on &lt;a class="link" href="https://ebpf.io/" target="_blank" rel="noopener"
&gt;eBPF&lt;/a&gt; as a replacement for iptables (although there are other implementations / other technologies that solve this problem).&lt;/p&gt;
&lt;h2 id="how-did-we-get-here"&gt;How did we get here?
&lt;/h2&gt;&lt;p&gt;To be honest, I just read the docs and trusted them&amp;hellip;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We introduced eBPF-based host-routing in Cilium 1.9 to fully bypass iptables and the upper host stack, and to achieve a faster network namespace switch compared to regular veth device operation. &lt;strong&gt;This option is automatically enabled if your kernel supports it&lt;/strong&gt;. To validate whether your installation is running with eBPF host-routing, run cilium status in any of the Cilium pods and look for the line reporting the status for &amp;ldquo;Host Routing&amp;rdquo; which should state &amp;ldquo;BPF&amp;rdquo;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Basically, &lt;a class="link" href="https://docs.cilium.io/en/stable/operations/system_requirements/" target="_blank" rel="noopener"
&gt;according to the official docs, if you have the right kernel and the right modules&lt;/a&gt;, the cilium installation is supposed to automatically enable eBPF mode&amp;hellip; Except that we can clearly see above that this isn&amp;rsquo;t the case, despite a recent kernel (6.2).&lt;/p&gt;
&lt;p&gt;Digging a little deeper into the logs, here&amp;rsquo;s what you can find:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl -n cilium logs cilium-7b5cp
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;...&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;info &lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;BPF host routing requires enable-bpf-masquerade. Falling back to legacy host routing (enable-host-legacy-routing=true).&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;subsys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;daemon
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Well&amp;hellip; apparently, an option is missing in the Helm chart.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-diff" data-lang="diff"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[...]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; kubeProxyReplacement: strict
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ bpf:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ masquerade: true
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[...]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Problem solved, end of article?&lt;/p&gt;
&lt;h2 id="problem"&gt;Problem
&lt;/h2&gt;&lt;p&gt;Yes, of course, we can modify the chart like a bull in a china shop and end of story.&lt;/p&gt;
&lt;p&gt;But let&amp;rsquo;s say we don&amp;rsquo;t want to cut production traffic&amp;hellip; How do we do it?&lt;/p&gt;
&lt;p&gt;The cleanest solution that comes to mind is the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;we &lt;code&gt;drain&lt;/code&gt; a node&lt;/li&gt;
&lt;li&gt;we change its configuration&lt;/li&gt;
&lt;li&gt;we &lt;code&gt;uncordon&lt;/code&gt; it to put some traffic back on it&lt;/li&gt;
&lt;li&gt;we check that the new configuration works
AND&lt;/li&gt;
&lt;li&gt;we check that nodes can talk to each other between those with &lt;code&gt;iptables&lt;/code&gt; and those with &lt;code&gt;eBPF&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because yeah, it would be a shame to have half the cluster unable to communicate with the other half&amp;hellip;&lt;/p&gt;
&lt;h2 id="checking-connectivity"&gt;Checking connectivity
&lt;/h2&gt;&lt;p&gt;What&amp;rsquo;s cool about cilium (did I tell you I love cilium?) is that we already have tooling to test everything.&lt;/p&gt;
&lt;p&gt;The cilium CLI has a &lt;code&gt;cilium connectivity test&lt;/code&gt; subcommand that launches dozens of internal/external tests to check that everything is OK.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;➜ ~ cilium -n cilium connectivity &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ℹ️ Monitor aggregation detected, will skip some flow validation steps
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✨ &lt;span class="o"&gt;[&lt;/span&gt;node&lt;span class="o"&gt;]&lt;/span&gt; Deploying echo-same-node service...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✨ &lt;span class="o"&gt;[&lt;/span&gt;node&lt;span class="o"&gt;]&lt;/span&gt; Deploying DNS &lt;span class="nb"&gt;test&lt;/span&gt; server configmap...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✨ &lt;span class="o"&gt;[&lt;/span&gt;node&lt;span class="o"&gt;]&lt;/span&gt; Deploying same-node deployment...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✨ &lt;span class="o"&gt;[&lt;/span&gt;node&lt;span class="o"&gt;]&lt;/span&gt; Deploying client deployment...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;...&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✅ All &lt;span class="m"&gt;32&lt;/span&gt; tests &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="m"&gt;265&lt;/span&gt; actions&lt;span class="o"&gt;)&lt;/span&gt; successful, &lt;span class="m"&gt;2&lt;/span&gt; tests skipped, &lt;span class="m"&gt;0&lt;/span&gt; scenarios skipped.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There&amp;rsquo;s also a &lt;code&gt;cilium-health&lt;/code&gt; command, embedded in the cilium-agent container, which allows you to get periodic latency statistics between all the cluster nodes. Useful!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl -n cilium &lt;span class="nb"&gt;exec&lt;/span&gt; -it cilium-ccccc -c cilium-agent -- cilium-health status
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Probe time: 2023-09-12T14:47:03Z
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Nodes:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; node-02 &lt;span class="o"&gt;(&lt;/span&gt;localhost&lt;span class="o"&gt;)&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Host connectivity to 172.31.0.152:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ICMP to stack: OK, &lt;span class="nv"&gt;RTT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;860.424µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; HTTP to agent: OK, &lt;span class="nv"&gt;RTT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;110.142µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Endpoint connectivity to 10.0.2.56:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ICMP to stack: OK, &lt;span class="nv"&gt;RTT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;783.861µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; HTTP to agent: OK, &lt;span class="nv"&gt;RTT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;256.419µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; node-01:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Host connectivity to 172.31.0.151:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ICMP to stack: OK, &lt;span class="nv"&gt;RTT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;813.324µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; HTTP to agent: OK, &lt;span class="nv"&gt;RTT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;553.445µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Endpoint connectivity to 10.0.1.53:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ICMP to stack: OK, &lt;span class="nv"&gt;RTT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;865.976µs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; HTTP to agent: OK, &lt;span class="nv"&gt;RTT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3.440655ms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;...&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And finally, we can just look at the &lt;code&gt;cilium status&lt;/code&gt; command which tells us who is &amp;ldquo;reachable&amp;rdquo; (again, in the cilium-agent container)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;➜ kubectl -n cilium &lt;span class="nb"&gt;exec&lt;/span&gt; -ti cilium-ddddd -- cilium status
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;...&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Host Routing: Legacy
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Masquerading: IPTables &lt;span class="o"&gt;[&lt;/span&gt;IPv4: Enabled, IPv6: Disabled&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;...&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Cluster health: 5/5 reachable &lt;span class="o"&gt;(&lt;/span&gt;2023-09-14T12:01:51Z&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="changing-a-nodes-configuration"&gt;Changing a node&amp;rsquo;s configuration
&lt;/h2&gt;&lt;p&gt;We&amp;rsquo;re in luck, because since cilium version 1.13 (the latest to date is 1.14), it&amp;rsquo;s possible to apply different configurations to a subset of nodes (official documentation for &lt;a class="link" href="https://docs.cilium.io/en/stable/configuration/per-node-config/#per-node-configuration" target="_blank" rel="noopener"
&gt;Per-node configuration&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Note: before that, it was still possible, &lt;em&gt;temporarily&lt;/em&gt;, by manually editing the cilium ConfigMap, then restarting the concerned pods for it to take effect.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s now a CRD to do this, called &lt;strong&gt;CiliumNodeConfig&lt;/strong&gt;. The only things we have to do are add a label to a node (io.cilium.enable-ebpf: &amp;ldquo;true&amp;rdquo;) and add the following manifest to our cluster:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat &amp;gt; cilium-fix.yaml &lt;span class="s"&gt;&amp;lt;&amp;lt; EOF
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;apiVersion: cilium.io/v2alpha1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;kind: CiliumNodeConfig
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;metadata:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; namespace: cilium
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; name: cilium-switch-from-iptables-ebpf
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;spec:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; nodeSelector:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; matchLabels:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; io.cilium.enable-ebpf: &amp;#34;true&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; defaults:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; enable-bpf-masquerade: &amp;#34;true&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl apply -f cilium-fix.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl label node node-05 --overwrite &lt;span class="s1"&gt;&amp;#39;io.cilium.enable-ebpf=true&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you&amp;rsquo;re in production, the cleanest approach is to &lt;code&gt;kubectl drain&lt;/code&gt; the Node beforehand.&lt;/p&gt;
&lt;p&gt;Out of curiosity, I still tried to do it &amp;ldquo;hot&amp;rdquo;, for fun.&lt;/p&gt;
&lt;p&gt;I deployed a daemonset containing &lt;a class="link" href="https://github.com/zwindler/vhelloworld" target="_blank" rel="noopener"
&gt;a V(lang) app that simply returns the container name in a web page&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;cat &amp;gt; vhelloworld-daemonset.yaml &amp;lt;&amp;lt; EOF&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;apps/v1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;DaemonSet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;vhelloworld-daemonset&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;matchLabels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;vhelloworld&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;vhelloworld&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;containers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;vhelloworld&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;zwindler/vhelloworld:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;containerPort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8081&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Always&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;EOF&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;kubectl apply -f vhelloworld-daemonset.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl get pods -o wide
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vhelloworld-daemonset-q4h44 1/1 Running &lt;span class="m"&gt;0&lt;/span&gt; 3m31s 10.0.4.87 nodekube-05 &amp;lt;none&amp;gt; &amp;lt;none&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vhelloworld-daemonset-w97pv 1/1 Running &lt;span class="m"&gt;0&lt;/span&gt; 3m31s 10.0.3.85 nodekube-04 &amp;lt;none&amp;gt; &amp;lt;none&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then I created containers with a shell and &lt;code&gt;curl&lt;/code&gt; to periodically check from multiple nodes that the containers were accessible:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl run -it --image curlimages/curl:latest curler -- /bin/sh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;If you don&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;t see a &lt;span class="nb"&gt;command&lt;/span&gt; prompt, try pressing enter.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;~ $ curl http://10.0.4.87:8081
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hello from vhelloworld-daemonset-g2zdw
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;~ $ curl http://10.0.3.85:8081
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hello from vhelloworld-daemonset-65k2n
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;~ $ &lt;span class="k"&gt;while&lt;/span&gt; true&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; date
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; curl http://10.0.4.87:8081
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; curl http://10.0.3.85:8081
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; sleep &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once the label was applied to a node and its cilium pod killed (via &lt;code&gt;kubectl delete&lt;/code&gt;), the pods remained accessible:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Fri Sep &lt;span class="m"&gt;15&lt;/span&gt; 14:05:34 UTC &lt;span class="m"&gt;2023&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hello from vhelloworld-daemonset-g2zdw
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hello from vhelloworld-daemonset-65k2n
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Fri Sep &lt;span class="m"&gt;15&lt;/span&gt; 14:05:35 UTC &lt;span class="m"&gt;2023&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hello from vhelloworld-daemonset-g2zdw
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hello from vhelloworld-daemonset-65k2n
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Fri Sep &lt;span class="m"&gt;15&lt;/span&gt; 14:05:36 UTC &lt;span class="m"&gt;2023&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hello from vhelloworld-daemonset-g2zdw
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hello from vhelloworld-daemonset-65k2n
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At some point, cilium restarted, noticed the presence of existing pods, and took over with eBPF!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;evel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;info &lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Rewrote endpoint BPF program&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;containerID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8b7be1b032 &lt;span class="nv"&gt;datapathPolicyRevision&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="nv"&gt;desiredPolicyRevision&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="nv"&gt;endpointID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1018&lt;/span&gt; &lt;span class="nv"&gt;identity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;4773&lt;/span&gt; &lt;span class="nv"&gt;ipv4&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10.0.3.85 &lt;span class="nv"&gt;ipv6&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;k8sPodName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;default/vhelloworld-daemonset-w97pv &lt;span class="nv"&gt;subsys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;endpoint
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;info &lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Restored endpoint&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;endpointID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1018&lt;/span&gt; &lt;span class="nv"&gt;ipAddr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;[10.0.3.85 ]&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;subsys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;endpoint
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="does-it-work-but-is-it-better-in-terms-of-resource-consumption"&gt;Does it work, but is it better in terms of resource consumption?
&lt;/h2&gt;&lt;p&gt;That was &lt;strong&gt;the&lt;/strong&gt; good news. I was expecting gains because the cluster was really close to the dreaded death by iptables.&lt;/p&gt;
&lt;p&gt;The cilium containers were using 10% of my nodes&amp;rsquo; CPU, and several GB of RAM in &lt;code&gt;iptables&lt;/code&gt; mode. It could quickly have gone much higher if I had grown the cluster beyond 50 nodes / 2000 pods.&lt;/p&gt;
&lt;p&gt;Switching to eBPF mode allowed us to return to totally painless levels (1% CPU per node, 1-2% RAM) compared to my machines&amp;rsquo; configurations.&lt;/p&gt;
&lt;p&gt;Not bad, huh?&lt;/p&gt;
&lt;h2 id="bonus"&gt;Bonus
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://isovalent.com/blog/post/tutorial-migrating-to-cilium-part-1/" target="_blank" rel="noopener"
&gt;A really hairy tutorial to fully migrate CNI hot (from flannel to cilium)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Running Docker containers with Proxmox VE (and LXC)</title><link>https://blog.zwindler.fr/en/2022/11/07/running-docker-containers-with-proxmox-ve-and-lxc/</link><pubDate>Mon, 07 Nov 2022 06:30:00 +0200</pubDate><guid>https://blog.zwindler.fr/en/2022/11/07/running-docker-containers-with-proxmox-ve-and-lxc/</guid><description>&lt;img src="https://blog.zwindler.fr/2019/10/proxmox.webp" alt="Featured image of post Running Docker containers with Proxmox VE (and LXC)" /&gt;&lt;h2 id="introduction"&gt;Introduction
&lt;/h2&gt;&lt;p&gt;I found several tutorials to install Docker (engine) &lt;strong&gt;inside&lt;/strong&gt; an LXC container under Proxmox VE to then run Docker containers inside it. The problem is that you have to log &lt;strong&gt;into&lt;/strong&gt; the LXC container before you can interact with your Docker containers.&lt;/p&gt;
&lt;p&gt;You can also install Docker directly on Proxmox VE but it&amp;rsquo;s not really recommended and you don&amp;rsquo;t have the ease of use of LXC with Proxmox GUI.&lt;/p&gt;
&lt;p&gt;And actually, it turns out you can run Docker images &lt;strong&gt;as the OS&lt;/strong&gt; of an LXC container and thus have Docker controlled by Proxmox. So we won&amp;rsquo;t have 100% identical behavior to if you had done a &lt;code&gt;docker run&lt;/code&gt; on your machine.&lt;/p&gt;
&lt;p&gt;However, we&amp;rsquo;ll gain in exchange a container totally identical to your other LXC containers in your cluster (with all associated functions) but launched with a Docker image.&lt;/p&gt;
&lt;p&gt;But first I suggest we go back a bit before showing you how&amp;hellip;&lt;/p&gt;
&lt;h2 id="context"&gt;Context
&lt;/h2&gt;&lt;p&gt;You know, I&amp;rsquo;ve been doing Proxmox VE for a while. Proxmox VE is a great turnkey and production-ready distribution for server virtualization (it&amp;rsquo;s even hyperconverged if you enable the Ceph part).&lt;/p&gt;
&lt;p&gt;There are lots of little things I like about Proxmox VE, and one of them is that we can create Linux containers with LXC rather than QEMU virtual machines. &lt;a class="link" href="https://blog.zwindler.fr/2022/10/22/proxmox-tips-tricks/" &gt;I wrote an article to talk about it&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In use (except for specific kernel configuration), the LXC container behaves strictly identically to a VM in Proxmox, but for a fraction of the resources! We have an OS, we install applications on it, like a VM.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2022/11/nginx.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;An nginx frontend for several applications. The complete LXC container only consumes a few MB&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Admittedly it&amp;rsquo;s much less isolated than a real VM (because it&amp;rsquo;s a container, we&amp;rsquo;re isolated from other processes, but we run on the hypervisor&amp;rsquo;s kernel directly).&lt;/p&gt;
&lt;p&gt;But with so few resource needs, I can host many small applications in different contexts with very small physical machines (Atom 4GB RAM at €6 per month at OneProviders) which would be impossible with a real VM.&lt;/p&gt;
&lt;h2 id="its-good-but-not-enough"&gt;It&amp;rsquo;s good, but not enough
&lt;/h2&gt;&lt;p&gt;However, LXC is not the most known/hyped containerization technology. For years on the forum, whenever people ask for Docker support in Proxmox VE, they get sent away. Not always very nicely either&amp;hellip;&lt;/p&gt;
&lt;p&gt;Yet, there are several reasons to want to run Docker containers on your Proxmox infrastructure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First, Docker isn&amp;rsquo;t as much stateless and/or ephemeral workloads as we think. Many containerized applications would have their place on a virtualization cluster.&lt;/li&gt;
&lt;li&gt;Then because many software vendors have become lazy. Some no longer manage installation processes (or only on one OS) and just provide a Docker image that they maintain up to date.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So we have to settle for VMs in Proxmox to run Docker containers (see my point at the beginning).&lt;/p&gt;
&lt;h2 id="lxc-supports-oci-images"&gt;LXC supports OCI images
&lt;/h2&gt;&lt;p&gt;Plot twist. A few days ago, I discovered that LXC, Proxmox VE&amp;rsquo;s containerization engine, was compatible with the OCI format.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://www.buzzwrd.me/index.php/2021/03/10/creating-lxc-containers-from-docker-and-oci-images/" target="_blank" rel="noopener"
&gt;buzzwrd.me - Creating LXC containers from docker and OCI images&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For those who don&amp;rsquo;t know OCI, it&amp;rsquo;s an organization aiming to create a standard to unify the way containers are stored.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The Open Container Initiative is an open governance structure for the express purpose of creating open industry standards around container formats and runtimes.
&lt;a class="link" href="https://opencontainers.org/" target="_blank" rel="noopener"
&gt;opencontainers.org/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And Docker respects this format: we can therefore theoretically run from LXC containers whose base image is a Docker image.&lt;/p&gt;
&lt;h2 id="so-when-do-we-start"&gt;So, when do we start?
&lt;/h2&gt;&lt;p&gt;Disclaimer: what follows is tinkering. Nothing is supported and I don&amp;rsquo;t recommend it in production. There are also likely limitations (especially regarding storage).&lt;/p&gt;
&lt;p&gt;First, some dependencies are missing for LXC&amp;rsquo;s OCI function to be usable under Proxmox VE:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo apt install skopeo umoci jq
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then, by default, LXC containers attach to a Linux bridge called &lt;code&gt;lxcbr0&lt;/code&gt;. However this bridge probably doesn&amp;rsquo;t exist on your Proxmox VE installation (not installed by default).&lt;/p&gt;
&lt;p&gt;In &lt;em&gt;quick and dirty&lt;/em&gt;, we can just replace the default name, although ideally we should find the flag in the CLI to change the bridge. Depending on how you configured the network in your Proxmox VE server, you&amp;rsquo;ll probably use &lt;code&gt;vmbr0&lt;/code&gt; or &lt;code&gt;vmbr1&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sed -i &amp;#39;s/lxcbr0/vmbr1/g&amp;#39; /etc/lxc/default.conf
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now that we have everything, we can run our LXC containers from docker images. Here I&amp;rsquo;m running the official &lt;code&gt;alpine:latest&lt;/code&gt; image, but I could have taken any other:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;lxc-create 500 -t oci -- --url docker://alpine:latest
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I created a container called &amp;ldquo;500&amp;rdquo; (because VMs and LXC containers in proxmox VE are called with numbers). The image is downloaded then the container created:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2022/11/lxc_docker_pull.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;We can now start it (with &lt;code&gt;lxc-execute&lt;/code&gt; we run the container and open a prompt directly inside it, but we also have &lt;code&gt;lxc-start&lt;/code&gt; and &lt;code&gt;lxc-attach&lt;/code&gt;)&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;lxc-execute 500
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2022/11/alpine.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;As is, Proxmox VE doesn&amp;rsquo;t know our container exists and it won&amp;rsquo;t appear in the UI. But it works ;-).&lt;/p&gt;
&lt;h2 id="bonus"&gt;Bonus
&lt;/h2&gt;&lt;p&gt;We can even cheat and make Proxmox believe it&amp;rsquo;s an LXC container it created itself by creating a &lt;code&gt;500.conf&lt;/code&gt; file, in the &lt;code&gt;/etc/pve/lxc/&lt;/code&gt; folder&lt;/p&gt;
&lt;p&gt;The only mandatory fields are the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;arch (amd64 unless you&amp;rsquo;re on ARM)&lt;/li&gt;
&lt;li&gt;cores (the CPU limit to impose on the container)&lt;/li&gt;
&lt;li&gt;hostname&lt;/li&gt;
&lt;li&gt;memory (the RAM limit to impose on the container)&lt;/li&gt;
&lt;li&gt;ostype&lt;/li&gt;
&lt;li&gt;rootfs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2022/11/lxc_config.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;Once the file is created, the container automatically appears in the host&amp;rsquo;s VM list. We can start it and it works :)&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.zwindler.fr/2022/11/docker_lxc_running.avif"
loading="lazy"
&gt;&lt;/p&gt;
&lt;p&gt;The container can be started, stopped, etc. All from the Proxmox VE GUI :)&lt;/p&gt;
&lt;p&gt;Have fun!&lt;/p&gt;
&lt;h2 id="source"&gt;Source
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://pve.proxmox.com/wiki/Linux_Container" target="_blank" rel="noopener"
&gt;Proxmox VE - Linux Container&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://thehomelab.wiki/books/promox-ve/page/setup-and-install-docker-in-a-promox-7-lxc-conainer" target="_blank" rel="noopener"
&gt;Setup and Install Docker in a Promox 7 LXC Conainer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.buzzwrd.me/index.php/2021/03/10/creating-lxc-containers-from-docker-and-oci-images/" target="_blank" rel="noopener"
&gt;buzzwrd.me - Creating LXC containers from docker and OCI images&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>