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/v1kind: Routemetadata: name: api-route namespace: productionspec: 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/v1kind: Routemetadata: name: api-edge-tls namespace: productionspec: 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/v1kind: Routemetadata: name: api-passthrough namespace: productionspec: 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/v1kind: Routemetadata: name: api-reencrypt namespace: productionspec: 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/v1kind: Routemetadata: name: api-canary namespace: productionspec: 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 CLIoc set route-backends api-canary \ api-v1=80 api-v2=20oc set route-backends api-canary \ api-v1=50 api-v2=50oc set route-backends api-canary \ api-v1=0 api-v2=100# Full cutover — remove alternate backendoc 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/v1kind: IngressControllermetadata: name: default namespace: openshift-ingress-operatorspec: # 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 externallyapiVersion: operator.openshift.io/v1kind: IngressControllermetadata: name: internal namespace: openshift-ingress-operatorspec: 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 routerapiVersion: route.openshift.io/v1kind: Routemetadata: name: admin-internal labels: router: internal # ← picked up by internal routerspec: 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 OCPapiVersion: networking.k8s.io/v1kind: Ingressmetadata: 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 pluginapiVersion: cert-manager.io/v1kind: Certificatemetadata: name: api-cert namespace: productionspec: secretName: api-tls-secret dnsNames: - api.apps.cluster.acme.com issuerRef: name: letsencrypt-prod kind: ClusterIssuer---# Route uses the cert secretapiVersion: route.openshift.io/v1kind: Routemetadata: name: api-route annotations: cert-manager.io/issuer-name: letsencrypt-prod cert-manager.io/issuer-kind: ClusterIssuerspec: 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 OCPoc 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
# GatewayClassapiVersion: gateway.networking.k8s.io/v1kind: GatewayClassmetadata: name: nginxspec: controllerName: gateway.nginx.org/nginx-gateway-controller---# GatewayapiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata: name: main-gateway namespace: openshift-ingressspec: gatewayClassName: nginx listeners: - name: https port: 443 protocol: HTTPS tls: mode: Terminate certificateRefs: - name: wildcard-tls---# HTTPRoute — same as vanilla KubernetesapiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: name: api-route namespace: productionspec: 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 serviceapiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-router namespace: productionspec: 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 namespaceapiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: route-creator namespace: team-arules:- 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 policyapiVersion: operator.openshift.io/v1kind: IngressControllermetadata: name: defaultspec: routeAdmission: namespaceOwnership: Strict # ← namespace must own the hostname wildcardPolicy: WildcardsDisallowed
Quick Reference — OCP CLI Commands
# Create a route quicklyoc expose service api-service \ --hostname=api.apps.cluster.acme.com \ --port=8080# Create TLS edge routeoc create route edge api-tls \ --service=api-service \ --hostname=api.apps.cluster.acme.com \ --cert=api.crt \ --key=api.key# Create passthrough routeoc create route passthrough api-pass \ --service=api-service \ --hostname=api.apps.cluster.acme.com# View all routesoc get routes -A# Describe routeoc describe route api-route -n production# Check router podsoc get pods -n openshift-ingress# View router logsoc logs -n openshift-ingress \ deployment/router-default -f# Check IngressController statusoc get ingresscontroller -n openshift-ingress-operatoroc describe ingresscontroller default \ -n openshift-ingress-operator# Scale routeroc patch ingresscontroller default \ -n openshift-ingress-operator \ --type=merge \ -p '{"spec":{"replicas":3}}'
Route vs Ingress vs Gateway API in OCP
| OCP Route | K8s Ingress | Gateway API | |
|---|---|---|---|
| Native to OCP | ✅ | ⚠️ Converted to Route | ❌ Needs controller |
| TLS modes | Edge/Passthrough/Reencrypt | Edge only | All modes |
| Traffic splitting | ✅ Native weights | ❌ Annotations | ✅ Native |
| HAProxy tuning | ✅ Annotations | ⚠️ Limited | ⚠️ Controller-specific |
| Role separation | ❌ | ❌ | ✅ |
| TCP/gRPC | ❌ | ❌ | ✅ |
| Portability | OCP only | ✅ | ✅ |
| Recommended for | OCP-native workloads | Migration from K8s | New multi-team setups |
Decision Guide for OCP
New OCP cluster, single team? └──▶ Use OCP Routes — simplest, most nativeMigrating from vanilla Kubernetes? └──▶ Use Kubernetes Ingress — auto-converted to Routes migrate to Routes over timeMulti-team cluster, need role separation? └──▶ Use Gateway API with supported controllerNeed passthrough TLS (app owns cert)? └──▶ Use OCP Route with termination: passthroughNeed internal-only routing? └──▶ Create separate IngressController with Internal LB label Routes with router: internalNeed 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.