OpenShift Routes vs Kubernetes Ingress: A Comprehensive Guide

Ingress in OpenShift (OCP)

OpenShift vs Kubernetes Ingress

OpenShift takes a different approach to ingress than vanilla Kubernetes. It has its own native routing layer that predates Kubernetes Ingress and is more powerful out of the box.

Kubernetes: OpenShift:
───────────────────────── ─────────────────────────
Ingress resource Route resource (native OCP)
+ Ingress Controller + HAProxy Router (built-in)
(you install separately) (pre-installed, managed)
Also supports: Also supports:
Gateway API Kubernetes Ingress
Gateway API
Ingress Operator

OCP Routing Architecture

┌──────────────────────────────────────────────────────────────┐
│ OPENSHIFT CLUSTER │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ INGRESS OPERATOR │ │
│ │ Manages and configures IngressController resources │ │
│ └───────────────────────┬────────────────────────────────┘ │
│ │ manages │
│ ┌───────────────────────▼────────────────────────────────┐ │
│ │ INGRESSCONTROLLER (HAProxy) │ │
│ │ - Default router in openshift-ingress namespace │ │
│ │ - Watches Route + Ingress resources │ │
│ │ - Handles TLS termination │ │
│ │ - Wildcard DNS (*.apps.cluster.domain.com) │ │
│ └───────┬───────────────┬───────────────┬────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ Route A Route B Ingress C │
│ │ │ │ │
│ Service A Service B Service C │
│ │ │ │ │
│ Pods Pods Pods │
└──────────────────────────────────────────────────────────────┘
Wildcard DNS
*.apps.cluster.acme.com

OCP Route — The Native Way

The Route is OpenShift’s own resource — more feature-rich than Kubernetes Ingress and available before Ingress existed.

Basic Route

apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: api-route
namespace: production
spec:
host: api.apps.cluster.acme.com # ← auto-generated if omitted
to:
kind: Service
name: api-service
weight: 100
port:
targetPort: 8080
wildcardPolicy: None

Auto-generated hostname

# If you omit spec.host, OCP generates:
# <route-name>-<namespace>.apps.<cluster-domain>
# Example: api-route-production.apps.cluster.acme.com

Route TLS Modes

OCP Routes have three TLS termination modes — more flexible than Kubernetes Ingress:

Edge Termination:
Client ──HTTPS──▶ Router (terminates TLS) ──HTTP──▶ Pod
(cert lives on router)
Passthrough:
Client ──HTTPS──▶ Router (passes through) ──HTTPS──▶ Pod
(cert lives on pod, router can't inspect)
Re-encrypt:
Client ──HTTPS──▶ Router (terminates TLS) ──HTTPS──▶ Pod
(two separate TLS sessions)

Edge TLS (Most Common)

apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: api-edge-tls
namespace: production
spec:
host: api.apps.cluster.acme.com
to:
kind: Service
name: api-service
port:
targetPort: 8080
tls:
termination: edge # ← TLS at router
insecureEdgeTerminationPolicy: Redirect # HTTP → HTTPS
certificate: | # ← custom cert (optional)
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
key: |
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
caCertificate: | # ← optional CA cert
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----

Passthrough TLS

apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: api-passthrough
namespace: production
spec:
host: api.apps.cluster.acme.com
to:
kind: Service
name: api-service
port:
targetPort: 8443 # ← must be HTTPS port
tls:
termination: passthrough # ← router passes raw TLS

Re-encrypt TLS

apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: api-reencrypt
namespace: production
spec:
host: api.apps.cluster.acme.com
to:
kind: Service
name: api-service
port:
targetPort: 8443
tls:
termination: reencrypt # ← two TLS hops
destinationCACertificate: | # ← verify backend cert
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
insecureEdgeTerminationPolicy: Redirect

Route Traffic Splitting (Blue/Green & Canary)

OCP Routes natively support weighted traffic splitting — no annotations needed:

apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: api-canary
namespace: production
spec:
host: api.apps.cluster.acme.com
to:
kind: Service
name: api-v1 # primary
weight: 90 # ← 90% traffic
alternateBackends:
- kind: Service
name: api-v2 # canary
weight: 10 # ← 10% traffic
port:
targetPort: 8080

Progressive canary:

# Shift traffic gradually using oc CLI
oc set route-backends api-canary \
api-v1=80 api-v2=20
oc set route-backends api-canary \
api-v1=50 api-v2=50
oc set route-backends api-canary \
api-v1=0 api-v2=100
# Full cutover — remove alternate backend
oc patch route api-canary \
--type=json \
-p='[{"op":"remove","path":"/spec/alternateBackends"}]'

Route Annotations (HAProxy Tuning)

metadata:
annotations:
# Timeouts
haproxy.router.openshift.io/timeout: 60s
haproxy.router.openshift.io/timeout-tunnel: 1h
# Load balancing algorithm
haproxy.router.openshift.io/balance: leastconn
# Options: roundrobin | leastconn | source | random
# Rate limiting
haproxy.router.openshift.io/rate-limit-connections: "true"
haproxy.router.openshift.io/rate-limit-connections.concurrent-tcp: "100"
haproxy.router.openshift.io/rate-limit-connections.rate-tcp: "100"
# Sticky sessions
haproxy.router.openshift.io/disable_cookies: "false"
haproxy.router.openshift.io/cookie-name: "ROUTE_SESSION"
# IP whitelisting
haproxy.router.openshift.io/ip_whitelist: "10.0.0.0/8 192.168.1.0/24"
# Response headers
haproxy.router.openshift.io/hsts_header: >
max-age=31536000;includeSubDomains;preload
# Custom headers to backend
haproxy.router.openshift.io/set-forwarded-headers: append
# WebSocket support
haproxy.router.openshift.io/timeout-tunnel: 1h

IngressController Resource

OCP manages HAProxy routers via IngressController CRDs — the Ingress Operator watches these:

apiVersion: operator.openshift.io/v1
kind: IngressController
metadata:
name: default
namespace: openshift-ingress-operator
spec:
# Number of router pods
replicas: 3
# Which domain this controller handles
domain: apps.cluster.acme.com
# Where to expose (LoadBalancer, NodePort, HostNetwork)
endpointPublishingStrategy:
type: LoadBalancerService
loadBalancer:
scope: External # External or Internal
# Which routes this controller handles
routeSelector:
matchLabels:
router: default # only routes with this label
# Which namespaces
namespaceSelector:
matchLabels:
network: public
# TLS security profile
tlsSecurityProfile:
type: Custom
custom:
ciphers:
- ECDHE-RSA-AES256-GCM-SHA384
- ECDHE-RSA-AES128-GCM-SHA256
minTLSVersion: VersionTLS12
# Node placement for router pods
nodePlacement:
nodeSelector:
matchLabels:
node-role.kubernetes.io/infra: ""
tolerations:
- key: node-role.kubernetes.io/infra
effect: NoSchedule

Multiple IngressControllers (Multi-tenant)

Run separate routers for different teams or traffic types:

┌─────────────────────────────────────────────────┐
│ OPENSHIFT CLUSTER │
│ │
│ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ IngressController│ │ IngressController │ │
│ │ (default) │ │ (internal) │ │
│ │ │ │ │ │
│ │ *.apps.acme.com │ │ *.internal.acme.com │ │
│ │ External LB │ │ Internal LB only │ │
│ │ Public routes │ │ Private routes │ │
│ └──────────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────┘
# Internal router — not exposed externally
apiVersion: operator.openshift.io/v1
kind: IngressController
metadata:
name: internal
namespace: openshift-ingress-operator
spec:
domain: internal.apps.cluster.acme.com
replicas: 2
endpointPublishingStrategy:
type: LoadBalancerService
loadBalancer:
scope: Internal # ← internal LB only
routeSelector:
matchLabels:
router: internal # ← only internal routes
nodePlacement:
nodeSelector:
matchLabels:
node-role.kubernetes.io/infra: ""
---
# Route that uses internal router
apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: admin-internal
labels:
router: internal # ← picked up by internal router
spec:
host: admin.internal.apps.cluster.acme.com
to:
kind: Service
name: admin-service

Kubernetes Ingress in OCP

OCP also fully supports standard Kubernetes Ingress resources — the Ingress Operator converts them to Routes automatically:

# Standard Kubernetes Ingress works in OCP
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
namespace: production
annotations:
# OCP-specific annotation to select router
route.openshift.io/termination: "edge"
spec:
ingressClassName: openshift-default
rules:
- host: api.apps.cluster.acme.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
tls:
- hosts:
- api.apps.cluster.acme.com
secretName: api-tls-secret

What happens under the hood:

kubectl apply Ingress
Ingress Operator watches it
Auto-creates equivalent Route
HAProxy Router picks up Route
Traffic flows

cert-manager with OCP Routes

# cert-manager supports OCP Routes natively
# Install the OCP route plugin
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: api-cert
namespace: production
spec:
secretName: api-tls-secret
dnsNames:
- api.apps.cluster.acme.com
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
---
# Route uses the cert secret
apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: api-route
annotations:
cert-manager.io/issuer-name: letsencrypt-prod
cert-manager.io/issuer-kind: ClusterIssuer
spec:
host: api.apps.cluster.acme.com
tls:
termination: edge
# cert-manager injects cert automatically
to:
kind: Service
name: api-service

Gateway API in OCP

OCP 4.12+ supports Gateway API — recommended for new deployments:

# Enable Gateway API in OCP
oc apply -f https://github.com/kubernetes-sigs/gateway-api/releases/
download/v1.1.0/standard-install.yaml
# Install supported controller (e.g. NGINX Gateway Fabric)
helm install ngf oci://ghcr.io/nginxinc/charts/nginx-gateway-fabric \
--namespace nginx-gateway \
--create-namespace
# GatewayClass
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: nginx
spec:
controllerName: gateway.nginx.org/nginx-gateway-controller
---
# Gateway
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: main-gateway
namespace: openshift-ingress
spec:
gatewayClassName: nginx
listeners:
- name: https
port: 443
protocol: HTTPS
tls:
mode: Terminate
certificateRefs:
- name: wildcard-tls
---
# HTTPRoute — same as vanilla Kubernetes
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-route
namespace: production
spec:
parentRefs:
- name: main-gateway
namespace: openshift-ingress
hostnames:
- api.apps.cluster.acme.com
rules:
- backendRefs:
- name: api-service
port: 8080

OCP Ingress Security

Network Policies with Routes

# Allow only router pods to reach your service
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-router
namespace: production
spec:
podSelector:
matchLabels:
app: api-service
ingress:
- from:
- namespaceSelector:
matchLabels:
network.openshift.io/policy-group: ingress

Restrict Route Creation (RBAC)

# Only allow developers to create Routes in their namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: route-creator
namespace: team-a
rules:
- apiGroups:
- route.openshift.io
resources:
- routes
verbs:
- get
- list
- create
- update
- patch
---
# Restrict to specific hostnames via admission webhook
# or use OCP's built-in route admission policy
apiVersion: operator.openshift.io/v1
kind: IngressController
metadata:
name: default
spec:
routeAdmission:
namespaceOwnership: Strict # ← namespace must own the hostname
wildcardPolicy: WildcardsDisallowed

Quick Reference — OCP CLI Commands

# Create a route quickly
oc expose service api-service \
--hostname=api.apps.cluster.acme.com \
--port=8080
# Create TLS edge route
oc create route edge api-tls \
--service=api-service \
--hostname=api.apps.cluster.acme.com \
--cert=api.crt \
--key=api.key
# Create passthrough route
oc create route passthrough api-pass \
--service=api-service \
--hostname=api.apps.cluster.acme.com
# View all routes
oc get routes -A
# Describe route
oc describe route api-route -n production
# Check router pods
oc get pods -n openshift-ingress
# View router logs
oc logs -n openshift-ingress \
deployment/router-default -f
# Check IngressController status
oc get ingresscontroller -n openshift-ingress-operator
oc describe ingresscontroller default \
-n openshift-ingress-operator
# Scale router
oc patch ingresscontroller default \
-n openshift-ingress-operator \
--type=merge \
-p '{"spec":{"replicas":3}}'

Route vs Ingress vs Gateway API in OCP

OCP RouteK8s IngressGateway API
Native to OCP⚠️ Converted to Route❌ Needs controller
TLS modesEdge/Passthrough/ReencryptEdge onlyAll modes
Traffic splitting✅ Native weights❌ Annotations✅ Native
HAProxy tuning✅ Annotations⚠️ Limited⚠️ Controller-specific
Role separation
TCP/gRPC
PortabilityOCP only
Recommended forOCP-native workloadsMigration from K8sNew multi-team setups

Decision Guide for OCP

New OCP cluster, single team?
└──▶ Use OCP Routes — simplest, most native
Migrating from vanilla Kubernetes?
└──▶ Use Kubernetes Ingress — auto-converted to Routes
migrate to Routes over time
Multi-team cluster, need role separation?
└──▶ Use Gateway API with supported controller
Need passthrough TLS (app owns cert)?
└──▶ Use OCP Route with termination: passthrough
Need internal-only routing?
└──▶ Create separate IngressController with Internal LB
label Routes with router: internal
Need canary / blue-green?
└──▶ Use OCP Route alternateBackends (native)
or Argo Rollouts + Route integration

OCP Routes are the most integrated and operationally simple option for OpenShift — they work out of the box with HAProxy, support all TLS modes, and have native traffic splitting. Use Kubernetes Ingress for portability and Gateway API when you need multi-team role separation or advanced L7 features.

Leave a Reply