Understanding Kubernetes Networking, Part 1: The Fundamentals You Need First

Kubernetes is, at its core, a networking problem wearing an orchestration costume. Scheduling pods, managing deployments, scaling replicas — none of that matters if the pieces can’t talk to each other reliably. Before we get into cluster networks, pod networks, CNI plugins, Services, and Network Policies later in this series, we need a shared vocabulary. This first post lays that foundation: the OSI model (the parts that actually matter for containers), physical and virtual network appliances, overlay networks, and the Linux primitives that make container networking possible in the first place.
If you’ve spent years in traditional data center networking, some of this will feel like review — but pay attention to how these concepts get reused and repurposed once you drop them into a containerized world. If you’re coming from an application or DevOps background, this is the layer beneath the YAML that makes kubectl actually work.
Why Start With the OSI Model?
The OSI model gets a bad reputation as an academic artifact — seven layers nobody memorizes correctly after their first networking class. But it survives because it’s still the best shared mental model for reasoning about where a problem lives.
For Kubernetes networking specifically, four layers do almost all the work:
- Layer 2 (Data Link) — Ethernet frames, MAC addresses, switching. This is where bridges, VLANs, and the virtual switches inside your hosts operate.
- Layer 3 (Network) — IP addressing and routing. Pod-to-pod communication across nodes, and most CNI plugins, live here.
- Layer 4 (Transport) — TCP/UDP, ports, and load balancing decisions. Kubernetes Services fundamentally operate at this layer (with kube-proxy doing the heavy lifting via iptables or IPVS).
- Layer 7 (Application) — HTTP/gRPC-aware routing. This is where Ingress controllers and service meshes like Istio start to add value beyond what Layer 4 alone can do.
The reason this matters isn’t trivia — it’s diagnostic. When a pod can’t reach a service, “where in the stack is this actually broken?” is the first question, and the OSI model gives you the checklist: Is it DNS (L7)? Is it a Service/kube-proxy issue (L4)? Is it routing between nodes (L3)? Is it the virtual bridge or veth pair (L2)? Skipping this framework is why so much container network troubleshooting turns into random guessing.
Physical and Virtual Network Appliances
Every concept you know from physical networking has a virtual counterpart inside a hypervisor or a Kubernetes node, and understanding the physical version first makes the virtual one click.
Switches forward frames based on MAC addresses within a broadcast domain. In a Kubernetes node, this role is played by a Linux bridge (or an Open vSwitch instance) — a virtual switch living entirely in the kernel, connecting pods on the same host the same way a physical switch connects servers in a rack.
Routers move packets between different IP subnets, using routing tables to decide next hops. In Kubernetes, this is exactly what happens when a pod on Node A talks to a pod on Node B — some component (the CNI plugin, cloud provider routes, or a control-plane-managed route table) has to know how to get a packet from one pod subnet to another.
Firewalls enforce access control based on packet attributes. Physically, this might be a dedicated appliance or a router ACL. In Kubernetes, this maps directly to iptables/nftables rules on each node, and at a higher level of abstraction, to Network Policies — which we’ll cover in depth later in this series.
Load balancers distribute traffic across multiple backend destinations. Physical or virtual load balancers (F5, NSX, cloud LBs) map to Kubernetes Services (ClusterIP/NodePort/LoadBalancer) and Ingress controllers.
The pattern here is consistent: Kubernetes doesn’t invent new networking primitives. It automates and abstracts primitives that have existed in data center networking for decades, and re-implements most of them in software running on each node.
Overlay Networks: Solving the “Too Many Subnets” Problem
Here’s the core problem overlay networks solve: Kubernetes wants every pod, cluster-wide, to have its own routable IP address, and it wants that to work regardless of the underlying physical network topology, cloud provider, or on-prem fabric. Your physical network team is not going to let you inject arbitrary new subnets into their routing tables for every Kubernetes cluster you spin up.
An overlay network solves this by encapsulating pod traffic inside packets that the underlying (physical/”underlay”) network already knows how to route. The pod’s original packet — source pod IP, destination pod IP — gets wrapped inside a new packet addressed between the nodes, which the underlay network handles like any other traffic. When it arrives at the destination node, it gets unwrapped, and the original pod-to-pod packet is delivered locally.
VXLAN is the most common encapsulation protocol used for this in Kubernetes (Flannel’s VXLAN backend, Calico’s VXLAN mode, and others all use it). It wraps Layer 2 Ethernet frames inside UDP packets, which is why it’s often described as “Layer 2 over Layer 3” — you get a flat, pod-addressable network overlaid on top of a purely routed underlay.
The trade-off is encapsulation overhead (extra header bytes, and some CPU cost for encap/decap) versus the flexibility of not touching the physical network’s routing configuration at all. Some CNI plugins offer non-overlay (“native routing”) modes as an alternative — worth knowing that overlay isn’t the only option, just the most portable one.

The underlay network only ever sees traffic between node IPs — it has no idea pod subnets exist. Encapsulation and decapsulation happen entirely at the node level.
Container Networking Fundamentals
This is where it all gets implemented. Everything above is context; this is the actual mechanism.
Network namespaces are the Linux kernel feature that makes container networking possible at all. A network namespace gives a process its own isolated network stack — its own interfaces, routing table, ARP table, and iptables rules, completely separate from the host’s. Every container (and every pod, at the pod-sandbox level in Kubernetes) gets its own network namespace. This is why two containers can each bind to port 8080 without conflicting — they’re not sharing a network stack at all.
veth pairs (virtual Ethernet pairs) are how a namespace gets connected to the outside world. A veth pair is exactly what it sounds like: two virtual interfaces that act as a connected pipe — anything sent into one end comes out the other. One end gets placed inside the container’s network namespace, the other end stays on the host and gets attached to a bridge. This is the physical (well, virtual) plumbing between an isolated container and the rest of the network.
The Linux bridge on the host is where those veth pairs land. It behaves like an actual Layer 2 switch, forwarding frames between all the containers on that host based on MAC address — and providing the uplink to the host’s own network interface for anything that needs to leave the node.

Everything above is happening entirely inside one node. The pod never sees the bridge directly — it just has an interface (eth0) that happens to be one end of a pipe terminating on the bridge.
Put together, the flow looks like this: a container generates a packet inside its network namespace → it exits through its side of the veth pair → arrives at the host-side veth end, attached to the bridge → the bridge either switches it directly to another local container, or forwards it out toward the host’s physical/virtual NIC for anything off-host.
In Kubernetes specifically, this whole per-container pattern gets shifted to the pod level — all containers in a pod share a single network namespace (via a pause/sandbox container), which is exactly why containers in the same pod can reach each other over localhost. The CNI plugin’s job, which we’ll dig into in Part 2, is to set up this networking for every pod: allocate an IP, create the veth pair, attach it to the bridge (or an overlay equivalent), and program routes so that pod can be reached from anywhere else in the cluster.
Try It Yourself: Building This by Hand
Everything above is easier to internalize once you’ve built it with your own commands instead of reading about it. This lab reproduces the pod-networking mechanics from scratch on two plain Ubuntu VMs — no Kubernetes, no CNI plugin, just ip commands — so you can see exactly what a CNI plugin is automating for you later in this series.
The topology: each VM gets two network namespaces (standing in for two containers), each connected through a veth pair to a local Linux bridge, with the bridge’s host uplinking through eth0 to a shared physical switch connecting both VMs.

Setup on Ubuntu1 (172.16.0.0/24, eth0: 192.168.0.10)
# Create two network namespaces — these stand in for two containers
sudo ip netns add ns1
sudo ip netns add ns2
# Create the veth pairs. Each pair has one end that stays on the host
# (veth10, veth20) and one end that moves into a namespace (veth11, veth21).
sudo ip link add veth11 type veth peer name veth10
sudo ip link add veth21 type veth peer name veth20
# Move the container-side end of each pair into its namespace
sudo ip link set veth11 netns ns1
sudo ip link set veth21 netns ns2
# Assign an IP to each namespace's interface and bring it up
sudo ip netns exec ns1 ip addr add 172.16.0.2/24 dev veth11
sudo ip netns exec ns1 ip link set veth11 up
sudo ip netns exec ns1 ip link set lo up
sudo ip netns exec ns2 ip addr add 172.16.0.3/24 dev veth21
sudo ip netns exec ns2 ip link set veth21 up
sudo ip netns exec ns2 ip link set lo up
# Create the bridge and give it the .1 address for this subnet
sudo ip link add br0 type bridge
sudo ip addr add 172.16.0.1/24 dev br0
sudo ip link set br0 up
# Attach the host-side veth ends to the bridge and bring them up
sudo ip link set veth10 master br0
sudo ip link set veth20 master br0
sudo ip link set veth10 up
sudo ip link set veth20 up
# Allow the host to forward packets between interfaces
sudo sysctl -w net.ipv4.ip_forward=1
Setup on Ubuntu2 (172.16.1.0/24, eth0: 192.168.1.11)
Identical steps, using the 172.16.1.0/24 addressing from the diagram:
sudo ip netns add ns1
sudo ip netns add ns2
sudo ip link add veth11 type veth peer name veth10
sudo ip link add veth21 type veth peer name veth20
sudo ip link set veth11 netns ns1
sudo ip link set veth21 netns ns2
sudo ip netns exec ns1 ip addr add 172.16.1.2/24 dev veth11
sudo ip netns exec ns1 ip link set veth11 up
sudo ip netns exec ns1 ip link set lo up
sudo ip netns exec ns2 ip addr add 172.16.1.3/24 dev veth21
sudo ip netns exec ns2 ip link set veth21 up
sudo ip netns exec ns2 ip link set lo up
sudo ip link add br0 type bridge
sudo ip addr add 172.16.1.1/24 dev br0
sudo ip link set br0 up
sudo ip link set veth10 master br0
sudo ip link set veth20 master br0
sudo ip link set veth10 up
sudo ip link set veth20 up
sudo sysctl -w net.ipv4.ip_forward=1
Verifying What Works — and What Doesn’t Yet
On Ubuntu1, same-host namespace-to-namespace traffic works immediately, because it never leaves the bridge:
# ns1 reaching the bridge itself
sudo ip netns exec ns1 ping -c 3 172.16.0.1
# ns1 reaching ns2, across the bridge
sudo ip netns exec ns1 ping -c 3 172.16.0.3
Both should succeed — this is Part 1’s namespace/veth/bridge mechanism working exactly as described above, just with real commands instead of a diagram. Now try reaching across VMs:
# From ns1 on Ubuntu1, try to reach ns1 on Ubuntu2
sudo ip netns exec ns1 ping -c 3 172.16.1.2
This one fails, and it’s worth understanding precisely why: Ubuntu1’s kernel has no route for the 172.16.1.0/24 network at all — it only knows about 172.16.0.0/24, the subnet directly attached to its own bridge. Ubuntu1 and Ubuntu2 are on the same physical switch and can reach each other’s eth0 IPs (192.168.0.10 and 192.168.1.11) just fine, but neither host has any idea how to get a packet from its own bridge subnet to the other host’s bridge subnet.
You can fix this by hand, and doing so previews exactly what Part 2’s Flannel host-gw backend automates for you:
# On Ubuntu1: route Ubuntu2's pod subnet via Ubuntu2's eth0
sudo ip route add 172.16.1.0/24 via 192.168.1.11 dev eth0
# On Ubuntu2: route Ubuntu1's pod subnet via Ubuntu1's eth0
sudo ip route add 172.16.0.0/24 via 192.168.0.10 dev eth0
Re-run the cross-VM ping and it should now succeed. What you just did by hand — giving every host a route to every other host’s local pod subnet, using the physical network as the next hop — is precisely the mechanism Flannel’s host-gw backend programs automatically across an entire cluster, and it’s why that backend requires all nodes to sit on the same flat L2 segment: the via address in those routes only resolves if the physical switch can actually deliver an ARP request between 192.168.0.10 and 192.168.1.11 directly, exactly like it just did here.
If you want to see the VXLAN alternative from earlier in this post instead of a flat host-gw route, this same two-VM setup is a reasonable place to hand-build a flannel.1-style VXLAN interface and compare — but that’s a good exercise to attempt on your own once Part 2 has walked through what Flannel automates either way.
Where This Goes Next
With this vocabulary in place — OSI layers as a debugging framework, physical-to-virtual appliance mapping, overlay networking as the answer to “how do all these pods get routable IPs without touching the physical fabric,” and namespaces/veth pairs/bridges as the actual Linux mechanics — the rest of the series builds directly on top of it:
- Part 2: The pod network, the CNI specification, and the Flannel CNI plugin
- Part 3: Calico CNI in depth
- Part 4: Kubernetes Services
- Part 5: Intro to Network Policies
- Part 6: Calico Network Policies
Each of those posts assumes you’re comfortable with everything above. If any of it felt shaky, this is worth a second pass before moving on — nearly every “why can’t this pod reach that service” debugging session eventually traces back to one of these fundamentals.