Install Calico and prove NetworkPolicy enforcement
The exam says install and configure a CNI of your choice, flannel or Calico, and then adds one line that makes the choice for you: it must support NetworkPolicy enforcement. flannel does not enforce policies, so this is a Calico question. Let's install it from the manifests, verify it, and prove a policy actually blocks traffic.
🎥 Watch the video: https://www.youtube.com/watch?v=7Eaf7Wogsa4
This is a CKA Services & Networking walkthrough. Every command below is real output from a live cluster, and you can reproduce the whole thing yourself (scripts at the end).
The scenario
Here is the setup. A fresh cluster has no CNI, so its nodes are NotReady and CoreDNS is Pending. The question offers flannel or Calico, says install from manifest files, do not use Helm, and lists three requirements: the CNI is properly installed and configured, pods can communicate with each other, and it supports NetworkPolicy enforcement. That last requirement is the trap. flannel gives you pod networking but silently ignores NetworkPolicy objects. Only Calico satisfies all three.
- A fresh cluster with NO CNI: nodes NotReady, CoreDNS Pending
- Install a CNI of your choice (flannel or Calico) from manifests, no Helm
- Pods must communicate with each other, and the CNI must be properly installed
- 'Must support NetworkPolicy enforcement' => flannel is out, Calico is the answer
How the Calico operator install works
Calico's manifest install is operator based and comes in three parts. First the operator CRDs, then the Tigera operator itself, a deployment that knows how to install and manage Calico. Then you hand the operator an Installation resource describing the Calico you want, and it deploys everything into the calico-system namespace to match. Two things to know before you touch the keyboard. The Installation's IP pool cidr must agree with the cluster's pod CIDR, which is 192.168.0.0/16 here. And these manifests need kubectl create, not apply: the CRD file is bigger than the annotation kubectl apply attaches, so apply fails on it.
No CNI: nodes NotReady
Start by confirming the starting state. Every node is NotReady, the classic symptom of a missing CNI. And listing pods across all namespaces shows two things: CoreDNS is Pending because there is no pod network for it to join, and there are no calico or flannel pods anywhere, so no CNI is installed yet. This is a fresh cluster, and the network plugin is ours to install.
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
cka-scenario9-control-plane NotReady control-plane 8s v1.36.1
cka-scenario9-worker NotReady <none> 0s v1.36.1
$ kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-589f44dc88-522x8 0/1 Pending 0 1s
kube-system coredns-589f44dc88-zsxh2 0/1 Pending 0 1s
kube-system etcd-cka-scenario9-control-plane 0/1 Running 0 8s
kube-system kube-apiserver-cka-scenario9-control-plane 1/1 Running 0 8s
kube-system kube-controller-manager-cka-scenario9-control-plane 1/1 Running 0 8s
kube-system kube-proxy-ds9pp 1/1 Running 0 1s
kube-system kube-proxy-lwx6m 0/1 Pending 0 3s
kube-system kube-scheduler-cka-scenario9-control-plane 0/1 Running 0 8s
local-path-storage local-path-provisioner-855c7b7774-75cbk 0/1 Pending 0 1s
Create the CRDs + operator
Install the operator pieces with kubectl create. The first file registers the custom resource definitions, thirty two of them. The second deploys the Tigera operator itself. Note the verb: create, not apply. Some of these CRDs are larger than the last-applied annotation kubectl apply wants to attach, so apply fails on this file, and the Calico docs say create for exactly that reason. A few seconds later the operator pod is Running in the tigera-operator namespace, waiting for us to tell it what to install.
$ kubectl create -f operator-crds.yaml
...
customresourcedefinition.apiextensions.k8s.io/ipamconfigs.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipamhandles.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ippools.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipreservations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/kubecontrollersconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/networkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/networksets.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/stagedglobalnetworkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/stagedkubernetesnetworkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/stagednetworkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/tiers.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/adminnetworkpolicies.policy.networking.k8s.io created
customresourcedefinition.apiextensions.k8s.io/baselineadminnetworkpolicies.policy.networking.k8s.io created
$ kubectl create -f tigera-operator.yaml
namespace/tigera-operator created
serviceaccount/tigera-operator created
clusterrole.rbac.authorization.k8s.io/tigera-operator-secrets created
clusterrole.rbac.authorization.k8s.io/tigera-operator created
clusterrolebinding.rbac.authorization.k8s.io/tigera-operator created
rolebinding.rbac.authorization.k8s.io/tigera-operator-secrets created
deployment.apps/tigera-operator created
$ kubectl get pods -n tigera-operator
NAME READY STATUS RESTARTS AGE
tigera-operator-696d7c8fc4-59bf5 1/1 Running 0 4s
The Installation resource
Now describe the Calico you want. This Installation resource is the heart of the stock custom-resources file, and the field that matters is the IP pool cidr: 192.168.0.0/16, matching this cluster's pod CIDR. If those two disagree you get the same class of pain as a flannel CIDR mismatch, so always check before you create it. Create it and the operator takes over, pulling Calico up piece by piece. The tigerastatus resource is your progress bar: when every row reports Available True, the install is done.
$ cat custom-resources.yaml
# The Installation resource: the Tigera operator watches for this and deploys Calico
# to match it. The ipPool cidr MUST agree with the cluster's pod CIDR (192.168.0.0/16).
apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
name: default
spec:
calicoNetwork:
ipPools:
- cidr: 192.168.0.0/16
encapsulation: VXLANCrossSubnet
$ kubectl create -f custom-resources.yaml
installation.operator.tigera.io/default created
$ kubectl get tigerastatus
NAME AVAILABLE PROGRESSING DEGRADED SINCE
calico True False False 0s
ippools True False False 55s
Calico up, nodes Ready
Trust but verify. In calico-system there is a calico-node pod on every node, that is the dataplane that wires up pods and enforces policy, plus typha and the kube-controllers, all Running. And the payoff: both nodes have flipped to Ready, and CoreDNS is finally scheduled. The CNI is properly installed and configured, which was requirement one.
$ kubectl get pods -n calico-system -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
calico-kube-controllers-6c8496f5c8-flpxp 1/1 Running 0 63s 192.168.227.197 cka-scenario9-control-plane <none> <none>
calico-node-dpqqj 1/1 Running 0 63s 172.18.0.15 cka-scenario9-worker <none> <none>
calico-node-pdgjt 1/1 Running 0 63s 172.18.0.16 cka-scenario9-control-plane <none> <none>
calico-typha-6f8c54fdf6-mnvnn 1/1 Running 0 63s 172.18.0.15 cka-scenario9-worker <none> <none>
csi-node-driver-6mlr5 2/2 Running 0 63s 192.168.227.194 cka-scenario9-control-plane <none> <none>
csi-node-driver-ntcxl 2/2 Running 0 63s 192.168.178.65 cka-scenario9-worker <none> <none>
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
cka-scenario9-control-plane Ready control-plane 82s v1.36.1
cka-scenario9-worker Ready <none> 73s v1.36.1
Prove pods can talk
Requirement two: pods must communicate with each other. Create two test pods pinned to different nodes and list them wide, each gets an IP from 192.168.0.0/16 on a different host. Then exec into test one and ping test two's pod IP: replies come back across the nodes, so pod-to-pod networking works end to end. One practical note: ping the IP, not the pod name. Bare pods do not get DNS records, so a name lookup fails with bad address even on a perfectly healthy network.
$ kubectl apply -f connectivity-test.yaml
pod/test1 created
pod/test2 created
$ kubectl get pods -o wide -l app=conn-test
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
test1 1/1 Running 0 3s 192.168.178.66 cka-scenario9-worker <none> <none>
test2 1/1 Running 0 3s 192.168.227.198 cka-scenario9-control-plane <none> <none>
$ kubectl exec test1 -- ping -c 3 <test2-ip>
PING 192.168.227.198 (192.168.227.198): 56 data bytes
64 bytes from 192.168.227.198: seq=0 ttl=62 time=0.351 ms
64 bytes from 192.168.227.198: seq=1 ttl=62 time=0.091 ms
64 bytes from 192.168.227.198: seq=2 ttl=62 time=0.100 ms
--- 192.168.227.198 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.091/0.180/0.351 ms
Prove policy enforcement
Requirement three is the one flannel cannot do: NetworkPolicy enforcement. This default-deny policy comes straight from the Kubernetes docs. The empty podSelector selects every pod in the default namespace, and listing both Ingress and Egress with no rules denies all traffic in both directions. Apply it, confirm it exists, and run the exact same ping again. Three packets, zero replies, one hundred percent loss. The policy is not just stored in the API, Calico is enforcing it on the wire. That failing ping is the proof the question asked for.
$ cat deny-all.yaml
# Default-deny-all, straight from the Kubernetes docs: the empty podSelector selects
# EVERY pod in the namespace; both policyTypes with no rules = deny all traffic.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: default
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
$ kubectl apply -f deny-all.yaml
networkpolicy.networking.k8s.io/default-deny-all created
$ kubectl get networkpolicy
NAME POD-SELECTOR AGE
default-deny-all <none> 0s
$ kubectl exec test1 -- ping -c 3 -w 5 <test2-ip>
PING 192.168.227.198 (192.168.227.198): 56 data bytes
--- 192.168.227.198 ping statistics ---
3 packets transmitted, 0 packets received, 100% packet loss
command terminated with exit code 1
Exam tips
A few traps. When a question says the CNI must support NetworkPolicy enforcement, that one phrase eliminates flannel; pick Calico and move on. Use kubectl create for the Calico operator manifests, because apply chokes on the big CRD file. Check that the Installation's IP pool cidr matches the cluster pod CIDR before you create it, then watch kubectl get tigerastatus until everything is Available. And verification means behavior, not status: prove the ping works, then prove a default-deny breaks the same ping.
- 'Must support NetworkPolicy enforcement' => flannel is out, use Calico
- kubectl create -f (not apply) for the Calico manifests: the CRD file is too big for apply
- Installation ipPool cidr must match the cluster pod CIDR; watch 'kubectl get tigerastatus'
- Verify behavior: ping works, then a default-deny makes the same ping fail
Recap
- NetworkPolicy enforcement required => Calico, installed with kubectl create (no Helm)
- operator CRDs -> Tigera operator -> Installation (ipPool = cluster pod CIDR)
- tigerastatus Available, nodes Ready, cross-node ping works
- default-deny => the same ping fails: enforcement proven; subscribe + dev.to writeup
Reproduce this yourself
The entire scenario is scripted on a throwaway kind cluster: https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios
git clone https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios.git
cd TCS_CKA_2026_Exam_Scenarios/learning/scenarios/scenario9-calico-network-policy
./setup.sh # creates the cluster AND arms the scenario
# solve it by hand, or:
./solution.sh # apply the answer key and verify
If this helped, subscribe to The Cyber SideKick on YouTube for more CKA drills, and grab the newsletter at https://thecybersidekick.beehiiv.com.
Top comments (0)