Skip to main content
In this tutorial, we’ll secure a sample application using Cilium Network Policies. We’ll progress from a default Kubernetes NetworkPolicy (Layer 3) to a full L7 HTTP policy with header-based access control. Table of Contents
  1. Demo App Overview
  2. Default Kubernetes NetworkPolicy
  3. Cilium Layer 3 Policy
  4. Cilium Layer 4 Policy
  5. Cilium Layer 7 HTTP Policy
  6. Adding an API Key Header
  7. Further Reading

Demo App Overview

Our demo application runs as a single Pod with two containers listening on ports 5000 and 80. It exposes two corresponding ClusterIP Services.
kubectl get all
NAME                                  READY   STATUS    RESTARTS   AGE
pod/demo-deployment-7ccd685fcc-7z9wf  2/2     Running   0          5m

NAME                   TYPE        CLUSTER-IP       PORT(S)    AGE
service/app-svc-5000   ClusterIP   10.111.51.97     5000/TCP   5m
service/app-svc-80     ClusterIP   10.102.122.72     80/TCP    5m
service/kubernetes     ClusterIP   10.96.0.1         443/TCP  10m
Both containers serve the same Flask app. We’ll lock down access so only Pods labeled app=admin can communicate.

1. Default Kubernetes NetworkPolicy

We begin with a basic Kubernetes NetworkPolicy named demo-netpol. It selects Pods labeled app=demo and allows ingress from Pods labeled app=admin on all ports.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: demo-netpol
spec:
  podSelector:
    matchLabels:
      app: demo
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: admin

Verifying the Default Policy

  1. Allowed: Pod with app=admin can reach both ports.
    kubectl run --rm -i --tty admin --image=curlimages/curl \
      --labels app=admin --restart=Never -- \
      curl --connect-timeout 2 app-svc-80
    # → Have a great day!
    
    kubectl run --rm -i --tty admin --image=curlimages/curl \
      --labels app=admin --restart=Never -- \
      curl --connect-timeout 2 app-svc-5000
    # → Have a great day!
    
  2. Denied: Pod without the label is blocked.
    kubectl run --rm -i --tty client --image=curlimages/curl \
      --restart=Never -- \
      curl --connect-timeout 2 app-svc-80
    # → curl: (28) Failed to connect...
    
Before applying Cilium policies, delete the existing Kubernetes NetworkPolicy so that Cilium’s default behavior (allow all) is restored.
kubectl delete networkpolicy demo-netpol

2. Cilium Layer 3 Policy

Create cilium-l3.yaml to reimplement the same L3 selector using Cilium’s CRD:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: demo-cilium-l3
spec:
  endpointSelector:
    matchLabels:
      app: demo
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: admin
Apply and test:
kubectl apply -f cilium-l3.yaml

# Unlabeled Pod → denied:
kubectl run --rm -i --tty client --image=curlimages/curl \
  --restart=Never -- \
  curl --connect-timeout 2 app-svc-80
# Labeled Pod → allowed:
kubectl run --rm -i --tty admin --image=curlimages/curl \
  --labels app=admin --restart=Never -- \
  curl --connect-timeout 2 app-svc-5000
# → Have a great day!

3. Cilium Layer 4 Policy

Tighten access to only TCP port 80. Update to cilium-l4.yaml:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: demo-cilium-l4
spec:
  endpointSelector:
    matchLabels:
      app: demo
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: admin
      toPorts:
        - ports:
            - port: "80"
              protocol: TCP
kubectl apply -f cilium-l4.yaml

# Port 80 → allowed:
kubectl run --rm -i --tty admin --image=curlimages/curl \
  --labels app=admin --restart=Never -- \
  curl --connect-timeout 2 app-svc-80
# Port 5000 → denied:
kubectl run --rm -i --tty admin --image=curlimages/curl \
  --labels app=admin --restart=Never -- \
  curl --connect-timeout 2 app-svc-5000
# → curl: (28) Failed to connect...

4. Cilium Layer 7 HTTP Policy

Leverage Cilium’s L7 HTTP inspection to allow only GET /healthz and GET /api. Define cilium-l7.yaml:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: demo-cilium-l7
spec:
  endpointSelector:
    matchLabels:
      app: demo
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: admin
      toPorts:
        - ports:
            - port: "80"
              protocol: TCP
          rules:
            http:
              - method: GET
                path: /healthz
              - method: GET
                path: /api
kubectl apply -f cilium-l7.yaml

# Default path → denied:
kubectl run --rm -i --tty admin --image=curlimages/curl \
  --labels app=admin --restart=Never -- \
  curl --connect-timeout 2 app-svc-80
# /api → allowed:
kubectl run --rm -i --tty admin --image=curlimages/curl \
  --labels app=admin --restart=Never -- \
  curl --connect-timeout 2 app-svc-80/api
# /healthz → allowed:
kubectl run --rm -i --tty admin --image=curlimages/curl \
  --labels app=admin --restart=Never -- \
  curl --connect-timeout 2 app-svc-80/healthz
# → {"status":"OK"}

5. Adding an API Key Header

Finally, require an X-API-KEY header for the /api endpoint. Update to cilium-l7-header.yaml:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: demo-cilium-l7-header
spec:
  endpointSelector:
    matchLabels:
      app: demo
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: admin
      toPorts:
        - ports:
            - port: "80"
              protocol: TCP
          rules:
            http:
              - method: GET
                path: /healthz
              - method: GET
                path: /api
                headers:
                  - name: X-API-KEY
                    value: ABC123
kubectl apply -f cilium-l7-header.yaml

# Missing header → denied:
kubectl run --rm -i --tty admin --image=curlimages/curl \
  --labels app=admin --restart=Never -- \
  curl --connect-timeout 2 app-svc-80/api
# With header → allowed:
kubectl run --rm -i --tty admin --image=curlimages/curl \
  --labels app=admin --restart=Never -- \
  curl -H "X-API-KEY: ABC123" --connect-timeout 2 app-svc-80/api
# → Have a great day!

Policy Progression

StageLayerFileDescription
KubernetesL3demo-netpolPod selector + ingress from app=admin
CiliumL3cilium-l3.yamlSame selector using Cilium CRD
CiliumL4cilium-l4.yamlRestrict to TCP port 80
CiliumL7cilium-l7.yamlAllow only GET /healthz & /api
CiliumL7+Hcilium-l7-header.yamlAdds X-API-KEY header check

Further Reading

With Cilium Network Policies, you have full L3–L7 control to secure your workloads. Experiment with custom rules to fit your security requirements!