Print or save as PDF

Choose “Save as PDF” as the destination in your browser's print dialog.

Back

UXAtom Learn · Kubernetes

Kubernetes Networking

Services, Ingress and network policy — how a packet actually reaches a pod.

Updated August 6, 2026 · 45 min · 3 pages

Summary

Kubernetes networking rests on one rule: every pod gets its own IP and can reach every other pod without NAT. Everything above that — Services, Ingress, NetworkPolicy — exists because pod IPs are not stable and not exposed. Services give you a stable virtual IP and load balancing. Ingress puts an HTTP router in front of Services so many hostnames share one entry point. NetworkPolicy takes the default-allow flat network and narrows it down. Get those three roles clear and the failure modes stop being mysterious.

Contents

  1. 01Services
  2. 02Ingress
  3. 03Network policies

Services

A pod IP is an unstable identifier. Pods are rescheduled, replaced on deploy, and scaled in and out. Anything that hard-codes a pod IP breaks on the first restart.

A Service is a stable name and virtual IP in front of a changing set of pods.

How the selector works

A Service does not reference pods directly. It declares a label selector, and a controller keeps a matching EndpointSlice up to date:

apiVersion: v1
kind: Service
metadata:
  name: payments
spec:
  selector:
    app: payments
  ports:
    - port: 80
      targetPort: 8080

Any pod carrying app: payments and passing its readiness probe is added to the endpoint list. Any pod that fails readiness is removed. This is the reconciliation model again: the Service is a declaration, the endpoint list is reality, and a controller closes the gap.

The three types

TypeAllocatesReachable from
ClusterIPA virtual IP inside the clusterInside the cluster only
NodePortA ClusterIP plus the same port on every nodeAnything that can reach a node
LoadBalancerA NodePort plus an external load balancerThe internet, via the cloud provider

The types are cumulative, not alternatives. A LoadBalancer Service still has a ClusterIP and still has a node port — the cloud controller simply provisions an external balancer that points at those node ports.

What ClusterIP really is

The ClusterIP is not assigned to any interface. Nothing answers ARP for it. It exists only as a set of rules in each node's kernel — iptables or IPVS entries installed by kube-proxy — that rewrite the destination address to a real pod IP as the packet leaves.

That has a practical consequence worth internalising:

Load balancing granularity

kube-proxy picks an endpoint per connection, not per request. For HTTP/1.1 with connection reuse, and especially for HTTP/2 and gRPC where a single long-lived connection carries every request, this means traffic pins to one pod.

This is why gRPC services behind a plain ClusterIP often show badly skewed load. The fix is client-side load balancing, a proxy that understands HTTP/2, or a service mesh — not a different Service type.

Headless Services

Setting clusterIP: None disables the virtual IP entirely. DNS then returns the pod IPs directly, one A record per ready endpoint:

spec:
  clusterIP: None
  selector:
    app: cassandra

This is what StatefulSets use. When each replica is individually addressable and identity matters — database members, brokers, anything with a quorum — you want the caller to see the real topology rather than a single virtual IP hiding it.

Ingress

A LoadBalancer Service gives you one external IP per Service. Thirty services means thirty load balancers and thirty bills. Ingress exists to collapse that into one entry point that routes by hostname and path.

The resource is only a declaration

This is the part that catches people out. An Ingress object is inert. It describes routing rules; it does not implement them. Without an ingress controller running in the cluster — ingress-nginx, Traefik, HAProxy, a cloud-native one — creating an Ingress does exactly nothing.

A minimal rule set

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: storefront
spec:
  ingressClassName: nginx
  rules:
    - host: shop.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api
                port:
                  number: 80
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80

ingressClassName decides which controller claims this object. With two controllers installed and no class set, either both claim it or neither does — both outcomes are confusing to debug.

pathType matters more than it looks

ValueMatches
ExactThe path string exactly, case-sensitive
PrefixSplit on /, element by element
ImplementationSpecificWhatever the controller decides

Prefix compares path elements, not characters. /api matches /api and /api/orders, but not /apiary. That distinction surprises people expecting a plain string prefix.

ImplementationSpecific is where portability quietly dies — an ingress-nginx regex path will not survive a move to a different controller.

TLS

spec:
  tls:
    - hosts:
        - shop.example.com
      secretName: shop-tls

The Secret must be type kubernetes.io/tls, must live in the same namespace as the Ingress, and must already exist. A missing Secret does not block the Ingress from being created — it just serves the controller's default self-signed certificate, which looks like a certificate problem rather than a missing-object problem.

When Ingress is not enough

Ingress only models HTTP and HTTPS. Raw TCP, UDP, gRPC routing rules and weighted traffic splits all sit outside the spec, which is why every controller grew its own annotations — and why those annotations do not port between controllers.

The Gateway API is the successor that models these properly, with separate resources for infrastructure and routing. New clusters should look at it before committing to controller-specific annotations.

Network policies

By default, every pod in a cluster can reach every other pod, in any namespace. The flat network is a deliberate simplification — and a poor security posture the moment you run more than one team's workloads.

NetworkPolicy narrows it.

Selecting turns on default-deny

A NetworkPolicy does not add rules to a permissive baseline. The moment any policy selects a pod, that pod switches to default-deny for the directions the policy mentions, and only the listed rules are permitted.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: payments-ingress
spec:
  podSelector:
    matchLabels:
      app: payments
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: checkout
      ports:
        - port: 8080

Payments now accepts traffic from checkout on 8080 and nothing else. Egress is untouched, because policyTypes does not list it.

Namespace selectors need labels

namespaceSelector matches on namespace labels, not names. Namespaces have no useful labels by default beyond kubernetes.io/metadata.name, which the control plane sets automatically:

ingress:
  - from:
      - namespaceSelector:
          matchLabels:
            kubernetes.io/metadata.name: monitoring

Combining namespaceSelector and podSelector in a single from entry is an AND. Listing them as two entries is an OR. One character of YAML indentation separates "Prometheus pods in the monitoring namespace" from "anything in monitoring, plus Prometheus pods anywhere".

The CNI has to implement it

Like Ingress, the object is only a declaration. Calico, Cilium and Antrea enforce policies. Flannel, on its own, does not.

A sensible starting point

Deny everything inbound in a namespace, then open specific paths:

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

An empty podSelector selects every pod in the namespace. With no ingress rules, nothing is allowed in. Layer permissive policies on top per service — policies are additive, so any rule that allows traffic wins over the baseline denial.

Kubernetes Networking — uxatom.com/learn/en/courses/networking