Certified Kubernetes Security Specialist (CKS) #2: NetworkPolicy in depth — default deny, ingress/egress (Cluster Setup)
In CKS #1 we got the big picture of the exam environment and the six domains. Now we step into the first domain, Cluster Setup. The heart of this domain is network isolation, and the tool for it is NetworkPolicy. By default, Kubernetes is a flat network where every Pod can talk freely to every other Pod. That means once an attacker takes over a single Pod, lateral movement across the whole cluster becomes possible. NetworkPolicy is a firewall that carves this flat network into small compartments, keeping only the communication you need and blocking the rest.
The basics of NetworkPolicy were covered in CKA #20 and K8s Intermediate #7. This post builds on that and focuses on the depth CKS actually demands — default deny design, the egress-and-DNS trap, and the AND vs OR difference in selector combinations.
Back to the basic behavior of NetworkPolicy #
To handle NetworkPolicy precisely on CKS, you first need to keep two pieces of default behavior straight.
No policy means all-allow #
In a namespace with no NetworkPolicy at all, every Pod communicates freely in every direction. This is the Kubernetes default. Isolation is a feature you must explicitly turn on, not something that is on from the start.
A Pod with a policy attached becomes a whitelist #
The moment any NetworkPolicy matches a given Pod, that Pod’s traffic in that direction (ingress or egress) switches to a whitelist model. Only traffic the policy explicitly allows gets through, and everything else is blocked. Two rules matter here.
- Policies are additive. When multiple policies match the same Pod, the union of the traffic each policy allows is permitted. NetworkPolicy has no deny rule at all — only the union of allows exists.
- If a Pod has only an ingress policy attached, egress is still all-allow. Directions are controlled independently. That is why declaring explicitly, with the
policyTypesfield, which direction a policy controls is central to CKS.
The default deny pattern follows from these two rules. If you match a policy that allows no traffic at all against every Pod, the starting point of the union becomes “block everything.”
default deny: block everything to begin with #
The starting point used most often on the CKS exam and in practice is to lay down a default deny across an entire namespace and open only the communication you need with separate policies. podSelector: {} selects every Pod in the namespace, and you write the directions to block in policyTypes.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: secure
spec:
podSelector: {} # applies to every Pod in the namespace
policyTypes:
- Ingress
- Egress
# no ingress/egress rules, so both directions are fully blockedWhen the ingress and egress keys themselves are absent, it means no traffic is allowed in that direction at all. Since both directions are listed in policyTypes, every Pod in this namespace has both inbound and outbound traffic blocked.
Knowing the pattern for locking down each direction separately is also useful on the exam.
# default deny ingress only (egress stays all-allow)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: secure
spec:
podSelector: {}
policyTypes:
- IngressListing only Ingress in policyTypes and leaving the ingress rule empty blocks all inbound traffic while leaving outbound traffic uncontrolled. It applies directly to the “lock only the traffic coming into this namespace” type of task.
Restricting ingress: who do you let in from #
After laying down a default deny, you add policies that open the traffic you need. In an ingress rule, from specifies the source and ports specifies the destination port. There are three kinds of selectors for choosing the source.
podSelector: picks source Pods by label within the same namespacenamespaceSelector: picks entire namespaces carrying a specific label as the sourceipBlock: picks an IP range by CIDR. Used for sources outside the cluster
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-api
namespace: secure
spec:
podSelector:
matchLabels:
app: api # this policy controls traffic into app=api Pods
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend # only traffic coming from app=frontend Pods
ports:
- protocol: TCP
port: 8080For app=api Pods, this policy allows only traffic coming in on TCP 8080 from app=frontend Pods in the same namespace. If a default deny is in place alongside it, all other ingress is blocked, so this single rule effectively becomes the whitelist.
Restricting egress: the DNS trap #
An egress rule specifies the destination with to and the destination port with ports. The structure is symmetric to ingress, but there is one trap: the moment you block egress with a default deny, almost all communication breaks. The culprit is DNS.
For a Pod to talk to a Service name like api.secure.svc.cluster.local, it must first do a DNS lookup to turn that name into an IP. That lookup is traffic going to CoreDNS on port 53 (both UDP and TCP). Lay down an egress default deny and this DNS traffic gets blocked too, so the Pod can resolve no name and every connection fails. Even if you allowed the destination IP directly, it is useless if you cannot turn the name into an IP.
So when you lock down egress, including a DNS allow is effectively mandatory.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: secure
spec:
podSelector: {} # every Pod in the namespace
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns # CoreDNS Pods
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53kubernetes.io/metadata.name is a label Kubernetes automatically attaches to every namespace, so you can safely pick kube-system, where CoreDNS lives. DNS usually uses UDP 53, but large responses fall back to TCP 53, so on the exam it is safest to allow both protocols.
Adding a separate policy for your application egress on top of this completes the isolation: “this namespace can only go out to DNS and a specific backend.”
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-egress-to-db
namespace: secure
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432When the three policies — default deny egress, allow-dns, and allow-egress-to-db — match together, the outbound traffic of an app=api Pod is restricted to port 53 on CoreDNS and port 5432 on app=postgres only. Thanks to the union rule, this design of stacking up small policies is possible.
Selector combinations: the AND vs OR trap #
The point people get wrong most often on CKS is the meaning of using namespaceSelector and podSelector together. Putting both selectors in a single item means AND; splitting them into separate items means OR. A single space of YAML indentation flips the meaning to the exact opposite.
# (A) AND: allow only Pods that are "inside" the prod namespace AND are app=client
ingress:
- from:
- namespaceSelector:
matchLabels:
env: prod
podSelector:
matchLabels:
app: clientAbove, both selectors sit inside a single element of the from list. So it becomes the intersection condition “a Pod that belongs to the env=prod namespace AND at the same time has the app=client label.” Note that there is only one dash (-).
# (B) OR: allow all Pods in the prod namespace OR app=client Pods anywhere
ingress:
- from:
- namespaceSelector:
matchLabels:
env: prod
- podSelector:
matchLabels:
app: clientAbove, from contains two elements, so traffic is allowed if either “all Pods in the env=prod namespace” or “app=client Pods in the current namespace” matches. This opens things far wider than intended — a common accident — so always check the number of dashes to confirm whether it is AND or OR.
ipBlock and except #
ipBlock allows a CIDR range, but you can carve part of it back out with except.
ingress:
- from:
- ipBlock:
cidr: 10.0.0.0/16
except:
- 10.0.5.0/24 # exclude only this subnetThis allows all of 10.0.0.0/16 but blocks 10.0.5.0/24. Use it when you want to open a specific trusted range but cut out a risky segment within it. The except range must fall entirely within the cidr range.
Two recurring exam scenarios #
The NetworkPolicy types that come up repeatedly on CKS are formulaic. Let’s go over two types so your hands remember them first.
Type 1: isolate a namespace but allow DNS only #
This is the “isolate the secure namespace from the outside, but keep DNS resolution working” type. You apply two policies together: default deny and allow-dns. The combination of default-deny-all and allow-dns built above is exactly this answer. Not forgetting to open DNS while blocking egress is the grading point.
Type 2: ingress from a specific label only #
This is the “make it so app=db Pods can be reached only from app=app Pods” type. You lay a default-deny-ingress targeting app=db and add a policy that opens only the ingress coming from app=app.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-app-to-db
namespace: secure
spec:
podSelector:
matchLabels:
app: db
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: appOmitting ports allows all ports, so if the problem specifies a port, be sure to include ports.
Verification: confirming with a communication test #
Once you’ve applied a policy, you must test whether it actually blocks and opens as intended. Grading is done by the policy’s effect, so if only the YAML is correct but the behavior differs, you get no points. Try a connection directly from a throwaway Pod.
# check the list of applied policies
kubectl get networkpolicy -n secure
# check the detailed rules of a specific policy
kubectl describe networkpolicy default-deny-all -n secure# from the secure namespace, try connecting to the api Service with a temporary Pod
kubectl run test -n secure --rm -it --image=busybox --restart=Never -- \
wget -qO- --timeout=3 http://api:8080
# check DNS resolution alone, separately
kubectl run test -n secure --rm -it --image=busybox --restart=Never -- \
nslookup apiIf wget times out, ingress is blocked; if a response comes back, it is open. If nslookup fails, that is a sign DNS egress is blocked, so check the allow-dns policy. One caveat: for a policy to match the temporary Pod, that Pod’s label and namespace must meet the policy conditions, so it matters to launch it with the source label set correctly.
One thing to watch for is the fact that what actually enforces NetworkPolicy is the CNI plugin. A policy-supporting CNI such as Calico or Cilium must be installed for it to take effect. In some environments, applying a policy may have no effect at all, so work on the premise that the exam environment’s CNI supports NetworkPolicy.
Exam points #
- Default behavior. No policy means all-allow. Once a policy matches, that direction becomes a whitelist. NetworkPolicy has no deny rule, only the union of allows.
- default deny. Pick every Pod with
podSelector: {}and write the directions to block inpolicyTypes. If theingress/egresskeys are absent, that direction is fully blocked. - DNS trap. Block egress with a default deny and port 53 gets blocked, breaking name resolution. Open UDP/TCP 53 to CoreDNS in
kube-systemwith an allow-dns policy. - AND vs OR. Two selectors within a single item of
from/tomean AND; splitting them into separate items means OR. Tell them apart by the number of dashes. - ipBlock/except. Allow an external range with CIDR and exclude part of it with
except. - Verification. Test communication directly with a temporary Pod. The enforcing agent is the CNI plugin.
Wrap-up #
NetworkPolicy is a whitelist firewall that carves the flat Kubernetes network into compartments. The design of blocking everything with a default deny and adding back only the communication you need with small policies is the correct CKS pattern. When you block egress, don’t forget to open DNS along with it; when you combine selectors, distinguish AND from OR precisely by indentation; and finally, verify the effect with a temporary Pod. Get just these three into your hands and the network isolation work of Cluster Setup reliably turns into points.
Next: CIS benchmark #
Now that we’ve got network isolation down, the next step is to inspect the cluster’s own configuration.
In #3 CIS benchmark (kube-bench), component security, Ingress TLS, binary verification, we’ll work through, running it all firsthand, how to auto-check the CIS benchmark with kube-bench, how to fix the security settings of components like the API server, kubelet, and etcd against the check items, how to apply TLS to Ingress, and how to verify the hash and signature of a downloaded binary.