Aller au contenu

Mastering Cilium Network Policies: Zero-Trust Security for Kubernetes

Sitemap

“Trust No One. Authenticate Everyone. Secure Everything!”

Photo by Sandra Seitamaa on Unsplash

Kubernetes provides a powerful platform for deploying and managing containerized applications, but securing network traffic between pods is a critical challenge. This is where Cilium Network Policies come into play.

Cilium is an eBPF-powered networking solution that enhances Kubernetes security by enforcing fine-grained network access control. Unlike traditional Kubernetes Network Policies, which operate at Layer 3/4 (IP and port-based filtering), Cilium extends security controls to Layer 7 (application-aware filtering for HTTP, gRPC, and Kafka). This means you can enforce policies based on HTTP methods, paths, and even API calls, providing deeper security for microservices communication.

With Cilium Network Policies (CNPs), you can:
✅ Restrict traffic between pods based on labels, protocols, or ports.
✅ Apply Layer 7 filtering to allow or deny requests based on URLs, HTTP methods, and headers.
✅ Secure applications without modifying them, using eBPF to enforce rules transparently.
✅ Improve observability with detailed metrics and logs for network traffic.

The code in available in my github repo

In this article, we’ll explore how to use Cilium Network Policies to secure Kubernetes workloads, enforce access control, and apply advanced traffic filtering with practical examples.

  1. Install KIND k8s:
    #install kind
    # For AMD64 / x86_64
    $ [ $(uname -m) = x86_64 ] && curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.27.0/kind-linux-amd64
    
    $ chmod +x ./kind
    
    $ sudo mv ./kind /usr/local/bin/kind
    
    # Create test cluster and delete it:
    $ sudo kind create cluster --name=test-prometheus-grafana
    
    #get cluster
    $ kind get clusters
    
    #delete cluster:
    $ kind delete cluster
    
  2. Create and configure the cluster (file name is cilium-cluster.yaml), Here defaultCNI is disabled as we will install cilium CNI.
    kind: Cluster
    apiVersion: kind.x-k8s.io/v1alpha4
    name: cilium
    networking:
      disableDefaultCNI: true  # Ensures KIND does not use the default CNI, allowing Cilium installation
    nodes:
    - role: control-plane
      kubeadmConfigPatches:
      - |
        kind: InitConfiguration
        nodeRegistration:
          kubeletExtraArgs:
            node-labels: "ingress-ready=false"
      extraPortMappings:
      - containerPort: 80
        hostPort: 8080
        protocol: TCP
      - containerPort: 443
        hostPort: 44300
        protocol: TCP
    
    # kind cluster with port forwarding to host
    $ kind create cluster --config cilium-cluster.yaml
    
    $ kubectl cluster-info --context kind-cilium # output: 
    Kubernetes control plane is running at https://127.0.0.1:39185
    CoreDNS is running at https://127.0.0.1:39185/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    

2. Install Helm & Cilium (Debian/Ubuntu)

# Install Helm
$ curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
$ sudo apt-get install apt-transport-https --yes
$ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
$ sudo apt-get update
$ sudo apt-get install helm

# Install Cilium
$ helm install cilium cilium/cilium --version 1.10.1 \--namespace kube-system

$ cilium status #output:

cilium status
    /¯¯\
 /¯¯\__/¯¯\    Cilium:             OK
 \__/¯¯\__/    Operator:           OK
 /¯¯\__/¯¯\    Envoy DaemonSet:    OK
 \__/¯¯\__/    Hubble Relay:       disabled
    \__/       ClusterMesh:        disabled

DaemonSet              cilium                   Desired: 1, Ready: 1/1, Available: 1/1
DaemonSet              cilium-envoy             Desired: 1, Ready: 1/1, Available: 1/1
Deployment             cilium-operator          Desired: 1, Ready: 1/1, Available: 1/1
Containers:            cilium                   Running: 1
                       cilium-envoy             Running: 1
                       cilium-operator          Running: 1
                       clustermesh-apiserver    
                       hubble-relay             
Cluster Pods:          14/14 managed by Cilium
Helm chart version:    1.17.2
Image versions         cilium             quay.io/cilium/cilium:v1.17.2@sha256:3c4c9932b5d8368619cb922a497ff2ebc8def5f41c18e410bcc84025fcd385b1: 1
                       cilium-envoy       quay.io/cilium/cilium-envoy:v1.31.5-1741765102-efed3defcc70ab5b263a0fc44c93d316b846a211@sha256:377c78c13d2731f3720f931721ee309159e782d882251709cb0fac3b42c03f4b: 1
                       cilium-operator    quay.io/cilium/operator-generic:v1.17.2@sha256:81f2d7198366e8dec2903a3a8361e4c68d47d19c68a0d42f0b7b6e3f0523f249: 1

Cilium deploys several components in kube-system name space, lets check:

$ kubectl get pods -n kube-system
NAME                                           READY   STATUS    RESTARTS      AGE
cilium-envoy-r2gf5                             1/1     Running   1 (26h ago)   46h
cilium-hfckl                                   1/1     Running   1 (26h ago)   46h
cilium-operator-59944f4b8f-9jgtg               1/1     Running   1 (26h ago)   46h

The **cilium-hfckl** pod is a Cilium agent running on a Kubernetes node, responsible for enforcing network policies, managing eBPF programs, and handling networking for pods. The **cilium-operator-59944f4b8f-9jgtg** pod is a control-plane component that performs higher-level tasks like managing Cilium resources, synchronizing network policies, and handling cluster-wide networking. The **cilium-envoy-r2gf5** pod runs Envoy, acting as a Layer 7 proxy to enforce HTTP-aware policies, provide observability, and enable service mesh capabilities. We will use layer 7 networking later in this article.

3. Deploy Microservices (see: Python frontend + PostgreSQL Backend)

# Create secret used by flaks connection string 
$ kubectl -n db create secret generic postgresql \
  --from-literal POSTGRES_USER="postgres" \
  --from-literal POSTGRES_PASSWORD='postgres' \
  --from-literal POSTGRES_DB="mydb" \
  --from-literal REPLICATION_USER="postgres" \
  --from-literal REPLICATION_PASSWORD='postgres'

# Create namespace db and actiavte it
$ kubectl create namespace db

# deploy postgres db
$ kubectl apply -f https://raw.githubusercontent.com/yogenderPalChandra/prometheus-grafana-flask-db/main/flask-postgres/postgres-sts.yaml -n db

# deploy flask app and svc
$ kubectl apply -f https://raw.githubusercontent.com/yogenderPalChandra/prometheus-grafana-flask-db/main/flask-postgres/postgres-flask.yaml -n db

# deploy configmap for postgres db
$ kubectl apply -f https://raw.githubusercontent.com/yogenderPalChandra/prometheus-grafana-flask-db/main/flask-postgres/configmap.yaml -n db

#deploy postgres svc
$ kubectl apply -f https://raw.githubusercontent.com/yogenderPalChandra/prometheus-grafana-flask-db/main/flask-postgres/postgres-svs.yaml -n db

# deploy ingress:
$ kubectl apply -f https://raw.githubusercontent.com/yogenderPalChandra/prometheus-grafana-flask-db/main/flask-postgres/postgres-ingress.yaml -n db

# To make flask.postgres accessible from your local machine add this domain in your host file
$ sudo nano /etc/hosts

# Add this line at the end:
127.0.0.1 flask.postgres

# check what pods deployed in db namespace (should be running state):

$ kubectl get pods -n db
NAME                        READY   STATUS    RESTARTS      AGE
flask-app-fc5658747-9qr5g   1/1     Running   1 (26h ago)   4d
flask-app-fc5658747-gnn8n   1/1     Running   2 (26h ago)   4d
flask-app-fc5658747-wz9mb   1/1     Running   1 (26h ago)   4d
postgres-0                  1/1     Running   1 (26h ago)   4d

Get the node IP: kubectl get node -o wide and hit the browser with the address: http://: 8080, and boom:

Python REST API mapped to localhost

4. Pod connectivity tests within the same namespace:

(No network policy in place — L3 Policy)

By default, any pod can connect to any other pod in any namespace in K8S. This is though undesirable makes K8S implimentaion simple. Lets check this:

  • From Frontend python flask pod to Backend postgres pod.
    # Create pod
    $ kubectl run debug -n db --image=curlimages/curl --restart=Never -- sleep infinity
    
    # get name of the flask pod:
    $ kubectl get pods -n db #fecth the name of the pod
    # Output:
    NAME                        READY   STATUS    RESTARTS      AGE
    debug                       1/1     Running   0             19m
    flask-app-fc5658747-9qr5g   1/1     Running   1 (27h ago)   4d
    flask-app-fc5658747-gnn8n   1/1     Running   2 (27h ago)   4d
    flask-app-fc5658747-wz9mb   1/1     Running   1 (27h ago)   4d
    postgres-0                  1/1     Running   1 (27h ago)   4d
    
    # get cluster IP of flask pod:
    $ kubectl get pod flask-app-fc5658747-9qr5g -o wide -n db
    
    # run shell in the pod
    $ kubectl exec -it debug -n db -- sh
    
    # run curl command inside the debug pod:
    $ curl http://<Flask pod IP>:5000 # pod Ip looks like this: 10.244.0.193
    # Output:<!doctype html><html> ...</html>
    

When you curl from the debug pod the clusterIP of flask it throws the html content which is equivalent to what you saw in the localhost in the first blue image.

  • From/To Postgres pod directly:
    # get name of the Postgres pod:
    $ kubectl get pods -n db #fetch the name of the pod
    
    # get IP of Postgres pod:
    $ kubectl get pod postgres-0 -n db -o wide
    
    # in debug pod shell which you opened previosuly do:
    $ nc -v -z -w 2 10.244.0.254 5432
    10.244.0.254 (10.244.0.254:5432) open
    

We cant use curl command for postgres pod at 5432 port as only TCP traffic is allowed at this port. For this reason we would use the Netcat nc connectivity tool, The nc Netcat command checks if port 5432 (PostgreSQL) on 10.244.0.254 is open, confirming that the service is accessible within the namespace.

  • From Flask servcie (flask-app-service) to postgres pod:
    $ kubectl get svc -n db #output:
    NAME                               TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
    cilium-ingress-flask-app-ingress   ClusterIP   10.96.185.103   <none>        80/TCP,443/TCP   2d2h
    flask-app-service                  NodePort    10.96.126.204   <none>        80:30008/TCP     4d
    mydb-service                       ClusterIP   None            <none>        5432/TCP         4d1h
    
    # Run again the curl command in the debug pod with the clusterIP of flask-app-service
    $ curl http://10.96.126.204:80 # port 80 becasue ist a service exposed to 80 and mapped to localhost at 8080
    #output: <!doctype html><html> ...</html>
    

The output is again the html doc eliment. This means there is also conectivity from flask service.

5. Pod connectivity test from/to different namespace:

(No network policy in place — L3 Policy)

# create a new namespace and create a debug pod there:
$ kubectl create namespace dummy

#Create a pod called debug in dummy namespace and keep it alive and run TCP connectivit using nc to postgres pod running in db namepace:
kubectl run debug -n dummy --image=curlimages/curl --restart=Never -- sleep infinity

#run this pod
kubectl exec -it debug -n dummy -- sh

$ nc -v -z -w 2 10.244.0.254 5432 # output:
10.244.0.254 (10.244.0.254:5432) open

The output is printed as open. This means the TCP connection to postgres pod in db namespace is open from a yet different namepsace called dummy. This is security risk anyone can have access to our database.

Kubernetes allows open communication between pods because the default CNI (Container Network Interface) implementation does not enforce any restrictions. To restrict pod-to-pod communication, you need to create NetworkPolicies (standard Kubernetes NetworkPolicies or Cilium NetworkPolicies) to explicitly allow or deny traffic. Lets restirct the access uisng cilium network policies.

6. Pod connectivity tests from within same namespace

(Cilium network policy in place — L3 policy)

Let’s first restrict access to the database pod to only the web server. Apply the network policy that only allows traffic from the web server pod to the database (file name: postgres-networkPolicy.yaml):

apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "flask-app-to-db"
  namespace: db
spec:
  endpointSelector:
    matchLabels:
      app: postgres
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: flask-app
# Apply this policy in db namespace:
$ kubectl apply -f postgres-networkPolicy.yaml -n db

# Chek if its succesfull:
$ k get cnp -n db # output: 
NAME              AGE   VALID
flask-app-to-db   2s    True

This Cilium Network Policy allows traffic from pods labeled **app: flask-app** to reach pods labeled **app: postgres** in the **db** namespace. It enforces ingress restrictions on PostgreSQL, ensuring that only Flask app pods can communicate with it while blocking all other traffic.

  • From Frontend python flask pod Backend postgres pod:
    # Get Flask pods clusterIP as described in setp 4
    # run curl command inside the debug pod in db namespace:
    $ curl http://<Flask pod IP>:5000 # pod Ip looks like this: 10.244.0.193
    # Output:<!doctype html><html> ...</html>
    

This means we have connectivity from flask pod in db namespace to the postgres pod in the same db namespace

  • From/To Postgres pod directly
    # Get Postgres Pod clusterIP as described in step 4
    # in debug pod shell in db namespace, which you opened previosuly do:
    $ nc -v -z -w 2 10.244.0.254 5432 # output:
    nc: 10.244.0.254 (10.244.0.254:5432): Operation timed out
    

With the network policy applied, the debug pod can no longer reach the postgres database pod; we can see this in the timeout, while the Flask pod is still connected to the Postgres database pod, thus making it more secure.

  • From Flask servcie (flask-app-service) to postgres pod:
    # Run again the curl command in the debug pod with the clusterIP of flask-app-service
    $ curl http://10.96.126.204:80 #port 80 becasue check this:github cilium
    #output: <!doctype html><html> ...</html>
    

This means HTTP traffic from flask-app-service service is also allowed.

7. Pod connectivity test from/to different namespace:

kubectl exec -it debug -n dummy -- sh
~ $ nc -v -z -w 2 10.244.0.254 5432
nc: 10.244.0.254 (10.244.0.254:5432): Operation timed out

The ClusterIP typed above is the IP of of Postgres pod. The output is printed as Operation timed out. This means the TCP connection to postgres pod in db namespace is restricted from all other namspaces including dummy namespace and is allowed from only flask pod and service.

8. Pod connectivity tests — Namespace Agnostic

(No network policy— L7 policy)

Cilium is layer 7 aware so that we can block or allow a specific request on the HTTP URI paths. In our example policy, we allow HTTP GETs on / and /plot/temp, and also on /metrics let’s test that:

# in debug pod in db OR dummy namespace:
$ curl http://<flask service clusterIP>:80/metrics # curl the flask-app-service cluster IP OR
$ curl http://<flask pod clusterIP>:5000/metrics # curl the flask-app pod cluster IP 

# ouputs:
# HELP python_gc_objects_collected_total Objects collected during gc
# TYPE python_gc_objects_collected_total counter
python_gc_objects_collected_total{generation="0"} 927.0
python_gc_objects_collected_total{generation="1"} 332.0
python_gc_objects_collected_total{generation="2"} 95.0
# HELP python_gc_objects_uncollectable_total Uncollectable objects found during GC
# TYPE python_gc_objects_uncollectable_total counter
...

The command **curl http://<Flask-app-service clusterIP>:80/metrics** OR **curl http://<Flask-app pod ClusterIP>:5000/metrics** makes an HTTP request to the servcie (port 80) or pod (port 5000). The response contains Prometheus-style metrics, which are used for monitoring and observability. These metrics must be visible to observability team and must be masked from dev team. The ouput shows that the request is successfull thus breaching a security aspect. Now lets restrict this /metrics path: (filename: flask-cnp-l7.yaml)

9. Pod connectivity tests — Namespace Agnostic

(Cilium network policy in place— L7 policy)

apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "l7-rule"
spec:
  endpointSelector:
    matchLabels:
      app: flask-app
  ingress:
    - toPorts:
        - ports:
            - port: '5000'
              protocol: TCP
          rules:
            http:
              - method: GET
                path: "/"

kubectl apply -f flask-cnp-l7.yaml -n db commands applies this policy in db namespace. This Cilium Network Policy applies to pods labeled **app: flask-app** and restricts incoming traffic. It allows only GET requests to the **/** path on port 5000, while blocking all other request like on path /metrics. This ensures that unauthorized HTTP paths or methods cannot be accessed on the Flask application. Try it:

# In debug pod in db or dummy namespace:
$ curl http://<Flask-app pod clusterIp>:5000/metrics # output: 
Access denied
$ curl http://<Flask-app-service clusterIP>:80/metrics # output:
Access denied

So we secured our Micoservice. That is the end of it. Cheers!

If you want to understand how Microservices are working in k8s, try to code this:## Microservices deployment on Kubernetes

Deploying Flask frontend, and PostgreSQL server as backend

yogender027mae.medium.com

View original

If you want to understand the monitoring part of this microservices, try this:## Microservices Monitoring with Grafana & Prometheus in Kubernetes

Deploy K8S cluster, Deploy Prometheus & Grafana, Deploy Microservices, Monitor Microservices — Life is easy

yogender027mae.medium.com

View original

If you want to understand the config files and deployment of PostgreSQL server in K8S:## Deploying custom PostgreSQL server on OpenShift/Kubernetes

Custom PostgreSQL server comes up with pre-initiallised database, tables, relations and more.

yogender027mae.medium.com

View original

Thank you

I talk about programing, AWS, and two hamsters in my room

More from Yogender Pal

[

See more recommendations

](https://medium.com/?source=post_page---read_next_recirc--58cc00518602---------------------------------------)