Week: 5-6 -- eBPF/XDP + NSM at Scale
Points: 15
Time estimate: 60 min lab + 2.5 hr independent
Deliverable: lab-6-report.md + policy YAML files
Objectives
- Deploy a local Kubernetes cluster with Cilium as the CNI plugin using
kind. - Configure a
CiliumNetworkPolicythat enforces L7 HTTP path filtering between two microservices. - Use Hubble CLI to observe traffic flows and confirm policy enforcement.
- Deploy Tetragon and observe a
sys_execvesystem-call event from inside a running pod.
Prerequisites
Install the toolchain (covered in SETUP.md):
# kind cluster manager
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64
chmod +x ./kind && sudo mv ./kind /usr/local/bin/
# Cilium CLI
CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
curl -L --fail --remote-name-all \
https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-amd64.tar.gz
sudo tar xzvfC cilium-linux-amd64.tar.gz /usr/local/bin
# Hubble CLI (Cilium observability layer)
HUBBLE_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/hubble/master/stable.txt)
curl -L --fail --remote-name-all \
https://github.com/cilium/hubble/releases/download/$HUBBLE_VERSION/hubble-linux-amd64.tar.gz
sudo tar xzvfC hubble-linux-amd64.tar.gz /usr/local/bin
# Helm (for Cilium/Tetragon install)
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
Part A: Cilium Cluster Bring-Up (20 min)
Create a kind cluster configured for Cilium:
# kind config -- no default CNI so Cilium can install its own
cat > lab6/kind-cilium.yaml << 'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
networking:
disableDefaultCNI: true
podSubnet: "10.244.0.0/16"
EOF
kind create cluster --name lab6 --config lab6/kind-cilium.yaml
# Install Cilium with Hubble enabled
cilium install \
--set hubble.relay.enabled=true \
--set hubble.ui.enabled=true
# Wait for Cilium to be ready
cilium status --wait
# Verify all nodes are Ready
kubectl get nodes
Expected output: all 3 nodes in Ready state; cilium status showing green across datapath, controller, hubble.
Deploy the two microservices for policy testing:
# Create the test namespace
kubectl create namespace lab6
# Deploy a simple HTTP server (the "backend")
cat > lab6/backend.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: lab6
spec:
replicas: 1
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
tier: api
spec:
containers:
- name: httpbin
image: kennethreitz/httpbin:latest
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: backend
namespace: lab6
spec:
selector:
app: backend
ports:
- port: 80
targetPort: 80
EOF
# Deploy a client pod (the "frontend")
cat > lab6/frontend.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
name: frontend
namespace: lab6
labels:
app: frontend
tier: web
spec:
containers:
- name: curl
image: curlimages/curl:latest
command: ["sleep", "infinity"]
EOF
kubectl apply -f lab6/backend.yaml
kubectl apply -f lab6/frontend.yaml
# Wait for pods to be running
kubectl wait --for=condition=Ready pod -l app=backend -n lab6 --timeout=120s
kubectl wait --for=condition=Ready pod frontend -n lab6 --timeout=60s
# Confirm baseline connectivity (before policy)
kubectl exec -n lab6 frontend -- curl -s http://backend/get | head -5
kubectl exec -n lab6 frontend -- curl -s http://backend/post -X POST | head -5
Record: do both /get and /post return HTTP 200 before any policy is applied?
Part B: L7 CiliumNetworkPolicy (20 min)
Apply a policy that permits GET to /get and /status/* but blocks POST and all other paths:
cat > lab6/l7-policy.yaml << 'EOF'
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: backend-l7-policy
namespace: lab6
spec:
endpointSelector:
matchLabels:
app: backend
ingress:
- fromEndpoints:
- matchLabels:
app: frontend
toPorts:
- ports:
- port: "80"
protocol: TCP
rules:
http:
- method: "GET"
path: "/get"
- method: "GET"
path: "/status/[0-9]+"
EOF
kubectl apply -f lab6/l7-policy.yaml
# Wait for policy to propagate
sleep 5
Test policy enforcement:
# Allowed: GET /get
kubectl exec -n lab6 frontend -- curl -s -o /dev/null -w "%{http_code}" http://backend/get
# Expected: 200
# Allowed: GET /status/200
kubectl exec -n lab6 frontend -- curl -s -o /dev/null -w "%{http_code}" http://backend/status/200
# Expected: 200
# Blocked: POST to /post (method not in policy)
kubectl exec -n lab6 frontend -- curl -s -o /dev/null -w "%{http_code}" -X POST http://backend/post
# Expected: 403
# Blocked: GET /anything (path not in policy)
kubectl exec -n lab6 frontend -- curl -s -o /dev/null -w "%{http_code}" http://backend/anything
# Expected: 403
Record the HTTP response codes for all four tests.
Part C: Hubble Flow Observation (15 min)
Enable Hubble port-forward and use the CLI to observe flows:
# Forward Hubble relay port
cilium hubble port-forward &
sleep 3
# Check Hubble status
hubble status
# Observe all flows in the lab6 namespace (run in background)
hubble observe --namespace lab6 --follow &
HUBBLE_PID=$!
# Trigger some traffic
kubectl exec -n lab6 frontend -- curl -s http://backend/get > /dev/null
kubectl exec -n lab6 frontend -- curl -s -X POST http://backend/post > /dev/null
kubectl exec -n lab6 frontend -- curl -s http://backend/anything > /dev/null
sleep 3
kill $HUBBLE_PID
From the Hubble output, identify:
- The flow entries for the allowed GET /get request: what verdict does Hubble report?
- The flow entries for the blocked POST /post request: what verdict does Hubble report?
- Does Hubble show the full HTTP method and URL path for L7 flows?
# Query specific flows after the fact
hubble observe --namespace lab6 \
--verdict DROPPED \
--last 20
hubble observe --namespace lab6 \
--verdict FORWARDED \
--last 20
Part D: Tetragon System-Call Tracing (5 min)
Deploy Tetragon and observe execve events from inside a pod:
# Install Tetragon via Helm
helm repo add cilium https://helm.cilium.io
helm repo update
helm install tetragon cilium/tetragon -n kube-system
# Wait for Tetragon daemonset
kubectl rollout status -n kube-system ds/tetragon --timeout=60s
# Apply the TracingPolicy for execve events
cat > lab6/execve-tracing.yaml << 'EOF'
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
name: execve-tracing
spec:
kprobes:
- call: "sys_execve"
syscall: true
args:
- index: 0
type: "string"
EOF
kubectl apply -f lab6/execve-tracing.yaml
# Watch Tetragon events
kubectl exec -n kube-system ds/tetragon -c export-stdout -- \
tetra getevents --namespaces lab6 &
TETRA_PID=$!
# Trigger an execve inside the frontend pod
kubectl exec -n lab6 frontend -- sh -c "ls /tmp"
sleep 3
kill $TETRA_PID 2>/dev/null
Record: what process_kprobe event does Tetragon report for the ls /tmp command? What fields are visible (namespace, pod name, binary path, arguments)?
Lab Report
Create lab-6-report.md with:
- Cluster bring-up:
cilium statusoutput confirming all green - Baseline connectivity: HTTP codes for GET /get and POST /post before policy
- Policy enforcement table:
| Request | Method | Path | HTTP Code (after policy) | Expected |
|---|---|---|---|---|
| 1 | GET | /get | 200 | |
| 2 | GET | /status/200 | 200 | |
| 3 | POST | /post | 403 | |
| 4 | GET | /anything | 403 |
- Hubble observations: copy 3-4 representative flow lines (one FORWARDED, one DROPPED); note whether L7 HTTP method/path is visible
- Tetragon event: paste the JSON event for the
ls /tmpexecve; identify thepod,binary, andargumentsfields - One-paragraph analysis: "Cilium enforces L7 policies without a sidecar proxy. Explain how eBPF achieves this compared to a service-mesh model like Istio/Envoy. Name one operational advantage and one limitation."
Grading
| Component | Points |
|---|---|
| Cluster bring-up: Cilium status green, pods Running | 3 |
| L7 policy: correct HTTP codes for all 4 test cases | 5 |
| Hubble: FORWARDED and DROPPED flows identified with verdicts | 4 |
| Tetragon: execve event captured with pod + binary fields | 3 |
| Total | 15 |