Kubernetes Networking Deep Dive: Pod Communication, DNS, Ingress, and NetworkPolicy
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 clusterThis 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 clusterSame-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 packetNo 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 packetOne 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 changesCoreDNS — 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/TCPWhat 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:5The 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 podFQDN structure — every service gets one
Every Kubernetes Service gets a DNS entry following this exact pattern:
<service-name>.<namespace>.svc.cluster.localYour 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.15You 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.taskmanagerapplication.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/taskdbHeadless 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: 3306With 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-3This 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 ClusterIPnslookup backend-service.taskmanager
nslookup google.com # external DNS also works via CoreDNS forwardexit
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/monthWith Ingress:
ONE LoadBalancer → Ingress Controller → routes to all services
Total: 1 load balancer ≈ $14/monthIngress 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 rulesWithout 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 pathFull 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/commentsHost-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: 3000Two 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 namespacepolicyTypes:
- 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 neededAllow 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: 8080Allow 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: 3306Allow 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: 443Apply 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
EOFVerify 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] succeededThree 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 subnetMost 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: 53Without 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 tasksQuick reference
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.







