Skip to content

5.1. Networking

When to Look at This

  • When a deployed application cannot be reached
  • When checking the external access address
  • When communication between services is blocked

Why Pod Addresses Are a Problem

A pod receives a new IP address each time it is created. Recreating it changes the address, and increasing replicas produces several addresses.

SituationThe pod address
The pod restartsIt changes
A new version is deployedThey all change
Three replicasThree addresses
Moved to another nodeIt changes

So pods must not be called by address directly. The caller would have to discover the address every time, and decide where to send when there are several.

Kubernetes solves this with Services.

How the Network Resources Relate

How the network resources relate

What a Service Is

It is the resource that gives a group of pods a fixed name and address.

Creating a Service produces three things.

What you getDescription
A fixed IP (ClusterIP)This address stays the same even as pods change
A DNS nameYou can call it by name instead of by address
Load balancingWith several pods behind it, traffic is distributed

The caller only has to know the Service name. It does not have to care how many pods are behind it, which node they are on, or whether one just restarted.

The DNS naming rules are as follows.

Where you call fromThe name to use
The same namespace<service-name>
A different namespace<service-name>.<namespace>
The full name<service-name>.<namespace>.svc.cluster.local

How a Service Finds Pods

A Service does not designate pods by name. It selects them by label.

How a Service selects pods

Because of this, the Service configuration does not have to change when pods appear or disappear. Anything with matching labels joins the target set automatically.

Conversely, one character of difference in a label means no connection. Half of all connection problems start here.

Service Types

TypeAccess scopeWhen to use it
ClusterIPInside the cluster onlyThe default. For internal services calling each other
NodePortFrom outside, on a specific port of a nodeTemporary checks, environments without a load balancer
LoadBalancerThrough an external load balancerCloud environments
ExternalNameForwards to an external addressCalling a system outside the cluster by name

Web services usually use the ClusterIP plus Ingress combination.

NodePort uses ports in the 30000-32767 range. You have to remember the port number, and the access address changes when the node address changes, so it is not recommended beyond temporary checks.

The Service List

Go to Network > Services.

Service list
ColumnDescription
NameThe Service name. This is the name used to call it inside the cluster
NamespaceThe namespace it belongs to
TypeHow it is exposed
Cluster IPThe internal cluster address
PortsThe receiving port and the port it forwards to

Service Detail

Selecting the name opens the detail screen.

Service detail
ItemWhat to check
SelectorsWhich labeled pods to send to. It must match the pod labels exactly
PortsThe receiving port (port) and the pod's port (targetPort)
EndpointsThe list of pod addresses actually connected

It helps to know why there are two ports.

NameMeaning
portThe port the Service receives on. The number the caller uses
targetPortThe port the pod actually opens

The two can differ. For example, callers use 80 from outside while the application opens 8080. If targetPort differs from the container's actual port, there is no connection.

Empty endpoints mean traffic has nowhere to go. In that case, compare the selector with the pod labels (see the pod detail in 3.1).

What an Endpoint Is

It is the list of pod addresses a Service actually sends traffic to. Kubernetes creates and manages it automatically.

You can also view them separately under Network > Endpoints.

A pod drops out of the endpoints if it is not Ready. This is the key mechanism of zero-downtime deployment. A new pod that is not ready yet receives no traffic and joins the list once it is.

Readiness is determined by the readiness probe (see 3.1).

SituationEndpoints
The pod is readyIt joins
The readiness probe is failingIt drops out
The pod is being deletedIt drops out
There is no readiness probe at allIt joins as soon as the container starts

If a pod is up but not in the endpoints, its readiness probe is failing. Check on the pod detail that the probe path and port are correct.

Conversely, with no readiness probe traffic arrives the moment the container starts. If the application is still starting, those requests fail. If brief errors appear on every deployment, suspect this case.

What an Ingress Is

It is the rule connecting incoming external HTTP requests to internal Services.

External exposure is possible with Services alone (NodePort, LoadBalancer), but there are limits.

ProblemHow Ingress solves it
Each service needs a port or a load balancerMany services share one entry point
Cannot be distinguished by domain nameSends to different Services per host
Cannot be split by pathSends /api and /web to different Services
HTTPS configured per serviceHandled once at the Ingress

The Difference Between Ingress and Service

ServiceIngress
Layer it handlesTCP/UDP portsHTTP requests (host and path)
Basis for decisionsPort numberDomain name and URL path
HTTPS handlingNoneHandled by attaching a certificate
What it needsNothingAn Ingress controller must be installed

An Ingress is only a rule. What actually handles traffic is the Ingress controller installed in the cluster. Without a controller, creating an Ingress does nothing.

COP installs the HAProxy Ingress Controller. Its class name is default, and the whole cluster shares this one controller.

COP's Ingress Controller Defaults

These values are decided at installation. They apply as they are when you create an Ingress, so knowing them narrows problems quickly.

ItemValueMeaning
Class namedefaultThe value to write in the Ingress ingressClassName
Cluster defaultYesThis controller handles it even without ingressClassName
PlacementOne per nodeIt accepts traffic to any node address
Receiving ports80 · 443It uses the node's ports directly
Default certificatewildcard-tls in kube-systemHTTPS works even without TLS in the Ingress
HTTPS redirectOn (to 443)See "Allowing Access Over HTTP" below
X-Forwarded-ForOnSee "Knowing the Client's Address" below
X-Forwarded-ProtoFixed to httpsSee the same section
Header buffer size16KBSee below

Four Things Worth Knowing

HTTPS works even without TLS in the Ingress. A default certificate (wildcard-tls) is configured, so HTTPS access is open even if you omit spec.tls. However, going outside the address range that certificate covers makes the browser warn. If you use a separate domain, obtain a certificate and write it in spec.tls (see 7.3).

It works even without ingressClassName, because it is designated the cluster default class. Writing it is still recommended — if controllers are added later, it becomes impossible to tell where an Ingress without it will go.

One controller is up on every node. The same rules apply no matter which node address traffic arrives at. If one node goes down, service continues through another node.

Large response headers cause 502 errors. The header buffer is 16KB. Applications that put long authentication tokens in cookies can exceed this limit. If the pod is fine but the browser shows 502, suspect this and ask operations staff. This value cannot be changed per Ingress; it is a cluster-wide setting.

The Ingress List

Go to Network > Ingresses.

Ingress list
ColumnDescription
NameThe Ingress name
ClassThe Ingress controller that will handle it
HostThe external access address
PathThe connection rules per path after the address

IngressClass is the label that decides which controller handles it when several are in use. COP uses only default, and it is designated the default, so you do not have to specify it.

Ingress Detail

ItemDescription
RulesWhich Service and port to send to, per host and path
TLSThe name of the certificate Secret for HTTPS
AnnotationsSettings that adjust controller behavior

Important settings go in the annotations. COP's HAProxy controller uses names beginning with haproxy.org/. The main ones are these.

AnnotationWhat it does
haproxy.org/path-rewriteRewrites a request arriving at /api/users to /users
haproxy.org/ssl-redirectRedirects HTTP to HTTPS (see below)
haproxy.org/route-aclSends only requests matching a condition to this Service. Used for canary deployment (see 4.8)
haproxy.org/timeout-serverExtends the time limit for slow services
haproxy.org/load-balanceSets how requests are distributed among the pods behind it

For certificate issuance, see 7.3. If the Secret specified in TLS does not exist, HTTPS access fails.

Allowing Access Over HTTP

COP enables HTTPS redirect cluster-wide. Every request arriving over HTTP is redirected to HTTPS. That is right in most cases, but sometimes a particular service has to accept HTTP.

WhenDescription
An internal-only APICalled only inside the cluster, where attaching a certificate is cumbersome
Older clients that cannot handle HTTPSPrograms that cannot follow a redirect
HTTPS already terminated in frontA separate appliance handles encryption and forwards in plain text

Attaching the annotation to just that Ingress is enough. Other Ingresses keep redirecting.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: internal-api
namespace: my-app
annotations:
haproxy.org/ssl-redirect: "false" # allows HTTP for this Ingress only
spec:
ingressClassName: default
rules:
- host: internal-api.apps.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: internal-api
port:
number: 8080

There are three related annotations, and all can be attached to an Ingress.

AnnotationWhat it doesValue
haproxy.org/ssl-redirectTurns the redirect on or off"true" · "false"
haproxy.org/ssl-redirect-portThe port to redirect to443 by default
haproxy.org/ssl-redirect-codeThe response code301 · 302 (default) · 303

Wrap values in quotes. Annotation values are strings, so writing false without quotes is rejected on apply.

Remove spec.tls as well. An Ingress with a TLS section has the redirect on even without the annotation. To accept HTTP only, drop the tls section and set the annotation to "false".

HTTP does not encrypt its contents. Do not use it for services exchanging sign-in or personal information. Even on an internal network, the contents are visible to anyone on that network.

Knowing the Client's Address (X-Forwarded-For)

The address the application sees is not the real user's address. Because the request comes through the Ingress controller, what connected, from the application's point of view, is the controller.

Where the client address changes

So access logging, address-based access restriction, and region detection all see the same one address.

The solution is the X-Forwarded-For header. When forwarding the request, the controller writes the original user's address into the header. The application reads that header instead of the connection address.

COP's Defaults

It is on without any configuration. It applies to every Ingress.

HeaderThe value it carriesState
X-Forwarded-ForThe user's real addressOn
X-Forwarded-ProtoThe scheme the user used (https)On

It is worth knowing about X-Forwarded-Proto as well. The controller receives HTTPS and forwards to the pod over HTTP, so an application looking only at the connection scheme mistakes it for HTTP. Reading this header is how it knows the user connected over HTTPS.

There is a reason COP fixes this value to https. The feature that returns users to their original screen after sign-in builds the address from this value. Without it, an address beginning http:// is generated and sent, and the browser returns over HTTPS with the sign-in information lost. SSO sign-in repeating endlessly is the classic symptom (see 1.1).

Reading It in the Application

Each framework has its own setting for reading this header. It has to be turned on for the value to be used in place of the connection address.

FrameworkSetting
Spring Bootserver.forward-headers-strategy=NATIVE
TomcatAdd RemoteIpValve to the server configuration
Node.js (Express)app.set('trust proxy', true)
Nginx (inside the container)set_real_ip_from · real_ip_header X-Forwarded-For

There Can Be Several Values

X-Forwarded-For appends in the order it passed through. With another appliance in front, there are several values.

X-Forwarded-For: 203.0.113.5, 198.51.100.20
↑ the real user ↑ intermediate appliance

The first one is the real user. Most frameworks handle this for you, but if you read it directly, use the first value.

Do not use this value for security decisions as it stands. The header can be set arbitrarily by the user. To use it for access restriction or authentication, the setup has to strip and refill this header ahead of the controller, which is a matter to discuss with operations staff. Access logging and statistics are fine.

Turning It Off

Rarely needed, but if you must, turn it off with an annotation on the Ingress or Service.

metadata:
annotations:
haproxy.org/forwarded-for: "false"

What a Network Policy Is

By default, every pod in the cluster can communicate with every other. Different namespaces are not blocked.

A network policy restricts this. It defines "this pod accepts requests only from pods like these".

View them under Network > Network Policies.

Network policy list
DirectionDescription
IngressAllow rules for incoming traffic
EgressAllow rules for outgoing traffic

Allowed targets are specified in three ways.

MethodDescription
Pod labelsPods with particular labels
Namespace labelsAll pods in particular namespaces
IP rangeAddress ranges outside the cluster

Creating Them — YAML

Service

apiVersion: v1
kind: Service
metadata:
name: my-app
namespace: my-app
spec:
type: ClusterIP
selector:
app: my-app # sends to pods with this label
ports:
- name: http
port: 80 # the port used when calling the Service
targetPort: 8080 # the port the container actually opens
protocol: TCP
FieldDescription
selectorMust match the pod labels exactly. One difference and the target set is empty
portThe number other pods use when calling, as in http://my-app:80
targetPortThe number the container actually listens on. A name can also be used

port and targetPort are easy to confuse. Swapped, the Service looks fine but simply does not connect. Pods do appear on the Endpoint screen, which makes the cause hard to find.

Ingress

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app
namespace: my-app
spec:
ingressClassName: default # the COP default — the HAProxy controller handles it
tls:
- hosts:
- app.example.com
secretName: app-tls-secret # the Secret holding the certificate (see [7.3](/docs/cop-user-guide/cert-manager))
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app # the Service name created above
port:
number: 80 # the Service's port (not targetPort)
FieldDescription
ingressClassNameWhich Ingress controller handles it. For COP it is default
pathTypePrefix matches everything starting with that path; Exact matches exactly
backend.service.port.numberWrite the Service's port. Not the container port

You can send different paths to different services.

paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-service
port:
number: 8080
- path: /
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80

Write longer paths first. Writing / first can send /api requests there too.

Network Policy

The first thing to create is a policy that opens DNS. As explained in "Pitfalls" below, adding even one policy switches that pod to allow-list mode, so without DNS open, all communication by name is cut off.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: my-app
spec:
podSelector: {} # every pod in this namespace
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53

podSelector: {} is correct as an empty value. It means "no condition", so every pod in that namespace is the target.

Then open the communication you actually need. Below is an example letting only pods labeled web reach my-app on port 8080.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-web-to-app
namespace: my-app
spec:
podSelector:
matchLabels:
app: my-app # what to protect
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: web # web pods in the same namespace
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: frontend # the whole frontend namespace
ports:
- protocol: TCP
port: 8080

Entries in the from list are "or". The example above accepts requests from web pods or from the frontend namespace.

Two conditions in one entry become "and". Below allows only web pods in the frontend namespace — note where the list marker (-) sits.

from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: frontend
podSelector: # in the same entry — both must be satisfied
matchLabels:
app: web

This difference is one character of YAML with a very different result. It either opens wider than intended or lets nothing through.

Before Adding a Policy

  1. Add the DNS policy first.
  2. Try the policy on just one target pod (narrow the podSelector).
  3. Confirm communication works, then widen the scope.
  4. Keep the policy YAML so you can revert.

Network Policy Pitfalls

Once even one policy applies to a pod, all traffic that policy does not allow is blocked.

With no policy everything is allowed, but the moment one exists it switches to allow-list mode. Not knowing this leads to adding one policy and cutting off the whole service.

If communication breaks after adding a policy, check whether you missed the following.

Easy to missDescription
DNS lookupsPort 53 in the kube-system namespace. Blocked, all communication by name fails
Probeskubelet sends them from the node, so they are not caught by pod labels
Monitoring collectionThe metric collector has to reach the pods
The outbound directionOpening only Ingress and forgetting Egress blocks external API calls

Diagnostic Order When a Connection Fails

Checking in this order finds the cause in most cases.

OrderWhat to checkWhere
1Whether the pod is Running and readyWorkloads > Pods (see 3.1)
2Whether the Service's endpoints are non-emptyService detail
3Whether the Service's targetPort matches the container portService detail, pod detail
4Whether the Ingress host and path match the requestIngress detail
5Whether a network policy is blocking itNetwork Policies
6For HTTPS, whether the certificate is readyCert-manager (see 7.3)

Step 2 is where it stops most often. One character of difference between the Service selector and the pod labels means no connection.

To check from inside a pod, call another service from the web terminal (see 3.1). If the name fails but the IP works, it is a DNS problem; if both fail, it is a network policy or port problem.