Ingress & Load Balancing

Duration: 25 minutes
Format: Hands-on ingress configuration

The Scenario

Your apps are deployed and running - but how does external traffic actually reach them? In this module you’ll examine the Ingress Controller, create routes with TLS, and then protect them: rate limiting under load, HTTP-to-HTTPS enforcement, and security headers. You’ll prove each one works by testing before and after.

OpenShift Ingress Load Balancer Flow

Ingress Controller & Load Balancing

The Ingress Controller (based on HAProxy) handles all external traffic entering the cluster.

View Ingress Controller Configuration

oc get ingresscontroller default -n openshift-ingress-operator -o yaml | head -60

Key configuration options:

  • replicas - Number of router pods for HA

  • routeAdmission.wildcardPolicy - Whether wildcard routes are allowed

  • defaultCertificate - TLS certificate for *.apps domain

  • endpointPublishingStrategy - How the router is exposed (HostNetwork, LoadBalancer, NodePort)

View Router Pods

oc get pods -n openshift-ingress -o wide

Router pods handle incoming HTTP/HTTPS traffic. On this cluster they run on the control-plane nodes using HostNetwork, meaning they bind directly to the node’s network interfaces on ports 80 and 443.

You can also view these in the OCP Console at Workloads → Pods. Toggle Show default projects in the project dropdown, then select openshift-ingress:

Router pods in the openshift-ingress namespace

Check Endpoint Publishing Strategy

oc get ingresscontroller default -n openshift-ingress-operator -o jsonpath='{.status.endpointPublishingStrategy}' | jq . 2>/dev/null || oc get ingresscontroller default -n openshift-ingress-operator -o jsonpath='{.status.endpointPublishingStrategy}' | python3 -m json.tool

Routes: OpenShift’s Ingress Resource

OpenShift Routes expose services externally. They’re more feature-rich than Kubernetes Ingress.

Set Up the Application

This section uses a test application. Run the following to ensure the project and app are ready:

# Wait for namespace to be fully removed if it's still terminating from a previous module
while oc get namespace app-management 2>/dev/null | grep -q Terminating; do
  echo "Waiting for app-management namespace to finish deleting..."
  sleep 5
done
oc new-project app-management 2>/dev/null || oc project app-management
oc get deployment weathernow -n app-management 2>/dev/null || oc new-app --name=weathernow --image=registry.access.redhat.com/ubi9/httpd-24
If you see pod status streaming in the terminal, press Ctrl+C to get your prompt back.
oc create route edge weathernow --service=weathernow 2>/dev/null || echo "Route already exists"

Wait for the pod to be ready:

oc rollout status deployment/weathernow -n app-management --timeout=60s
This may take a minute to complete. Only press Ctrl+C if it has been running for more than 5 minutes.

Now let’s examine its networking. In the OCP Console, navigate to Networking → Routes (project: app-management). You’ll see the weathernow route with its status, clickable URL, and target service:

Weathernow route in the console

Click the route name to see the full details including TLS settings and router information:

Weathernow route details showing TLS edge termination

View the Route Configuration

oc get route weathernow -o yaml

Key route fields:

  • spec.host - The external hostname

  • spec.to.name - The backend Service

  • spec.tls.termination - TLS termination type (edge in this case)

Test the Route

Test with HTTPS (edge TLS):

curl -sk https://$(oc get route weathernow -o jsonpath='{.spec.host}') | head -5

This request flows: Client → Ingress Controller → Service → Pod

You should see HTML returned from the web server (the default RHEL test page).

TLS Termination Strategies

OpenShift Routes support three TLS termination types:

Type Description Use Case

edge

TLS terminates at the router, traffic to pod is HTTP

Most common. Router handles certificates.

passthrough

TLS passes through to the pod unchanged

Application manages its own certificates

reencrypt

TLS terminates at router, new TLS to pod

End-to-end encryption with router certificate validation

Examine TLS Configuration

View the TLS settings on the edge route:

oc get route weathernow -o jsonpath='{.spec.tls}' | jq . 2>/dev/null || oc get route weathernow -o jsonpath='{.spec.tls}' | python3 -m json.tool

Edge termination means:

  • TLS terminates at the Ingress Controller (HAProxy)

  • Traffic from router to pod is unencrypted HTTP

  • The router uses the cluster’s wildcard certificate

View Routes Across Cluster

oc get routes -A | head -20

Notice different termination types: edge, passthrough, reencrypt.

In the OCP Console, navigate to Networking → Routes (select All Projects) to see all routes across the cluster:

All routes across the cluster with TLS termination badges

Advanced Configuration

Protect Your Routes

Routes support annotations that control rate limiting, security headers, and HTTP behavior. Let’s apply them and prove they work.

HTTP to HTTPS Redirect

By default, HTTP requests to an edge route get a generic 503 page. In production, you want HTTP to redirect to HTTPS automatically.

Test the current HTTP behavior:

curl -sk -o /dev/null -w "HTTP request: %{http_code}\n" "http://$(oc get route weathernow -o jsonpath='{.spec.host}')"

Now enable the redirect:

oc patch route weathernow -n app-management --type=merge \
  -p '{"spec":{"tls":{"insecureEdgeTerminationPolicy":"Redirect"}}}'
sleep 3
ROUTE_HOST=$(oc get route weathernow -o jsonpath='{.spec.host}')
echo "HTTP request (should 302 redirect):"
curl -sk -o /dev/null -w "  Status: %{http_code} → %{redirect_url}\n" "http://$ROUTE_HOST"
echo "Following the redirect:"
curl -skL -o /dev/null -w "  Status: %{http_code}\n" "http://$ROUTE_HOST"

The 302 confirms HTTP is redirecting to HTTPS. The 403 after following the redirect is the app’s normal response (the default httpd test page returns 403 because there’s no custom content deployed). The important thing is the redirect itself - HTTP traffic is no longer served unencrypted.

The three options for insecureEdgeTerminationPolicy:

  • Redirect - HTTP 302 to HTTPS (recommended)

  • None - reject HTTP entirely (503)

  • Allow - serve content over HTTP too (not recommended)

HSTS (HTTP Strict Transport Security)

HSTS tells browsers to always use HTTPS - even if the user types http://. This prevents downgrade attacks.

Check the current response headers - you should see No HSTS header because we haven’t enabled it yet:

curl -sk -D - "https://$(oc get route weathernow -o jsonpath='{.spec.host}')" 2>&1 | grep -i strict || echo "No HSTS header"

Now apply HSTS:

oc annotate route weathernow -n app-management \
  haproxy.router.openshift.io/hsts_header="max-age=31536000;includeSubDomains" --overwrite
sleep 3
curl -sk -D - "https://$(oc get route weathernow -o jsonpath='{.spec.host}')" 2>&1 | grep -i strict

The strict-transport-security header is now in every response. Browsers that see this will refuse to use HTTP for this domain for one year (max-age=31536000).

Rate Limiting Under Load

Apply rate limiting to restrict concurrent connections:

oc annotate route weathernow -n app-management \
  haproxy.router.openshift.io/rate-limit-connections=true \
  haproxy.router.openshift.io/rate-limit-connections.concurrent-tcp=2 \
  haproxy.router.openshift.io/rate-limit-connections.rate-http=10 \
  --overwrite

The annotations are set - but do they actually work? Let’s prove it. Send 20 concurrent requests:

echo "=== With rate limiting (max 2 concurrent) ==="
ROUTE_HOST=$(oc get route weathernow -o jsonpath='{.spec.host}')
sleep 5
RESULTS=""
for i in $(seq 1 20); do
  curl -sk -o /dev/null -w "%{http_code}\n" "https://$ROUTE_HOST" &
done
wait 2>/dev/null
If the output hangs, press Ctrl+C to get your prompt back.

You’ll see a mix of 403 (request reached the app) and 000 (connection dropped by HAProxy before reaching the app). The 000 responses prove the router is dropping connections beyond the concurrent limit.

The exact ratio of 403 to 000 responses will vary between runs depending on timing and system load.

Now remove rate limiting and try the same test:

oc annotate route weathernow -n app-management \
  haproxy.router.openshift.io/rate-limit-connections- \
  haproxy.router.openshift.io/rate-limit-connections.concurrent-tcp- \
  haproxy.router.openshift.io/rate-limit-connections.rate-http-
echo "=== Without rate limiting ==="
sleep 5
for i in $(seq 1 20); do
  curl -sk -o /dev/null -w "%{http_code}\n" "https://$(oc get route weathernow -o jsonpath='{.spec.host}')" &
done
wait 2>/dev/null
If the output hangs, press Ctrl+C to get your prompt back.

All 403 - every request reaches the app (the httpd default page returns 403, which is expected). No 000 responses means no connections were dropped. Compare this to the previous test where HAProxy was actively dropping connections - that’s the difference one annotation makes.

Ingress Controller Scaling

The ingress controller can be scaled by changing spec.replicas on the IngressController CR. On cloud platforms with LoadBalancer services, adding replicas puts more routers behind the external load balancer for higher throughput.

On this compact cluster, one router per node is already the maximum - scaling up would leave pods Pending (anti-affinity prevents two routers on the same node), and scaling down would drop traffic on the node that loses its router.

echo "Current replicas: $(oc get ingresscontroller default -n openshift-ingress-operator -o jsonpath='{.spec.replicas}')"
echo "Router pods:"
oc get pods -n openshift-ingress -o wide

In production cloud environments, you’d scale with:

# Example - not run on this cluster
oc patch ingresscontroller default -n openshift-ingress-operator --type=merge -p '{"spec":{"replicas":5}}'

You can view ingress controller health in the console at Administration → Cluster Settings → ClusterOperators, then click ingress:

Ingress operator conditions showing Available

Cleanup & Summary

# Remove annotations and reset route
oc annotate route weathernow -n app-management \
  haproxy.router.openshift.io/hsts_header- \
  haproxy.router.openshift.io/rate-limit-connections- \
  haproxy.router.openshift.io/rate-limit-connections.concurrent-tcp- \
  haproxy.router.openshift.io/rate-limit-connections.rate-http- 2>/dev/null
oc patch route weathernow -n app-management --type=merge \
  -p '{"spec":{"tls":{"insecureEdgeTerminationPolicy":""}}}' 2>/dev/null
echo "Cleanup complete"

Summary

What you learned:

  • The Ingress Controller (HAProxy) handles external traffic on HostNetwork

  • Routes expose services with TLS termination options (edge, passthrough, reencrypt)

  • insecureEdgeTerminationPolicy: Redirect enforces HTTPS

  • HSTS headers prevent browser downgrade attacks

  • Rate limiting protects routes from connection floods - and you proved it works

Key operational commands:

# Check ingress controller
oc get ingresscontroller -n openshift-ingress-operator

# View routes
oc get routes -A

# View router pods
oc get pods -n openshift-ingress -o wide

Additional Resources