‹ All study guides

Kubernetes NetworkPolicy, explained for CKA, CKAD and CKS

Updated 12 October 2026 · 7 min read

Out of the box, every Pod in a Kubernetes cluster can open a connection to every other Pod, in every namespace. That is convenient until a compromised web Pod can talk straight to the payroll database. NetworkPolicy is the built-in way to say which Pods may talk to which, on which ports, and it is a small API with a surprising number of ways to get it wrong.

All three Kubernetes exams use it. CKAD asks you to let one app reach another and nothing else. CKA adds default-deny policies and the debugging that follows them, usually a DNS failure. CKS treats it as a security control and asks what it can and can't protect. The YAML is short; the marks are in the semantics.

The model: default allow, then isolation

A Pod that no policy selects is non-isolated: all traffic in and out is allowed. The moment any NetworkPolicy selects a Pod and lists Ingress in its policyTypes, that Pod becomes isolated for ingress, and only connections some policy explicitly allows can reach it. Egress works the same way, independently. A Pod can be isolated for ingress and wide open for egress.

Three rules follow from this, and most exam questions hang on one of them:

  • Policies only add allows. There is no deny rule and no priority. The traffic a Pod may receive is the union of every policy that selects it, so the order in which they were created makes no difference. A "default deny" policy is just a policy that selects Pods and allows nothing.
  • Both ends must agree. A connection from Pod A to Pod B needs A's egress (if A is isolated for egress) and B's ingress (if B is isolated for ingress) to allow it. Fixing one side is not enough when both are locked down.
  • Replies are automatic. If a connection is allowed, its return traffic is allowed too. You never write a rule for the response.

A few things are always allowed and can't be blocked by NetworkPolicy: traffic between a Pod and the node it runs on, and a Pod talking to itself.

Anatomy of a policy

A NetworkPolicy is namespaced and has four parts that matter:

  • podSelector chooses which Pods in the policy's own namespace the policy applies to. An empty selector, {}, means every Pod in the namespace.
  • policyTypes lists Ingress, Egress or both. If you leave it out, Ingress is always assumed and Egress is added only when the policy has egress rules. Write it out explicitly; a policy meant to deny all egress with no egress section and no policyTypes will only touch ingress.
  • ingress is a list of rules, each with from peers and ports. A rule allows traffic that matches its peers and its ports.
  • egress is the same shape with to instead of from.

Peers come in three forms. podSelector picks Pods in the policy's own namespace. namespaceSelector picks whole namespaces by label. ipBlock picks a CIDR range, optionally with except ranges, and is meant for addresses outside the cluster, since Pod IPs change. Ports can be numbers or named ports, and endPort turns a port into an inclusive range.

Namespaces can't be named directly. Instead, every namespace carries an immutable label, kubernetes.io/metadata.name, set to its own name, so namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: monitoring}} selects exactly one namespace.

The AND/OR gotcha

This is the most tested detail in the whole API, and it comes down to one YAML dash.

When namespaceSelector and podSelector sit in the same list element (one dash, two keys), they are combined with AND: Pods with the given label, inside namespaces with the given label.

When they are separate list elements (two dashes), they are combined with OR: any Pod in a matching namespace, or any Pod with the label in the policy's own namespace. The second form is far more permissive, and it looks almost identical in an editor.

So a rule meant to allow only Prometheus Pods from the monitoring namespace must have one - before namespaceSelector and podSelector indented underneath it with no dash. If you add a dash before podSelector, every Pod in monitoring gets in, plus any Pod in your own namespace that happens to carry the Prometheus label. When unsure, kubectl describe networkpolicy prints how Kubernetes read it.

Default deny and the DNS trap

The usual starting point is a namespace-wide default deny: podSelector: {} with policyTypes: [Ingress, Egress] and no rules. Every Pod in the namespace becomes isolated in both directions, and you then add narrow allow policies on top.

The trap is DNS. Once Pods are isolated for egress, their DNS queries to CoreDNS in kube-system are blocked like everything else. The application never gets as far as connecting to the database; it fails resolving the database's name, and the logs say "lookup failed" or "no such host" rather than "connection refused". The fix is an egress rule allowing UDP and TCP port 53 to the DNS Pods, selected with a namespaceSelector for kube-system and a podSelector for the DNS Pods' label in the same element. TCP matters too, because large DNS responses fall back to it.

Your CNI has to enforce it

The API server stores NetworkPolicy objects whether or not anything enforces them. Enforcement is the network plugin's job. With a plugin that doesn't support NetworkPolicy, kubectl apply succeeds, kubectl get networkpolicy lists your policy, and traffic flows exactly as before. Calico and Cilium enforce it; some simple plugins do not. If a policy "has no effect", check the CNI before rewriting YAML.

Other limits worth knowing for CKS:

  • It works at layers 3 and 4 only, and is guaranteed only for TCP, UDP and SCTP. Behaviour for ICMP and other protocols depends on the plugin.
  • There is no TLS, no logging of dropped connections, no rule by Service name, and no cluster-wide default that applies to new namespaces. Those need a service mesh, admission control or a plugin's own policy resources.
  • Pods on the host network are usually treated as node traffic, and processes on the node such as the kubelet are outside its reach.
  • New policies take effect asynchronously, and whether an existing connection is cut when a policy changes depends on the implementation.

How to choose

  • Lock down a namespace: one default-deny policy with podSelector: {} and both policyTypes.
  • Let one app reach another: an ingress policy on the destination selecting the source by label, plus an egress policy on the source if it is isolated for egress.
  • Allow traffic from a specific set of Pods in another namespace: namespaceSelector and podSelector in the same peer element.
  • Allow a whole namespace, such as an ingress controller's: namespaceSelector alone, using kubernetes.io/metadata.name.
  • Allow an external API or a managed database: ipBlock with the provider's CIDR.
  • Any egress lockdown: add DNS on port 53, UDP and TCP, first.

Common exam traps

  • Believing a deny policy overrides an allow policy. Nothing overrides anything; policies are unioned.
  • Forgetting DNS after default-deny egress, then chasing the database port.
  • Splitting namespaceSelector and podSelector into two elements and opening access far wider than intended.
  • Omitting policyTypes on an egress-only deny and getting an ingress deny instead.
  • Applying a policy on a cluster whose CNI doesn't enforce NetworkPolicy and assuming the YAML is wrong.
  • Using a NetworkPolicy to protect a node-level service such as the kubelet API.

A worked example

In namespace shop, the api Pods must accept traffic only from the web Pods in the same namespace on TCP 8080, and must only be able to reach the postgres Pods on 5432. A default-deny policy for both directions already exists, and so does an egress policy that lets web reach api and the cluster DNS. After adding an ingress policy on api that allows web on 8080, the web pages load. After adding an egress policy on api that allows postgres on 5432, the API logs show database connection errors that mention a failed name lookup.

The web-to-api path works because both ends now allow it: web's egress and api's ingress. The api-to-database path fails because api is isolated for egress and the only destination it may reach is postgres, so its query to CoreDNS for postgres.shop.svc.cluster.local is dropped. The fix is a further egress rule from api (or from every Pod in shop) to the DNS Pods in kube-system on port 53 over UDP and TCP. One more check: postgres is also under the default deny, so it needs its own ingress allow for api on 5432, or the connection is refused at the other end.

Practice questions

Further reading

Practice questions