click the screen · press Enter
← back to blog
Kubernetes Journey · Topic 14

Kubernetes Pods Talk to Everything by Default

Kubernetes network policies explained - Kubernetes Journey

Here's a fact that should bother you more than it does. In a fresh Kubernetes cluster, every single pod can talk to every other pod. Your public-facing web pod can open a direct connection to your database pod, no permission needed. So can a pod an attacker just popped. Kubernetes ships wide open on purpose, and it's your job to close it. The tool for that is the network policy, and it's the closest thing Kubernetes has to an internal firewall.

This one's about locking pod-to-pod traffic down to only what should exist. I'll build up a normal three-tier app, show the default free-for-all, then fence it in step by step. Usual shape: what, how, why, takeaway.

A normal app, and how traffic moves through it

Let me set the scene with a classic setup, three tiers. A web pod serves the site, an API pod handles the logic, and a database pod stores the data. A request walks through them in order and the answer walks back out:

01
User → Web

request hits the web pod on port 80

02
Web → API

web calls the API pod on port 5000

03
API → DB

API queries the database on port 3306

04
Answer flows back

DB → API → Web → user

The data comes back along the same chain. Only the direction the connection starts in matters for policy.

Ingress vs egress (the only two words to get right)

Two terms run through everything here, so let me pin them down. Ingress is traffic coming into a pod. Egress is traffic leaving a pod. That's it. The trick is that you classify by where the connection originates, not which way data physically flows. A user hitting the web pod is ingress to the web pod. The web pod then calling the API is egress from the web pod and ingress to the API pod.

The bit that trips people up Response traffic doesn't count. When a pod replies to a request you allowed in, that reply is allowed back out automatically, you don't write a rule for it. So you only ever think about the direction a new connection is opened, never the answer coming back.

If you write out the rules each tier actually needs, it's short and obvious:

What each tier should allow
PodIngress (allow in)Egress (allow out)
Webport 80 from usersport 5000 to API
APIport 5000 from webport 3306 to database
Databaseport 3306 from APInothing
Notice the database needs no egress at all. It only ever answers, it never starts a conversation. That's your tightest pod.

Why the default is a problem

Now the uncomfortable default. Kubernetes gives every pod its own IP and joins them all onto one big flat virtual network, so any pod can reach any other by IP, by pod name (via DNS), or by service name. Out of the box there's an implied "allow all", nothing is blocked. In our three-tier app that means this is all permitted:

default cluster: everything talks to everything
web → api allowed # fine, expected api → database allowed # fine, expected web → database allowed # NOT fine, web should never touch the DB database → web allowed # NOT fine, why is the DB calling the frontend?
The two red lines are free lateral movement. Pop the web pod and the database is one hop away.

From my chair, the interesting bit is that the web pod is the one exposed to the internet, so it's the most likely to get compromised. In a default cluster, an attacker who lands there can reach straight into the database. Network policies are how you cut those extra lines, so a foothold in the web tier can't just walk to the crown jewels. This is the same lateral-movement problem I go on about with the kubelet, just pod-to-pod.

What a network policy actually is

A network policy is a Kubernetes object that says which pods are allowed to talk to which. It works using two things you'll use constantly:

  • A label is just a key-value tag you stick on a pod, like app: database. It's how you name a group of pods.
  • A selector is how a policy picks which pods it's talking about, by matching labels. A podSelector matching app: database means "this rule is about the database pods".

So the pattern is always: use a selector to point the policy at some pods, then describe the traffic you'll allow to or from them. Let me lock down the database.

Locking the database with an ingress policy

Goal: the database should accept connections only from the API pod, on port 3306, and nothing else. Here's the whole policy, then I'll read it line by line:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-policy
spec:
  podSelector:
    matchLabels:
      role: db          # this policy applies to pods labelled role: db
  policyTypes:
  - Ingress             # we're only controlling incoming traffic
  ingress:
  - from:
    - podSelector:
        matchLabels:
          name: api-pod # allow connections from pods labelled name: api-pod
    ports:
    - protocol: TCP
      port: 3306        # only on TCP 3306

Reading it in plain words: this policy targets pods labelled role: db (the podSelector at the top). It sets policyTypes: Ingress, so we're only governing what comes in. The ingress block then allows traffic from pods labelled name: api-pod, and only on TCP 3306. Everything else in is denied. And because responses are automatic, the database can still reply to the API without any egress rule. You'd only need egress if the database itself started outbound connections (say, pushing a backup somewhere).

after the policy is applied
api → database allowed # from api-pod on 3306, matches the rule web → database blocked # web isn't api-pod, denied # the lateral hop from web to DB is gone
Verify it worked: the API still reaches the database, the web pod no longer can. That's the whole win.
The gotcha that will bite you Network policies are only enforced if your cluster's network plugin (CNI) supports them. Calico, Cilium, Weave Net and Kube-router enforce them; Flannel does not. The nasty part: with an unsupported plugin, Kubernetes accepts your policy without any error, it just silently ignores it. You think you're protected and you're not. Always confirm your CNI enforces policies. Check it with kube-bench or a quick connectivity test.

Getting more specific: combining selectors

Real clusters need finer control. Say your API pods exist in three namespaces, dev, test and prod, and only the prod API should reach the database. You combine a podSelector with a namespaceSelector so both must be true:

  ingress:
  - from:
    - podSelector:
        matchLabels:
          name: api-pod
      namespaceSelector:
        matchLabels:
          name: prod        # AND: must ALSO be in the prod namespace

Now traffic is allowed only if the pod is labelled api-pod and it lives in the prod namespace. Both conditions, together.

AND vs OR: the one-character mistake that opens a hole This is the subtlest, most dangerous bit of the whole topic. Look at the dash. If podSelector and namespaceSelector sit under the same list item (one dash), they're combined with AND, both must match. If you give each its own dash, they become separate entries and are combined with OR, either can match. One stray dash quietly changes "prod api pods only" into "any api pod, OR anything in prod", which can let in traffic you never meant to. Read your indentation twice.

Allowing something outside the cluster: ipBlock

Selectors only work on things Kubernetes knows about, pods and namespaces. But sometimes you need to allow a machine that lives outside the cluster, like an external backup server. For that you use an ipBlock, which allows a specific IP address or range (written in CIDR notation, where /32 means one exact IP):

  ingress:
  - from:
    - ipBlock:
        cidr: 192.168.5.10/32   # allow just this one external IP

You can mix all of these in one rule. Here the database accepts traffic if it's from the prod API pod (the combined AND selector) or from that specific backup IP, on port 3306:

  ingress:
  - from:
    - podSelector:
        matchLabels:
          name: api-pod
      namespaceSelector:
        matchLabels:
          name: prod
    - ipBlock:
        cidr: 192.168.5.10/32
    ports:
    - protocol: TCP
      port: 3306

See the two dashes under from? That's deliberate OR this time: "the prod API pod" or "the backup IP". Exactly the structure the callout above warned about, used on purpose.

Adding an egress rule

Everything so far controlled traffic coming in. You need egress only when the pod itself opens outbound connections. Classic example: the database pushing a nightly backup to that external server. Because the database is the one starting that connection, it's egress. You add Egress to policyTypes and describe where it's allowed to go:

  policyTypes:
  - Ingress
  - Egress
  # ...ingress block as above...
  egress:
  - to:
    - ipBlock:
        cidr: 192.168.5.10/32   # backup server
    ports:
    - protocol: TCP
      port: 80                  # allowed out on port 80 only

Now the full policy does two jobs: it only lets the prod API pod (or the backup IP) reach the database on 3306, and it only lets the database reach the backup server on port 80. Tight both ways. Ingress guards what can reach a pod; egress guards where a pod can reach; and you only need egress rules when the pod is the one initiating the connection.

Where hardening starts to feel real

This is the topic where Kubernetes finally starts to feel like something I can actually harden. Everything before was about who you are and what you can ask the API; this is about stopping a compromised pod from strolling across the network to the database. The mental model that stuck: default is allow-all, a policy is a targeted allow-list, and the direction the connection starts is the only thing that decides ingress or egress. The AND/OR dash thing genuinely unsettles me, it's exactly the kind of silent misconfiguration I'd hunt for on an engagement, so I want to build a lab and prove to myself I can spot it from the traffic. And I still need to properly test the Flannel "accepted but ignored" trap with my own eyes. Next I think I'll look at default-deny policies, starting from "block everything" and opening only what's needed, which is the way you'd actually run this in production.

If this made network policies click, come say hi on LinkedIn or the contact page, and tell me what to break down next. More in the Kubernetes Journey.

Further reading

FAQ

What is a Kubernetes network policy?

A network policy is a Kubernetes object that controls which pods can talk to which. By default every pod can reach every other pod. A policy uses labels and selectors to allow only specific traffic, for example letting the database pod accept connections only from the API pod on port 3306.

What is the difference between ingress and egress in Kubernetes?

Ingress is traffic coming into a pod; egress is traffic leaving a pod. You classify by where the connection starts. Response traffic is allowed automatically, so if you permit an incoming request, the reply back out does not need its own rule.

Why is my Kubernetes network policy not being enforced?

Network policies only work if your cluster's network plugin supports them. Calico, Cilium, Weave Net and Kube-router enforce them; Flannel does not. With an unsupported plugin the policy is accepted without error but silently ignored, which is a dangerous false sense of security.