Beyond NetworkPolicy: eBPF for Granular L7 Security in Kubernetes

14 min read
Goh Ling Yong
Technology enthusiast and software architect specializing in AI-driven development tools and modern software engineering practices. Passionate about the intersection of artificial intelligence and human creativity in building tomorrow's digital solutions.

The Inadequacy of `NetworkPolicy` in a Zero-Trust World

As architects of distributed systems, we're intimately familiar with the Kubernetes NetworkPolicy resource. It was a foundational step towards network segmentation within the flat cluster network. However, in complex production environments, its limitations become starkly apparent:

  • L3/L4 Granularity Only: NetworkPolicy operates solely on IP addresses (via CIDR blocks) and L4 ports. It has no awareness of the application protocol. You can allow traffic to a pod on port 8080, but you cannot differentiate between a GET /healthz check and a POST /admin/delete-database request on that same port. This is a significant security gap in API-driven microservices.
  • Performance at Scale: Most CNI (Container Network Interface) plugins implement NetworkPolicy using iptables. While functional, iptables rules are sequential. In a large cluster with thousands of pods and hundreds of policies, this can lead to massive, unwieldy iptables chains, introducing tangible latency to packet processing and new connection setup.
  • IP-based Brittleness: The reliance on IP addresses for policy (even when abstracted by pod selectors) is a fragile paradigm. IP addresses are ephemeral in Kubernetes. While selectors help, the underlying enforcement mechanism is still IP-centric, which complicates scenarios involving traffic leaving the cluster or interacting with external services.
  • This is where eBPF (extended Berkeley Packet Filter) introduces a paradigm shift. By attaching sandboxed programs to various hooks within the Linux kernel, we can implement networking, observability, and security logic with near-native performance. Projects like Cilium leverage eBPF to provide a CNI that not only implements networking but also offers a vastly more powerful security model.

    This post assumes you understand what eBPF is and have a working knowledge of Cilium. We will not cover the basics. Instead, we'll dive directly into advanced policy implementation patterns that solve real-world production security challenges.


    Pattern 1: Identity-Based L4 Policy for Multi-Tenant Egress

    Before tackling L7, let's establish a robust L3/L4 foundation using Cilium's identity-based security model. Cilium assigns a security identity to each endpoint (pod) based on its labels. Policies are then defined based on these identities, not ephemeral IP addresses. This completely decouples policy from networking location.

    Scenario: Consider a multi-tenant cluster where each tenant has its own namespace (tenant-a, tenant-b). There is also a shared monitoring namespace containing a Prometheus instance. The security requirement is:

    Pods in a tenant namespace can freely communicate with other pods within the same namespace*.

    * Pods in any tenant namespace must be able to send metrics to the Prometheus service in the monitoring namespace on port 9090.

    * Cross-tenant communication is strictly forbidden.

    * By default, all ingress and egress should be denied (default-deny posture).

    The Standard `NetworkPolicy` Approach (and its flaws)

    You could attempt this with standard NetworkPolicy, but it becomes verbose and requires careful management of namespaceSelector and podSelector for every rule.

    The Superior `CiliumNetworkPolicy` Implementation

    Cilium extends the Kubernetes API with its own CRDs, including CiliumNetworkPolicy (CNP) and CiliumClusterwideNetworkPolicy (CCNP). Let's implement our default-deny posture and then layer the required rules.

    First, a cluster-wide default deny policy. This is a critical first step in a zero-trust model.

    yaml
    # ccnp-default-deny.yaml
    apiVersion: "cilium.io/v2"
    kind: CiliumClusterwideNetworkPolicy
    metadata:
      name: "default-deny-all"
    spec:
      endpointSelector: {}
      ingress:
      - {}
      egress:
      - {}

    This policy selects all endpoints (endpointSelector: {}) and applies an empty ingress and egress rule, effectively blocking all traffic.

    Now, let's create the specific policies for our tenants.

    yaml
    # tenant-a-policy.yaml
    apiVersion: "cilium.io/v2"
    kind: CiliumNetworkPolicy
    metadata:
      name: "tenant-a-rules"
      namespace: tenant-a
    spec:
      endpointSelector:
        matchLabels:
          # Apply this policy to all pods in the tenant-a namespace
          io.kubernetes.pod.namespace: tenant-a
      # Rule 1: Allow intra-namespace communication
      ingress:
      - fromEndpoints:
        - matchLabels:
            io.kubernetes.pod.namespace: tenant-a
      egress:
      - toEndpoints:
        - matchLabels:
            io.kubernetes.pod.namespace: tenant-a
      
      # Rule 2: Allow egress to the shared monitoring service
      - toEndpoints:
        - matchLabels:
            io.kubernetes.pod.namespace: monitoring
            app: prometheus
        toPorts:
        - ports:
          - port: "9090"
            protocol: TCP

    A similar policy would be created for tenant-b. The key here is the use of fromEndpoints and toEndpoints with label selectors that span namespaces. Cilium's eBPF programs resolve these label selectors to security identities and enforce the policy efficiently in the kernel, without ever building complex IP-based rules.


    Pattern 2: Advanced L7 Policy Enforcement for Microservice APIs

    This is where eBPF and Cilium truly outshine traditional approaches. We can inspect application-layer protocols like HTTP and enforce policy based on paths, methods, and headers.

    Scenario: We have a payments microservice (payments-api) in the payments namespace. The security requirements are:

  • The frontend-app in the web namespace can make GET and POST requests to /api/v1/charges to create and view charges.
  • The analytics-service in the data namespace can only make GET requests to /api/v1/charges to fetch data for reports.
  • The finance-tool in the ops namespace can make POST requests to /api/v1/refunds to process refunds. No other service can access this sensitive endpoint.
  • All other API calls to the payments-api are denied, even from services that have partial access.
  • Implementation with `CiliumNetworkPolicy`

    Here is the complete policy that enforces these L7 rules on the payments-api pods.

    yaml
    # l7-payments-api-policy.yaml
    apiVersion: "cilium.io/v2"
    kind: CiliumNetworkPolicy
    metadata:
      name: "payments-api-l7-access-control"
      namespace: payments
    spec:
      endpointSelector:
        matchLabels:
          app: payments-api
      ingress:
      - fromEndpoints:
          # Rule for frontend-app
          - matchLabels:
              io.kubernetes.pod.namespace: web
              app: frontend-app
        toPorts:
          - ports:
              - port: "8080"
                protocol: TCP
            rules:
              http:
                - method: "GET"
                  path: "/api/v1/charges"
                - method: "POST"
                  path: "/api/v1/charges"
    
      - fromEndpoints:
          # Rule for analytics-service
          - matchLabels:
              io.kubernetes.pod.namespace: data
              app: analytics-service
        toPorts:
          - ports:
              - port: "8080"
                protocol: TCP
            rules:
              http:
                - method: "GET"
                  path: "/api/v1/charges"
      
      - fromEndpoints:
          # Rule for finance-tool
          - matchLabels:
              io.kubernetes.pod.namespace: ops
              app: finance-tool
        toPorts:
          - ports:
              - port: "8080"
                protocol: TCP
            rules:
              http:
                - method: "POST"
                  path: "/api/v1/refunds"

    How It Works Under the Hood

    When this policy is applied:

  • Cilium identifies all pods matching app: payments-api.
    • It enables an eBPF-powered proxy (Envoy, by default) for traffic destined for these pods on port 8080.
  • The eBPF programs attached to the network interface (tc hook) are updated. They are identity-aware and know which source identities are allowed to connect to the payments-api identity on that port.
  • When a packet arrives from an allowed source (e.g., frontend-app), the eBPF program redirects it to the proxy for L7 inspection.
  • The proxy evaluates the HTTP request against the rules defined in the CNP. If the method and path match, the request is forwarded to the application container. If not, the proxy returns an HTTP 403 Forbidden response.
  • Crucially, if a connection attempt is made from an unauthorized identity (e.g., a pod in tenant-a), the eBPF program at the tc hook drops the packet before it ever reaches the L7 proxy or the pod's network namespace, providing maximum efficiency.
  • Verification and Testing

    To verify this, you would exec into the respective pods:

    bash
    # From the frontend-app pod (in namespace 'web')
    # This should succeed (HTTP 200 or similar)
    curl -X POST http://payments-api.payments:8080/api/v1/charges
    
    # This should fail (HTTP 403)
    curl -X POST http://payments-api.payments:8080/api/v1/refunds
    
    # From the analytics-service pod (in namespace 'data')
    # This should succeed
    curl -X GET http://payments-api.payments:8080/api/v1/charges
    
    # This should fail (HTTP 403)
    curl -X POST http://payments-api.payments:8080/api/v1/charges

    Edge Case: Handling `hostNetwork: true` Pods

    A common challenge in Kubernetes security is dealing with pods that run in the host's network namespace, such as monitoring agents like Datadog or Node Problem Detector. These pods bypass the standard pod network, and traditional NetworkPolicy often doesn't apply to them effectively.

    Cilium addresses this by providing host-level firewall capabilities. You can apply policies to the host's network interfaces.

    Scenario: You want to allow a hostNetwork Datadog agent pod to send egress traffic only to the Datadog API endpoints and the internal Kubernetes API server, while blocking all other outbound traffic from the node itself.

    This is achieved using a CiliumClusterwideNetworkPolicy that selects host endpoints.

    yaml
    apiVersion: "cilium.io/v2"
    kind: CiliumClusterwideNetworkPolicy
    metadata:
      name: "host-firewall-policy"
    spec:
      # This special selector targets the host itself as an endpoint
      nodeSelector: {}
      egress:
      # Rule 1: Allow DNS to kube-dns
      - toEndpoints:
        - matchLabels:
            k8s-app: kube-dns
            io.kubernetes.pod.namespace: kube-system
        toPorts:
        - ports:
          - port: "53"
            protocol: UDP
      # Rule 2: Allow access to Kubernetes API Server
      - toPorts:
        - ports:
          - port: "6443"
            protocol: TCP
      # Rule 3: Allow egress to Datadog's known CIDRs (example)
      - toCIDR:
        - 162.244.50.0/24 # Example Datadog IP range
        - 162.213.130.0/24 # Example Datadog IP range

    This policy is enforced by eBPF programs attached to the physical/virtual network interfaces of the host. It ensures that even privileged pods running on the host cannot exfiltrate data to arbitrary destinations.


    Observability and Debugging: The Power of Hubble

    Defining complex policies is only half the battle. When traffic is unexpectedly dropped, senior engineers need powerful tools to diagnose the issue quickly. iptables logs are notoriously difficult to parse. Cilium's observability tool, Hubble, provides deep, policy-aware visibility.

    Scenario: A developer reports that the frontend-app can no longer connect to the payments-api. You suspect a policy issue.

    Using the Hubble CLI, you can inspect traffic flows in real-time.

    bash
    # Enable the Hubble UI and CLI
    # cilium hubble enable
    
    # Follow flows from the frontend-app to the payments-api
    hubble observe --from-pod web/frontend-app-7b5b... --to-pod payments/payments-api-6c4c... --follow

    This command will stream network flow data. A dropped packet will show a verdict of DROPPED and, most importantly, a reason.

    Example Hubble Output for a Policy Drop:

    text
    TIMESTAMP           SOURCE                      DESTINATION                 TYPE      VERDICT   REASON
    Jan 10 15:30:01.123 web/frontend-app-7b5b... -> payments/payments-api-6c4c... L4-NEW    DROPPED   Policy denied at egress

    For L7 denials, the output is even more informative:

    text
    TIMESTAMP           SOURCE                      DESTINATION                 TYPE      VERDICT   SUMMARY
    Jan 10 15:32:10.543 web/frontend-app-7b5b... -> payments/payments-api-6c4c... http-request  FORWARDED HTTP/1.1 200 GET /api/v1/charges
    Jan 10 15:32:15.876 web/frontend-app-7b5b... -> payments/payments-api-6c4c... http-request  DROPPED   HTTP/1.1 403 POST /api/v1/refunds

    This level of detail is invaluable. It tells you not just that a packet was dropped, but why (Policy denied), at which layer (L4 vs http-request), and provides the full context of the request. This reduces mean time to resolution (MTTR) for network connectivity issues from hours to minutes.


    Performance Considerations and Production Patterns

    While eBPF is exceptionally fast, nothing is free. As a senior engineer, you must understand the performance trade-offs.

    * L3/L4 Policy Performance: For identity-based L3/L4 policies, Cilium's eBPF implementation is significantly faster than iptables. The enforcement is done via a hash table lookup on the security identity, which is an O(1) operation, regardless of the number of policies. In contrast, iptables is O(n), where n is the number of rules.

    * L7 Policy Overhead: L7 policy enforcement requires redirecting traffic to a user-space proxy. This transition from kernel to user-space and back introduces latency compared to pure in-kernel processing. However, this cost is paid only for traffic that is already allowed at L3/L4 and is destined for a port with an L7 policy. The performance is generally on par with a traditional service mesh sidecar proxy, but with the benefit of a more efficient and secure L3/L4 pre-filter in the kernel.

    * Resource Management: The Cilium agent and its associated proxies consume CPU and memory. For high-throughput clusters, it is critical to set appropriate resource requests and limits for the cilium daemonset. Monitor its resource consumption under load to ensure it doesn't become a bottleneck.

    Gradual Rollout: In a brownfield environment, do not apply a default-deny policy to the entire cluster at once. Use Cilium's policy-enforcement-mode setting. Start in default-allow or even audit mode. In audit mode, policies are evaluated, and Hubble will log what would have been* dropped, but no traffic is actually blocked. This allows you to build and validate your policy set without causing a production outage.

    Inspecting Loaded eBPF Programs

    For ultimate low-level debugging, you can use tools like bpftool from a privileged pod or directly on the node to inspect the eBPF programs and maps Cilium has loaded.

    bash
    # List eBPF programs attached to a specific network interface (e.g., eth0)
    bpftool net show dev eth0
    
    # Dump the contents of a Cilium BPF map (e.g., the policy map)
    # The map ID can be found from 'bpftool map list'
    bpftool map dump id <MAP_ID>

    This is an advanced debugging technique but provides undeniable proof of what rules are active directly in the kernel, bypassing all Kubernetes abstractions.

    Conclusion: A New Baseline for Cloud-Native Security

    eBPF is not just an incremental improvement over iptables; it is a fundamental shift in how we implement networking and security in cloud-native environments. By moving enforcement from brittle IP/port constructs to cryptographically secure, workload-aware identities, Cilium provides a robust foundation for zero-trust networking. The ability to enforce granular L7 policies without the complexity of a full service mesh for every use case gives platform and security teams a powerful, performant tool to secure east-west traffic.

    Adopting this model requires a shift in thinking, moving away from traditional firewall analogies towards a declarative, identity-first approach. For senior engineers responsible for the stability, performance, and security of large-scale Kubernetes clusters, mastering eBPF-based policy enforcement is no longer an emerging skill—it is rapidly becoming the new standard.

    Found this article helpful?

    Share it with others who might benefit from it.

    More Articles