❌

Vue normale

Reçu avant avant-hierBlog – Cloud Native Computing Foundation

Cilium 1.20: Gateway API ExternalAuth, TCPRoute/UDPRoute, ENI IPAM for IPv6, and more

Cilium v1.20 release blog image

Cilium 1.20, the second major open source Cilium release of 2026 after Cilium 1.19, is finally here. Three themes stand out in this release:

  • Gateway API becomes a much broader traffic management layer. Cilium jumps from Gateway API v1.4 to v1.6 and adds ExternalAuth, CORS filters, ListenerSets, and TCPRoute and UDPRoute for non-HTTP traffic. Platform and security teams can manage more of their north-south traffic through the same API, with authentication and authorization handled before a request reaches the application. If you are still running Ingress NGINX, now is the time to let the CNI you already run take on that traffic management too.
  • Cilium becomes a platform that cloud providers can extend. Cilium is already a common networking foundation across hyperscalers and neoclouds. Datapath plugins, developed by Google, make Cilium less like a sealed networking appliance and more like a network operating system: a stable core that cloud providers can extend with their own eBPF programs, independently of the Cilium release cycle.
  • Innovation and standardization move forward together. Cilium continues to push the datapath with innovations such as netkit, while aligning more closely with the wider Kubernetes ecosystem through stable MCS API support and Kubernetes ClusterNetworkPolicy. As capabilities Cilium supported early, including multi-cluster services and cluster-wide policy, take shape as upstream Kubernetes APIs, Cilium supports those APIs alongside its own established CRDs. Users get a choice between Cilium-native features and portable Kubernetes resources.
  • choice between Cilium-native features and portable Kubernetes resources.

Thank you to every contributor, reviewer and maintainer who made Cilium 1.20 possible, including engineers from Datadog, Google, Microsoft and many other organisations across the Cilium community.

Networking

ENI IPAM mode with IPv6

One of the final gaps that stopped us from claiming feature parity across IPv4 and IPv6 in Cilium is gone. It even makes a footnote in Chapter 4 of Cilium: Up and Running redundant, and while we are a little sad that a note in the book is obsolete so soon after publication, we are glad that ENI mode on AWS finally supports IPv6, four years after the feature request was first raised.

Let’s recap. Up until now, running Cilium in AWS ENI IPAM mode (the AWS-specific mode that gives pods real VPC addresses) was only possible on IPv4. Now, as a beta feature in Cilium 1.20, ENI IPAM can allocate and hand out IPv6 as well. The operator attaches an IPv6 /80 prefix to each node’s ENI through Prefix Delegation (which we covered in detail in an earlier blog on how to overcome IP address exhaustion in Kubernetes), and the agent assigns pod addresses from it.

An image of the CiliumNode CRD


Enable ENI IPAM and IPv6 together:

ipam:
  mode: eni
eni:
  enabled: true
ipv6:
  enabled: true


Pods on EKS then boot up dual-stack with VPC-routable IPv6:

NAME                              IPS
dualstack-demo-7f8876746b-5xvnk   192.168.128.71,2a05:d01c:38d:4c02:909e::e771
dualstack-demo-7f8876746b-8lj6z   192.168.177.118,2a05:d01c:38d:4c00:1cf5::57d1
dualstack-demo-7f8876746b-p84p7   192.168.169.190,2a05:d01c:38d:4c00:1cf5::f7ea



And here is the other side of it in the AWS console: the /80 IPv6 prefix delegated to a worker node’s ENI. The IPv6 address from the first pod above comes straight out of this 2a05:d01c:38d:4c02:909e::/80 range.

Screenshot of network interface summary

Kudos to the folks at Datadog, one of the most impactful organizations contributing to Cilium for years now, for building ENI IPv6 support and finally closing that gap. Watch the video for a short demo.

If you’d like to learn more about this feature, check the documentation for more information.

Automatic datapath mode selection (netkit auto)

In our post covering netkit, we described the newer pod network device as “The Final Frontier in Container Networking Performance”. You can call us out on the hyperbole, but since its release, Meta has rolled it out across millions of containers, while ByteDance has been deploying at a similar scale, reporting a 10% performance improvement in its tests.

The TL;DR on netkit is this: it replaces the traditional veth pair and its overhead, bringing pod networking performance to host-level throughput.

Diagram of pod to eth0 flow

When it was initially released in Cilium, it needed a recent kernel (>= 6.8), so on a fleet with mixed kernel versions you either gave up on netkit everywhere or carved your nodes into pools by kernel version and configured them separately.

Cilium 1.20 adds bpf.datapathMode=auto: each agent probes its own host at startup, uses netkit when the kernel supports it, and quietly falls back to veth when it does not. That means you only need to configure one setting across a mixed fleet, making netkit easier to adopt across your environment. You can see which mode each node actually landed on through cilium-dbg status and the cilium_feature_datapath_config metric:

$ cilium-dbg status | grep Device
Device Mode:             netkit [Configured: auto]
 
$ cilium-dbg metrics list | grep datapath_config
cilium_feature_datapath_config		configured_mode=auto operational_mode=netkit 1.000000

While the default stays veth (meaning nothing changes until you opt in), we feel that the days of veth are numbered.

Check the documentation for more info.

Datapath Plugins

Previously, extending or instrumenting Cilium’s eBPF datapath beyond what the project already supported left you with two hard options: upstream your change, or maintain a fork. Both are high-effort, and a fork means carrying datapath patches across every Cilium upgrade.

While most end-users just use the off-the-shelf Cilium, some cloud providers – like Google, the authors of this new feature – would manage their own Cilium fork, with the maintenance efforts it comes up with.

Now, Cilium 1.20 introduces datapath plugins (beta): third-party code can instrument Cilium’s eBPF datapath as its own plugin, running as a separate process that Cilium reaches out to, without patching or forking Cilium itself. Because a plugin is its own image, it is built, versioned and rolled out on its own cadence and can be upgraded independently of Cilium. And because it runs on its own, a plugin crash does not take the agent down with it. It opens the door to specialised observability, security and networking extensions that ship and evolve independently of the Cilium release cycle, with no fork required.

A flow diagram of the Cilium agent

A plugin registers itself with Cilium through a deliberately small CiliumDatapathPlugin custom resource:

apiVersion: cilium.io/v2alpha1
kind: CiliumDatapathPlugin
metadata:
  name: example
spec:
  # Fail closed if Cilium cannot reach the plugin.
  attachmentPolicy: Always
  # Bump this to reinitialise the datapath for a new plugin release.
  version: 0.0.

Thanks to jrife for the feature.

If you’d like to learn more about this feature, check the documentation for more information.

Per-pod disable source IP verification

By default, Cilium enforces source IP verification: a pod may only send packets whose source IP matches its own. This stops a compromised or misbehaving pod from spoofing another workload’s address. There are – admittedly rare – circumstances where some workloads legitimately need to emit packets with a different source IP: for example, NAT gateways, firewalls and other appliance-style pods (such as VPN/Tailscale) .

Until now, source IP verification was a single cluster-wide switch (enable-source-ip-verification, on by default) with no per-pod control – so letting one workload use a foreign source IP meant turning it off for the entire cluster, weakening anti-spoofing everywhere.

Image of the 1.20: per-pod opt-in

Cilium 1.20 makes it a per-pod, opt-in decision, gated at two levels. First, a cluster administrator permits it for a namespace by annotating the namespace with config.cilium.io/delegate-source-ip-verification: “true”. Only then can a pod owner opt a specific workload out of source IP verification by annotating it with config.cilium.io/disable-source-ip-verification: “true”. Both annotations are required, so a namespace owner cannot quietly turn anti-spoofing off on their own, and the protection stays on everywhere else. This feature solves a problem for the workloads that need it, but make sure to use Kubernetes RBAC to restrict who can modify namespace annotations, so you don’t accidentally reduce your security posture.

If you’d like to learn more about this feature, check the documentation for more information.

Migrate cluster-pool IPAM to multi-pool IPAM

Multi-pool IPAM is steadily becoming the preferred way to hand out pod IPs in Cilium. Instead of drawing every address from one flat cluster-wide range, you carve the space into named pools and assign them where you want them – per namespace, per workload, per tenant. What had been difficult until now was the migration from the default cluster scope mode. Until now, if your cluster was already running cluster-scope IPAM, moving to multi-pool meant standing up a fresh cluster and migrating workloads across, which is not something anyone wants to do on a live platform.

Cilium 1.20 adds an in-place migration path. You can switch an existing cluster from cluster-pool to multi-pool, with no rebuild and no re-IP’ing, by using the operator option enable-cluster-pool-to-multi-pool-migration. Check out the migration documentation for more info.

Traffic distribution PreferSameZone / PreferSameNode

Introduced in Cilium 1.16, Service Traffic Distribution became the preferred method for keeping Service traffic close to its client, replacing the older topology-aware hints method. Service Traffic Distribution enables users to reduce latency and avoid cross-zone data-transfer charges, as Dean covered in this excellent post.

Until now, Cilium only supported the PreferClose value, indicating a preference for routing traffic to endpoints that are topologically proximate to the client.

In Cilium 1.20, Cilium honours recent changes to the upstream Kubernetes trafficDistribution specification and now supports two newer values. PreferSameZone keeps traffic within the client’s zone while a healthy backend is available there, and PreferSameNode prefers a backend on the very same node.

apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  trafficDistribution: PreferSameZone
  selector:
    app: frontend
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: ClusterIP

Both values gracefully fall back to the rest of the cluster’s backends when no local one is healthy, so you get the locality benefit without trading away availability. Because this is the standard Kubernetes field rather than a Cilium-specific annotation, the same Service spec behaves consistently across conformant implementations.

Check the official Kubernetes documentation for more information on service traffic distribution.

EndpointSlice weights for Maglev backends

Cilium has supported Maglev as its load balancer hashing mechanism since Cilium 1.10. Originated at Google and named after the Japanese magnetic levitation train, Maglev provides consistent hashing to minimize disruption during node or backend changes (you can find more information about Maglev and other performance optimization features in Chapter 8 of Cilium Up and Running).

In prior releases, there was no supported way to give some backends of a Service a larger share of Maglev traffic than others, or to gracefully drain a group of backends. Traffic was evenly distributed across all available backends.

Now, Cilium 1.20 reads a service.cilium.io/weight annotation directly off the EndpointSlice. The weight applies to every backend in that slice, with valid values running from 0 to 65535 (note the weights are relative, so they do not need to add up to 100). This is aimed squarely at selectorless Services backed by several manually managed EndpointSlice objects, where each slice can carry a different weight: a classic weighted split such as 70/30 across two slices becomes a two line annotation change.

apiVersion: v1
kind: Service
metadata:
  name: example-service
  annotations:
    service.cilium.io/lb-algorithm: maglev
spec:
  type: ClusterIP
  ports:
    - name: http
      port: 8080
      protocol: TCP
      targetPort: 8080
  # no selector: endpoints come from the manual EndpointSlices below
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: example-service-1
  labels:
    kubernetes.io/service-name: example-service
  annotations:
    service.cilium.io/weight: "70"     # applies to every backend in this slice
addressType: IPv4
ports:
  - name: http
    protocol: TCP
    port: 8080
endpoints:
  - addresses: ["10.0.0.11"]
  - addresses: ["10.0.0.12"]
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: example-service-2
  labels:
    kubernetes.io/service-name: example-service
  annotations:
    service.cilium.io/weight: "30"     # set to "0" to drain these backends
addressType: IPv4
ports:
  - name: http
    protocol: TCP
    port: 8080
endpoints:
  - addresses: ["10.0.0.21"]
  - addresses: ["10.0.0.22"]

The special case is a weight of 0. Instead of removing the backends, it marks them as being in maintenance: existing connections continue to work, but the backends leave the Maglev lookup table so no new connections are steered to them. That gives you a clean, connection-preserving drain, which is exactly what you want when taking a group of backends out of rotation for maintenance. Invalid values are ignored and fall back to the default behaviour, so a typo never takes a Service down.

Gateway API

Cilium 1.19 introduced support for Gateway API v1.4. Cilium 1.20 moves from Gateway API v1.4 to v1.6 and implements several of the capabilities that graduated across those releases.

From v1.5, TLSRoute (a feature Cilium introduced back in 1.14), the HTTPRoute CORS filter and ListenerSets all reached the Standard channel as generally available, and Gateway (and HTTPRoute) level authentication landed as an experimental feature. From v1.6, TCPRoute and UDPRoute graduated to Standard as well.

Gateway API continues to be a thriving project. Its accelerated development is a boon to anyone looking at migrating off Ingress – and its most popular implementation, Ingress NGINX – and Cilium keeps up with its pace.

Gateway API ExternalAuth filter

Cilium’s security features have always stood out. Built-in traffic encryption within or between meshed clusters, network policy for cluster segmentation, mutual TLS (now with zTunnel, which we cover later) provide a compelling set of security capabilities. One gap remained at the north/south boundary: authenticating users, scripts, and agents arriving from outside wasn’t easy to accomplish natively and, in prior releases, required customizing Cilium’s built-in Envoy proxy.

Cilium 1.20 closes that gap, by supporting the ExternalAuth filter in Gateway API (GEP-1494).

The idea is simple. You attach the filter to an HTTPRoute, and for every matching request the gateway checks with an external authorization service before forwarding any traffic. If the caller hasn’t authenticated yet, the authorizer sends them through an authentication flow first – a 302 redirect to a login page for a human in a browser, or a 401 / 403 for a request that presents no valid credentials. Only once the caller has authenticated and the authorizer returns 200 OK does the gateway forward the request to your backend. On the way through, the authorizer can also inject identity headers (who the caller is and which groups they belong to) so your application makes the final authorization call without ever touching a password or a token.

An image of the Cilium Gateway workflow

The feature applies to very different callers on the same gateway. A human hitting the dashboard in a browser gets redirected to an SSO provider (such as Authelia or Duo) and comes back with a session cookie. A CI job or script presents a JWT that oauth2-proxy validates against an identity provider (such as Keycloak, Duo or Okta). And an AI agent talking MCP authenticates with a service-account client, with no interactive login at all.

A flow diagram of the Cilium gateway

The filter sits directly on the HTTPRoute:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: hr-dashboard
spec:
  parentRefs:
    - name: acme-gateway
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      filters:
        - type: ExternalAuth
          externalAuth:
            protocol: HTTP
            backendRef:
              name: authelia
              port: 80
            http:
              path: /api/authz/ext-authz
              allowedHeaders:
                - cookie
              allowedResponseHeaders:
                - Remote-User
                - Remote-Email
                - Remote-Name
                - Remote-Groups
      backendRefs:
        - name: hr-dashboard
          port: 8080

The authorizer’s response determines what the gateway does:

  • 200: forward the request to the backend.
  • 302: redirect the caller, for example to an SSO login page.
  • 401 or 403: block the request at the gateway.

If you’d like to learn more, check out our External Auth lab!

Gateway API TCPRoute and UDPRoute support

Ever since Gateway API arrived in the 1.13 release, Cilium has kept in lockstep with every Gateway API release, adding TLS termination, TLS Passthrough, GRPCRoute, GAMMA (for east-west use cases) and many more features along the way.

Cilium 1.20 carries on in this vein. On top of the ExternalAuth feature mentioned above, a big addition to Cilium’s Gateway API support is TCPRoute and UDPRoute. Prior to this release, to expose a plain TCP or UDP service (a database, a DNS server, a game server) you had to drop out of the Gateway API model and back to raw LoadBalancer or NodePort Services. With TCPRoute and UDPRoute support, you can now manage L4 services through the same Gateway API you already use for HTTP or gRPC.

UDPRoute is ideal for applications such as DNS, VoIP, gaming, streaming media, IoT and telemetry (GEP-2645), while TCPRoute is handy for databases, message brokers, mail protocols and other TCP-based services (GEP-2644).

You can attach HTTP, TCP and UDP routes to a single shared Gateway:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: demo-gateway
  namespace: default
spec:
  gatewayClassName: cilium
  listeners:
    - name: web
      protocol: HTTP
      port: 80
      allowedRoutes:
        kinds:
          - kind: HTTPRoute
    - name: mongo
      protocol: TCP
      port: 27017
      allowedRoutes:
        kinds:
          - kind: TCPRoute
    - name: dns
      protocol: UDP
      port: 53
      allowedRoutes:
        kinds:
          - kind: UDPRoute

In this example, an HTTPRoute serves Dean’s eBee Pac-Man app, a TCPRoute connects to the MongoDB database holding its high scores, and a UDPRoute sends DNS queries to CoreDNS:

apiVersion: gateway.networking.k8s.io/v1
kind: TCPRoute
metadata:
  name: mongo
  namespace: default
spec:
  parentRefs:
    - name: demo-gateway
      sectionName: mongo
  rules:
    - backendRefs:
        - name: pacman-mongo
          port: 27017
---
apiVersion: gateway.networking.k8s.io/v1
kind: UDPRoute
metadata:
  name: dns
  namespace: default
spec:
  parentRefs:
    - name: demo-gateway
      sectionName: dns
  rules:
    - backendRefs:
        - name: coredns
          port: 53

Each backend is reachable through the same Gateway address (172.18.255.200) on its own port. After playing a round of eBee Pac-Man over the HTTPRoute, the score lands in MongoDB and can be read back over the TCPRoute:

$ mongosh mongodb://172.18.255.200:27017/pacman --quiet \
    --eval 'db.highscore.find({}, {_id:0, name:1, score:1, level:1}).sort({score:-1})'
[ { name: 'Nico', score: 400, level: 1 } ]

CoreDNS is available over the UDPRoute:

$ dig @172.18.255.200 -p 53 ebeepacman.cilium.rocks
 
; <<>> DiG 9.20.23 <<>> @172.18.255.200 -p 53 ebeepacman.cilium.rocks
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 15177
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
 
;; QUESTION SECTION:
;ebeepacman.cilium.rocks.       IN      A
 
;; ANSWER SECTION:
ebeepacman.cilium.rocks. 3600   IN      A       192.0.2.42
 
;; Query time: 1 msec
;; SERVER: 172.18.255.200#53(172.18.255.200) (UDP)

Thanks to external contributor eminaktas for landing this long-standing request.

Gateway API ListenerSets

Shared Gateways create an awkward ownership problem: the platform team controls the Gateway object, but application teams own the endpoints behind it. Adding another hostname or port therefore requires someone with write access to modify that central resource, creating a potential bottleneck in multi-tenant clusters. Without a delegation mechanism, this risks reproducing the same centralized configuration problem found in the inflexible Ingress API, one of the limitations Gateway API was designed to address.

Cilium 1.20 implements Gateway API ListenerSets, allowing application teams to attach and manage additional Listeners from their own namespaces. These Listeners share the parent Gateway’s address and infrastructure, while the platform team retains ownership of the Gateway and controls which namespaces may extend it. The result is a clean delegation model with per-namespace ownership and correct hostname filtering for attached routes.

The following manifests show this delegation in practice: a platform-owned Gateway authorizes a labelled application namespace to add its own Listener, while an HTTPRoute in that namespace attaches directly to the delegated Listener.

---
apiVersion: v1
kind: Namespace
metadata:
  name: listenerset-demo
  labels:
    gateway-access: "true"
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: shared-gateway
  namespace: default
spec:
  gatewayClassName: cilium
  allowedListeners:
    namespaces:
      from: Selector
      selector:
        matchLabels:
          gateway-access: "true"
  listeners:
  - name: base-http
    protocol: HTTP
    port: 8081
---
apiVersion: gateway.networking.k8s.io/v1
kind: ListenerSet
metadata:
  name: delegated-listeners
  namespace: listenerset-demo
spec:
  parentRef:
    name: shared-gateway
    namespace: default
  listeners:
  - name: echo
    hostname: echo.example.com
    protocol: HTTP
    port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: echo
  namespace: listenerset-demo
spec:
  parentRefs:
  - group: gateway.networking.k8s.io
    kind: ListenerSet
    name: delegated-listeners
    sectionName: echo
  hostnames:
  - echo.example.com
  rules:
  - backendRefs:
    - name: echo-1
      port: 8080

For more details on ListenerSet, check the documentation for more information.

More Gateway API improvements

Alongside the move from Gateway API v1.4 to v1.6, Cilium 1.20 fills in several smaller but useful gaps in its HTTP traffic handling.

Cilium now supports the Gateway API CORS filter natively. You can declare allowed origins, methods and headers, exposed response headers, credential handling and cache duration directly on the route.

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: echo-cors
  namespace: default
spec:
  parentRefs:
    - name: cors-gateway
  rules:
    - filters:
        - type: CORS
          cors:
            allowOrigins:
              - "https://www.foo.com"
              - "https://*.bar.com"
            allowMethods:
              - GET
              - OPTIONS
            allowHeaders:
              - "x-custom-header"
            exposeHeaders:
              - "x-response-header"
            allowCredentials: true
            maxAge: 3600
      backendRefs:
        - name: echo-1
          port: 8080

Redirect handling is more complete too. In addition to 301 and 302 redirect codes we covered in the deep dive tutorial on “Redirect, Rewrite, and Mirror HTTP with Cilium Gateway API”, Cilium now formally supports and tests the optional 303, 307 and 308 HTTPRoute redirect codes. This gives users more precise control over whether a redirect is temporary or permanent and whether the original HTTP method and body should be preserved. You can try out redirection in the Advanced Gateway API Use Cases lab.

Finally, CiliumGatewayClassConfig can now control the HTTP Server response header. Its OVERWRITE, APPEND_IF_ABSENT and PASS_THROUGH modes let operators replace the header, add one only when the backend has not supplied it, or preserve the backend value. This is useful for applying a consistent value or preventing backend implementation details from being exposed.

Security

ztunnel: sidecarless mTLS service mesh

Cilium 1.19 introduced ztunnel, a purpose-built per-node proxy that brings sidecarless mutual TLS to pod-to-pod traffic, and Cilium 1.20 keeps that work moving.

The longer-term direction is for ztunnel to supersede Cilium’s original mutual authentication feature, which has been Beta since 1.14. That feature intentionally separated authentication from encryption. Cilium agents performed an out-of-band mTLS handshake to authenticate each new identity pair, while Cilium’s existing IPsec or WireGuard transparent encryption protected the workload traffic. This allowed mutual authentication to build on encryption capabilities Cilium already provided. One consequence of this design was that the first packet could be dropped while the authentication handshake completed. In addition, because transparent encryption operates between nodes, traffic between pods on the same node was not encrypted.

ztunnel takes a different approach by providing authentication and encryption together in the traffic path. Once you enroll a namespace by applying the io.cilium/mtls-enabled=true label, ztunnel holds each new connection while it establishes an mTLS session using TLS 1.3 over an HBONE (HTTP-Based Overlay Network Environment) tunnel. This avoids the first-packet drop and encrypts every supported TCP flow between enrolled pods, including pods running on the same node.

ztunnel remains Beta, but Cilium 1.20 improves its operational readiness. The certificate authority is now configurable: the default internal mode runs ztunnel mTLS without requiring SPIRE, while spire mode uses SPIFFE workload identities, with SPIRE issuing and rotating certificates for teams that want the CNCF-graduated SPIFFE and SPIRE identity stack. There are also new Prometheus metrics for enrollment and connection health. The dashboard below combines those metrics with Kubernetes workload data to show protected workloads, enrollment activity, failures and the connection state of each node’s ztunnel.

A screenshot of the Cilium ztunnel

These improvements continue the joint engineering effort between Microsoft and Isovalent and build on the upstream ztunnel project, originally created for Istio’s ambient mesh.

As ztunnel matures, it is becoming the clear successor to the original mutual authentication feature. In Cilium 1.20, the maintainers marked legacy Mutual Authentication as deprecated, with removal planned for a later release.

Ztunnel support in Cilium remains currently in Beta. Make sure to review the documentation for current limitations and configuration guidance before rolling this out in production. For more information, see the blog and the ztunnel introduction in the Cilium 1.19 release blog.

Kubernetes ClusterNetworkPolicy (KCNP) support

A Kubernetes NetworkPolicy (KNP) is namespaced and additive: it is written by application teams, and there is no native way for a cluster administrator to set a rule that applies cluster-wide or that takes precedence over what a namespace owner writes. The upstream network-policy-api subgroup has been working on filling that gap, and its latest iteration consolidates the earlier AdminNetworkPolicy and BaselineAdminNetworkPolicy into a single ClusterNetworkPolicy.

Cilium 1.20 implements ClusterNetworkPolicy. It is cluster-scoped and tiered: an Admin tier that takes precedence over namespaced NetworkPolicy, and a Baseline tier that acts as a default which namespaced policy can override.

An image of the ClusterNetworkPolicy tiers: evaluation order

A policy selects a subject (by namespace or pod), sets a priority, and lists ingress/egress rules with an action of Accept, Deny or Pass. Those selectors accept matchExpressions in addition to matchLabels so you can target sets of namespaces or pods with set-based rules like In, NotIn and Exists. This gives platform teams the guardrails they have wanted: enforce a cluster-wide baseline, or hard-deny some traffic that no namespace can re-open.

For example, an Admin-tier policy can deny all ingress to a selected namespace:

apiVersion: policy.networking.k8s.io/v1alpha2
kind: ClusterNetworkPolicy
metadata:
  name: deny-to-server
spec:
  tier: Admin
  priority: 10
  subject:
    namespaces:
      matchLabels:
        kubernetes.io/metadata.name: kcnp-server
  ingress:
    - name: deny-all
      action: Deny
      from:
        - namespaces: {}

For users who prefer using upstream APIs like KNP and KCNP rather than Cilium-specific Cilium Network Policies and ClusterWide Cilium Network Policies, this is a welcome addition to your segmentation toolkit.

If you’d like to learn more about this feature, check the documentation for more information.

New cluster-mesh policy entity & policy map aggregation

In previous Cilium versions, allowing traffic from “anywhere in my Cluster Mesh” in Cilium Network Policies meant either enumerating remote clusters by name or falling back to label selectors that spanned the mesh, since the cluster entity only covered the local cluster. Such policy was verbose to write, easy to get wrong as clusters came and went, and costly at scale because each remote identity consumed its own BPF policy-map entry.

Now, Cilium 1.20 adds a cluster-mesh policy entity that selects every endpoint in every meshed cluster in a single word. You write fromEntities: cluster-mesh once and it keeps matching as clusters join or leave the mesh. Underneath, both cluster and cluster-mesh now aggregate their identities into far fewer policy-map entries, so the mesh-wide rule is also the cheaper one to enforce at scale.

The policy is as simple as using the new entity in fromEntities:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: backend-ingress
spec:
  endpointSelector:
    matchLabels:
      app: backend
  ingress:
    - fromEntities:
        - cluster-mesh

The diagrams below illustrate the changes:

An image of the cluster-mesh policy entity
An image of the cluster-mesh policy entity

If you’d like to learn more about this feature, check the documentation for more information.

Hubble policy correlation for audit verdicts

Previously, when a flow was dropped by a default-deny (nothing matched it) or logged under Policy Audit Mode, Hubble showed the drop but left the policy fields empty. You could see that a flow was denied, but not which policy, or the absence of one, was responsible. That gap made audit mode harder to trust: the whole point of audit mode is to preview what a policy would block before you enforce it, but the verdict did not tell you what would have caught the flow.

Now, in Cilium 1.20, Hubble correlates policy for audit verdicts. Audit flows (Verdict_AUDIT) populate the ingress_allowed_by / egress_allowed_by and ingress_denied_by / egress_denied_by fields, so you can see exactly what would have denied the flow before you flip enforcement on.

For example, given this ingress deny policy:

# Explicitly deny alliance ships to the deathstar.
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "deny-alliance"
spec:
  endpointSelector:
    matchLabels: {class: deathstar}
  ingressDeny:
    - fromEndpoints:
        - matchLabels: {org: alliance}

When xwing with label alliance tries to curl deathstar, an audited flow now names the policy that would have denied it on ingress:

$ hubble observe --type policy-verdict --verdict AUDIT 
--print-policy-names
Jul 15 10:11:40.451: default/xwing:40218 (ID:41050) -> 
default/deathstar:80 (ID:11009) \
  policy-verdict:L3-Only INGRESS AUDITED BY deny-alliance (CiliumNetworkPolicy) (TCP Flags: SYN)

Here is an example of a correlation flow log for a packet allowed on egress, with the corresponding network policy:

apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "xwing-egress"
spec:
  endpointSelector:
    matchLabels: {org: alliance, class: xwing}
  egress:
    - toEndpoints:
        - matchLabels: {class: deathstar}
      toPorts:
        - ports: [{port: "80", protocol: TCP}]
    - toEndpoints:
        - matchLabels:
            io.kubernetes.pod.namespace: kube-system
            k8s-app: kube-dns
      toPorts:
        - ports: [{port: "53", protocol: UDP}]
$ hubble observe --type policy-verdict --verdict FORWARDED 
--print-policy-names
  Jul 15 10:11:40.449: default/xwing:40218 (ID:41050) -> 
default/deathstar:80 (ID:11009) \
    policy-verdict:L3-L4 EGRESS ALLOWED BY xwing-egress
 (CiliumNetworkPolicy) (TCP Flags: SYN)

Thanks to Rajneesh180 for the contribution.

Day 2 Operations & Scale

MCS-API reaches stable support in ClusterMesh

Cilium Cluster Mesh provides cross-cluster service discovery and load balancing across up to 255 clusters by default, with built-in support for network policy (as highlighted earlier), observability and service affinity.ia Placeholder

While Cluster Mesh is a popular Cilium feature, it is coupled tightly with Cilium. not portable and vendor-neutral. The Kubernetes SIG multi-cluster working group drove the creation of the portable and vendor-neutral Multi-Cluster Services API standard. Cilium introduced beta MCS-API support a few releases ago and in 1.20, promoted its MCS-API implementation to a stable support level.

The MCS-API provides a standard way to make a Service discoverable across multiple clusters. Imagine that an application runs in two clusters called site-a and site-b, behind a Service named web in the default namespace. Normally, each Service is visible only inside its own cluster. To make web available across the mesh, you create a ServiceExport with the same name and namespace in both clusters. That is the only additional resource the application team needs to manage.

apiVersion: multicluster.x-k8s.io/v1beta1
kind: ServiceExport
metadata:
  name: web
  namespace: default

Cilium then combines the exported Services and their backends into a single multi-cluster Service. Workloads in any connected cluster can reach it through the standard MCS-API DNS name web.default.svc.clusterset.local. Because this naming convention and the ServiceExport resource are part of the Kubernetes MCS-API, the same application manifests can be used with any conformant implementation.

Under the hood, the Cilium operator creates the corresponding ServiceImport, allocates the clusterset virtual IP and reconciles the derived Kubernetes Service used to connect to backends across the mesh. Cilium 1.20 moves this implementation to the multicluster.x-k8s.io/v1beta1 CRDs and installs the required CRDs automatically.

Of course, Cilium Global Services remain available, but you now also have a stable, supported alternative for cross-cluster service discovery that uses a portable, standard Kubernetes API.

Thanks to Arthur Outhenin-Chalandre (@MrFreezeex) from Ledger, who authored both the move to the v1beta1 API and the work to promote Cilium’s MCS-API support to stable.

If you’d like to learn more about this feature, check the documentation for more information.

Shrink cilium-cni binary by 80%

Every Cilium agent ships the cilium-cni plugin and installs it onto the node at /opt/cni/bin/cilium-cni by default, where the kubelet calls it to wire up each pod. Over time what was originally a small binary had grown: the CNI plugin was pulling in large chunks of the agent codebase through Go imports, and by 1.19 it weighed 76 MB. In 1.20, the plugin is no longer importing packages it never needs and the same binary is 16 MB, resulting in a 79% cut.

# In a 1.19 cluster
root@cilium-119-worker:/# ls -la /opt/cni/bin/cilium-cni
-rwxr-xr-x 1 root root 76625528 Jun 16 12:06 /opt/cni/bin/cilium-cni
# In a 1.20 cluster
root@cilium-120-worker:/# ls -la /opt/cni/bin/cilium-cni
-rwxr-xr-x 1 root root 16210104 Jul 17 08:59 /opt/cni/bin/cilium-cni

A smaller binary on every node means a smaller image layer to pull and less to copy during each CNI install.

remote-node / world policy identity aggregation

In previous Cilium versions, a policy that selected a broad entity like world or remote-node expanded, under the hood, into one entry per matching identity in the BPF policy map. On a large or heavily meshed cluster that could mean thousands of entries for a single rule, and in the worst case the policy map could fill up.

An image of Cilium 1.19

Now, Cilium 1.20 teaches the policy map a new kind of wildcard for exactly these semantic groups. Selecting world or remote-node now inserts a single aggregated wildcard entry instead of one entry per matching identity, and the datapath does an extra wildcard lookup to match. The verdict is identical, but the footprint collapses from thousands of entries to a handful, so policy enforcement stays cheap as the cluster and the mesh grow.

Cilium 1.20

BGP Tooling Improvements

Cilium’s BGP support is commonly used in self-managed Kubernetes clusters, to interconnect with an existing network fabric. Cilium BGP relies on the GoBGP platform, a robust and mature Go-based implementation of BGP. In previous versions, debugging a Cilium BGP session from the agent was sometimes challenging: the on-agent BGP commands gave limited output, and there was no way to see which route policies were actually being applied to the speaker.

Cilium 1.20 improves the on-agent BGP tooling, now surfaced through the hive-shell from inside the Cilium agent (cilium-dbg shell -- bgp/...). A brand-new bgp/route-policies command shows the route policies Cilium has actually programmed into GoBGP – something you previously had no way to inspect.

$ cilium-dbg shell -- bgp/route-policies
Instance   Policy Name       Type     Statement Name   Match Peers   Match Families   Match Prefixes (Min..Max Len)   RIB Action
64513      allow-local       import   accept-local                                                                    accept
           peer-frr-export   export   PodCIDR-ipv4     172.18.0.5                     10.0.0.0/24 (24..24)             accept

In addition, bgp/peers gains a --format option (table, JSON or detailed); providing a detailed view of per-peer timers, negotiated capabilities and graceful-restart state.

$ cilium-dbg shell -- bgp/peers --format detailed
Instance: 64513
  Peer: frr
    Address: 172.18.0.5
    PeerAsn: 64512
    LocalAsn: 64513
    Session State: established
    Uptime: 12m10s
    Address Families:
      ipv4/unicast:
        Received Routes: 0
        Advertised Routes: 1
    Timers:
      Hold Time:      { Configured: 1m30s, Applied: 1m30s }
      Keep Alive Time:{ Configured: 30s,   Applied: 30s }
    Local Capabilities:  multiprotocol (ipv4-unicast), route-refresh, extended-message, 4-octet-as
    Remote Capabilities: multiprotocol (ipv4-unicast), route-refresh, extended-message

Heads-up worth calling out: the on-agent cilium-dbg bgp subcommands and the local REST BGP API are deprecated in 1.20. Move to the hive-shell equivalents – cilium-dbg shell -- bgp/peers, bgp/routes, bgp/route-policies. The user-facing cilium bgp CLI is unchanged.

Conclusion

Cilium 1.20 is the product of a community that continues to grow well beyond the code itself. Asana, Etraveli, Michelin, OpenAI, Suse, Telefónica’s acens, and Zynga have all shared stories about running Cilium in production. If your organization has a story to tell, we would love to hear it.

The community also gathered at CiliumCon and the Cilium Developer Summit in Amsterdam, with the CiliumCon talks now available to watch online. The next opportunity to meet in person will be at KubeCon + CloudNativeCon North America in Salt Lake City, where CiliumCon and another Developer Summit will take place from November 9 to 12. We look forward to seeing you there!

Securing CI/CD for an open source project, part 3: Credentials, verification, and what’s next

This is the third and final post in a series on how Cilium hardens its CI/CD pipeline. Part 1 covered access control and Part 2 covered dependency hardening. This post covers the last layer: keeping CI and production credentials isolated, signing and attesting every release, and the gaps we’re still working to close.

Protecting credentials

We assume any individual layer can fail. If a CI workflow ever gets compromised, it’s important the attacker can’t reach anything that matters.

Strong defaults

By default our GITHUB_TOKENs are scoped to minimal read permissions on contents and packages. Workflows that need anything more have to opt in explicitly, so a workflow that forgets to declare permissions doesn’t end up with broad org-wide write access.

We keep two distinct sets of registry credentials behind separate GitHub protected environments:

  • CI credentials can push to our development image registry (quay.io/cilium/*-ci) and are available to CI builds. Even if a CI workflow is compromised somehow, these credentials cannot push to production image tags.
  • Production credentials sit behind the release environment, which requires an explicit maintainer approval before a workflow run can touch them. No fork, no feature branch, and no CI build can reach those secrets. Only tag-triggered release builds that a maintainer has approved can.

Worst-case, in a CI compromise, the attacker can publish a malicious -ci image. They cannot publish to quay.io/cilium/cilium:v1.x.x or docker.io/cilium/cilium:v1.x.x. The credentials simply aren’t on the runner.

Every actions/checkout call also sets persist-credentials: false, so the GITHUB_TOKEN never ends up in the runner’s git config where a later step could grab it.

Signing and attesting what we ship

Every container image we release (cilium, operator-*, hubble-relay, clustermesh-apiserver) is signed with Sigstore Cosign using keyless OIDC. There are no long-lived signing keys for anyone to steal.

A reusable composite action handles the signing pipeline:

.github/actions/cosign/action.yaml
- name: Install Cosign
  uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1

- name: Generate SBOM
  uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
  with:
    artifact-name: sbom_${{ inputs.sbom_name }}.spdx.json
    output-file: ./sbom_${{ inputs.sbom_name }}.spdx.json
    image: ${{ inputs.image_tag }}

- name: Sign Container Image
  shell: bash
  run: cosign sign -y "${{ inputs.image }}"

- name: Attach SBOM Attestation
  shell: bash
  run: |
    cosign attest -y \
      --predicate "./sbom_${{ inputs.sbom_name }}.spdx.json" \
      --type spdxjson \
      "${{ inputs.image }}"

This runs for every release image build and for our Helm chart OCI artifacts. Verification instructions are in the Cilium docs.

Release builds also run inside protected environments (release, release-tool, release-helm) so production registry credentials are gated behind environment protection rules. You can’t trigger a release build from a fork or a feature branch.

The Cilium security team

If you’ve ever reported a security issue to the project (via GitHub security advisories or security@cilium.org), you’ve already interacted with Cilium’s Security Team. Beyond triaging vulnerability reports, the team also runs the operational side of supply chain security:

  • Auditing and rotating credentials and permissions across the GitHub organization.
  • When necessary, carrying out incident investigation and audits.
  • Monitoring for patterns in our security issues and industry developments in order to propose mitigations and controls in areas where our security posture is weak.

Additional layers

A few smaller things worth mentioning:

  • Tag immutability. Once a GitHub release is published, the tags and assets attached to it can’t be modified. The setting lives in the repository’s Settings → Releases page.
  • DCO sign-off enforcement. Every commit must carry a Signed-off-by line. Our maintainers-little-helper config blocks merges with a dont-merge/needs-sign-off label until a sign-off is present.
  • Third-party security audits. We’ve been audited by ADA Logics, and we maintain a published threat model.

What we’re still working on

We audited our .github/ directory against current best practices (OpenSSF Scorecard, SLSA, StepSecurity recommendations) and turned up a number of real gaps. The bigger ones:

  • No SLSA provenance. Every docker/build-push-action call sets provenance: false. We sign images with Cosign, but we don’t generate SLSA build provenance attestations. Consumers can verify who signed an image, but not how it was built. Adopting slsa-framework/slsa-github-generator (or at minimum enabling BuildKit-native provenance) is on the list.
  • No dependency review at PR time. We rely on Renovate’s vulnerabilityAlerts to flag known-vulnerable dependencies, but that’s reactive. Wiring in actions/dependency-review-action would catch malicious or vulnerable new dependencies before they merge.
  • No govulncheck in CI. We fuzz and we lint, but we don’t yet run Go’s official vulnerability scanner, which checks whether our code actually calls vulnerable functions rather than just whether a vulnerable package shows up in go.sum.
  • 68 internal @main references. A bunch of conformance and scale-test workflows reference cilium/cilium/.github/actions/set-commit-status@main, which is a mutable branch ref. It’s lower risk than a third-party tag, but inconsistent with our SHA-pinning policy. The plan is to move all of our composite actions out of cilium/cilium into a dedicated repository, which removes the need for @main here.

A few smaller items in the same audit:

  • No OpenSSF Scorecard workflow for continuous supply chain health monitoring.
  • Our SECURITY-INSIGHTS.yml expired in January 2025 and hasn’t been updated. (We actually noticed this while writing this post.)
  • No go mod verify step to validate vendor directory integrity against go.sum checksums.

If any of these look like a good first issue, please send a PR.

GitHub’s 2026 Actions security roadmap and how it maps to what we do

In April 2026, GitHub published their Actions security roadmap describing platform-level changes across three layers: ecosystem, attack surface, and infrastructure. Reading it felt like validation of problems we’ve been working around for years, and a real signal that the platform is finally catching up to what large open source projects need. Here’s how it maps to what we do today.

Dependency locking: making SHA pinning first-class

We pin every action by SHA and lean on Renovate to keep those pins current, but we still have a blind spot for transitive references. GitHub’s planned dependencies: section in workflow YAML would lock all direct and transitive dependencies by commit SHA, with hash verification before execution starts. That closes the gap.

Policy-driven execution: centralizing what we enforce per-file today

We restrict who can trigger workflows (Ariane’s allow-list), which events are allowed (per-workflow configuration), and who can approve releases (protected environments). All of that is currently encoded across dozens of YAML files plus a custom bot, and auditing the full picture means reading every file.

GitHub’s planned workflow execution protections, built on rulesets, would let us define those controls centrally at the org level: which actors can trigger workflows, which events are permitted, which repositories the rules apply to. We could prohibit pull_request_target org-wide except for the workflows where we’ve intentionally designed a safe two-phase checkout, instead of relying on code review and CODEOWNERS to enforce it.

Scoped secrets: closing the implicit inheritance gap

CI vs. production credential isolation is one of our strongest controls, but within a given environment, secrets are still scoped pretty broadly: any workflow running in that environment can access them.

Scoped secrets would let us bind credentials to specific workflow paths, branches, or even individual reusable workflows. A release credential could be restricted not just to the release environment but to the specific release.yaml workflow file, so a new workflow added to that environment (by accident or by an attacker) wouldn’t inherit the credentials. 

The roadmap also separates secret management from repository write access. GitHub plans to move secret management into a dedicated custom role, which lines up with the least-privilege principle we already apply to workflow permissions but can’t currently apply to secret administration.

Native egress firewall

GitHub’s planned native egress firewall would restrict outbound network access from GitHub-hosted runners. It runs outside the runner VM at L7, so it’s immutable even if an attacker gets root inside the runner. Organizations would define allowed domains, IP ranges, and HTTP methods, and anything else gets blocked.

For Cilium it’s less critical than the rest. Our most security-sensitive workflows already run with credential isolation and least-privilege permissions, which limits what a compromised step could do even with unrestricted network access. Building an accurate egress allow-list would be a significant chunk of work. Public preview is expected in 6 to 9 months, so we’ll evaluate then.

Actions data stream: making CI observable

Our workflows produce logs, but we don’t have centralized telemetry for them. If a workflow starts behaving oddly (resolving unexpected dependencies, running longer than usual, making strange network calls), we’d have to notice it manually.

Actions Data Stream would deliver near real-time execution telemetry to external systems (S3, Azure Event Hub), covering workflow execution details, dependency resolution patterns, and eventually network activity.

The point

Supply chain security is mostly the practice of repeatedly asking “what if this thing I trust gets compromised?” and adding a layer that limits the blast radius when it does.

We’ve tried to build defense in depth: access controls so only trusted people can trigger builds, pinned digests so a compromised tag can’t reach us, least-privilege permissions so a rogue action can’t exfiltrate secrets, credential isolation so CI can never touch production, and signatures so users can verify what they’re running.

None of this makes us invulnerable. But security by obscurity isn’t really a thing, and the inverse is also true: the more open source projects share their defenses openly, the higher the collective bar for attackers. We’ve shown you ours, including the parts that aren’t great yet. If you’re running CI/CD for an open source project and you’ve solved something we haven’t, open an issue, write your own post, or come tell us on Slack. The open source supply chain is only as strong as its weakest project, and the only way to strengthen it is together.

André Martins is a Cilium maintainer and Software Engineer, Isovalent at Cisco. Feroz Salam is a member of the Cilium Security Team and a Security Engineer, Isovalent at Cisco. Find Cilium on GitHub and join the community on Slack.

❌