<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Mikhail [azalio] Petrov</title>
    <description>The latest articles on Forem by Mikhail [azalio] Petrov (@azalio).</description>
    <link>https://forem.com/azalio</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F28117%2Fafa7e52f-04c8-4fff-8169-1a9b4082698f.jpeg</url>
      <title>Forem: Mikhail [azalio] Petrov</title>
      <link>https://forem.com/azalio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/azalio"/>
    <language>en</language>
    <item>
      <title>Kubernetes Agent Blind to New Mounts? Demystifying Mount Propagation</title>
      <dc:creator>Mikhail [azalio] Petrov</dc:creator>
      <pubDate>Wed, 23 Apr 2025 20:43:54 +0000</pubDate>
      <link>https://forem.com/azalio/kubernetes-agent-blind-to-new-mounts-demystifying-mount-propagation-38ic</link>
      <guid>https://forem.com/azalio/kubernetes-agent-blind-to-new-mounts-demystifying-mount-propagation-38ic</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frokcbj6j4rv4yorwa8wc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frokcbj6j4rv4yorwa8wc.png" alt="mountpropagation" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How &lt;code&gt;mountPropagation: HostToContainer&lt;/code&gt; leverages Linux namespaces to solve a common agent problem
&lt;/h2&gt;

&lt;p&gt;Ever run into this frustrating scenario? You deploy a Kubernetes agent (like an I/O limiter, monitoring tool, or operator) that needs to interact with PersistentVolumes mounted by Kubelet for other pods. It works fine initially, seeing all existing mounts. But then, a &lt;em&gt;new&lt;/em&gt; pod gets scheduled, its PV gets mounted by Kubelet... and your agent is completely blind to it! 😠 Restarting the agent fixes it, but that's just a workaround, not a solution.&lt;/p&gt;

&lt;p&gt;I recently wrestled with this exact issue while building an I/O limiter that needed device &lt;code&gt;MAJOR:MINOR&lt;/code&gt; numbers from pod mounts. Let's dive into why this happens and how to fix it properly using Kubernetes &lt;code&gt;mountPropagation&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Root Cause: Linux Mount Namespaces
&lt;/h3&gt;

&lt;p&gt;The core of the issue lies in &lt;strong&gt;Linux mount namespaces&lt;/strong&gt;. By default, each container in Kubernetes gets its own isolated mount namespace. Think of it as a private view of the system's mount points. When a container starts, it gets a &lt;em&gt;copy&lt;/em&gt; of the host's mount table at that moment.&lt;/p&gt;

&lt;p&gt;Crucially, subsequent mount events happening on the host (like Kubelet mounting a new PV under &lt;code&gt;/var/lib/kubelet/pods/&lt;/code&gt;) are &lt;em&gt;not&lt;/em&gt; automatically reflected inside the container's isolated namespace. This default behavior corresponds to &lt;code&gt;private&lt;/code&gt; propagation in Linux terminology.&lt;/p&gt;

&lt;p&gt;In Kubernetes, this default isolation is represented by:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;mountPropagation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;None&lt;/span&gt; &lt;span class="c1"&gt;# (Default if not specified)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isolation is great for security and preventing containers from interfering with each other or the host, but it causes the "blind agent" problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution: Kubernetes &lt;code&gt;mountPropagation&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Kubernetes provides the &lt;code&gt;mountPropagation&lt;/code&gt; field within a &lt;code&gt;volumeMount&lt;/code&gt; definition to control how mount events are shared between the host and the container's namespace. It bridges the isolation gap when needed. There are three modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;None&lt;/code&gt; (Default):&lt;/strong&gt; Complete isolation. No mount events propagate in either direction. (Linux: &lt;code&gt;private&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;HostToContainer&lt;/code&gt;:&lt;/strong&gt; One-way street! Mount events from the host &lt;strong&gt;propagate into&lt;/strong&gt; the container. Mounts created &lt;em&gt;inside&lt;/em&gt; the container do &lt;strong&gt;not&lt;/strong&gt; propagate back to the host. (Linux: &lt;code&gt;rslave&lt;/code&gt; - recursive slave). &lt;strong&gt;This is exactly what we need for our agent!&lt;/strong&gt; 👍&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Bidirectional&lt;/code&gt;:&lt;/strong&gt; Two-way street. Mounts propagate from host to container AND from container back to the host (and other containers in the pod using &lt;code&gt;Bidirectional&lt;/code&gt;). (Linux: &lt;code&gt;rshared&lt;/code&gt; - recursive shared). &lt;strong&gt;Use with extreme caution!&lt;/strong&gt; This can affect the host system and requires the container to be privileged or have &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Recipe: Fixing the Blind Agent
&lt;/h3&gt;

&lt;p&gt;To allow our agent container to see new PVs mounted by Kubelet under &lt;code&gt;/var/lib/kubelet/pods&lt;/code&gt; &lt;em&gt;after&lt;/em&gt; the agent has started, we need to mount that host directory into the agent and set &lt;code&gt;mountPropagation: HostToContainer&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Example Pod spec for the agent&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-observing-agent&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-agent-image:latest&lt;/span&gt;
    &lt;span class="c1"&gt;# Optional: If your agent needs to *do* something with mounts,&lt;/span&gt;
    &lt;span class="c1"&gt;# it might need privileges beyond just seeing them.&lt;/span&gt;
    &lt;span class="c1"&gt;# Bidirectional *requires* privileged or CAP_SYS_ADMIN.&lt;/span&gt;
    &lt;span class="c1"&gt;# HostToContainer itself doesn't require extra privileges just to see mounts.&lt;/span&gt;
    &lt;span class="c1"&gt;# securityContext:&lt;/span&gt;
    &lt;span class="c1"&gt;#   privileged: false&lt;/span&gt;
    &lt;span class="c1"&gt;#   capabilities:&lt;/span&gt;
    &lt;span class="c1"&gt;#     add: ["SYS_ADMIN"] # Example if needed for other operations&lt;/span&gt;
    &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubelet-pods-dir&lt;/span&gt;
      &lt;span class="c1"&gt;# The path inside the container where the host dir will be mounted&lt;/span&gt;
      &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/mnt/kubelet-pods&lt;/span&gt;
      &lt;span class="c1"&gt;# The magic setting!&lt;/span&gt;
      &lt;span class="na"&gt;mountPropagation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HostToContainer&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubelet-pods-dir&lt;/span&gt;
    &lt;span class="na"&gt;hostPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# The directory on the host we want to observe&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/lib/kubelet/pods&lt;/span&gt;
      &lt;span class="c1"&gt;# Ensures the path exists on the host&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DirectoryOrCreate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this configuration, any new directory or mount point created by Kubelet under &lt;code&gt;/var/lib/kubelet/pods&lt;/code&gt; on the host will automatically appear under &lt;code&gt;/mnt/kubelet-pods&lt;/code&gt; inside the &lt;code&gt;my-observing-agent&lt;/code&gt; container, without requiring a restart. Problem solved! ✅&lt;/p&gt;

&lt;h3&gt;
  
  
  Under the Hood: How It Works
&lt;/h3&gt;

&lt;p&gt;What happens when you set &lt;code&gt;HostToContainer&lt;/code&gt;?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Kubelet:&lt;/strong&gt; Sees &lt;code&gt;mountPropagation: HostToContainer&lt;/code&gt; in the Pod spec.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CRI Request:&lt;/strong&gt; Kubelet translates this to the corresponding enum (&lt;code&gt;PROPAGATION_HOST_TO_CONTAINER&lt;/code&gt;) in its request to the Container Runtime Interface (CRI).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container Runtime (containerd, CRI-O):&lt;/strong&gt; Receives the request and uses Linux system calls (specifically the &lt;code&gt;mount&lt;/code&gt; syscall with flags like &lt;code&gt;MS_SLAVE | MS_REC&lt;/code&gt;) to configure the container's mount point (&lt;code&gt;/mnt/kubelet-pods&lt;/code&gt; in our example) as a &lt;strong&gt;recursive slave&lt;/strong&gt; (&lt;code&gt;rslave&lt;/code&gt;) of the corresponding host mount point (&lt;code&gt;/var/lib/kubelet/pods&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Propagation:&lt;/strong&gt; Because the container's mount is now a slave of the host's mount, any mount events occurring under the host path are automatically propagated by the Linux kernel into the container's mount namespace at the slave mount point.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Important Considerations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security (&lt;code&gt;Bidirectional&lt;/code&gt;):&lt;/strong&gt; Seriously, be careful with &lt;code&gt;Bidirectional&lt;/code&gt;. A container mount propagating back to the host can have significant security implications, especially in multi-tenant clusters. It generally requires the container to run as privileged or have &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privileges (&lt;code&gt;HostToContainer&lt;/code&gt;):&lt;/strong&gt; Simply &lt;em&gt;observing&lt;/em&gt; mounts with &lt;code&gt;HostToContainer&lt;/code&gt; doesn't inherently require extra privileges. However, if your agent needs to &lt;em&gt;perform actions&lt;/em&gt; within those propagated mounts (like modifying files owned by root, mounting things itself), it might need appropriate capabilities (&lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt;) or run as privileged, independent of the &lt;code&gt;mountPropagation&lt;/code&gt; setting itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugging:&lt;/strong&gt; If things aren't working, check the mount table inside the container (&lt;code&gt;findmnt&lt;/code&gt; or &lt;code&gt;cat /proc/self/mountinfo&lt;/code&gt;) and compare it to the host's. Look for the &lt;code&gt;master:&lt;/code&gt; entries or propagation flags (&lt;code&gt;shared&lt;/code&gt;, &lt;code&gt;slave&lt;/code&gt;) associated with your mount point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Historical Note:&lt;/strong&gt; For a brief period around Kubernetes 1.10, the default was accidentally changed to &lt;code&gt;HostToContainer&lt;/code&gt; before being reverted. If you're managing a very old cluster, be aware of this possibility (&lt;a href="https://github.com/kubernetes/kubernetes/pull/62462" rel="noopener noreferrer"&gt;#62462&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Kubernetes &lt;code&gt;mountPropagation&lt;/code&gt; is a powerful mechanism rooted in Linux mount namespaces that allows you to selectively break container isolation for specific use cases. Understanding the &lt;code&gt;HostToContainer&lt;/code&gt; mode (&lt;code&gt;rslave&lt;/code&gt;) is key to solving the common problem of agents needing visibility into dynamically created host mounts, like those managed by Kubelet. By applying it correctly, you can build more robust and reliable agents and operators without resorting to restarting them.&lt;/p&gt;




&lt;p&gt;Connect with me on &lt;a href="https://www.linkedin.com/in/azalio/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>linux</category>
      <category>storage</category>
      <category>namespaces</category>
    </item>
    <item>
      <title>Securing Kubernetes API Server Health Checks Without Anonymous Access</title>
      <dc:creator>Mikhail [azalio] Petrov</dc:creator>
      <pubDate>Sun, 13 Apr 2025 19:11:00 +0000</pubDate>
      <link>https://forem.com/azalio/securing-kubernetes-api-server-health-checks-without-anonymous-access-31f9</link>
      <guid>https://forem.com/azalio/securing-kubernetes-api-server-health-checks-without-anonymous-access-31f9</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs23cgocckdh557tp8rj6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs23cgocckdh557tp8rj6.png" alt="Image description" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to disable anonymous auth globally but keep /livez, /readyz, and /healthz accessible
&lt;/h2&gt;

&lt;p&gt;Recently, a security-savvy colleague posed an interesting question: "Is it possible to disable anonymous access to the Kubernetes API server entirely, but still allow the &lt;code&gt;/livez&lt;/code&gt;, &lt;code&gt;/readyz&lt;/code&gt;, and &lt;code&gt;/healthz&lt;/code&gt; endpoints to work?" 🤔&lt;/p&gt;

&lt;p&gt;Honestly, I didn't know the answer offhand, so I dove into the Kubernetes source code and relevant KEPs (Kubernetes Enhancement Proposals). Here's what I found and how you can implement it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Default Scenario: Anonymous Access Enabled
&lt;/h3&gt;

&lt;p&gt;As you probably know, by default, &lt;code&gt;kube-apiserver&lt;/code&gt; allows anonymous authentication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--anonymous-auth     Default: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Enables anonymous requests to the secure port of the API server. Requests that are not rejected by another authentication method are treated as anonymous requests. Anonymous requests have a username of &lt;code&gt;system:anonymous&lt;/code&gt;, and a group name of &lt;code&gt;system:unauthenticated&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When you set up a cluster using &lt;code&gt;kubeadm&lt;/code&gt; with default settings, anonymous requests to health endpoints (and potentially others like &lt;code&gt;/version&lt;/code&gt;, depending on default bindings) succeed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# curl -k https://&amp;lt;YOUR_API_SERVER_IP&amp;gt;:6443/livez ; echo&lt;/span&gt;
ok
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looking at the audit logs, we can confirm the request is handled by &lt;code&gt;system:anonymous&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Event"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"apiVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"audit.k8s.io/v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Metadata"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"auditID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2caca66d-ccec-4b1f-8116-86741193cdce"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Example&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ID&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ResponseComplete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requestURI"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/livez"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"get"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system:anonymous"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Anonymous&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;user&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"groups"&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="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"system:unauthenticated"&lt;/span&gt;&lt;span class="w"&gt;
    &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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceIPs"&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="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;SOURCE_IP&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userAgent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"curl/7.88.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"responseStatus"&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="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requestReceivedTimestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-04-13T14:41:33.630618Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Example&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;timestamp&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stageTimestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-04-13T14:41:33.630908Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Example&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;timestamp&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"annotations"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"authorization.k8s.io/decision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"authorization.k8s.io/reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RBAC: allowed by ClusterRoleBinding &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;system:public-info-viewer&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;&lt;span class="w"&gt;
  &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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, anonymous users can't access other API paths like &lt;code&gt;/apis&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;audit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;metadata&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requestURI"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/apis"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"get"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system:anonymous"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"groups"&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="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system:unauthenticated"&lt;/span&gt;&lt;span class="w"&gt; &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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"responseStatus"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Failure"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"forbidden: User &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;system:anonymous&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; cannot get path &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;/apis&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Forbidden"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Forbidden&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;timestamps&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;annotations&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"annotations"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"authorization.k8s.io/decision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"forbid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"authorization.k8s.io/reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;
  &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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this default setup restricts access, leaving anonymous authentication enabled at all, even for specific paths, can be a security concern. Misconfigured RBAC or potential vulnerabilities could expose more than intended. 😟&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution: KEP-4633 and AuthenticationConfiguration
&lt;/h3&gt;

&lt;p&gt;Digging into the Kubernetes source code led me to &lt;a href="https://github.com/kubernetes/enhancements/blob/master/keps/sig-auth/4633-anonymous-auth-configurable-endpoints/README.md" rel="noopener noreferrer"&gt;KEP-4633: Make anonymous authentication configuration endpoints configurable&lt;/a&gt;. This KEP addresses the exact concern of wanting to disable anonymous access globally while still allowing essential health checks (which don't necessarily need full TCP checks to be useful).&lt;/p&gt;

&lt;p&gt;The solution, documented &lt;a href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/#anonymous-authenticator-configuration" rel="noopener noreferrer"&gt;here&lt;/a&gt;, involves using an &lt;code&gt;AuthenticationConfiguration&lt;/code&gt; object.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to Implement It:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Disable Global Anonymous Auth:&lt;/strong&gt;&lt;br&gt;
Modify your &lt;code&gt;kube-apiserver&lt;/code&gt; manifest (usually a static pod in &lt;code&gt;/etc/kubernetes/manifests/&lt;/code&gt;) to include these flags:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;kube-apiserver&lt;/span&gt;
    &lt;span class="c1"&gt;# ... other flags ...&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--anonymous-auth=false&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;--- Disable global anonymous auth&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--authentication-config=/etc/kubernetes/auth-config.yaml&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;--- Point to the config file&lt;/span&gt;
    &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# ... other volume mounts ...&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/kubernetes/auth-config.yaml&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auth-config-volume&lt;/span&gt;
      &lt;span class="na"&gt;readOnly&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ... other volumes ...&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auth-config-volume&lt;/span&gt;
    &lt;span class="na"&gt;hostPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/kubernetes/auth-config.yaml&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;File&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create the Authentication Configuration File:&lt;/strong&gt;&lt;br&gt;
Create the following file on your control plane node(s) at &lt;code&gt;/etc/kubernetes/auth-config.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /etc/kubernetes/auth-config.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apiserver.config.k8s.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AuthenticationConfiguration&lt;/span&gt;
&lt;span class="na"&gt;anonymous&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Enable anonymous access ONLY for the paths listed below&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;conditions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/livez&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/readyz&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/healthz&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;em&gt;Ensure the &lt;code&gt;kube-apiserver&lt;/code&gt; pod restarts to pick up the changes.&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Result: Granular Anonymous Access
&lt;/h3&gt;

&lt;p&gt;With this configuration:&lt;/p&gt;

&lt;p&gt;✅ Requests to &lt;code&gt;/livez&lt;/code&gt;, &lt;code&gt;/readyz&lt;/code&gt;, and &lt;code&gt;/healthz&lt;/code&gt; are still allowed for &lt;code&gt;system:anonymous&lt;/code&gt;. The audit log looks similar to the default case for these paths.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;audit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;metadata&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requestURI"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/livez"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"get"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system:anonymous"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Still&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;anonymous&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"groups"&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="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system:unauthenticated"&lt;/span&gt;&lt;span class="w"&gt; &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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"responseStatus"&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="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Still&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;OK&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;timestamps&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;annotations&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;❌ Requests to any &lt;em&gt;other&lt;/em&gt; path (like &lt;code&gt;/apis&lt;/code&gt;) without valid credentials will now receive a &lt;code&gt;401 Unauthorized&lt;/code&gt; instead of a &lt;code&gt;403 Forbidden&lt;/code&gt;, because the request isn't even treated as anonymous anymore.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# curl -k https://&amp;lt;YOUR_API_SERVER_IP&amp;gt;:6443/apis ; echo&lt;/span&gt;
Unauthorized
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The audit log confirms this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;audit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;metadata&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requestURI"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/apis"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"get"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user"&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="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;No&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;identified&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;even&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;anonymous)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"responseStatus"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Failure"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Unauthorized"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Unauthorized"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Unauthorized&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;timestamps&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Feature Availability
&lt;/h3&gt;

&lt;p&gt;According to the &lt;a href="https://github.com/kubernetes/enhancements/issues/4633" rel="noopener noreferrer"&gt;KEP issue tracker&lt;/a&gt;, this feature graduated to Alpha in Kubernetes v1.31 and Beta in v1.32.&lt;/p&gt;

&lt;p&gt;This approach provides a more secure posture by default, explicitly allowing anonymous access only where strictly necessary for health checks, without relying solely on RBAC for protection against anonymous requests to other endpoints. Now you can tighten security without breaking essential cluster monitoring! 👍&lt;/p&gt;




&lt;p&gt;Connect with me on &lt;a href="https://www.linkedin.com/in/azalio/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>security</category>
      <category>devops</category>
      <category>infosec</category>
    </item>
    <item>
      <title>Complete Guide: Cilium L2 Announcements for LoadBalancer Services in Bare-Metal Kubernetes</title>
      <dc:creator>Mikhail [azalio] Petrov</dc:creator>
      <pubDate>Wed, 22 Jan 2025 13:06:36 +0000</pubDate>
      <link>https://forem.com/azalio/complete-guide-cilium-l2-announcements-for-loadbalancer-services-in-bare-metal-kubernetes-3jl2</link>
      <guid>https://forem.com/azalio/complete-guide-cilium-l2-announcements-for-loadbalancer-services-in-bare-metal-kubernetes-3jl2</guid>
      <description>&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;Environment Setup&lt;/li&gt;
&lt;li&gt;Nginx Deployment&lt;/li&gt;
&lt;li&gt;Ingress Configuration&lt;/li&gt;
&lt;li&gt;LoadBalancer IP Pool Configuration&lt;/li&gt;
&lt;li&gt;ARP Issue&lt;/li&gt;
&lt;li&gt;Enabling L2 Announcements&lt;/li&gt;
&lt;li&gt;
How It Works

&lt;ul&gt;
&lt;li&gt;L2 Policy Configuration&lt;/li&gt;
&lt;li&gt;Lease Acquisition&lt;/li&gt;
&lt;li&gt;BPF Map for ARP&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Packet Path&lt;/li&gt;
&lt;li&gt;
Additional Resources

&lt;ul&gt;
&lt;li&gt;
Cilium L2 Announcements vs Proxy ARP
&lt;/li&gt;
&lt;li&gt;Cached vs Non-Cached Maps in Cilium&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Introduction: Why L2 Announcements in Kubernetes? &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Target Audience
&lt;/h3&gt;

&lt;p&gt;This material is intended for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kubernetes cluster administrators&lt;/li&gt;
&lt;li&gt;Network engineers working with Cilium technologies (eBPF, CNI)&lt;/li&gt;
&lt;li&gt;Infrastructure specialists familiar with L2/L3 networking basics&lt;/li&gt;
&lt;li&gt;Professionals interested in Kubernetes networking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📚 Terminology (recommended)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gratuitous ARP&lt;/strong&gt; - Special ARP packet broadcast by a node when changing its MAC or IP address to update neighbor ARP caches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kubernetes Lease&lt;/strong&gt; - Resource leasing mechanism through API, ensuring exclusive access to resources (IP addresses in this case) for cluster nodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bare-Metal Kubernetes&lt;/strong&gt; - Cluster infrastructure deployed on physical servers without cloud providers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem Statement
&lt;/h3&gt;

&lt;p&gt;When working with Kubernetes in bare-metal environments, accessing LoadBalancer services from external networks often becomes challenging. Traditional solutions like &lt;strong&gt;MetalLB&lt;/strong&gt; require additional components and can be overkill for simple scenarios.&lt;/p&gt;

&lt;p&gt;Cilium provides native &lt;a href="https://docs.cilium.io/en/latest/network/l2-announcements/" rel="noopener noreferrer"&gt;L2 announcement capabilities&lt;/a&gt; with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ARP response handling for LoadBalancer IPs&lt;/li&gt;
&lt;li&gt;High availability through lease mechanism&lt;/li&gt;
&lt;li&gt;Integration with existing network infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This guide covers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configuring L2 announcements in Cilium&lt;/li&gt;
&lt;li&gt;ARP response mechanics through BPF&lt;/li&gt;
&lt;li&gt;Practical tips for working with Cilium&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The primary goal is to demonstrate Kubernetes service accessibility in bare-metal environments using Cilium's native capabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment Setup &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Environment setup is described in the &lt;a href="https://github.com/azalio/cilium-l2-announcements-workshop/blob/main/README.md" rel="noopener noreferrer"&gt;README.md&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Network diagram:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcki1vj0e3ilkwzab4m8k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcki1vj0e3ilkwzab4m8k.png" alt="Network scheme" width="800" height="206"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pod Subnets&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;10.200.0.0/24&lt;/code&gt; for server&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;10.200.1.0/24&lt;/code&gt; for node-0&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;10.200.2.0/24&lt;/code&gt; for node-1&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Kubernetes and Cilium Configuration&lt;/p&gt;

&lt;p&gt;VMs created on ARM Mac using Vagrant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;jumpbox - 192.168.56.10&lt;/strong&gt; - client outside Kubernetes cluster.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq9g78b6r7knu2ggvjpnm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq9g78b6r7knu2ggvjpnm.png" alt="jumpbox network" width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;server - 192.168.56.20&lt;/strong&gt; - control plane  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwtgmrx68gpq15ilr76td.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwtgmrx68gpq15ilr76td.png" alt="Server networking" width="800" height="250"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;node-0 - 192.168.56.50&lt;/strong&gt; - k8s node  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjoz8tjuz5rsrlsjx7ocb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjoz8tjuz5rsrlsjx7ocb.png" alt="node-0 networking" width="800" height="248"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;node-1 - 192.168.56.60&lt;/strong&gt; - k8s node  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxejwxyp1fnxhoa5jual3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxejwxyp1fnxhoa5jual3.png" alt="node-1 networking" width="800" height="248"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;Cilium with native routing (no tunnels). Version v1.16.5.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm upgrade &lt;span class="nt"&gt;--install&lt;/span&gt; cilium cilium/cilium &lt;span class="nt"&gt;--version&lt;/span&gt; 1.16.5 &lt;span class="nt"&gt;--namespace&lt;/span&gt; kube-system &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; l2announcements.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; externalIPs.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; &lt;span class="nv"&gt;kubeProxyReplacement&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; ipam.mode&lt;span class="o"&gt;=&lt;/span&gt;kubernetes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; &lt;span class="nv"&gt;k8sServiceHost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;192.168.56.20 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; &lt;span class="nv"&gt;k8sServicePort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6443 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; operator.replicas&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; &lt;span class="nv"&gt;routingMode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;native &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; &lt;span class="nv"&gt;ipv4NativeRoutingCIDR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10.200.0.0/22 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; endpointRoutes.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; ingressController.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; ingressController.loadbalancerMode&lt;span class="o"&gt;=&lt;/span&gt;dedicated 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Kernel info:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# uname -a&lt;/span&gt;
Linux node-1 6.1.0-20-arm64 &lt;span class="c"&gt;#1 SMP Debian 6.1.85-1 (2024-04-11) aarch64 aarch64 aarch64 GNU/Linux&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Deploy Nginx &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;In this section we'll deploy a simple Nginx web server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1. Create Deployment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# vagrant ssh server&lt;/span&gt;
&lt;span class="c"&gt;# sudo bash&lt;/span&gt;

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: nginx
  namespace: default
spec:
  selector:
    app: nginx
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2. Verify Deployment
&lt;/h3&gt;

&lt;p&gt;Check service status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# kubectl get pod -o wide&lt;/span&gt;
NAME                   READY   STATUS    RESTARTS   AGE   IP             NODE     NOMINATED NODE   READINESS GATES
nginx-96b9d695-25swg   1/1     Running   0          55s   10.200.2.189   node-1   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;

&lt;span class="c"&gt;# kubectl get svc nginx&lt;/span&gt;
NAME    TYPE        CLUSTER-IP    EXTERNAL-IP   PORT&lt;span class="o"&gt;(&lt;/span&gt;S&lt;span class="o"&gt;)&lt;/span&gt;   AGE
nginx   ClusterIP   10.96.79.80   &amp;lt;none&amp;gt;        80/TCP    89s

&lt;span class="c"&gt;# curl 10.96.79.80&lt;/span&gt;
&amp;lt;&lt;span class="o"&gt;!&lt;/span&gt;DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;&lt;span class="nb"&gt;head&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&amp;lt;title&amp;gt;Welcome to nginx!&amp;lt;/title&amp;gt;
...

&lt;span class="c"&gt;# curl 10.200.2.189&lt;/span&gt;
&amp;lt;&lt;span class="o"&gt;!&lt;/span&gt;DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;&lt;span class="nb"&gt;head&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&amp;lt;title&amp;gt;Welcome to nginx!&amp;lt;/title&amp;gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;↑ Table of Contents | ← Back&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure Ingress &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.cilium.io/en/stable/network/servicemesh/ingress/#kubernetes-ingress-support" rel="noopener noreferrer"&gt;Cilium documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Apply ingress manifest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: basic-ingress
  namespace: default
spec:
  ingressClassName: cilium
  rules:
  - http:
      paths:
      - backend:
          service:
            name: nginx
            port:
              number: 80
        path: /
        pathType: Prefix
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# kubectl get ingress&lt;/span&gt;
NAME            CLASS    HOSTS   ADDRESS   PORTS   AGE
basic-ingress   cilium   &lt;span class="k"&gt;*&lt;/span&gt;                 80      40s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;This creates a corresponding service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# kubectl get svc cilium-ingress-basic-ingress&lt;/span&gt;
NAME                           TYPE           CLUSTER-IP      EXTERNAL-IP   PORT&lt;span class="o"&gt;(&lt;/span&gt;S&lt;span class="o"&gt;)&lt;/span&gt;                      AGE
cilium-ingress-basic-ingress   LoadBalancer   10.96.156.194   &amp;lt;pending&amp;gt;     80:31017/TCP,443:32600/TCP   115s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;The &lt;code&gt;EXTERNAL-IP&lt;/code&gt; remains in &lt;code&gt;pending&lt;/code&gt; state due to missing IP pool configuration.&lt;/p&gt;

&lt;p&gt;↑ Table of Contents | ← Back&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure LoadBalancer IP Pool &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Cilium supports IP assignment for LoadBalancer services.&lt;/p&gt;

&lt;h3&gt;
  
  
  CiliumLoadBalancerIPPool
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.cilium.io/en/stable/network/lb-ipam/" rel="noopener noreferrer"&gt;Official documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Create CiliumLoadBalancerIPPool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | kubectl apply -f -
apiVersion: "cilium.io/v2alpha1"
kind: CiliumLoadBalancerIPPool
metadata:
  name: "blue-pool"
spec:
  blocks:
  - cidr: "10.0.10.0/24"
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;Now the service gets an IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# kubectl get svc cilium-ingress-basic-ingress&lt;/span&gt;
NAME                           TYPE           CLUSTER-IP      EXTERNAL-IP   PORT&lt;span class="o"&gt;(&lt;/span&gt;S&lt;span class="o"&gt;)&lt;/span&gt;                      AGE
cilium-ingress-basic-ingress   LoadBalancer   10.96.156.194   10.0.10.0     80:31017/TCP,443:32600/TCP   4m9s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;Service is now accessible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# curl 10.0.10.0&lt;/span&gt;
&amp;lt;&lt;span class="o"&gt;!&lt;/span&gt;DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;&lt;span class="nb"&gt;head&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&amp;lt;title&amp;gt;Welcome to nginx!&amp;lt;/title&amp;gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;↑ Table of Contents | ← Back&lt;/p&gt;

&lt;h2&gt;
  
  
  ARP Issue &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Testing from jumpbox client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# vagrant ssh jumpbox&lt;/span&gt;
root@jumpbox:/home/vagrant# curl 10.0.10.0
curl: &lt;span class="o"&gt;(&lt;/span&gt;7&lt;span class="o"&gt;)&lt;/span&gt; Failed to connect to 10.0.10.0 port 80 after 3074 ms: Couldn&lt;span class="s1"&gt;'t connect to server
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why? No ARP responses received.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@server:/home/vagrant# tcpdump &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; any arp host 10.0.10.0
tcpdump: data &lt;span class="nb"&gt;link type &lt;/span&gt;LINUX_SLL2
tcpdump: verbose output suppressed, use &lt;span class="nt"&gt;-v&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;v]... &lt;span class="k"&gt;for &lt;/span&gt;full protocol decode
listening on any, link-type LINUX_SLL2 &lt;span class="o"&gt;(&lt;/span&gt;Linux cooked v2&lt;span class="o"&gt;)&lt;/span&gt;, snapshot length 262144 bytes

21:15:05.927064 eth1  B   ARP, Request who-has 10.0.10.0 tell 192.168.56.10, length 46
21:15:06.948513 eth1  B   ARP, Request who-has 10.0.10.0 tell 192.168.56.10, length 46
21:15:07.973210 eth1  B   ARP, Request who-has 10.0.10.0 tell 192.168.56.10, length 46
21:15:08.998950 eth1  B   ARP, Request who-has 10.0.10.0 tell 192.168.56.10, length 46
21:15:10.024080 eth1  B   ARP, Request who-has 10.0.10.0 tell 192.168.56.10, length 46
21:15:11.050053 eth1  B   ARP, Request who-has 10.0.10.0 tell 192.168.56.10, length 46

root@jumpbox:/home/vagrant# arp &lt;span class="nt"&gt;-n&lt;/span&gt; 10.0.10.0
Address                  HWtype  HWaddress           Flags Mask            Iface
10.0.10.0                        &lt;span class="o"&gt;(&lt;/span&gt;incomplete&lt;span class="o"&gt;)&lt;/span&gt;                              eth1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;↑ Table of Contents | ← Back&lt;/p&gt;

&lt;h2&gt;
  
  
  Enable L2 Announcements &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Enable &lt;a href="https://docs.cilium.io/en/stable/network/l2-announcements/" rel="noopener noreferrer"&gt;ARP announcements&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmg8xweo3yumagadg25vw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmg8xweo3yumagadg25vw.png" alt="L2 Announcements" width="800" height="1250"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@server:/home/vagrant# tcpdump &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; any arp host 10.0.10.0 &amp;amp; &lt;span class="c"&gt;# background tcpdump&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;1] 17207

root@server:/home/vagrant# &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | kubectl apply -f -
apiVersion: "cilium.io/v2alpha1"
kind: CiliumL2AnnouncementPolicy
metadata:
  name: policy1
spec:
  nodeSelector:
    matchExpressions:
      - key: node-role.kubernetes.io/control-plane
        operator: DoesNotExist
  interfaces:
  - ^eth[0-9]+
  externalIPs: true
  loadBalancerIPs: true
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;ciliuml2announcementpolicy.cilium.io/policy1 created

21:18:52.093372 eth1  B   ARP, Reply 10.0.10.0 is-at 00:0c:29:0d:b7:76, length 46
21:18:52.102795 eth0  B   ARP, Reply 10.0.10.0 is-at 00:0c:29:0d:b7:6c, length 46

root@jumpbox:/home/vagrant# tcpdump &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; any arp
tcpdump: data &lt;span class="nb"&gt;link type &lt;/span&gt;LINUX_SLL2
tcpdump: verbose output suppressed, use &lt;span class="nt"&gt;-v&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;v]... &lt;span class="k"&gt;for &lt;/span&gt;full protocol decode
listening on any, link-type LINUX_SLL2 &lt;span class="o"&gt;(&lt;/span&gt;Linux cooked v2&lt;span class="o"&gt;)&lt;/span&gt;, snapshot length 262144 bytes

21:18:52.113122 eth1  B   ARP, Reply 10.0.10.0 is-at 00:0c:29:0d:b7:76, length 46
21:18:52.113211 eth1  B   ARP, Reply 10.0.10.1 is-at 00:0c:29:e3:b1:b2, length 46
21:18:52.122245 eth0  B   ARP, Reply 10.0.10.1 is-at 00:0c:29:e3:b1:a8, length 46
21:18:52.122495 eth0  B   ARP, Reply 10.0.10.0 is-at 00:0c:29:0d:b7:6c, length 46

root@jumpbox:/home/vagrant# arp &lt;span class="nt"&gt;-n&lt;/span&gt; 10.0.10.0
Address                  HWtype  HWaddress           Flags Mask            Iface
10.0.10.0                ether   00:0c:29:0d:b7:76   C                     eth1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MAC &lt;code&gt;00:0c:29:0d:b7:76&lt;/code&gt; belongs to node-1's eth1 interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@node-1:/home/vagrant# ip addr sh | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; 00:0c:29:0d:b7:76
3: eth1: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc fq_codel state UP group default qlen 1000
    &lt;span class="nb"&gt;link&lt;/span&gt;/ether 00:0c:29:0d:b7:76 brd ff:ff:ff:ff:ff:ff
    altname enp18s0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify lease acquisition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# kubectl get lease -n kube-system cilium-l2announce-default-cilium-ingress-basic-ingress&lt;/span&gt;
NAME                                                     HOLDER   AGE
cilium-l2announce-default-cilium-ingress-basic-ingress   node-1   8m49s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;↑ Table of Contents | ← Back&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvifkl9o80qhuk4k1k3bn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvifkl9o80qhuk4k1k3bn.png" alt="arp-path" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;L2 Policy Configuration&lt;/li&gt;
&lt;li&gt;Lease Acquisition&lt;/li&gt;
&lt;li&gt;BPF Map for ARP&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For detailed documentation see &lt;a href="https://docs.cilium.io/en/latest/network/l2-announcements/" rel="noopener noreferrer"&gt;Cilium L2 Announcements&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  L2 Policy Configuration &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Configure L2 announcement policy&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cilium.io/v2alpha1"&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CiliumL2AnnouncementPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;policy1&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nodeSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchExpressions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node-role.kubernetes.io/control-plane&lt;/span&gt;
        &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DoesNotExist&lt;/span&gt;
  &lt;span class="na"&gt;interfaces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;^eth[0-9]+&lt;/span&gt;
  &lt;span class="na"&gt;externalIPs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;loadBalancerIPs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Lease Acquisition &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Cilium acquires lease &lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@server:/home/vagrant# kubectl get lease &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system | &lt;span class="nb"&gt;grep &lt;/span&gt;l2announce
cilium-l2announce-default-cilium-ingress-basic-ingress   node-1                                                                      16m
cilium-l2announce-kube-system-cilium-ingress             node-0                                                                      16m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  BPF Map for ARP &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;BPF map created for ARP responses &lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# kubectl get svc cilium-ingress-basic-ingress&lt;/span&gt;
NAME                           TYPE           CLUSTER-IP      EXTERNAL-IP   PORT&lt;span class="o"&gt;(&lt;/span&gt;S&lt;span class="o"&gt;)&lt;/span&gt;                      AGE
cilium-ingress-basic-ingress   LoadBalancer   10.96.156.194   10.0.10.0     80:31017/TCP,443:32600/TCP   24h
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@node-1:/home/cilium# bpftool map show pinned /sys/fs/bpf/tc/globals/cilium_l2_responder_v4
72: &lt;span class="nb"&gt;hash  &lt;/span&gt;name cilium_l2_respo  flags 0x1
    key 8B  value 8B  max_entries 4096  memlock 65536B
    btf_id 125
root@node-1:/home/cilium# bpftool map dump pinned /sys/fs/bpf/tc/globals/cilium_l2_responder_v4
&lt;span class="o"&gt;[{&lt;/span&gt;
        &lt;span class="s2"&gt;"key"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"ip4"&lt;/span&gt;: 655370, &lt;span class="c"&gt;# IP&lt;/span&gt;
            &lt;span class="s2"&gt;"ifindex"&lt;/span&gt;: 2   &lt;span class="c"&gt;# Interface index&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;,
        &lt;span class="s2"&gt;"value"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"responses_sent"&lt;/span&gt;: 0
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;,&lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"key"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"ip4"&lt;/span&gt;: 655370,
            &lt;span class="s2"&gt;"ifindex"&lt;/span&gt;: 3
        &lt;span class="o"&gt;}&lt;/span&gt;,
        &lt;span class="s2"&gt;"value"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"responses_sent"&lt;/span&gt;: 3 &lt;span class="c"&gt;# ARP responses count&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The number &lt;code&gt;655370&lt;/code&gt; represents IP 10.0.10.0 in little-endian format.&lt;/p&gt;

&lt;p&gt;Packet flow through ARP response system:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fphlwd6uxll2oqsm8m82q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fphlwd6uxll2oqsm8m82q.png" alt="arp request reply" width="800" height="149"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;↑ Table of Contents | ← Back&lt;/p&gt;

&lt;h2&gt;
  
  
  Packet Path &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;ARP request processing on node-1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scheme"&gt;&lt;code&gt; &lt;span class="nv"&gt;+-------------------+&lt;/span&gt;
 &lt;span class="nv"&gt;|&lt;/span&gt;   &lt;span class="nv"&gt;External&lt;/span&gt; &lt;span class="nv"&gt;Host&lt;/span&gt;   &lt;span class="nv"&gt;|&lt;/span&gt;
 &lt;span class="nv"&gt;|&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;jumpbox&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="nv"&gt;|&lt;/span&gt;
 &lt;span class="nv"&gt;+--------+----------+&lt;/span&gt;
          &lt;span class="nv"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;ARP&lt;/span&gt; &lt;span class="nv"&gt;Request&lt;/span&gt; &lt;span class="s"&gt;"Who has 10.0.10.0?"&lt;/span&gt;
          &lt;span class="nv"&gt;v&lt;/span&gt;
 &lt;span class="nv"&gt;+-------------------+&lt;/span&gt;
 &lt;span class="nv"&gt;|&lt;/span&gt;   &lt;span class="nv"&gt;Interface&lt;/span&gt; &lt;span class="nv"&gt;eth1&lt;/span&gt;  &lt;span class="nv"&gt;|&lt;/span&gt;
 &lt;span class="nv"&gt;|&lt;/span&gt;     &lt;span class="nv"&gt;node-1&lt;/span&gt;        &lt;span class="nv"&gt;|&lt;/span&gt;
 &lt;span class="nv"&gt;+--------+----------+&lt;/span&gt;
          &lt;span class="nv"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;TC&lt;/span&gt; &lt;span class="nv"&gt;ingress&lt;/span&gt;
          &lt;span class="nv"&gt;v&lt;/span&gt;
 &lt;span class="nv"&gt;+-------------------+&lt;/span&gt;
 &lt;span class="nv"&gt;|&lt;/span&gt;  &lt;span class="nv"&gt;BPF&lt;/span&gt; &lt;span class="nv"&gt;Program&lt;/span&gt;      &lt;span class="nv"&gt;|&lt;/span&gt;
 &lt;span class="nv"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;cil_from_netdev&lt;/span&gt;   &lt;span class="nv"&gt;|&lt;/span&gt;
 &lt;span class="nv"&gt;+--------+----------+&lt;/span&gt;
          &lt;span class="nv"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;handle_netdev&lt;/span&gt;
          &lt;span class="nv"&gt;v&lt;/span&gt;
 &lt;span class="nv"&gt;+-------------------+&lt;/span&gt;
 &lt;span class="nv"&gt;|&lt;/span&gt;  &lt;span class="nv"&gt;do_netdev&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;      &lt;span class="nv"&gt;|&lt;/span&gt;
 &lt;span class="nv"&gt;+--------+----------+&lt;/span&gt;
          &lt;span class="nv"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;ARP&lt;/span&gt; &lt;span class="nv"&gt;check&lt;/span&gt;
          &lt;span class="nv"&gt;v&lt;/span&gt;
 &lt;span class="nv"&gt;+-------------------+&lt;/span&gt;
 &lt;span class="nv"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;handle_l2_announcement&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nv"&gt;|&lt;/span&gt;
 &lt;span class="nv"&gt;+--------+----------+&lt;/span&gt;
          &lt;span class="nv"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Checks:&lt;/span&gt;
          &lt;span class="nv"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;Agent&lt;/span&gt; &lt;span class="nv"&gt;liveness&lt;/span&gt;
          &lt;span class="nv"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;Valid&lt;/span&gt; &lt;span class="nv"&gt;ARP&lt;/span&gt;
          &lt;span class="nv"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;L2_RESPONDER_MAP4&lt;/span&gt; &lt;span class="nv"&gt;entry&lt;/span&gt;
          &lt;span class="nv"&gt;v&lt;/span&gt;
 &lt;span class="nv"&gt;+-------------------+&lt;/span&gt;
 &lt;span class="nv"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;arp_respond&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;     &lt;span class="nv"&gt;|&lt;/span&gt;
 &lt;span class="nv"&gt;+--------+----------+&lt;/span&gt;
          &lt;span class="nv"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Prepare&lt;/span&gt; &lt;span class="nv"&gt;ARP&lt;/span&gt; &lt;span class="nv"&gt;reply&lt;/span&gt;
          &lt;span class="nv"&gt;v&lt;/span&gt;
 &lt;span class="nv"&gt;+-------------------+&lt;/span&gt;
 &lt;span class="nv"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;ctx_redirect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;    &lt;span class="nv"&gt;|&lt;/span&gt;
 &lt;span class="nv"&gt;+--------+----------+&lt;/span&gt;
          &lt;span class="nv"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Egress&lt;/span&gt; &lt;span class="nv"&gt;redirect&lt;/span&gt;
          &lt;span class="nv"&gt;v&lt;/span&gt;
 &lt;span class="nv"&gt;+-------------------+&lt;/span&gt;
 &lt;span class="nv"&gt;|&lt;/span&gt;   &lt;span class="nv"&gt;Interface&lt;/span&gt; &lt;span class="nv"&gt;eth1&lt;/span&gt;  &lt;span class="nv"&gt;|&lt;/span&gt;
 &lt;span class="nv"&gt;|&lt;/span&gt;     &lt;span class="nv"&gt;node-1&lt;/span&gt;        &lt;span class="nv"&gt;|&lt;/span&gt;
 &lt;span class="nv"&gt;+--------+----------+&lt;/span&gt;
          &lt;span class="nv"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;ARP&lt;/span&gt; &lt;span class="nv"&gt;Reply&lt;/span&gt;
          &lt;span class="nv"&gt;v&lt;/span&gt;
 &lt;span class="nv"&gt;+-------------------+&lt;/span&gt;
 &lt;span class="nv"&gt;|&lt;/span&gt;   &lt;span class="nv"&gt;External&lt;/span&gt; &lt;span class="nv"&gt;Host&lt;/span&gt;   &lt;span class="nv"&gt;|&lt;/span&gt;
 &lt;span class="nv"&gt;|&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;jumpbox&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="nv"&gt;|&lt;/span&gt;
 &lt;span class="nv"&gt;+-------------------+&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;↑ Table of Contents | ← Back&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cilium L2 Announcements vs Proxy ARP &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Q&lt;/em&gt;: How do Cilium L2 announcements differ from enabling &lt;a href="http://linux-ip.net/html/ether-arp-proxy.html" rel="noopener noreferrer"&gt;proxy_arp&lt;/a&gt;?&lt;br&gt;&lt;br&gt;
&lt;em&gt;A&lt;/em&gt;: Proxy ARP handles ARP at kernel level while Cilium uses Kubernetes API for distributed control and BPF for performance.&lt;/p&gt;

&lt;p&gt;Disable L2 announcements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@server:/home/vagrant# kubectl delete &lt;span class="nt"&gt;-f&lt;/span&gt; workshop/l2.yaml
ciliuml2announcementpolicy.cilium.io &lt;span class="s2"&gt;"policy1"&lt;/span&gt; deleted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable proxy_arp on node-1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@node-1:/home/vagrant# sysctl &lt;span class="nt"&gt;-w&lt;/span&gt; net.ipv4.conf.eth1.proxy_arp&lt;span class="o"&gt;=&lt;/span&gt;1
net.ipv4.conf.eth1.proxy_arp &lt;span class="o"&gt;=&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Proxy ARP responses come from kernel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@jumpbox:/home/vagrant# arping &lt;span class="nt"&gt;-I&lt;/span&gt; eth1 10.0.10.0
ARPING 10.0.10.0
Timeout
Timeout
Timeout
Timeout
60 bytes from 00:0c:29:0d:b7:76 &lt;span class="o"&gt;(&lt;/span&gt;10.0.10.0&lt;span class="o"&gt;)&lt;/span&gt;: &lt;span class="nv"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;449.655 msec
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cached vs Non-Cached BPF Maps &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Diagnostic tools behavior differs for cached maps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@node-1:/home/cilium# cilium-dbg map list
Name                       Num entries   Num errors   Cache enabled
cilium_policy_00215        3             0            &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;
cilium_l2_responder_v4     0             0            &lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Actual map contents via bpftool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@node-1:/home/cilium# bpftool map dump pinned /sys/fs/bpf/tc/globals/cilium_l2_responder_v4
&lt;span class="o"&gt;[{&lt;/span&gt;
        &lt;span class="s2"&gt;"key"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"ip4"&lt;/span&gt;: 655370,
            &lt;span class="s2"&gt;"ifindex"&lt;/span&gt;: 2
        &lt;span class="o"&gt;}&lt;/span&gt;,
        &lt;span class="s2"&gt;"value"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"responses_sent"&lt;/span&gt;: 0
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;,&lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"key"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"ip4"&lt;/span&gt;: 655370,
            &lt;span class="s2"&gt;"ifindex"&lt;/span&gt;: 3
        &lt;span class="o"&gt;}&lt;/span&gt;,
        &lt;span class="s2"&gt;"value"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"responses_sent"&lt;/span&gt;: 40
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;↑ Table of Contents | ← Back&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Benefits of Cilium L2 Announcements
&lt;/h2&gt;

&lt;p&gt;This guide demonstrated Cilium's L2 announcement implementation for bare-metal Kubernetes LoadBalancer services. Key aspects covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ARP request handling via BPF programs&lt;/li&gt;
&lt;li&gt;High availability through lease system&lt;/li&gt;
&lt;li&gt;Diagnostic techniques&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Key advantages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Native Kubernetes integration&lt;/li&gt;
&lt;li&gt;eBPF-based packet processing performance&lt;/li&gt;
&lt;li&gt;Automatic load distribution&lt;/li&gt;
&lt;li&gt;No external dependencies&lt;/li&gt;
&lt;li&gt;Standard L2 protocol support&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Particularly valuable for hybrid and on-premise environments requiring cloud-like service accessibility while maintaining bare-metal flexibility.&lt;/p&gt;

&lt;p&gt;↑ Table of Contents&lt;/p&gt;

</description>
      <category>cilium</category>
      <category>kubernetes</category>
      <category>bpf</category>
      <category>networking</category>
    </item>
  </channel>
</rss>
