<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Networking on DevOps Diaries: A Personal Blog</title>
    <link>https://blog.spanagiot.gr/tags/networking/</link>
    <description>Recent content in Networking on DevOps Diaries: A Personal Blog</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Sun, 05 Apr 2026 00:31:58 +0300</lastBuildDate><atom:link href="https://blog.spanagiot.gr/tags/networking/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>The Death of `externalIPs`: Migrating to Cilium Node IPAM</title>
      <link>https://blog.spanagiot.gr/posts/external_ips_cilium/</link>
      <pubDate>Sun, 05 Apr 2026 00:31:58 +0300</pubDate>
      
      <guid>https://blog.spanagiot.gr/posts/external_ips_cilium/</guid>
      <description>&lt;p&gt;If you&amp;rsquo;ve been running a bare-metal or self-hosted Kubernetes cluster with public IPs attached to specific nodes, you&amp;rsquo;ve probably been using &lt;code&gt;service.spec.externalIPs&lt;/code&gt;. It&amp;rsquo;s a simple way to expose an application using the public IP of a node. For those who haven&amp;rsquo;t used it, when network traffic arrives into the cluster, with the external IP (as destination IP) and the port matching that Service, rules and routes that Kubernetes has configured ensure that the traffic is routed to one of the endpoints for that Service.&lt;/p&gt;</description>
      <content>&lt;p&gt;If you&amp;rsquo;ve been running a bare-metal or self-hosted Kubernetes cluster with public IPs attached to specific nodes, you&amp;rsquo;ve probably been using &lt;code&gt;service.spec.externalIPs&lt;/code&gt;. It&amp;rsquo;s a simple way to expose an application using the public IP of a node. For those who haven&amp;rsquo;t used it, when network traffic arrives into the cluster, with the external IP (as destination IP) and the port matching that Service, rules and routes that Kubernetes has configured ensure that the traffic is routed to one of the endpoints for that Service.&lt;/p&gt;
&lt;p&gt;Well, the clock has finally run out. Kubernetes v1.36 (shipping April 2026) deprecates &lt;code&gt;service.spec.externalIPs&lt;/code&gt;, with full removal planned for v1.43. The reason is a real security issue (&lt;a href=&#34;https://nvd.nist.gov/vuln/detail/cve-2020-8554&#34;&gt;CVE-2020-8554&lt;/a&gt;) where any user with permission to create Services could specify arbitrary external IPs and intercept traffic. For multi-tenant clusters, that&amp;rsquo;s a legitimate concern. For those of us running single-admin single-tenant setups, it feels like mostly noise. But the field is going away regardless, so it&amp;rsquo;s worth figuring out the replacement before you&amp;rsquo;re forced to.&lt;/p&gt;
&lt;p&gt;The two replacements you&amp;rsquo;ll find everywhere are MetalLB and Cilium&amp;rsquo;s LB IPAM. Both are great tools, but they&amp;rsquo;re designed for a different situation: one where &lt;em&gt;you&lt;/em&gt; own a range of IP addresses that you can hand out and advertise via ARP or BGP. If your IPs come from a cloud provider and are permanently routed to a specific VM&amp;rsquo;s NIC, neither of these fits. MetalLB&amp;rsquo;s L2 mode sends ARP/NDP announcements that the cloud provider&amp;rsquo;s network will simply ignore, since the routing is already handled at the infrastructure level. Cilium&amp;rsquo;s LB IPAM (&lt;code&gt;CiliumLoadBalancerIPPool&lt;/code&gt;) by default allocates at most one IPv4 and one IPv6 per service, so if you have multiple nodes with different public IPv6 addresses that should all serve the same service, you will need to define in an annotation all the IPs you want to be handed to this service. But in our case, the IPs are a property of specific nodes.&lt;/p&gt;
&lt;p&gt;This is where I discovered a Cilium feature called Node IPAM LoadBalancer (&lt;code&gt;loadBalancerClass: io.cilium/node&lt;/code&gt;), and it seems that it&amp;rsquo;s exactly what &lt;code&gt;externalIPs&lt;/code&gt; was doing, just with a supported mechanism.&lt;/p&gt;
&lt;p&gt;Instead of declaring which IPs a Service should listen on, Cilium reads the addresses from the nodes themselves and automatically populates &lt;code&gt;status.loadBalancer.ingress&lt;/code&gt;. When a new node joins with a public IP, it appears. When a node is removed, it disappears. And all of this without having to manually adjust an annotation in all of your services. A good rule of thumb to choose between LB IPAM and Node IPAM: if you find yourself manually typing IP addresses into annotations, you&amp;rsquo;re probably looking at the wrong feature.&lt;/p&gt;
&lt;p&gt;Node IPAM is disabled by default, so you&amp;rsquo;ll need to add this to your Cilium Helm values and upgrade:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;nodeIPAM&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;enabled&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After upgrading, restart the cilium-operator&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;kubectl rollout restart deployment cilium-operator -n kube-system
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now we are ready to begin our migration.&lt;/p&gt;
&lt;p&gt;So a service that used to look like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;type&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;LoadBalancer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;externalIPs&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ae81ff&#34;&gt;198.51.100.1&lt;/span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# node-a IPv4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ae81ff&#34;&gt;2001&lt;/span&gt;:&lt;span style=&#34;color:#ae81ff&#34;&gt;db8::1  &lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;# node-a IPv6&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ae81ff&#34;&gt;2001&lt;/span&gt;:&lt;span style=&#34;color:#ae81ff&#34;&gt;db8::2  &lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;# node-b IPv6&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;becomes this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;type&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;LoadBalancer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;loadBalancerClass&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;io.cilium/node&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;externalTrafficPolicy&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Cluster&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;ipFamilies&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ae81ff&#34;&gt;IPv6&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ae81ff&#34;&gt;IPv4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;ipFamilyPolicy&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;PreferDualStack&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can also add an annotation to restrict which nodes get selected, otherwise every node in the cluster gets advertised, effectively controlling which node exposes which service:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;annotations&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;io.cilium.nodeipam/match-node-labels&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ingress-ready=true&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then label the nodes that have public IPs and you want to serve traffic for the service:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;kubectl label node node-a ingress-ready&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;true
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;kubectl label node node-b ingress-ready&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;true
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it. Cilium handles the rest.&lt;/p&gt;
&lt;p&gt;Node IPAM keeps the simplicity and the same mental model as &lt;code&gt;externalIPs&lt;/code&gt; while fitting into how Kubernetes actually expects LoadBalancer services to work. If you&amp;rsquo;re currently using &lt;code&gt;externalIPs&lt;/code&gt;, especially with Cilium as your CNI, this is the migration that requires the fewest moving parts.&lt;/p&gt;
</content>
    </item>
    
  </channel>
</rss>
