Lab 32 - Network policies with Calico
Lab 32 - Network policies with Calico
Goal: get to know the cluster’s CNI, then segment a three-tier app: only frontend -> backend -> db, nothing else. Chapter: Network policies and segmentation
Calico, up close
# 1 - the operator's view
kubectl get tigerastatus
kubectl get installation default -o yaml | grep -A8 calicoNetwork
kubectl get pods -n calico-system -o wide
# 2 - Calico resources, through the Calico API server
kubectl api-resources --api-group=projectcalico.org
kubectl get ippools -o wide
One IP pool 10.244.0.0/16, VXLAN encapsulation - the overlay you saw on
Day 2.
A three-tier app
# 3 -
kubectl create ns shop
for app in frontend backend db; do
kubectl run $app -n shop --image nginx:1.27 --labels app=$app --port 80
kubectl expose pod $app -n shop --port 80
done
kubectl create ns other
kubectl run intruder -n other --image busybox:1.36 --command -- sleep 3600
kubectl run tester -n shop --image busybox:1.36 --labels app=tester --command -- sleep 3600
kubectl wait -n shop --for=condition=Ready pod --all --timeout=60s
# 4 - a small test helper: can <pod> in <ns> reach <target>?
cat > ~/reach.sh <<'SH'
#!/bin/sh
# usage: reach.sh <namespace> <pod> <url>
kubectl exec -n "$1" "$2" -- wget -qO- -T 2 "$3" >/dev/null 2>&1 && echo "OPEN $2 -> $3" || echo "BLOCKED $2 -> $3"
SH
chmod +x ~/reach.sh
~/reach.sh shop frontend http://backend
~/reach.sh shop frontend http://db
~/reach.sh shop tester http://db
~/reach.sh other intruder http://db.shop
Everything OPEN - including the intruder from another namespace straight to
the database.
Default deny
# 5 -
cd ~
cat > deny.yaml <<'YAML'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: shop
spec:
podSelector: {}
policyTypes: ["Ingress"]
YAML
kubectl apply -f deny.yaml
~/reach.sh shop frontend http://backend # BLOCKED
~/reach.sh other intruder http://db.shop # BLOCKED
Allow exactly the flows you need
# 6 -
cat > allow.yaml <<'YAML'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-from-frontend
namespace: shop
spec:
podSelector:
matchLabels: { app: backend }
ingress:
- from:
- podSelector:
matchLabels: { app: frontend }
ports: [{ protocol: TCP, port: 80 }]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-from-backend
namespace: shop
spec:
podSelector:
matchLabels: { app: db }
ingress:
- from:
- podSelector:
matchLabels: { app: backend }
ports: [{ protocol: TCP, port: 80 }]
YAML
kubectl apply -f allow.yaml
~/reach.sh shop frontend http://backend # OPEN
~/reach.sh shop backend http://db # OPEN
~/reach.sh shop frontend http://db # BLOCKED
~/reach.sh shop tester http://db # BLOCKED
~/reach.sh other intruder http://db.shop # BLOCKED
Egress - and the DNS trap
# 7 - frontend may only talk to backend
cat > egress.yaml <<'YAML'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: frontend-egress
namespace: shop
spec:
podSelector:
matchLabels: { app: frontend }
policyTypes: ["Egress"]
egress:
- to:
- podSelector:
matchLabels: { app: backend }
ports: [{ protocol: TCP, port: 80 }]
YAML
kubectl apply -f egress.yaml
~/reach.sh shop frontend http://backend # BLOCKED?!
The name backend cannot be resolved - DNS (port 53 to CoreDNS) is egress too.
# 8 - allow DNS
cat >> egress.yaml <<'YAML'
- to:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: kube-system }
podSelector:
matchLabels: { k8s-app: kube-dns }
ports:
- { protocol: UDP, port: 53 }
- { protocol: TCP, port: 53 }
YAML
kubectl apply -f egress.yaml
~/reach.sh shop frontend http://backend # OPEN
~/reach.sh shop frontend http://www.google.com # BLOCKED
Calico: a cluster-wide guard rail
Traefik’s internal port 8080 (API, dashboard, /ping) should never be
reachable from workloads - whatever namespace policies teams write.
# 9 - before: any Pod reaches it
TRAEFIK=$(kubectl get pod -n traefik -l app.kubernetes.io/name=traefik -o jsonpath='{.items[0].status.podIP}')
~/reach.sh shop tester http://$TRAEFIK:8080/ping # OPEN
# 10 - a platform rule, cluster-wide
cat > gnp.yaml <<'YAML'
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
name: protect-traefik-admin
spec:
order: 10
selector: app.kubernetes.io/name == 'traefik'
types: ["Ingress"]
ingress:
- action: Deny
protocol: TCP
source:
notNets: ["192.168.128.0/24"] # nodes (kubelet probes) stay allowed
destination:
ports: [8080]
- action: Allow # everything else, e.g. web traffic on 8000
YAML
kubectl apply -f gnp.yaml
~/reach.sh shop tester http://$TRAEFIK:8080/ping # BLOCKED
curl -s -o /dev/null -w '%{http_code}\n' http://worker01:30080 # 404 - Traefik still serves
kubectl get pods -n traefik # still Ready
Deny and order are Calico features the standard API does not have. The
final Allow matters: once a policy selects a Pod, anything no rule allows is
dropped - without it Traefik would stop serving.
Discovery
InfodiscoverTask A. Open Whisker and find the denied flow intruder -> db.
Task B. Allow Pods from namespace other - but only those labelled
role=monitoring - to reach backend on port 80.
Task C. Write a Calico StagedNetworkPolicy that would deny all ingress
to frontend and find out in Whisker what it would break - without
breaking it.
Clean up
kubectl delete ns shop other
kubectl delete -f gnp.yaml