Skip to content

Kubernetes Networking Diagram: Pods, Services, CNI and Network Policies

How Kubernetes networking works, layer by layer: pod IPs and CNI plugins, overlays, Services and kube-proxy, CoreDNS, NetworkPolicy and the Gateway API.

· Founder & developer

Published

Two worker node frames. Users reach a Gateway that routes to a web pod on worker-1. The web pod looks up the api Service in CoreDNS, then connects to its ClusterIP, which leads to two api pods on worker-2. A NetworkPolicy selects them; a VXLAN tunnel joins the nodes.

A pod in the shop namespace calls http://api and the request times out. The pod is running, the Service exists, and its EndpointSlice lists two ready backends. The fault could be in DNS, in the rules kube-proxy wrote on the node, in the overlay between nodes, or in a NetworkPolicy someone applied last week. Each is a separate layer, run by a separate component.

This post walks through those layers with a diagram: the network model, pod-to-pod traffic, CNI plugins, Services, DNS, NetworkPolicy, and traffic in and out of the cluster.

The Kubernetes network model

The Kubernetes network model has three rules. Every pod gets its own cluster-wide IP address, shared by its containers. Every pod can reach every other pod, on any node, without proxies or NAT. And node agents, such as the kubelet, can reach all pods on their node.

Kubernetes defines the APIs, and components you install do the work:

Layer What it gives you Implemented by
Pod network An IP per pod, pod-to-pod reachability A CNI plugin
Services A stable virtual IP and name for a set of pods kube-proxy or an eBPF replacement
DNS Names such as api.shop.svc.cluster.local CoreDNS
NetworkPolicy Allow rules for pod traffic The CNI plugin
Ingress, Gateway API Routing from outside the cluster A controller you install

That split explains many surprises, such as a NetworkPolicy that the API server accepts but your plugin silently ignores.

Pod to pod on the same node

When the container runtime starts a pod, it calls the CNI plugin with ADD. Most plugins create a veth pair, a virtual cable with eth0 inside the pod and the other end on the host, and assign the IP. Flannel plugs the host end into a Linux bridge, cni0; Calico skips the bridge: each pod gets a /32 route, and the host acts as its router. Either way, same-node traffic needs no encapsulation and no NAT.

Pod to pod across nodes: overlay or native routing

Each node usually owns a slice of the pod range. With kube-controller-manager's default --node-cidr-mask-size of 24, a 10.244.0.0/16 range gives 256 nodes a /24 each, 256 addresses for the kubelet's default of 110 pods. Across nodes, the plugin has two options:

Overlay (VXLAN, Geneve, IP-in-IP) Native routing (BGP, cloud routes, VPC IPs)
Network between nodes sees Node IPs only Pod IPs
Requires IP reachability between nodes A network that routes pod ranges, or VPC addresses for pods
Overhead per packet 50 bytes for IPv4 VXLAN, 20 for IP-in-IP None
Examples Flannel and Cilium defaults, Calico VXLAN Calico with BGP, AWS VPC CNI, Azure CNI

The overhead is an MTU problem: on a 1,500-byte network, pods behind IPv4 VXLAN need an MTU of 1,450, per Calico's MTU guide. Get it wrong and large packets vanish, which looks like random hangs.

CNI plugins compared

Plugin Cross-node traffic NetworkPolicy Notes
Flannel VXLAN by default (UDP 8472); host-gw on a shared layer 2 network Not enforced by Flannel; add kube-network-policies or Calico Small and simple
Calico BGP, or VXLAN (UDP 4789) or IP-in-IP overlay Full, plus Calico's namespaced and global policies iptables, nftables, eBPF, VPP and Windows data planes
Cilium VXLAN by default (UDP 8472), Geneve, or native routing Full, plus layer 7 and DNS-based rules eBPF data path
AWS VPC CNI No overlay; pods get VPC addresses Since v1.14, with enableNetworkPolicy Pods per node limited by the instance type

The AWS limit bites early: the default maximum is ENIs × (IPs per ENI − 1) + 2, so an m5.large with 3 interfaces of 10 addresses runs 3 × 9 + 2 = 29 pods. Prefix delegation raises it.

Services: a stable IP in front of changing pods

Pod IPs change whenever a pod is replaced. A Service gives a set of pods one stable virtual IP, the ClusterIP, and a DNS name; EndpointSlices track the pods behind it.

Type What you get
ClusterIP (default) A virtual IP inside the cluster
NodePort Also a port on every node, 30000–32767 by default
LoadBalancer Also a cloud or add-on load balancer
ExternalName A DNS alias (CNAME), no proxying
Headless (clusterIP: None) No virtual IP; DNS returns the pod IPs

No single machine owns a ClusterIP. On every node, kube-proxy programs the kernel so that a packet to 10.96.48.20:80 is rewritten (destination NAT) to a pod address such as 10.244.2.22:8080 before it leaves the client's node. The Service is a logical step, not a server. The rules depend on the proxy mode:

Mode Status in Kubernetes 1.37 How it works
iptables Default on Linux Rules per Service and endpoint; random backend
nftables GA since 1.33; the planned future default Faster updates at scale; needs kernel 5.13 or later
ipvs Deprecated since 1.35; off by default in 1.40, removed in 1.43 Kernel IPVS tables, more balancing algorithms
eBPF (Cilium, Calico) Replaces kube-proxy Load balancing in eBPF programs

Set the mode explicitly so an upgrade cannot change it, and plan a move off IPVS. In nftables mode, NodePorts listen only on the node's primary addresses by default.

DNS with CoreDNS

CoreDNS is the default cluster DNS, and kubeadm runs it behind a Service still named kube-dns. Each Service gets a name, <service>.<namespace>.svc.cluster.local, that resolves to its ClusterIP, or to the pod IPs for a headless Service. The kubelet writes each pod's /etc/resolv.conf; in shop on a kubeadm cluster it typically reads:

nameserver 10.96.0.10
search shop.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

The catch is ndots:5: a name with fewer than five dots, such as api.stripe.com, is tried with every search domain first, so one external lookup can cost three failed queries. Add a trailing dot to hot names, lower ndots in the pod's dnsConfig, or run NodeLocal DNSCache. External names then take the path shown in how DNS works.

NetworkPolicy: from default allow to least privilege

By default every pod accepts traffic from everywhere. Once any NetworkPolicy selects a pod for ingress (or egress), only what some policy allows gets through; replies are allowed automatically. Policies only add allow rules, so order never matters. Start each namespace with a default deny:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: shop
spec:
  podSelector: {}
  policyTypes:
  - Ingress

Then open what you need, here TCP 8080 from web to api:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-allow-web
  namespace: shop
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: web
    ports:
    - protocol: TCP
      port: 8080

Four details catch people out:

  • The port is the pod's port, 8080, not the Service port 80.
  • The default deny isolates the web pods too, so the Gateway needs its own rule to reach them.
  • One dash changes the meaning. A namespaceSelector and a podSelector in one from entry must both match; as two entries, either one is enough.
  • Default-deny egress blocks DNS, so allow port 53 to kube-dns first.

NetworkPolicy has no deny rules, no cluster-wide scope and nothing for TLS; for mutual TLS between workloads, see the service mesh architecture diagram.

Traffic in and out: Ingress, Gateway API and egress

Ingress is stable but frozen, and the Kubernetes project recommends the Gateway API instead. The community Ingress NGINX controller was retired in March 2026 and gets no more security fixes, yet existing installs keep running unnoticed.

The Gateway API splits the job by role: platform owners run the Gateway, with its listeners and certificates, and application teams attach HTTPRoutes from their own namespaces. It reached GA in October 2023, and the 2026 releases made TLSRoute, TCPRoute and UDPRoute standard too. The Kubernetes ingress diagram follows that path hop by hop.

Egress is the forgotten direction. Traffic leaving the cluster is usually translated to the node's IP, as the AWS VPC CNI does for destinations outside the VPC. Limit it with egress rules, which see outside destinations only as IP ranges, DNS-aware rules such as Cilium's toFQDNs, or an egress gateway when a partner needs a fixed source IP.

The Kubernetes networking diagram

Two worker node frames. Users reach a Gateway that routes to a web pod on worker-1. The web pod looks up the api Service in CoreDNS, then connects to its ClusterIP, which leads to two api pods on worker-2. A NetworkPolicy selects them; a VXLAN tunnel joins the nodes.

15 components · 9 connections

Open in Archboard
Diagram contents
  • worker-1 · pods 10.244.1.0/24
  • worker-2 · pods 10.244.2.0/24
  • Users
  • public-gw
  • web · 10.244.1.12
  • coredns · 10.244.1.5
  • api · 10.244.2.21
  • api · 10.244.2.22
  • api · 10.96.48.20
  • api-allow-web
  • HTTPS 443
  • HTTPRoute
  • DNS: api.shop.svc.cluster.local via kube-dns 10.96.0.10
  • TCP 10.96.48.20:80
  • DNAT 10.244.2.21:8080
  • DNAT 10.244.2.22:8080
  • VXLAN overlay over UDP · node IP to node IP
One request through a two-node cluster: in through a Gateway, a DNS lookup, a ClusterIP that kube-proxy rewrites to a pod IP, a VXLAN hop to the other node and a NetworkPolicy that decides who may connect. Each layer fails in its own way.

How to read it:

  • Two worker nodes, each labeled with its /24 pod range.
  • Blue is north-south: users reach public-gw on HTTPS 443, and an HTTPRoute sends them to the web pod.
  • Amber is DNS: web asks CoreDNS, through kube-dns at 10.96.0.10, for api.shop.svc.cluster.local and gets 10.96.48.20.
  • Teal is the Service path: web connects to 10.96.48.20:80, and kube-proxy rules on worker-1 rewrite that to an api pod on port 8080.
  • Gray is the overlay: the packet crosses to worker-2 inside VXLAN, between node IPs.
  • Red is policy: api-allow-web lets only app=web pods reach the api pods.
  • The Markdown block lists the defaults behind the numbers.

Drawing Kubernetes networking diagrams clearly

  • Label nodes with pod ranges and pods with IPs, the details you search logs for.
  • Draw the Service as a logical step and note where the rewrite happens.
  • Label edges with address, port and protocol, switching to the pod port after the Service.
  • Color north-south and east-west differently, and draw the overlay once, between nodes.
  • Attach each policy to the pods it selects, with the allowed source.
  • Leave out the kubelet, the API server and extra replicas; the Kubernetes architecture diagram covers the control plane.
  • Common mistakes: kube-proxy in the packet path (it writes rules; the kernel forwards), no DNS, and a policy on a plugin that ignores it.

Build it in Archboard

  1. Sign in with Google or GitHub and create a diagram. The free Kubernetes YAML to diagram tool turns existing manifests into a first draft.
  2. Add two Frame nodes from the Core pack for worker-1 and worker-2, labeled with their pod ranges.
  3. From the Kubernetes pack, add four Pod nodes with their IPs: web and coredns in worker-1, two api pods in worker-2.
  4. Add a Service (ClusterIP, port 80 to 8080) between the frames, with labeled edges from web and to both api pods.
  5. Add a NetworkPolicy selecting app=api, with dotted edges to the api pods.
  6. Add an Internet node from the Network pack and a Gateway with an HTTPS 443 listener, and chain them to the web pod.
  7. Draw a dashed DNS edge from web to coredns and a thick two-way VXLAN edge between the frames.
  8. Add a Markdown block from the Core pack with the defaults, and short Text notes.

The Kubernetes use case page lists more objects. The example uses 11 diagram nodes, within the Free plan.

FAQ

How do pods communicate across nodes in Kubernetes? Through the CNI plugin: an overlay wraps pod packets between node IPs, or native routing carries pod addresses directly. Pods see each other's real IPs either way.

What is the difference between a pod IP and a ClusterIP? A pod IP belongs to one pod and changes when the pod is replaced. A ClusterIP is a Service's stable virtual IP, which kube-proxy or an eBPF replacement rewrites to a current pod IP.

Which kube-proxy mode should I use? In Kubernetes 1.37, iptables is the default, nftables has been GA since 1.33 and will become the default, and IPVS is deprecated. On kernel 5.13 or later, nftables is the better long-term choice.

Do NetworkPolicies work with every CNI plugin? No. The API accepts them everywhere, but only plugins that implement NetworkPolicy enforce them: Calico, Cilium and the AWS VPC CNI with network policy turned on do; Flannel alone does not.

Should I use Ingress or the Gateway API? The Gateway API for new work: Kubernetes recommends it, and Ingress is frozen. Existing Ingress resources keep working, but plan to move off Ingress NGINX, which no longer gets security fixes.

Start free — one seat, no credit card.

Related posts

← Back to blog

Open a canvas in a minute

Sign in with Google or GitHub. Free for a single seat. Upgrade when the team joins.