To install a KinD (Kubernetes in Docker) cluster with Network Policy support, you must disable KinD's default CNI (kindnetd) and install a third-party CNI plugin that enforces policies, such as Calico or Cilium. By default, KinD's built-in networking silently ignores Network Policy resources.
Here is the step-by-step procedure using Calico, which is the most common and native approach for testing network policies locally.
disableDefaultCNI: true. Create a file named kind-config.yamlkind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
disableDefaultCNI: true # Crucial step to allow Calico installation
podSubnet: "10.244.0.0/16" # Defines the subnet allocated for Pod IPs
nodes:
- role: control-plane
- role: worker
➜ k get nodes
NAME STATUS ROLES AGE VERSION
kind-control-plane NotReady control-plane 38s v1.36.1
kind-worker NotReady <none> 23s v1.36.1
Note: It's expected that all nodes are NotReady with the absence of CNI.
Install the Tigera Calico operator into your cluster to manage the lifecycle of the CNI:
# Install Tigera CRDs
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.32.1/manifests/v1_crd_projectcalico_org.yaml
# Install the Calico operator
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.32.1/manifests/tigera-operator.yaml
Install Calico by creating the necessary custom resource. For more information on configuration options available in this manifest. Noticably, we need to update the spec.calicoNetwork.ipPools[0].cidr to match the Pod subnet. (By default, Calica has the cidr set as 192.168.0.0/16):
curl -OL https://raw.githubusercontent.com/projectcalico/calico/v3.32.1/manifests/custom-resources.yaml
➜ grep cidr custom-resources.yaml
cidr: 10.244.0.0/16
➜ k create -f custom-resources.yaml
installation.operator.tigera.io/default created
apiserver.operator.tigera.io/default created
goldmane.operator.tigera.io/default created
whisker.operator.tigera.io/default created
Verify:
➜ k get pods -n calico-system
NAME READY STATUS RESTARTS AGE
calico-apiserver-848d68f88c-qbzj7 0/1 Pending 0 96s
calico-apiserver-848d68f88c-rwnzk 0/1 Pending 0 96s
calico-kube-controllers-c484dd47f-gfjc7 0/1 Pending 0 96s
calico-node-27r7n 0/1 Init:2/3 0 96s
calico-node-klzr6 0/1 Init:2/3 0 96s
calico-typha-5c9874974f-s9n8q 1/1 Running 0 96s
csi-node-driver-hzt82 0/2 ContainerCreating 0 96s
csi-node-driver-vd7fb 0/2 ContainerCreating 0 96s
goldmane-7d7d59c89-tzftx 0/1 Pending 0 96s
whisker-6c45b48986-wndpd 0/2 Pending 0 43s
A few moments later...
➜ k get pods -n calico-system
NAME READY STATUS RESTARTS AGE
calico-apiserver-848d68f88c-qbzj7 1/1 Running 0 4m2s
calico-apiserver-848d68f88c-rwnzk 1/1 Running 0 4m2s
calico-kube-controllers-c484dd47f-gfjc7 1/1 Running 0 4m2s
calico-node-27r7n 1/1 Running 0 4m2s
calico-node-klzr6 1/1 Running 0 4m2s
calico-typha-5c9874974f-s9n8q 1/1 Running 0 4m2s
csi-node-driver-hzt82 2/2 Running 0 4m2s
csi-node-driver-vd7fb 2/2 Running 0 4m2s
goldmane-7d7d59c89-tzftx 1/1 Running 0 4m2s
whisker-6857979d4f-p9bs5 2/2 Running 0 2m11s
We can see both control and worker nodes are in Ready states too.
➜ k get nodes
NAME STATUS ROLES AGE VERSION
kind-control-plane Ready control-plane 120m v1.36.1
kind-worker Ready <none> 120m v1.36.1
k run frontend --image=busybox --labels="app=frontend" -- sleep 3600
k run backend --image=nginx --labels="app=backend"
k get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
backend 1/1 Running 0 64s 10.244.162.134 kind-worker <none> <none>
frontend 1/1 Running 0 12s 10.244.162.135 kind-worker <none> <none>
Test to see that, by default, frontend can access backend:
➜ kubectl exec frontend -- wget --timeout 3 --spider $(kubectl get pod backend -o jsonpath='{.status.podIP}')
Connecting to 10.244.162.134 (10.244.162.134:80)
remote file exists
Apply a Deny Network Policy to isolate the backend:
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all-backend
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
role: authorized-frontend
EOF
Verify that frontend no longer can access the backend (lacking role: authorized-frontend label):
➜ kubectl exec frontend -- wget --timeout 3 --spider $(kubectl get pod backend -o jsonpath='{.status.podIP}')
Connecting to 10.244.162.134 (10.244.162.134:80)
wget: download timed out
command terminated with exit code 1
Let's label the frontend pod:
➜ kubectl label pods frontend role=authorized-frontend
pod/frontend labeled
➜ k get pods frontend --show-labels
NAME READY STATUS RESTARTS AGE LABELS
frontend 1/1 Running 0 5m30s app=frontend,role=authorized-frontend
➜ kubectl exec frontend -- wget --timeout 1 --spider $(kubectl get pod backend -o jsonpath='{.status.podIP}')
Connecting to 10.244.162.134 (10.244.162.134:80)
remote file exists