Skip to main content
5 shades of Network Policy

5 shades of Network Policy

Romain Boulanger
Author
Romain Boulanger
Infra/Cloud Architect with DevSecOps mindset
Table of Contents

Networking in Kubernetes: an essential reminder
#

When deploying applications in Kubernetes, it is easy to overlook an extremely important concept of the Kubernetes network model: All pods can communicate with all other pods, whether they are on the same node or on different nodes.

This characteristic is entirely by design. Kubernetes adopts a flat network model where every Pod receives its own cluster-level IP address. This means a Pod in the frontend namespace can freely communicate with a Pod in the database namespace without any default restrictions.

While this simplicity greatly facilitates communication between deployed services, it raises a crucial security question: how can communication between Pods be effectively limited?

In a production environment, this total permeability presents a considerable security risk. If a compromised Pod can freely contact any resource in the cluster, an attacker has a wide-open field for lateral movement.

For this very reason, adding network policies using the NetworkPolicy object is an essential part of any Kubernetes cluster security strategy.

But then, how can this control be implemented? And above all, how granular can filtering be?

In this article, I wanted to provide an overview of the possibilities offered by Kubernetes on this concept, both natively and by extending the API with CustomResourceDefinitions (CRDs).

The NetworkPolicy: A first line of defence
#

Its Advantages
#

The NetworkPolicy has been an integral part of the Kubernetes API for many years. It allows for the definition of filtering rules at Layer 3 and 4 of the OSI model, authorising or blocking traffic based on IP addresses, ports, and protocols.

Its strength lies in its native integration with the Kubernetes labelling system, making it relatively intuitive to use, particularly for a developer without network knowledge.

Consider a classic use case: an application composed of a frontend and a backend. The frontend must be able to contact the backend on port 8080, but no other Pod in the cluster should have access to this service.

This rule can be translated into a NetworkPolicy as follows:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080

A NetworkPolicy example

This policy applies to all Pods with the app: backend label in the production namespace. It only authorises incoming traffic from Pods labelled app: frontend on TCP port 8080. All other traffic is implicitly denied because as soon as a NetworkPolicy selects a Pod, that Pod adopts a default deny behaviour for the specified traffic types.

The declarative approach and compatibility with any Container Network Interface (CNI) that implements NetworkPolicies (Calico, Cilium, Weave, etc.) are undeniable advantages.

You define your rules in YAML format, and the CNI handles their translation into network configurations within the cluster. Pretty straightforward, isn’t it?

But also its limitations…
#

However, NetworkPolicy quickly reveals its limitations when implementing more sophisticated security policies. It operates exclusively at layers 3 and 4, meaning filtering is restricted to criteria like IP addresses, ports, and protocols. It is impossible to filter based on the HTTP method used or the path of a request.

Furthermore, NetworkPolicy is a namespaced resource. To apply a global security rule at the cluster level—for instance, blocking access to a specific metadata server, as is common in the cloud, an identical policy must be created in every single namespace. This approach quickly becomes tedious in clusters with tens or hundreds of namespaces.

The granularity of actions remains binary: allow or deny. It is not possible to audit or even gain visibility into the network traffic.

Finally, the resource does not provide a genuine deny action for a specific flow. Reusing the previous example of blocking a metadata server (169.254.169.254), it is necessary to allow everything except a single IP, making the approach feel unnatural:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-metadata-server
  namespace: production
spec:
  podSelector: {}  
  policyTypes:
  - Egress
  egress:
  - to:
    - ipBlock:
        cidr: 0.0.0.0/0
        except:
        - 169.254.169.254/32

A NetworkPolicy example blocking the cloud metadata server

For environments requiring a Zero Trust approach with access controls based on identity (ServiceAccount) and application context, the native NetworkPolicy also proves insufficient.

This is where alternatives from different players in the Kubernetes ecosystem come into play.

ClusterNetworkPolicy, here I am!
#

In response to the limitations of NetworkPolicy, several initiatives have emerged to enhance network filtering capabilities in Kubernetes. One such initiative is the ClusterNetworkPolicy, an extension developed by the Network Policy API Working Group within the Kubernetes SIG Network community.

The ClusterNetworkPolicy addresses a specific need: enabling cluster administrators to apply global network security rules that cannot be bypassed or overridden by namespace-level policies. Unlike NetworkPolicy, aimed mainly at application developers, ClusterNetworkPolicy explicitly targets cluster administrators responsible for enforcing global security guardrails.

One of the major contributions of this resource is its system of tiers and actions. Rather than being limited to just “allowing” or “denying” traffic, ClusterNetworkPolicy introduces three actions:

  • Allow: Authorises the traffic and stops the evaluation of subsequent rules;
  • Deny: Explicitly denies the traffic;
  • Pass: Delegates the evaluation to the next rule or a lower tier.

These actions are evaluated in a specific order defined by tiers: rules in the Admin tier are evaluated first, followed by standard NetworkPolicy rules at the namespace level, and then the Baseline tier.

This hierarchy ensures cluster administrators can define security policies that cannot be circumvented. For example, blocking traffic to sensitive subnets while leaving developers a degree of autonomy to manage internal traffic for their applications.

Below is an example of a ClusterNetworkPolicy that blocks all outbound traffic to a subnet considered malicious:

apiVersion: networking.x-k8s.io/v1alpha1
kind: ClusterNetworkPolicy
metadata:
  name: block-malicious-network
spec:
  tier: Admin
  priority: 100
  policyTypes:
    - Egress
  egress:
    - action: Deny
      to:
        - ipBlock:
            cidr: 203.0.113.0/24

This policy applies to all Pods in the acluster, across all namespaces. Even if a developer creates a NetworkPolicy authorising traffic to this address range, the Admin tier rule will take precedence and block the communication.

The main drawback of ClusterNetworkPolicy is its current status: it remains an alpha-stage resource within the Network Policy API project. Its adoption is not yet widespread and depends on support from CNI implementations. Nevertheless, it represents a promising direction for standardising cluster-level network controls within Kubernetes.

As of today, no stable version is available, but you can deploy the CustomResourceDefinition (CRD) to experiment with this concept.

Since AdminNetworkPolicy and BaselineAdminNetworkPolicy are no longer actively developed, they will not be covered here. ClusterNetworkPolicy is the successor to both.

NetworkPolicy and GlobalNetworkPolicy: The Calico approach
#

While the Kubernetes community works on standardising the ClusterNetworkPolicy, Calico, one of the most popular CNIs, has long provided its own extensions to overcome the limitations of NetworkPolicy. Two resources stand out: the Calico NetworkPolicy and the GlobalNetworkPolicy.

Although it shares the same name as the standard Kubernetes resource, Calico’s NetworkPolicy provides significantly richer functionality. It retains the namespaced nature of its native counterpart but introduces several advanced mechanisms.

Among these is the concept of priority order via the order field, allowing control over rule evaluation: policies with a lower order value are applied first. This capability resolves one of the major problems with the native NetworkPolicy, where the absence of priority makes composing complex rules difficult.

Calico also adds a set of extended actions: Allow, Deny, Log, and Pass.

The Log action makes it possible to record matching traffic without blocking or authorising it, proving invaluable for auditing and debugging. The Pass action functions similarly to its counterpart in ClusterNetworkPolicy, instructing the policy engine to skip the remaining rules and move to the profile associated with the endpoint.

But the most significant contribution is support for application-layer (Layer 7) policies. Calico enables filtering of HTTP traffic based on the request’s method or path:

apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: api-gateway-policy
  namespace: production
spec:
  order: 100
  selector: app == "api-gateway"
  types:
    - Ingress
  ingress:
    - action: Allow
      protocol: TCP
      http:
        methods: ["GET"]
        paths: ["/api/v1/users"]
      source:
        selector: app == "frontend"
    - action: Deny

A NetworkPolicy example using the Calico API

This policy only authorises HTTP GET requests to the /api/v1/users path from the frontend and denies all other traffic. This represents a significant step up in terms of granularity.

This feature is called Application Layer Policy (ALP) and requires some configuration with the addition of Sidecar, while introducing limitations, notably the inability to have a Service Mesh in parallel or filter only on ingress.

The GlobalNetworkPolicy, for its part, addresses the need for cluster-wide policies. Unlike NetworkPolicy (either native or Calico’s), it is not namespaced and applies to the entire cluster. This resource allows for defining global security rules in a single place, avoiding duplication.

A typical use case is to systematically block access to a set of sensitive ports for all Pods in the cluster:

apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: deny-sensitive-ports
spec:
  order: 10
  types:
    - Ingress
  ingress:
    - action: Deny
      protocol: TCP
      destination:
        ports: [22, 3389, 5432]

A GlobalNetworkPolicy example

This GlobalNetworkPolicy blocks SSH, RDP, and PostgreSQL access for all Pods, regardless of namespace. It can be combined with more permissive NetworkPolicy rules at the namespace level, thanks to the ordering system: by assigning a low order value to this global rule, it will be evaluated first.

The advantage of the Calico approach lies in its maturity and widespread adoption within the community. These resources have been production-proven for several years and benefit from an ecosystem of visualisation and debugging tools (like Calico Enterprise or the open-source project calicoctl).

The drawback, however, is the dependency on the CNI. These policies only function with Calico, a situation that can pose a problem if you plan to change the network layer or if it is mandated by your Kubernetes distribution.

CiliumNetworkPolicy, expanding possibilities with eBPF!
#

While Calico paved the way for advanced network policies, Cilium pushes the boundaries even further through its foundation on eBPF (Extended Berkeley Packet Filter). eBPF allows code to be executed directly in the Linux kernel securely and efficiently, providing filtering and observability capabilities impossible to achieve with traditional iptables-based solutions.

Cilium provides two primary resources for network policy management: CiliumNetworkPolicy and CiliumClusterwideNetworkPolicy. The first is namespaced, while the second operates at the cluster level. Both support rules extending from Layer 3 to Layer 7, with an expressiveness that far exceeds that of the classic NetworkPolicy.

At Layer 7, Cilium delivers a significant range of features, with native support for numerous application protocols: HTTP, gRPC, Kafka, DNS, and many others.

It is therefore possible to filter HTTP requests based on method, path, headers, or even query string parameters. For Kafka, access to specific topics can be controlled. For DNS, resolutions to certain domains can be allowed or blocked.

Here is an example of a CiliumNetworkPolicy that authorises only HTTP GET requests to a specific path, while blocking POST requests:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: frontend-api-policy
  namespace: production
spec:
  endpointSelector:
    matchLabels:
      app: api
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: frontend
      toPorts:
        - ports:
            - port: "8080"
              protocol: TCP
          rules:
            http:
              - method: "GET"
                path: "/api/v1/.*"

A CiliumNetworkPolicy example

This policy applies to Pods with the app: api label and only authorises HTTP GET requests to paths matching the regular expression /api/v1/.* from Pods having the app: frontend label. Any other HTTP method or path will be denied.

The CiliumClusterwideNetworkPolicy adds another dimension by making it possible to target not only Pods but also cluster nodes through the Node Selector. This capability is useful for applying specific security rules to certain nodes, for instance, nodes hosting sensitive workloads or located in a high-risk network zone.

The advantage of Cilium does not stop at its functional richness. Thanks to eBPF, packet processing performance is significantly superior to solutions relying on iptables. The rules are compiled into eBPF bytecode and executed directly in the kernel, reducing latency and improving throughput, even under heavy workloads.

Moreover, Cilium natively integrates observability tools like Hubble, allowing for real-time visualisation of network flows and policy debugging.

The drawback, once again, is the dependency on the CNI. If you use Cilium, you gain these advanced capabilities. If you switch to a different network solution, you will need to migrate them to an alternative.

Furthermore, Cilium’s learning curve is steeper than that of more traditional solutions, owing to the wealth of options and concepts to master.

AuthorizationPolicy, Zero Trust in its purest form
#

So far, the exploration has covered network filtering solutions operating mainly at Layers 3, 4, and 7 of the OSI model. These approaches rely on criteria such as IP addresses, ports, Kubernetes labels, or even HTTP methods. But what about workload identity? This is precisely where Istio enters the picture, specifically with its AuthorizationPolicy resource.

Istio is a Service Mesh that uses Envoy, either in Sidecar or Ambient mode, to intercept traffic. This proxy intercepts all incoming and outgoing traffic, thereby enabling the implementation of advanced security, observability, and traffic management features. The AuthorizationPolicy leverages this architecture to provide access control based on workload identity in SPIFFE format, established via mTLS (mutual TLS).

When Pod A wants to communicate with Pod B, the Envoy proxies establish an mTLS connection, allowing each to verify the other’s identity. The AuthorizationPolicy can then filter traffic based on a service account (ServiceAccount), a namespace, or even attributes extracted from JWT tokens.

The AuthorizationPolicy supports three main actions: ALLOW, DENY, and AUDIT. The ALLOW action authorises traffic matching the specified rules. The DENY action explicitly rejects it and takes precedence over ALLOW rules, so a “deny by default” security model can be easily implemented. The AUDIT action logs requests without affecting the flow, making it possible to test policies before application.

The strength of this type of policy is its ability to filter based on a diverse range of application criteria: HTTP methods, paths, headers, JWT claims, and even custom attributes from Istio metadata.

apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: backend-admin
  namespace: production
spec:
  selector:
    matchLabels:
      app: backend
  action: ALLOW
  rules:
    - from:
        - source:
            principals:
              - cluster.local/ns/production/sa/frontend
      to:
        - operation:
            methods: ["GET", "POST"]
            paths: ["/api/admin*"]
      when:
        - key: request.auth.claims[role]
          values: ["admin"]

An AuthorizationPolicy example

This policy applies to Pods with the app: backend label in the production namespace. It only authorises requests from a workload identified by the frontend service account in the same namespace, and only if the HTTP method is GET or POST, the path matches /api/admin*, and finally, the JWT claim has a role field with the value admin. All other traffic will be denied.

It should be noted that AuthorizationPolicy only allows for filtering ingress traffic; controlling egress flows is not possible. Nevertheless, it can be combined with a native NetworkPolicy to fill this gap.

This approach embodies the Zero Trust philosophy and the principle of least privilege. Rather than relying on volatile network criteria or labels that can be modified, it relies on an identity mechanism, potentially combined with application context, to make authorisation decisions. The AuthorizationPolicy can even delegate authorisation to an external system via the CUSTOM action, thereby making it possible to integrate third-party access management platforms and further extending the possibilities.

Istio does not strictly allow for creating cluster-wide policies as a distinct object type, but you can use the root namespace, istio-system, to apply them to the entire Mesh.

In summary, the main characteristic of Istio and AuthorizationPolicy lies in its depth of control and the numerous security mechanisms the tool provides.

On the other hand, adopting a Service Mesh involves significant operational complexity. It requires deploying an additional infrastructure layer within your cluster, either as a Sidecar or in Ambient mode, even though it is compatible with any CNI.¨

A few words in conclusion
#

As has been demonstrated, the possibilities within Kubernetes are vast, particularly due to the ability to extend the API with CustomResourceDefinitions (CRDs). A solution exists for almost every requirement.

One thing is sure: the basic NetworkPolicy, included directly in Kubernetes, proves to be limited in functionality, especially when there is a need to filter network traffic with very fine granularity.

Will ClusterNetworkPolicy complement NetworkPolicy in the near future? It is certainly hoped so, perhaps with additional features like Layer 7 filtering or identity-based controls similar to those found in Cilium and Istio.

Related