Skip to content

Network policies

By default, every Pod in a cluster can talk to every other Pod. There is no segmentation at all: a compromised front end can reach your database directly, and nothing stops it.

A NetworkPolicy is how you change that. It is worth understanding the model precisely before writing any, because the rules are not quite like a firewall.

Reference

Terminology

  • podSelector — Which Pods in this namespace the policy applies to. An empty selector, {}, means every Pod in the namespace. This is the object people mean when they say “default deny”.
  • policyTypes — Which directions the policy governs: Ingress, Egress, or both. Omitting it is a common mistake that changes the meaning of the object.
  • ingress from / egress to — Who may connect to the selected Pods, and where the selected Pods may connect. Each entry can combine podSelector, namespaceSelector and ipBlock.
  • ipBlock — A CIDR range, for traffic to or from outside the cluster.
  • CNI — The plugin that implements Pod networking. Whether a policy is enforced at all is a property of the CNI, not of Kubernetes — see below.

The Model: Additive Allow

There are no deny rules. Every policy is a list of things that are allowed, and anything not allowed is dropped.

no policies at all              → everything is allowed
one policy selecting a Pod      → only what its rules allow; everything else is dropped
two policies selecting one Pod  → the union of both (they never conflict)

Three consequences, in increasing order of surprise:

  1. Adding a policy can only ever remove access, never grant it. There is no way to write “allow everything except…”.
  2. Policies are additive. You cannot override or subtract with a second policy, which is why a security review of NetworkPolicy is about coverage — which Pods have no policy at all — rather than about contradictions.
  3. An ingress policy lives in the destination’s namespace. To allow traffic from frontend in namespace shop to db in namespace data, you write the policy in data, selecting db, with a from that permits shop. The client cannot grant itself access.

The AND/OR Trap

This is the mistake almost everyone makes once.

  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              env: shop
          podSelector:
            matchLabels:
              app: api
        - podSelector:              # <- a second list item
            matchLabels:
              app: admin
  • Within one from entry, namespaceSelector and podSelector are ANDed: Pods labelled app=api in namespaces labelled env=shop.
  • Across separate list items, the entries are ORed: either of the above, or any Pod labelled app=admin in this namespace.

Indentation decides which you get, and the two differ by a single dash. When a policy does not behave, look at this first.

Default Deny, Then Allow

Start by denying everything in the namespace, then open what you need:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
spec:
  podSelector: {}          # every Pod in this namespace
  policyTypes: ["Ingress"]

Then a policy that permits one thing:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: web-from-client
spec:
  podSelector:
    matchLabels:
      app: web
  policyTypes: ["Ingress"]
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: client
      ports:
        - protocol: TCP
          port: 8080

The second policy is not an exception to the first — it is an addition. Both are in force.

The DNS Egress Trap

Deny egress and you break DNS, because a Pod resolving a Service name talks to CoreDNS in kube-system. Every egress policy needs this rule, and forgetting it produces a confusing “connection timed out” on a name that resolves fine for other Pods:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
spec:
  podSelector: {}
  policyTypes: ["Egress"]
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

kubernetes.io/metadata.name is set automatically on every namespace, so it selects kube-system without you having to label anything.

Enforcement Depends On The CNI

Kubernetes accepts a NetworkPolicy object on any cluster. Whether anything acts on it is down to the CNI plugin — and a policy that is accepted but not enforced looks exactly like a policy that works. Traffic flows, nothing errors, and you learn the wrong lesson.

So verify it once per cluster rather than assuming. Create two Pods, confirm they reach each other, apply a default-deny, and check again:

kubectl -n <ns> exec client -- wget -qO- -T 3 http://server:8080 && echo reachable
kubectl apply -f default-deny.yaml
kubectl -n <ns> exec client -- wget -qO- -T 3 http://server:8080 && echo STILL reachable

If it is still reachable, nothing is enforcing. Whether your cluster enforces is a property of its CNI, so it is worth knowing before you rely on a policy. In this course the NetworkPolicy work is done in a namespace on the shared cluster, where Azure network policy is enabled — and you should still run the experiment above on any cluster you inherit, because a policy that is silently ignored is worse than no policy at all. On a self-managed cluster without enforcement, installing Calico or Cilium is the usual fix.

Exercises

  1. Establish the baseline. A namespace, a serving Pod and a client Pod, with the client confirmed to reach the server. Everything below is measured against that.

  2. Deny all ingress and watch it break.

    kubectl -n np-lab apply -f default-deny-ingress.yaml
    kubectl -n np-lab exec client -- wget -qO- -T 3 http://server:8080   # hangs, then fails

    A timeout is what a dropped packet looks like: no rejection, no event, no error on any object. That is why “it worked a minute ago and now it hangs” is the signature of a policy change.

  3. Allow one thing at a time. Permit app=client to reach app=server on the port it serves, and test between each change. Write the combined selector, guess the AND/OR result, then check — it is the fastest way to internalise that behaviour.

  4. Allow from another namespace. Create a second namespace, label it, and write the policy in the server’s namespace using namespaceSelector and podSelector together. Then remove the namespace label and watch access disappear without the policy changing: the policy is a query over labels, not a fixed list.

  5. Deny egress and fix DNS. Apply the egress deny without the DNS rule and try to resolve a Service name. Then add the rule and try again. Much cheaper to meet here than in production.

  6. Prove nothing is subtractive. With the allow policy in place, add a second policy selecting the same Pods with no ingress entries. Access is unchanged, because there are no deny rules — only a union of allows.

Debugging a Policy

kubectl -n <ns> get networkpolicy
kubectl -n <ns> describe networkpolicy <name>      # the selectors as evaluated
kubectl -n <ns> get pods --show-labels             # do the labels actually match?
kubectl -n <ns> get endpoints <service>            # is the Service the problem instead?

Four things to check, in this order: does the selector match the Pods you think; is the policy in the right namespace (the destination’s, for ingress); is policyTypes what you meant; and is it the policy at all — a Service with no endpoints produces the same timeout.

What to Take Away

  • There is no default segmentation. Without policies, everything reaches everything.
  • Policies are additive allows. There are no deny rules, and adding one can only remove access.
  • Indentation decides AND versus OR inside a from/to list — the commonest mistake by a distance.
  • Ingress policies belong to the destination namespace; you cannot grant yourself access.
  • Denying egress without allowing DNS breaks name resolution in a way that looks like something else entirely.
  • A policy is only real if the CNI enforces it. Test that once per cluster, not once per policy.