TT Lab
Get started
Learn Learning paths Courses

Kubernetes Operations

NetworkPolicy — A Firewall Drawn With Labels

Continue in TT Lab

Summary

The Kubernetes default is "every Pod can talk to every Pod," and NetworkPolicy is the only standard means of flipping that default on a per-Pod basis.

Why this matters

Suppose one Pod has been breached. In a cluster with the default settings, that Pod can simply connect to the database, the payment service, and the internal admin API. This is because the firewall is only at the cluster's outer boundary and the inside is a flat field. This phenomenon, in which a compromise does not end at one Pod but spreads sideways, is called lateral movement.

NetworkPolicy builds walls in this field. But the way it builds walls differs from a traditional firewall. It selects targets not by IP but by label. Pods die and come back with different IPs, but labels stay the same. If a policy says "Pods with app=api" instead of an IP, the policy stays valid whether autoscaling brings the Pods to ten or they move to another node.

How it works

Understanding just three rules explains most of it.

First, a policy attaches to the receiving side. podSelector selects the Pods this policy applies to, and ingress.from selects the parties that are allowed to send to those Pods. To allow traffic from web to api, the policy's podSelector must be api. Getting this direction backwards is overwhelmingly the most common mistake.

Second, a policy is a whitelist. The moment even one policy exists that selects a Pod, that Pod's traffic in that direction switches to "only what the policies list is allowed." So default deny is expressed as a policy with no rules at all.

spec:
  podSelector: {}          # 네임스페이스의 모든 파드
  policyTypes: [Ingress]   # 인그레스에 대해
  # ingress 규칙 없음 → 전부 거부

Third, items in the from array are ORed, and selectors within one item are ANDed. This is the most subtle.

from:
  - namespaceSelector: {matchLabels: {purpose: monitoring}}
    podSelector: {matchLabels: {app: prom}}      # ← 같은 항목: AND

The above means "the prom Pods in the monitoring namespace," but if you add one more hyphen and split the item in two, it becomes "all Pods in the monitoring namespace, or the prom Pods in any namespace." A single space of indentation changes the whole meaning.

Egress has one more trap. If you block egress by default, DNS dies first. A Pod has to ask CoreDNS in kube-system to turn a Service name into an IP, and that path is blocked. That is why a default egress deny is always accompanied by a DNS exception as a pair. And DNS does not use only UDP 53. When a response is large it moves to TCP 53, so you must open both protocols.

There is also ipBlock, which opens by IP range. It opens broadly with cidr and carves narrowly out with except. However, using IPs for traffic inside the cluster throws away the advantage of label-based policies, so it is best used only for addresses outside the cluster, such as an office range or an external gateway.

What it looks like in the field

First, if the CNI does not support it, the policy is decoration. A NetworkPolicy object gets created, but the actual blocking is done by the CNI plugin. On a CNI that does not support it, such as Flannel, nothing happens no matter how many policies you create. The author's homelab uses Cilium in eBPF mode, so even L7 rules and Hubble observation are possible. This lab environment is a simulation cluster in which no real traffic flows between Pods, so grading is based not on whether packets get dropped but on whether you wrote the policy correctly and understood the selector semantics correctly. You get the syntax and the meaning down here, and you can do the real blocking verification in an environment with a CNI, using Hubble or calicoctl.

Second, the mindset of putting default deny in first. This is an incident that really happened. An operations team applied default deny to production without testing and left out the DNS allowance. All service discovery failed and the readiness probes died in a chain. The right order is to lay down all the allow policies first and put the default deny in last.

Third, label hygiene. A policy selects targets only by labels, so a Pod or namespace with no labels becomes a blind spot of the policy. When selecting namespaces, kubernetes.io/metadata.name, which people commonly use, is convenient because Kubernetes attaches it automatically to every namespace.

What to do in the next lab

You place three Pods, web, api, and db, in one namespace, and build: a default ingress deny to start, a policy that opens only port 8080 from web to api, a default egress deny with a DNS exception, an allowance for monitoring traffic coming from another namespace, an IP range allowance with an exception, and finally a combined problem that puts both directions in one policy. At the end you draw a map, as JSON, of which policies apply to which Pods.