Sitemap

Kubernetes Networking Deep Dive: Pod Communication, DNS, Ingress, and NetworkPolicy

12 min readMay 14, 2026

--

Everything you need to know about how traffic flows inside and outside a Kubernetes cluster — with real YAML from a running TaskManager app

Networking is the part of Kubernetes that confuses most people. Pods talking to pods, services resolving by name, traffic entering from the internet, and security policies blocking unwanted connections — it all feels like magic until you understand the model underneath.

This article breaks down four core networking concepts with real examples from a Spring Boot + MySQL + React app running on Minikube:

  • Pod-to-pod communication — how pods talk to each other
  • DNS and service discovery — how pods find each other by name
  • Ingress and Ingress controllers — how external traffic enters the cluster
  • NetworkPolicy — how to control who can talk to whom

Part 1: Pod-to-Pod Communication

The flat network model

Kubernetes enforces three golden rules for networking, regardless of which underlying network plugin you use:

Rule 1 — Every pod gets its own unique IP address
Rule 2 — Pods communicate directly — no NAT (Network Address Translation)
Rule 3 — Any pod can reach any other pod anywhere in the cluster

This is called the flat network model. From any pod’s perspective, every other pod is directly reachable by IP — just like machines on the same LAN, even if they are on completely different physical nodes.

How a pod gets its IP

When Kubernetes schedules a pod onto a node, this happens automatically:

Scheduler assigns pod to Node 1
↓
kubelet calls the CNI plugin (Calico / Flannel / Cilium)
↓
CNI creates a veth pair — a virtual ethernet cable
one end lives inside the pod's network namespace
one end connects to the node's bridge (cbr0)
↓
CNI assigns an IP from that node's subnet
Node 1 pods → 192.168.1.0/24
Node 2 pods → 192.168.2.0/24
↓
Pod is now reachable at that IP from anywhere in the cluster

Same-node communication — direct via bridge

When your frontend pod (192.168.1.10) calls your backend pod (192.168.1.11) and both are on the same node:

Frontend pod
↓ packet to 192.168.1.11
veth pair out of pod namespace
↓
cbr0 bridge (virtual switch on the node)
↓ destination is on same bridge — direct delivery
veth pair into backend pod namespace
↓
Backend pod receives the packet
No network hops. Latency: microseconds.

Cross-node communication — via cluster network

When your backend pod (192.168.1.11) on Node 1 calls your MySQL pod (192.168.2.10) on Node 2:

Backend pod (Node 1)
↓ packet to 192.168.2.10
veth pair → cbr0 bridge on Node 1
↓ destination not on this node's bridge
Node 1 routing table → 192.168.2.0/24 is on Node 2
↓
Physical / virtual cluster network between nodes
↓
Node 2 routing table → 192.168.2.10 is here
↓
cbr0 bridge on Node 2 → veth pair
↓
MySQL pod receives the packet
One network hop. Latency: sub-millisecond on same AZ.

CNI plugins — who does the actual work

The CNI (Container Network Interface) plugin implements the three golden rules. Kubernetes defines the contract, CNI delivers it:

CNI Plugin Mechanism Best for Calico BGP routing Production, NetworkPolicy Flannel VXLAN overlay Simple clusters, learning Cilium eBPF High performance, advanced policy WeaveNet Gossip protocol Encrypted by default

Get Laxman Pajjuri’s stories in your inbox

Join Medium for free to get updates from this writer.

Your Minikube cluster uses Calico. Your pod YAML never changes regardless of which CNI you use — the networking is completely transparent to your application.

See it in action

# See actual pod IPs assigned by CNI
kubectl get pods -n taskmanager -o wide
# NAME IP NODE
# backend-xxx 192.168.1.11 minikube
# frontend-xxx 192.168.1.10 minikube
# mysql-pod 192.168.1.12 minikube
# Ping between pods directly by IP
kubectl exec -it <frontend-pod> -n taskmanager -- \
ping 192.168.1.11
# See the node's network bridges and veth pairs
minikube ssh -- bridge link show
minikube ssh -- ip route show

Part 2: DNS and Service Discovery

The problem DNS solves

Pod IPs change every time a pod restarts, scales, or gets rescheduled. You can never hardcode pod IPs in your application. This is why Services exist — and DNS is how you find them.

Without DNS:  backend connects to 192.168.1.12:3306 ← changes on restart
With DNS: backend connects to mysql-service:3306 ← never changes

CoreDNS — the DNS server inside K8s

CoreDNS runs as a Deployment inside the kube-system namespace. Its ClusterIP (usually 10.96.0.10) is automatically injected into every pod's /etc/resolv.conf as the nameserver.

# See CoreDNS running
kubectl get pods -n kube-system | grep coredns
# See CoreDNS Service — this is the DNS server IP
kubectl get svc -n kube-system | grep kube-dns
# kube-dns ClusterIP 10.96.0.10 53/UDP,53/TCP

What is inside every pod’s DNS config

# Shell into your backend pod
kubectl exec -it <backend-pod> -n taskmanager -- cat /etc/resolv.conf
# Output:
nameserver 10.96.0.10 ← CoreDNS
search taskmanager.svc.cluster.local \ ← search these domains
svc.cluster.local \ in order
cluster.local
options ndots:5

The search line is why mysql-service works as a short name — K8s automatically appends each search domain and tries to resolve until it succeeds.

Full DNS resolution flow

When your Spring Boot backend calls jdbc:mysql://mysql-service:3306/taskdb:

1. Pod calls: mysql-service
↓
2. Pod asks CoreDNS at 10.96.0.10:
"What is the IP for mysql-service?"
↓
3. CoreDNS searches:
mysql-service.taskmanager.svc.cluster.local → found! 10.96.1.25
↓
4. CoreDNS returns 10.96.1.25 to the pod
↓
5. Pod connects to 10.96.1.25:3306
MySQL Service receives the connection and routes to the MySQL pod

FQDN structure — every service gets one

Every Kubernetes Service gets a DNS entry following this exact pattern:

<service-name>.<namespace>.svc.cluster.local
Your TaskManager services:
mysql-service.taskmanager.svc.cluster.local → 10.96.1.25
backend-service.taskmanager.svc.cluster.local → 10.96.2.10
frontend-service.taskmanager.svc.cluster.local → 10.96.3.15

You can use short forms within the same namespace:

# All these work from inside the taskmanager namespace:
mysql-service ← shortest (same ns)
mysql-service.taskmanager ← with namespace
mysql-service.taskmanager.svc ← with type
mysql-service.taskmanager.svc.cluster.local ← full FQDN
# From a DIFFERENT namespace — must include namespace at minimum:
mysql-service.taskmanager

application.properties uses short DNS name

# Spring Boot just uses the short service name
# CoreDNS resolves it to the actual ClusterIP
spring.datasource.url=jdbc:mysql://mysql-service:3306/taskdb

Headless Service — DNS for StatefulSets

A regular Service DNS returns the ClusterIP (virtual IP). A headless Service returns actual pod IPs directly. StatefulSets use this so each pod gets its own stable DNS name:

apiVersion: v1
kind: Service
metadata:
name: mysql-headless
namespace: taskmanager
spec:
clusterIP: None # ← headless — no virtual IP
selector:
app: mysql
ports:
- port: 3306

With headless service each StatefulSet pod gets its own DNS entry:

mysql-0.mysql-headless.taskmanager.svc.cluster.local → pod-IP-1
mysql-1.mysql-headless.taskmanager.svc.cluster.local → pod-IP-2
mysql-2.mysql-headless.taskmanager.svc.cluster.local → pod-IP-3

This is how MySQL replicas find each other. mysql-0 is always the primary — stable name, stable identity, regardless of what IP the pod gets.

Verify DNS in your cluster

# Run a temporary DNS test pod
kubectl run dns-test --image=busybox:1.36 --rm -it \
--restart=Never -n taskmanager \
-- sh
# Inside the pod
nslookup mysql-service
# Server: 10.96.0.10
# Name: mysql-service.taskmanager.svc.cluster.local
# Address: 10.96.1.25 ← the ClusterIP
nslookup backend-service.taskmanager
nslookup google.com # external DNS also works via CoreDNS forward
exit

Part 3: Ingress and Ingress Controllers

Why Ingress exists — the cost problem

Without Ingress every service that needs external access requires its own cloud load balancer:

Without Ingress:
frontend → LoadBalancer → $0.02/hr
backend → LoadBalancer → $0.02/hr
monitoring → LoadBalancer → $0.02/hr
Total: 3 load balancers ≈ $43/month
With Ingress:
ONE LoadBalancer → Ingress Controller → routes to all services
Total: 1 load balancer ≈ $14/month

Ingress also gives you host-based routing, path-based routing, TLS termination, and redirects — none of which a basic Service can provide.

Ingress vs Ingress Controller — they are different things

Ingress resource    =  the RULES you write in YAML
Ingress Controller = the thing that READS and ENFORCES those rules
Without an Ingress Controller your Ingress YAML does nothing.
The rules exist but nobody reads them.

How traffic flows with Ingress

Internet request: GET http://taskmanager.local/api/tasks
↓
Cloud Load Balancer (or NodePort on Minikube)
↓
Ingress Controller pod (Nginx running in cluster)
↓ reads Ingress rules
path /api → backend-service:8080
path / → frontend-service:80
↓
Routes to correct Service
↓
Service routes to healthy Pod
↓
Response returns same path

Full Ingress YAML for TaskManager

# ingress-taskmanager.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: taskmanager-ingress
namespace: taskmanager
annotations:
# Which Ingress Controller handles this
nginx.ingress.kubernetes.io/rewrite-target: /
# Redirect HTTP to HTTPS automatically
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx # use Nginx Ingress Controller
# TLS — HTTPS termination at the Ingress
tls:
- hosts:
- taskmanager.local
secretName: taskmanager-tls # Secret with cert and key
rules:
- host: taskmanager.local
http:
paths:
# /api → Spring Boot backend
- path: /api
pathType: Prefix
backend:
service:
name: backend-service
port:
number: 8080
# / → React frontend (catches everything else)
- path: /
pathType: Prefix
backend:
service:
name: frontend-service
port:
number: 80

Install Nginx Ingress on Minikube

# Minikube has it built in
minikube addons enable ingress
# Wait for controller to be Running (~2 minutes)
kubectl get pods -n ingress-nginx -w
# Verify ingress class
kubectl get ingressclass
# NAME CONTROLLER
# nginx k8s.io/ingress-nginx

Apply and test on Minikube

kubectl apply -f ingress-taskmanager.yaml
# Check ingress created
kubectl get ingress -n taskmanager
# NAME CLASS HOSTS ADDRESS PORTS
# taskmanager-ingress nginx taskmanager.local 192.168.49.2 80
# Add to hosts file (Linux/Mac)
echo "$(minikube ip) taskmanager.local" | sudo tee -a /etc/hosts
# Test routing
curl http://taskmanager.local/ # → frontend HTML
curl http://taskmanager.local/api/tasks # → Spring Boot JSON

Path types — important difference

# Exact — only this exact path
pathType: Exact
path: /api/tasks # matches ONLY /api/tasks
# does NOT match /api/tasks/1
# Prefix — path and everything under it
pathType: Prefix
path: /api # matches /api
# matches /api/tasks
# matches /api/tasks/1/comments

Host-based routing — multiple apps on one cluster

rules:
# App 1 — TaskManager
- host: taskmanager.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend-service
port:
number: 80
  # App 2 — Grafana on the same cluster
- host: grafana.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: grafana-service
port:
number: 3000

Two completely different apps, one Ingress Controller, one load balancer. Traffic is routed by the Host HTTP header.

Popular Ingress Controllers

Controller Best for Nginx Ingress General purpose, Minikube, most teams Traefik Auto-discovery, microservices AWS ALB EKS on AWS — native cloud integration Istio Gateway Service mesh, advanced traffic management HAProxy Very high performance, financial services

Part 4: NetworkPolicy — Kubernetes Firewall

The default is wide open

By default every pod in Kubernetes can talk to every other pod with no restrictions. Your frontend can directly query MySQL. A compromised pod can reach every service in the cluster.

No NetworkPolicy:
frontend pod → mysql pod ← allowed (risky!)
any pod → any pod ← allowed (very risky!)

NetworkPolicy is a firewall rule for pods. It says “this pod can only receive traffic from X and can only send traffic to Y.”

Start with deny-all

Best practice is to lock everything down first then explicitly allow only what is needed:

# 01-deny-all.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
namespace: taskmanager
spec:
podSelector: {} # empty = selects ALL pods in namespace
  policyTypes:
- Ingress # block all incoming
- Egress # block all outgoing
# No rules = deny everything
# This is intentional
kubectl apply -f 01-deny-all.yaml
# Every pod is now completely isolated
# Nothing can reach anything
# Now selectively open what is needed

Allow frontend → backend only

# 02-allow-frontend-to-backend.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: taskmanager
spec:
# This policy protects the backend pod
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
# Only pods with this label can call backend
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080

Allow backend → MySQL only

# 03-allow-backend-to-mysql.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-to-mysql
namespace: taskmanager
spec:
# This policy protects the MySQL pod
podSelector:
matchLabels:
app: mysql
policyTypes:
- Ingress
ingress:
- from:
# Only backend pods can reach MySQL
- podSelector:
matchLabels:
app: backend
ports:
- protocol: TCP
port: 3306

Allow backend egress — including DNS

Backend needs to make outgoing calls. Always include a DNS rule or nothing resolves:

# 04-allow-backend-egress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-egress
namespace: taskmanager
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Egress
egress:
# Allow calling MySQL
- to:
- podSelector:
matchLabels:
app: mysql
ports:
- protocol: TCP
port: 3306
# CRITICAL - allow DNS resolution
# Without this nothing resolves by name
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
# Allow external HTTPS calls (payment APIs etc)
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8 # except internal range
ports:
- protocol: TCP
port: 443

Apply all policies in order

kubectl apply -f 01-deny-all.yaml
kubectl apply -f 02-allow-frontend-to-backend.yaml
kubectl apply -f 03-allow-backend-to-mysql.yaml
kubectl apply -f 04-allow-backend-egress.yaml
# Allow Ingress controller to reach frontend
cat << 'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress-to-frontend
namespace: taskmanager
spec:
podSelector:
matchLabels:
app: frontend
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- port: 80
EOF

Verify NetworkPolicy is enforced

# See all policies
kubectl get networkpolicy -n taskmanager
# Test frontend CAN reach backend (should succeed)
kubectl exec -it <frontend-pod> -n taskmanager -- \
wget -qO- http://backend-service:8080/api/tasks
# Test frontend CANNOT reach MySQL (should fail/timeout)
kubectl exec -it <frontend-pod> -n taskmanager -- \
nc -zv mysql-service 3306
# Expected: connection timed out or refused
# Test backend CAN reach MySQL (should succeed)
kubectl exec -it <backend-pod> -n taskmanager -- \
nc -zv mysql-service 3306
# Expected: Connection to mysql-service 3306 port [tcp] succeeded

Three selector types in NetworkPolicy

ingress:
- from:
# 1 — podSelector: allow specific pods by label
- podSelector:
matchLabels:
app: frontend
# 2 - namespaceSelector: allow entire namespace
- namespaceSelector:
matchLabels:
env: production
# 3 - ipBlock: allow external IP range
- ipBlock:
cidr: 10.0.0.0/8
except:
- 10.1.0.0/16 # except this subnet

Most common mistake — forgetting DNS egress

# WRONG — pod cannot resolve hostnames!
egress:
- to:
- podSelector:
matchLabels:
app: mysql
ports:
- port: 3306
# RIGHT - always add DNS alongside other egress rules
egress:
- to:
- podSelector:
matchLabels:
app: mysql
ports:
- port: 3306
- to: # DNS must be a separate rule
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53

Without the DNS rule mysql-service cannot be resolved to an IP and every connection fails — even though the MySQL policy looks correct.

NetworkPolicy requires a supporting CNI

NetworkPolicy only works if the CNI plugin enforces it:

CNI NetworkPolicy Calico ✅ Full support Cilium ✅ Full support + extras WeaveNet ✅ Full support Flannel ❌ No support — policies apply but do nothing

Putting it all together — your TaskManager traffic flow

User opens http://taskmanager.local
↓
DNS resolves taskmanager.local → Minikube IP (via /etc/hosts)
↓
Nginx Ingress Controller receives request
↓ Ingress rule: / → frontend-service:80
NetworkPolicy: ingress-nginx namespace allowed → frontend pod
↓
Frontend pod serves React HTML/JS
↓
Browser runs React, calls /api/tasks
↓
Nginx Ingress: /api → backend-service:8080
NetworkPolicy: frontend pod allowed → backend pod
↓
Spring Boot backend receives request
↓
Backend calls jdbc:mysql://mysql-service:3306/taskdb
CoreDNS resolves mysql-service → 10.96.1.25 (ClusterIP)
NetworkPolicy: backend pod allowed → mysql pod
↓
MySQL pod processes query, returns data
↓
Response travels back the same path
↓
User sees their tasks

Quick reference

Press enter or click to view image in full size

Concept: What it does Your TaskManager example Pod network Flat L3 network, every pod has unique IP All pods reachable cluster-wide CNI plugin Implements pod networking Calico on Minikube CoreDNS Resolves service names to ClusterIPs mysql-service → 10.96.1.25 Short DNS name Works within same namespace mysql-service Full FQDN Works cross-namespace mysql-service.taskmanager.svc.cluster.local Headless service DNS returns pod IPs directly StatefulSet MySQL replicas Ingress resource HTTP routing rules /api → backend, / → frontend Ingress Controller Enforces Ingress rules Nginx in ingress-nginx namespace NetworkPolicy Pod firewall frontend → backend → mysql only deny-all Block everything, allow selectively First policy applied

Conclusion

Kubernetes networking feels complex but follows a clean mental model once you see it clearly:

  • Flat pod network — any pod talks to any pod by IP, CNI makes it work
  • DNS — CoreDNS resolves service names so you never hardcode IPs
  • Ingress — one entry point routes all external HTTP traffic by host and path
  • NetworkPolicy — firewall rules that lock down which pods can talk to which

The most important takeaway: your application code never deals with any of this. Spring Boot calls mysql-service:3306. React calls /api/tasks. Kubernetes handles everything in between — name resolution, routing, load balancing, and security.

All examples in this article come from a real TaskManager app (React + Spring Boot + MySQL) running on Minikube on Windows. Every command was tested against a live cluster.

If this helped you understand K8s networking, leave a clap — it helps more people find this.

--

--

Laxman Pajjuri
Laxman Pajjuri

Senior Performance Test Engineer | DevOps Enthusiast | Java | JMeter | AWS | Kubernetes | Cloud | SRE & AI Learner