Sitemap

Complete Kubernetes Networking

11 min readAug 2, 2026

--

If you’ve ever felt like Kubernetes networking is a pile of disconnected concepts — this article will tie all of it into one story you can actually hold in your head.

Press enter or click to view image in full size

We’ll build it from the bottom up. No prior networking depth assumed; where a term matters, I’ll explain it in plain language as we go. Some things are simplified or omitted — I won’t cover every corner case — but for general understanding, that doesn’t matter.

In fact, I won’t even include code snippets or images, to keep things as simple as possible — so you can focus on understanding the concepts without bothering with code.

Let’s start at the bottom —

1. Every pod gets its own IP

In Kubernetes, every pod gets its own IP address. This solves a legacy problem: In the past, when we deployed applications on servers, two applications couldn’t listen on same port, because they shared the server’s IP address. Because of that, we would need to change ports for every application and keep track of it.

Kubernetes solves this problem by giving each pod its own IP address, so they can both listen on the same port number.

Problem solved — we can now reach every pod by its IP address and port number, right? Well, technically yes — but pods don’t last, so it is not the most convenient way.

That is why we have next layer — Kubernetes Services.

2. Services

Pods get restarted, rescheduled — and every time, they get a new IP address. You can’t build anything reliable on top of something that keeps changing.

A Service fixes this. It gives you one stable IP address that never changes for the life of the Service. You talk to the Service IP, and the Service forwards your traffic to whichever pods are currently behind it. The pods come and go; the address in front of them stays the same.

How does the Service know which pods are behind it? In Service definition, you define a selector which matches pods based on labels. For example, your service matches all pods with label “app: payment-service”. You need to label your pods accordingly, and Kubernetes keeps a live list (this list is called an Endpointslice) of the pods matching that label and updates it automatically, and forwards traffic from the Service to those pods.

All Services match pods by the same mechanism, but there are a few types of Services, depending on how far you need the traffic to reach:

  • ClusterIP — the default. Gives the Service a stable internal IP that’s only reachable inside the cluster. This is what you use for internal communication between your services (a backend talking to a database, for example). Most of your Services are this type.
  • LoadBalancer — provisions a real cloud load balancer (on AWS, Azure, GCP) in front of the Service, giving you a clean external IP. This is the normal way to expose something publicly. The catch: every LoadBalancer Service gets its own cloud load balancer, and each one costs money.
  • NodePort — opens a fixed port on every node, so you can reach the Service from outside the cluster by hitting any node’s IP on that port. It works, but it’s clumsy — you’re exposing nodes directly and juggling port numbers. Mostly used for quick testing, not production.
  • ExternalName — the odd one out. It doesn’t route to pods at all; it just maps the Service name to an external DNS name. Useful when you want to reach something outside the cluster using an internal name.

In short — you will mostly deal with these two types:

  • ClusterIP for internal communication,
  • LoadBalancer for exposing services to the internet.

So… all good right? We don’t need anything else?

Well, look at that LoadBalancer catch again: one cloud load balancer per Service gets expensive fast. If you have ten services to expose, that’s ten load balancers and ten bills. That is why there’s a better way to expose multiple services to the outside world — and that’s the next layer: Ingress.

3. Ingress

We ended the last section on cost: one cloud load balancer per Service gets expensive fast. And that’s true — but cost is really just the symptom. The deeper problem is that a LoadBalancer Service is dumb.

It takes traffic and forwards it to one Service, and that’s all it can do. It can’t look at a request and decide where it should go.

That’s the limitation Ingress fixes. Ingress gives you one entry point for external traffic that can actually make routing decisions based on the request.

The main thing it does is route by hostname and path. One Ingress, sitting in front of the cluster, can send (for example) “api.myapp.com” to the API service, and “myapp.com/shop” to the shop service — All through a single external IP, and a single load balancer.

You define the routing rules, and Ingress sends each request to the right Service. That’s something a plain LoadBalancer simply can’t do — it has no idea what’s inside the request.

So Ingress solves the real problem: not just “too many load balancers,” but “we need one smart entry point that can route traffic to the right place.”

What we also get for free with Ingress — TLS (HTTPS) can be handled in one place, for all your services, instead of setting it up on each Service individually.

There’s a catch, though…

Ingress itself is just a set of rules — it doesn’t do anything on its own. You need an Ingress controller (most popular ones are: nginx, Traefik, HAProxy, and others) to actually implement those rules.

And here’s where it gets messy: the basic Ingress definition only covers simple host and path routing.

Any advanced networking beyond that — redirects, rewrites, traffic splitting — has to be configured through annotations in Ingress definition. Why is this problematic? Because Kubernetes has no idea what annotations mean, they are like comments in code — only the Ingress Controller understands it, and they’re specific to each Ingress Controller. If you change Ingress Controller, you need to rewrite your configurations.

That messiness is exactly why a newer approach exists — and that’s the next section: Gateway API.

4. Gateway API

Gateway API fixes the annotation problem at the root — it takes configuration from annotations to actual specification — defined fields that Kubernetes understands and validates. You write it once, and it works the same across every controller that supports Gateway API.

The other thing Gateway API does is split responsibility across roles, which matters once more than one team shares a cluster. With Ingress, there’s basically one resource everyone edits, and no clean way to separate who owns what. Gateway API splits it into three:

  • GatewayClass — defines which controller is being used. Set up by the platform team.
  • Gateway — the actual entry point, with its ports and TLS. Owned by cluster operators.
  • HTTPRoute — the routing rules for a specific app. Owned by the app developers themselves.

So the platform team manages the infrastructure, and each app team manages its own routing, without stepping on each other. That separation is hard to do cleanly with Ingress.

In short: Gateway API is the more modern replacement for Ingress. Same job — getting external traffic to the right service — but the routing logic lives in the spec instead of in controller-specific annotations, and ownership is cleanly split. Many people consider it the future of this layer in Kubernetes.

So far we’ve been reaching everything by IP address — pod IPs, Service IPs, external IPs. But nobody wants to deal with raw IP addresses. That’s the next layer: DNS.

5. DNS

So far everything has been an IP address. Pods have IPs, Services have IPs, your external entry point has an IP. But nobody wants to hardcode 10.96.0.42 into their app. We want names — payment-service, database, api.myapp.com. Turning names into IP addresses is what DNS does, and Kubernetes has it built in.

Get Josip Matijašević’s stories in your inbox

Join Medium for free to get updates from this writer.

There are two sides to this: names inside the cluster, and names outside it.

Inside the cluster, there’s a DNS server running as a pod, called CoreDNS. Every time you create a Service, CoreDNS automatically gives it a name. So instead of chasing a Service’s IP, your app can just talk to payment-service and CoreDNS resolves it to the right ClusterIP. You create a Service, and it's instantly reachable by name. This is how your services find each other internally.

Outside the cluster, it’s a different problem. When you expose an app to the internet, you want a real public domain — myapp.com — to point at it. Normally you'd log into your DNS provider (Cloudflare, Route 53, Azure DNS) and create that record by hand. And you'd redo it every time the address changed.

A tool called ExternalDNS automates that. It watches your Services and Ingresses, and when it sees one that should be public, it goes and creates the DNS record with your provider automatically — and keeps it updated if the address changes.

It’s the same idea in both places: something watches what’s running and keeps names pointing at the right IPs, so you never have to deal with the IPs yourself. CoreDNS does it inside the cluster, ExternalDNS does it outside.

Now we can reach everything by name. Nice.

But there’s a small problem we’ve been ignoring this whole time, can you think what it is?

So far, anything can talk to anything. Every pod can reach every other pod, with no restrictions. That’s convenient, but it’s not really safe.

Controlling who’s allowed to talk to whom is the next layer: NetworkPolicy.

Let’s dive into it.

6. Network Policy

As we just said — by default, every pod in a Kubernetes cluster can talk to every other pod. For a production system, that’s a problem — if one pod gets compromised, the attacker can reach everything else from there.

NetworkPolicy is how you lock that down. It lets you define rules for which pods are allowed to talk to which — by pod labels, namespaces, IP ranges, and ports. For example: “only the backend may talk to the database, on port 5432, and nothing else.”

There’s one thing about NetworkPolicy that surprises people, and it’s worth knowing. Policies don’t work by blocking traffic — they work by allowing it.

Until you create your first NetworkPolicy targeting a pod, it allows everything — but at the moment you create your first NetworkPolicy and target a pod — its default flips: instead of allowing everything, it now denies everything except what you’ve explicitly allowed.

So a policy that selects a pod but lists no rules doesn’t do nothing — it cuts that pod off completely. That “default deny once selected” behaviour is the thing to keep in mind.

A common pattern is to use this on purpose: apply a policy that denies everything in a namespace, then add back only the specific traffic you want.

One last thing: NetworkPolicy is about traffic between pods inside the cluster — east-west traffic. Traffic coming in from the outside world is a separate concern, handled at the edge by the Ingress and Gateway API layer we already covered. Different problem, different layer.

Traffic is controlled now, and we are almost done here, only one small bit to take care of — the traffic between services isn’t encrypted or verified. Anyone who can see it on the network can read it.

Securing that is the last layer: mTLS.

7. mTLS and Service Mesh

Traffic between services isn’t encrypted, and services don’t actually verify who they’re talking to. If someone gets onto the network, they can read what’s flowing between your services — or pretend to be one of them.

To fix this, we borrow the same thing that secures websites: TLS. You’ve seen TLS every time you visit an HTTPS site — it encrypts the traffic, and it proves the server is who it claims to be (that’s the certificate behind the padlock in your browser). But regular TLS only checks one side: the server proves itself to the client, and the client stays anonymous.

Inside a cluster, that’s not enough. When your payment service receives a call, it wants to be sure the caller is really the service it claims to be — not just anyone who happened to reach it. So we use mTLS — mutual TLS. It’s the same as TLS, except both sides prove their identity. The caller presents a certificate too, and the receiver verifies it. Neither side will talk to a peer it can’t confirm.

This is the foundation of “zero trust” between services: nothing is trusted just for being inside the cluster. Every connection has to prove itself, every time.

The catch is that this is a lot of certificates. Every service needs its own, every service has to verify everyone else’s, and all of them need rotating regularly. Doing that by hand across dozens of services isn’t realistic.

So in practice, a service mesh handles it. A service mesh (Istio, Linkerd, or Cilium’s mesh) sits alongside your services and does all the certificate work automatically — issuing a unique certificate to each service, rotating them constantly, and verifying every connection. Your application code doesn’t change at all; the encryption and verification happen underneath it, invisibly.

A service mesh does more than just mTLS — it also handles things like traffic management and observability — but mTLS is one of its core reasons to exist. The tradeoff is that a mesh is a real piece of infrastructure to run, so it’s usually worth it for larger or security-sensitive clusters, and overkill for small simple ones.

And that’s the top of the stack. Traffic is routed, named, controlled, and now encrypted and verified end to end.

8. The thing underneath it all — CNI

We’ve now built the full Kubernetes networking picture. But there’s one thing in the background we never explained — the CNI (Container Network Interface). Why do we need it?

Because Kubernetes doesn’t do pod networking itself.

This surprises people. Something as fundamental as “give the pod an IP and connect it to the network” isn’t handled by Kubernetes core — it hands the whole job to a plugin called a CNI. Cilium, Calico, and Flannel are all CNIs. You pick one when you set up the cluster, and it does the actual work.

In fact, a fresh cluster with no CNI installed doesn’t work — the nodes sit there “NotReady” until you install one, because nothing can wire up pod networking until you do.

Everything in the main arc of this article — Services, DNS, policies — sits on top of the CNI, trusting that the flat network just works. And thanks to the CNI, it does.

9. Recap — the whole picture

Let’s put the whole thing back together, because the point of this article was never the individual pieces — it was how they connect.

Every layer we covered exists to solve a problem the layer below it left open:

  • Every pod gets its own IP — but pods are disposable, and their IPs keep changing.
  • So a Service puts one stable IP in front of them — but that only works inside the cluster.
  • So Ingress (and its modern successor, Gateway API) gives you one smart entry point to expose services to the outside — but it still deals in IP addresses.
  • So DNS puts names in front of the IPs — CoreDNS inside the cluster, ExternalDNS for public domains — but now anything can reach anything.
  • So NetworkPolicy controls which pods are allowed to talk to which — but that traffic still isn’t encrypted or verified.
  • So mTLS, through a service mesh, makes services prove who they are and encrypts what flows between them.

And underneath all of it, the CNI gives every pod its IP in the first place, so the flat network the whole stack relies on actually exists.

That’s the shape. Each layer trusts the stable thing the layer below hands it, and hides the mess underneath. Pods come and go, IPs change, things move constantly — and every piece in this stack is there to put something dependable in front of something that doesn’t stay still.

Once you see it that way, Kubernetes networking stops being a pile of separate concepts to memorise. It’s one design, built up one problem at a time. And that’s the whole thing — the picture I hoped you’d be able to hold in your head.

If you’ve read this far — thank you for following along.

If this article helped you, a share or a connect on LinkedIn means a lot: https://www.linkedin.com/in/josipcloud/

See you in the next one…

--

--

Josip Matijašević
Josip Matijašević

Written by Josip Matijašević

Highly experienced in Infrastructure, Ops, Networking, Cloud, Kubernetes and cloud native solutions. https://www.linkedin.com/in/josipcloud/