Cilium Series Part 16: CiliumNetworkPolicy Hands-On Lab

This article was last updated on: May 17, 2026 am

Series Articles

Introduction

Today we dive into Cilium security topics, walking through a detailed CiliumNetworkPolicy hands-on lab based on Cilium’s official Star Wars demo.

Scenario

You are part of the Empire’s platform engineering team, responsible for developing the Death Star API and deploying it to the Imperial Galactic Kubernetes Service (IGKS). You’ve deployed the service, but you need to ensure that only the Empire’s TIE fighters can make landing requests to the Death Star API via HTTP POST, and cannot use PUT on any other API paths (such as the exhaust port path. 😂😂😂 In the Star Wars series, the Death Star’s weakness is the exhaust port — it’s been blown up multiple times by proton torpedoes fired through the exhaust port…). It’s not that any TIE fighter pilot would intentionally put something in the exhaust port, but you never know — your team wants to use Cilium’s network policy support as a safeguard in case a TIE fighter pilot has a momentary lapse in judgment. You really don’t want Darth Vader to lose confidence in your ability to secure the Death Star service. You would find his lack of faith… disturbing.

Your goal is to create a CiliumNetworkPolicy resource that restricts access to the Death Star service so that TIE fighters can only make HTTP-based landing requests.

Prerequisites

First, you need a Kubernetes cluster with Cilium installed. The cluster created previously will suffice.

You also need to deploy a Death Star application, including service definitions, service backend pods, and TIE fighter client pods that access the service using internal-only cluster communication. The Cilium project has a Death Star demo application manifest example that you can use. You can install the manifest into the cluster’s default namespace using the following:

1
2
3
4
5
kubectl create -f https://raw.githubusercontent.com/cilium/cilium/HEAD/examples/minikube/http-sw-app.yaml
service/deathstar created
deployment.apps/deathstar created
pod/tiefighter created
pod/xwing created

│ 📝Notes

│ xwing: X-wing Starfighter. The Empire’s arch-nemesis: the Rebel Alliance’s primary fighter.

How did that get in there? No worries, we can make sure our network policy denies the X-wing access to the full Death Star service.

The Death Star service has been created with only a cluster-internal IP address, so only starships running within the cluster’s private network can access it:

1
2
3
4
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 5d17h
deathstar ClusterIP 10.43.91.186 <none> 80/TCP 6m50s

Additionally, a Cilium endpoint has been created for each new pod:

1
2
3
4
5
6
7
8
9
10
11
12
$ kubectl get pods,CiliumEndpoints
NAME READY STATUS RESTARTS AGE
pod/deathstar-8464cdd4d9-zpnfc 1/1 Running 0 7m45s
pod/deathstar-8464cdd4d9-md6zd 1/1 Running 0 7m45s
pod/tiefighter 1/1 Running 0 7m45s
pod/xwing 1/1 Running 0 7m45s

NAME ENDPOINT ID IDENTITY ID INGRESS ENFORCEMENT EGRESS ENFORCEMENT VISIBILITY POLICY ENDPOINT STATE IPV4 IPV6
ciliumendpoint.cilium.io/tiefighter 996 4829 <status disabled> <status disabled> <status disabled> ready 10.0.2.245
ciliumendpoint.cilium.io/deathstar-8464cdd4d9-zpnfc 404 30027 <status disabled> <status disabled> <status disabled> ready 10.0.1.192
ciliumendpoint.cilium.io/deathstar-8464cdd4d9-md6zd 421 30027 <status disabled> <status disabled> <status disabled> ready 10.0.2.177
ciliumendpoint.cilium.io/xwing 2596 2855 <status disabled> <status disabled> <status disabled> ready 10.0.2.81

Cilium has created endpoints corresponding to the Death Star backend pods as well as the X-wing and TIE fighter pods.

Note: The two deathstar-* endpoints share the same Identity ID. As discussed previously, they share the same Cilium Identity because they have the same set of security-relevant labels. The Cilium Agent uses identity IDs for endpoints matching relevant network policies to facilitate efficient key-value lookups by eBPF programs running in the network datapath.

Back to the task at hand! There are no network policies yet, so nothing should prevent the X-wing or TIE fighter from accessing the cluster-internal Death Star service via its fully qualified domain name (FQDN), and then having kube-proxy or Cilium forward the HTTP-based landing request to one of the Death Star backend pods.

Time to issue some landing requests from the TIE fighter and X-wing:

1
2
3
4
5
kubectl exec xwing -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
Ship landed

kubectl exec tiefighter -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
Ship landed

The Death Star service is up and running. Now it’s time to implement network policies to restrict which pods can access the Death Star service.

Hands-On Lab

Empire Ingress Allow Policy

The simplest way to ensure the X-wing pod cannot access the Death Star service endpoints in this cluster is to write a label-based L3 policy that leverages the different labels used by the pods. An L3 policy restricts access to all network ports on the endpoint. If you want to restrict access to a specific port number, you can write a label-based L4 policy.

If you inspect the xwing pod, you’ll find it has the label org=alliance, while the tiefighter pod has the label org=empire:

1
2
3
4
5
6
7
8
9
10
11
$ kubectl describe pod/xwing
Name: xwing
Namespace: default
Priority: 0
Service Account: default
Node: cilium-62-3/192.168.2.26
Start Time: Thu, 27 Jul 2023 17:04:31 +0800
Labels: app.kubernetes.io/name=xwing
class=xwing
org=alliance
...
1
2
3
4
5
6
7
8
9
10
$ kubectl describe pod/tiefighter
Name: tiefighter
Namespace: default
Priority: 0
Service Account: default
Node: cilium-62-3/192.168.2.26
Start Time: Thu, 27 Jul 2023 17:04:31 +0800
Labels: app.kubernetes.io/name=tiefighter
class=tiefighter
org=empire

An L4 network policy referencing TCP port 80 that only allows pods labeled org=empire will block the xwing pod from accessing the Death Star service endpoints. We can use the <networkpolicy.io> policy editor to craft this policy.

First, edit the central service-map element to configure the policy name and endpointSelector, adding the org=empire and class=deathstar labels to ensure the policy applies only to pods serving as Death Star service endpoints.

Configure the endpointSelector using the policy editor

Then, create a new policy in the interactive service-map UI and add an “In Namespace” Ingress policy rule that uses org=empire as the pod selector expression and 80|TCP as the value for the “To Ports” input field.

Configuring Ingress Policy with the Policy Editor

Now, the service map in the policy editor should show that only ingress from pods labeled org=empire in the same namespace as the Death Star service can access TCP port 80 on the corresponding Death Star endpoints.

The read-only YAML from the policy editor should look similar to this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: allow-empire-in-namespace
spec:
endpointSelector:
matchLabels:
org: empire
class: deathstar
ingress:
- fromEndpoints:
- matchLabels:
org: empire
toPorts:
- ports:
- port: "80"
protocol: TCP

Note that this L4 policy specifically restricts ingress access to the deathstar-* pods serving as service endpoints, not to the Death Star service itself.

If you want to restrict a pod’s egress access to a limited number of services, you can create an egress policy for the client pods, referencing the allowed services by name in the egress policy’s toServices attribute. In our case, this would mean writing egress policies with different toServices information for the xwing and tiefighter pods. That’s possible, but this time it’s easier to achieve our goal using a single ingress policy that only allows Empire units to access the Death Star API while denying access to everyone else.

As for whether you should write an ingress or egress policy, it depends on your intent. Do you want to control who a pod is allowed to send traffic to? If so, egress is likely the policy you want to write. If you want to control which pods can initiate communication with a specific service or endpoint, then an ingress policy is most likely the simplest way to achieve that intent.

You can download the L4 policy from the policy editor UI to a file named allow-empire-in-namespace.yaml and apply it to the cluster:

1
2
$ kubectl apply -f allow-empire-in-namespace.yaml
ciliumnetworkpolicy.cilium.io/allow-empire-in-namespace created

Now with this policy in place, the X-wing can no longer access the landing request API:

1
2
$ kubectl exec xwing -- curl --connect-timeout 10 -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
command terminated with exit code 28

The curl command issued from the xwing pod will fail with an error.

But the same command issued from the tiefighter pod still succeeds:

1
2
kubectl exec tiefighter -- curl --connect-timeout 10 -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
Ship landed

Success! The X-wing pod can no longer access the Death Star API, but all other pods labeled org=empire can still access the full API, including the troublesome exhaust port:

1
2
kubectl exec tiefighter -- curl -s -XPUT deathstar.default.svc.cluster.local/v1/exhaust-port
Panic: deathstar exploded

Oops! But we can fix this with an L7 HTTP policy to further restrict access, so that the exhaust port API endpoint is only accessible to Imperial maintenance droids, not rookie pilots who can’t tell a landing bay from an exhaust port. We can address the design flaw of the API exhaust port endpoint in the next development sprint. But for now, let’s use the CiliumNetworkPolicy custom resource definition to restrict access so this doesn’t happen again.

Adding L7 HTTP Path-Specific Allow Policy

Let’s extend the Empire access policy to explicitly include rules for both the landing path and the exhaust port path.

Now, these rules will match on both org and class labels.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: allow-empire-in-namespace
spec:
endpointSelector:
matchLabels:
org: empire
class: deathstar
ingress:
- fromEndpoints:
- matchLabels:
org: empire
class: tiefighter
toPorts:
- ports:
- port: "80"
protocol: TCP
rules:
http:
- method: "POST"
path: "/v1/request-landing"
- fromEndpoints:
- matchLabels:
org: empire
class: maintenance-droid
toPorts:
- ports:
- port: "80"
protocol: TCP
rules:
http:
- method: "PUT"
path: "/v1/exhaust-port"

Save this policy update to the file allow-empire-in-namespace.yaml and apply it to the cluster:

1
2
kubectl apply -f allow-empire-in-namespace.yaml
ciliumnetworkpolicy.cilium.io/allow-empire-in-namespace created

Now, the TIE fighter will receive an HTTP 403 Forbidden message instead of being able to access the exhaust port, because the Cilium Agent runs an embedded HTTP proxy on the node where the Death Star backend pods are running.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ kubectl exec tiefighter -- curl -v -s -XPUT deathstar.default.svc.cluster.local/v1/exhaust-port
* Trying 10.43.91.186...
* TCP_NODELAY set
* Connected to deathstar.default.svc.cluster.local (10.43.91.186) port 80 (#0)
> PUT /v1/exhaust-port HTTP/1.1
> Host: deathstar.default.svc.cluster.local
> User-Agent: curl/7.52.1
> Accept: */*
>
Access denied
< HTTP/1.1 403 Forbidden
< content-length: 15
< content-type: text/plain
< date: Thu, 27 Jul 2023 09:40:49 GMT
< server: envoy
<
{ [15 bytes data]
* Curl_http_done: called premature == 0
* Connection #0 to host deathstar.default.svc.cluster.local left intact

X-wing fighters attempting to access the Death Star API are still denied by the L4 policy, which drops packets and causes a connection timeout rather than returning an HTTP Forbidden status message.

We have successfully restricted access to the Death Star API so that TIE fighters can make landing requests without being able to access the exhaust port. We’ve also blocked any X-wing fighters in the cluster from accessing the Death Star API. Lord Vader will be pleased.

Note: The behavioral difference between L3/L4 policies and L7 policies in handling dropped packets is expected, as different implementations are used. For L3/L4 policies, eBPF programs running in the Linux network datapath drop the packets — they are essentially swallowed by a black hole in the network. L7 policies, on the other hand, run an embedded HTTP proxy that acts like an HTTP server, rejecting requests and providing an HTTP status response to the client explaining the reason for the denial. Regardless of which implementation is used, you can use Hubble to inspect network flows and track whether packets are being dropped at the Death Star endpoint’s ingress. We will cover this in a later chapter.

Summary

Setting aside the Star Wars whimsy, this hands-on lab demonstrates an approach to writing CiliumNetworkPolicy that helps secure access between pods running within a cluster. You can use CiliumNetworkPolicy to establish reasonable restrictions based on expected workload behavior (encoded as label metadata), rather than implicitly trusting that pods have full access to all services exposed by peer pods in the cluster. Restricting network access in this way prevents issues caused by misconfigured or vulnerable applications interacting with services in unexpected ways.