# Red Hat OpenShift: Enterprise Kubernetes Platform — Part 3: Networking, Routes & OpenShift Service Mesh

> **Series:** Red Hat OpenShift: Enterprise Kubernetes Platform (3 of 5)
> **Part 1:** `01-architecture-and-openshift-vs-kubernetes.md` — Architecture & What OpenShift Adds Over Vanilla Kubernetes
> **Part 2:** `02-projects-sccs-and-multi-tenancy.md` — Projects, Security Context Constraints & Multi-Tenancy
> **Part 3:** This file — Networking, Routes & OpenShift Service Mesh
> **Part 4:** `04-buildconfigs-s2i-imagestreams-and-pipelines.md` — BuildConfigs, S2I, ImageStreams & OpenShift Pipelines/GitOps
> **Part 5:** `05-day-2-operations-cluster-operators-and-observability.md` — Day-2 Operations: Cluster Operators, Upgrades & Observability
> **Questions:** `questions.md`

## Table of Contents

1. [Networking Layers in an OpenShift Cluster](#networking-layers-in-an-openshift-cluster)
2. [OVN-Kubernetes — OpenShift's Default CNI](#ovn-kubernetes--openshifts-default-cni)
3. [How Pod-to-Pod Traffic Actually Flows](#how-pod-to-pod-traffic-actually-flows)
4. [NetworkPolicy in Full Depth](#networkpolicy-in-full-depth)
5. [Controlling Egress: EgressIP and the Egress Firewall](#controlling-egress-egressip-and-the-egress-firewall)
6. [Routes — OpenShift's Layer Above Kubernetes Ingress](#routes--openshifts-layer-above-kubernetes-ingress)
7. [TLS Termination Types: Edge, Passthrough, and Re-Encrypt](#tls-termination-types-edge-passthrough-and-re-encrypt)
8. [A Worked Example: Creating and Securing a Route](#a-worked-example-creating-and-securing-a-route)
9. [The Ingress Operator and the Default Router](#the-ingress-operator-and-the-default-router)
10. [Router Sharding](#router-sharding)
11. [Kubernetes Ingress on OpenShift — Interop, Not Replacement](#kubernetes-ingress-on-openshift--interop-not-replacement)
12. [Multus — Attaching Additional Networks to a Pod](#multus--attaching-additional-networks-to-a-pod)
13. [A Worked Example: A NetworkAttachmentDefinition](#a-worked-example-a-networkattachmentdefinition)
14. [Cluster DNS: CoreDNS and the DNS Operator](#cluster-dns-coredns-and-the-dns-operator)
15. [OpenShift Service Mesh — When Routes and NetworkPolicy Aren't Enough](#openshift-service-mesh--when-routes-and-networkpolicy-arent-enough)
16. [The Sail Operator and Istio's Control Plane on OpenShift](#the-sail-operator-and-istios-control-plane-on-openshift)
17. [Quick Reference: Key Terms From This Chapter](#quick-reference-key-terms-from-this-chapter)
18. [Common Mistakes and Interview Traps](#common-mistakes-and-interview-traps)
19. [Worked Practice Problems](#worked-practice-problems)
20. [Summary and What's Next](#summary-and-whats-next)

## Networking Layers in an OpenShift Cluster

Part 2 closed with a deliberate promise: NetworkPolicy's default deny-across-Projects posture is a safe starting point with a well-defined escape hatch, not the whole story. This chapter is that whole story, and it spans four genuinely distinct layers that are easy to conflate if introduced all at once: the **cluster network** (how any Pod reaches any other Pod, cluster-wide, via the default CNI), **NetworkPolicy** (which of those reachable paths are actually permitted), **ingress** (how traffic from outside the cluster reaches a Pod at all, via Routes and/or Kubernetes `Ingress`), and **service mesh** (a layer some workloads add on top of all three for traffic shaping, mutual TLS, and observability that Routes and NetworkPolicy alone can't express).

| Layer | Question it answers | OpenShift's mechanism |
|---|---|---|
| Cluster network (CNI) | Can Pod A physically reach Pod B at all? | OVN-Kubernetes, the default CNI |
| NetworkPolicy | Of the paths the CNI makes physically possible, which are actually permitted? | Kubernetes-native `NetworkPolicy`, enforced by OVN-Kubernetes |
| Ingress | How does traffic from outside the cluster reach a Service? | Routes (OpenShift-native) and/or Kubernetes `Ingress` |
| Egress control | What can a workload reach outside the cluster, and with what source IP? | EgressIP, `EgressFirewall`, and egress routers, all OVN-Kubernetes-native |
| Service mesh | How is traffic between services inside the mesh shaped, secured, and observed? | OpenShift Service Mesh (Istio via the Sail Operator) |

Each layer builds on the one above it in this table, and a real production networking incident is often a confusion about *which* layer is actually the problem — a request timing out could be a missing `NetworkPolicy` rule, a misconfigured Route, or a service mesh sidecar's own mTLS policy, and this chapter is organized specifically so each layer's own failure signature is recognizable on its own.

```mermaid
sequenceDiagram
    participant Client as External client
    participant Router as Router (Route/Ingress)
    participant Mesh as Envoy sidecar (if meshed)
    participant NP as NetworkPolicy enforcement
    participant CNI as OVN-Kubernetes
    participant Pod as Destination Pod

    Client->>Router: HTTPS request to public hostname
    Router->>Router: TLS termination per this chapter's three types
    Router->>Mesh: Forward to Service (mTLS if meshed)
    Mesh->>NP: Request the destination Pod
    NP->>NP: Evaluate allow-list against every applicable NetworkPolicy
    NP->>CNI: If permitted, hand off to the cluster network
    CNI->>Pod: Deliver via OVN logical flow, Geneve if cross-node
```

A single failed request can fail at any one of these five points, and this diagram is deliberately the mental checklist worth working through top-to-bottom during an incident: is the Route/Ingress even admitted and routing correctly; if meshed, is the sidecar's own policy (covered later in this chapter) rejecting the request; is a `NetworkPolicy` denying it; and only once those are ruled out, is the underlying CNI layer itself actually broken — a genuinely rare last possibility, since OVN-Kubernetes failures tend to be all-or-nothing (a node losing connectivity entirely) rather than selectively blocking one specific request pattern.

## OVN-Kubernetes — OpenShift's Default CNI

Every OpenShift cluster's Pod-to-Pod networking runs on **OVN-Kubernetes**, a CNI plugin built on **Open Virtual Network (OVN)**, itself built on **Open vSwitch (OVS)** — the same virtual switching technology widely used in OpenStack and other virtualization platforms, repurposed here to give every node a programmable virtual network fabric rather than relying purely on Linux's own routing tables and iptables rules.

OVN-Kubernetes builds an **overlay network**: every node runs an OVS instance, and pod-to-pod traffic between nodes is encapsulated using the **Geneve** protocol (a more extensible successor to VXLAN) and tunneled across the underlying physical network, meaning Pods get a flat, cluster-wide IP space regardless of the actual physical network topology underneath. OVN's own centralized control plane (the northbound/southbound databases, running as part of the `network` Cluster Operator's own managed components) computes the logical flows every node's OVS instance needs, and pushes them down — the same "centralized desired state, continuously reconciled to every node" pattern Part 1 established for the cluster's own components, applied here specifically to network flow rules.

```mermaid
flowchart TB
    subgraph CP["Control Plane (network Cluster Operator)"]
        NB["OVN Northbound DB\n(logical network intent)"]
        SB["OVN Southbound DB\n(physical flow rules)"]
        NB --> SB
    end
    subgraph Node1["Node 1"]
        OVS1["OVS + ovn-controller"]
        PodA["Pod A"]
        PodA <--> OVS1
    end
    subgraph Node2["Node 2"]
        OVS2["OVS + ovn-controller"]
        PodB["Pod B"]
        PodB <--> OVS2
    end
    SB -->|Flow rules pushed down| OVS1
    SB -->|Flow rules pushed down| OVS2
    OVS1 <-->|Geneve-encapsulated overlay traffic| OVS2

    classDef cp fill:#1d4ed8,color:#fff,stroke:#1e40af
    classDef node fill:#047857,color:#fff,stroke:#065f46
    class NB,SB cp
    class OVS1,OVS2,PodA,PodB node
```

OVN-Kubernetes runs by default in **shared gateway mode**, where egress and ingress traffic for a node is handled through OVS directly rather than the host's own routing stack — a design choice made specifically to enable hardware offloading (SmartNICs capable of executing OVS flow rules in silicon) on platforms that support it, reducing CPU overhead for network-heavy workloads compared to routing every packet through the host kernel's own network stack.

| Property | What it means in practice |
|---|---|
| Overlay protocol | Geneve — more extensible than VXLAN, carries additional per-packet metadata OVN uses for policy enforcement |
| Control plane | Centralized northbound/southbound OVN databases, computing flows for the whole cluster |
| Per-node component | `ovn-controller` translates southbound flows into actual OVS rules on that node |
| NetworkPolicy enforcement | Native — OVN's own logical flows implement `NetworkPolicy` directly, no separate iptables layer bolted on |
| Multi-tenancy isolation | The default deny-across-namespace behavior from Part 2 is implemented as OVN logical flows, not a separate mechanism |

### Shared Gateway Mode vs. Local Gateway Mode

OVN-Kubernetes's gateway mode is a real, cluster-wide configuration choice made at install time (changing it later is a disruptive, supported-but-nontrivial migration), and it's worth understanding both options rather than assuming the default is automatically the right fit for every environment:

| Property | Shared gateway mode (default) | Local gateway mode |
|---|---|---|
| Egress/ingress path | Through OVS directly, bypassing the host's own routing stack | Through the host's own routing stack |
| Hardware offload (SmartNICs) | Supported, and the reason this is the default | Not applicable |
| Compatibility with host-level networking customizations | Lower — some host-level routing/firewall customizations conflict with OVS handling the path directly | Higher — the host's own routing stack stays in the path, so existing host-level tooling keeps working |
| Change cost after install | Disruptive cluster-wide migration | Same — neither mode is a lightweight post-install toggle |
| Typical fit | Standard cloud/bare-metal deployments, especially with offload-capable NICs | Environments with specific host-level network customization requirements that need to inspect or modify traffic via the host stack |

A platform team hitting an unexpected interaction between OVN-Kubernetes and a host-level network security tool is a common trigger for evaluating local gateway mode — worth knowing the option exists by name before assuming the interaction is an unfixable OVN-Kubernetes limitation.

### Diagnosing Connectivity with `ovnkube-trace`

```bash
oc exec -n openshift-ovn-kubernetes ds/ovnkube-node -- \
  ovnkube-trace -src-namespace payments-dev -src pod-a \
                -dst-namespace payments-dev -dst pod-b -tcp -dst-port 5432
```

```text
ovn-trace source pod to destination pod indicates success
ovn-controller flow output:
  table=8 (ls_in_acl), priority=2000, match=(...), action=allow
```

Reading `ovnkube-trace`'s output directly names the specific logical flow table and rule that allowed or denied the simulated packet — a materially faster diagnosis path than manually cross-referencing `NetworkPolicy` objects against OVN's own internal flow tables by hand, especially once several overlapping policies are in play across a namespace.

> **From the Trenches:** A team debugging intermittent cross-node Pod connectivity spent a day suspecting application-level retries and load-balancer health checks before realizing the actual symptom correlated exactly with nodes behind a specific top-of-rack switch that silently dropped packets above a certain MTU. Geneve's encapsulation overhead reduces the effective MTU available to Pod traffic below the physical network's own MTU, and that specific switch's configuration hadn't been updated to account for the overlay's larger frame size on the physical uplinks — a classic overlay-network MTU mismatch, invisible at the Kubernetes API level entirely, only visible once someone thought to check `ovnkube` node logs for fragmentation-related drops and cross-referenced them against the physical switch topology.

## How Pod-to-Pod Traffic Actually Flows

Tracing one real request end to end grounds the diagram above in an actual packet's journey, and is worth being able to narrate precisely in an interview or an incident:

```mermaid
sequenceDiagram
    participant PodA as Pod A (Node 1)
    participant OVS1 as OVS (Node 1)
    participant Net as Physical network (Geneve tunnel)
    participant OVS2 as OVS (Node 2)
    participant PodB as Pod B (Node 2)

    PodA->>OVS1: Packet to Pod B's cluster IP
    OVS1->>OVS1: Match logical flow rules\n(computed from NetworkPolicy + topology)
    OVS1->>Net: Geneve-encapsulate, send to Node 2
    Net->>OVS2: Encapsulated packet arrives
    OVS2->>OVS2: Decapsulate, match local flow rules
    OVS2->>PodB: Deliver to Pod B's veth interface
```

Every hop in this diagram is a place a NetworkPolicy denial, an MTU mismatch, or a node-level firewall rule can silently drop a packet — `ovnkube-trace` (a diagnostic tool shipped with OVN-Kubernetes) simulates exactly this flow and reports which specific logical flow rule allowed or denied the packet, the single most direct way to answer "why can't Pod A reach Pod B" without manually reconstructing OVN's flow tables by hand.

Same-node traffic (both Pods scheduled onto the same physical machine) skips the Geneve-encapsulation hop entirely — OVS routes directly between the two Pods' local interfaces without ever touching the physical network, which is both a performance characteristic worth knowing (same-node traffic is meaningfully cheaper than cross-node traffic) and a diagnostic one: a connectivity problem that only reproduces for cross-node traffic, never same-node, points specifically at the physical network or the Geneve overlay itself, rather than at OVN's logical flow rules, which apply identically in both cases.

## NetworkPolicy in Full Depth

Part 2 introduced the default `allow-from-same-namespace` policy a fresh Project receives; this section covers the full `NetworkPolicy` object model those defaults are built from. A `NetworkPolicy` is fundamentally a **allow-list** mechanism scoped to a namespace: once any `NetworkPolicy` selects a given Pod, that Pod's traffic (in whichever direction — ingress, egress, or both — the policy covers) is denied by default except for what the policy's rules explicitly permit; a Pod matched by no `NetworkPolicy` at all remains fully open in that direction, which is exactly the gap the Project template's default policy exists to close from the moment a namespace is created.

```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: web-allow-db-egress
  namespace: payments-dev
spec:
  podSelector:
    matchLabels: { app: web }
  policyTypes: ["Egress"]
  egress:
    - to:
        - podSelector:
            matchLabels: { app: postgres }
      ports:
        - protocol: TCP
          port: 5432
    - to:                         # DNS must be explicitly allowed too
        - namespaceSelector: {}
      ports:
        - protocol: UDP
          port: 53
```

That second egress rule is a common, easy-to-miss requirement: once a Pod is selected by any `NetworkPolicy` covering egress, **DNS resolution itself** is subject to the same default-deny unless explicitly allowed — a Pod that can otherwise reach its database perfectly well over a raw IP will still fail if it resolves that database's address by hostname first and the policy never explicitly permitted port 53 traffic to the cluster's DNS Pods.

| NetworkPolicy field | What it controls |
|---|---|
| `podSelector` | Which Pods in this namespace this policy applies to |
| `podSelector: {}` (empty) | Matches every Pod in the namespace — the basis of a default-deny-all baseline |
| `policyTypes` | Whether this policy governs `Ingress`, `Egress`, or both |
| `ingress[].from` | Allowed sources: `podSelector`, `namespaceSelector`, or `ipBlock` (a raw CIDR) |
| Combined `podSelector` + `namespaceSelector` in one `from` entry | Both must match the same source — a stricter AND, not an OR, of the two selectors |
| `egress[].to` | Allowed destinations, same selector types as `ingress[].from` |
| Multiple entries in one `from`/`to` list | Evaluated as an OR — a source matching any one entry is allowed |
| `ports` | Restricts an allow rule to specific protocols/ports, rather than all traffic to/from the matched source |
| `policyTypes` omitted entirely | Inferred from whichever of `ingress`/`egress` is present in the spec — worth setting explicitly to avoid surprises |

### A Default-Deny-Everything Baseline

The Project-template default from Part 2 only covers ingress; a namespace with a genuinely strict posture typically starts from an explicit deny-everything baseline for both directions, then layers specific allow rules on top:

```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: payments-prod
spec:
  podSelector: {}
  policyTypes: ["Ingress", "Egress"]
```

An empty `podSelector: {}` matches every Pod in the namespace, and an empty `ingress`/`egress` rule list (implicit here, since neither is specified) denies everything in both directions covered by `policyTypes` — the strictest possible starting point, from which every subsequent `NetworkPolicy` in the namespace adds back exactly the specific paths that namespace's workloads actually need, including the DNS-egress rule from the trenches story below.

### Allowing a Specific External CIDR

`ipBlock` is the selector type worth knowing for the "allow traffic to/from a specific external range, not another Pod or namespace" case — a partner's API range, or a specific external monitoring collector:

```yaml
  ingress:
    - from:
        - ipBlock:
            cidr: 203.0.113.0/24
            except: ["203.0.113.128/25"]
```

`except` carves a sub-range back out of an otherwise-allowed block — useful for "allow this partner's whole range except their known-decommissioned segment" without needing two separate, harder-to-reconcile policies.

> **From the Trenches:** A team added an egress-restricting `NetworkPolicy` to lock down a namespace after a security review, tested it against every internal service dependency, and shipped it — only to find every Pod in the namespace immediately unable to resolve any hostname at all, including ones needed for basic cluster operation (pulling images by tag from an internal registry hostname). The fix wasn't reverting the policy; it was adding exactly the DNS-egress rule shown above, which the team had genuinely not realized was a separate, explicit requirement — a strong argument for testing any new default-deny `NetworkPolicy` in a non-production namespace first, specifically watching for silent DNS failures, which don't always announce themselves as clearly as a direct connection refusal would.

## Controlling Egress: EgressIP and the Egress Firewall

NetworkPolicy governs traffic *within* the cluster; two further OVN-Kubernetes-native mechanisms govern traffic **leaving** the cluster toward external systems — a common enterprise requirement (a partner API allow-listing specific source IPs, a compliance policy restricting which external destinations a Project may reach) that NetworkPolicy alone can't express, since NetworkPolicy has no concept of "the cluster's own outbound source IP."

**EgressIP** assigns a specific, consistent external-facing IP address to a Project's outbound traffic — so a partner's firewall can allow-list one stable IP rather than an unpredictable, potentially-changing node IP, even as the actual Pods generating that traffic get rescheduled across different nodes over time.

**Egress Firewall** (an `EgressFirewall` custom resource) restricts which external destinations a Project's Pods may reach at all, functioning like a namespace-scoped outbound allow/deny list independent of NetworkPolicy's ingress/egress-between-Pods model:

```yaml
apiVersion: k8s.ovn.org/v1
kind: EgressFirewall
metadata:
  name: default
  namespace: payments-dev
spec:
  egress:
    - type: Allow
      to:
        cidrSelector: 203.0.113.0/24    # the approved partner API range
    - type: Deny
      to:
        cidrSelector: 0.0.0.0/0          # deny everything else
```

| Mechanism | What it controls | Typical driver |
|---|---|---|
| `NetworkPolicy` | Pod-to-Pod traffic, in-cluster | General multi-tenancy and application-level isolation |
| EgressIP | The external-facing source IP a Project's outbound traffic presents | A partner integration requiring a stable, allow-listable IP |
| `EgressFirewall` | Which external destinations a Project may reach at all | Compliance/data-exfiltration-prevention requirements |
| Egress router | Routes a Project's traffic to one specific destination through a dedicated, stable-IP Pod | Legacy destinations reachable only from one specific, whitelisted source address |

> **From the Trenches:** A team's `EgressFirewall` was written and tested against a single Pod replica, allowing only the partner's documented CIDR range — and passed every test. Weeks later, a routine node-pool rebalance rescheduled that Deployment's Pods onto different nodes, and the partner integration started failing intermittently. The `EgressFirewall` itself was correct and unaffected by the reschedule; the actual root cause was that the team had never configured EgressIP alongside it, so the Pods' outbound traffic was presenting whichever node's own external IP happened to host them at any given moment — the partner's own firewall, allow-listing a specific IP the team had shared informally rather than through EgressIP, started rejecting traffic the moment the Pods landed on a node with a different external IP. Configuring EgressIP to pin a single, stable, allow-listable IP to the Project — independent of which node its Pods actually land on — was the fix, and the underlying lesson is that `EgressFirewall` and EgressIP solve genuinely different problems and are frequently both needed together, not interchangeably.

## Routes — OpenShift's Layer Above Kubernetes Ingress

Everything above this point governs traffic already inside the cluster network. **Routes** are OpenShift's answer to the question every cluster eventually has to solve: how does traffic from outside the cluster reach a Service at all? A `Route` is conceptually similar to a Kubernetes `Ingress` object — both bind an external hostname to an internal Service — but Routes predate `Ingress` in OpenShift's own history and carry a richer, more opinionated feature set built directly into the object itself, rather than delegated to whichever ingress controller a cluster happens to run.

```yaml
apiVersion: route.openshift.io/v1
kind: Route
metadata:
  name: web
  namespace: payments-dev
spec:
  host: payments.apps.prod-east.example.com
  to:
    kind: Service
    name: web
  port:
    targetPort: 8080
  tls:
    termination: edge
    insecureEdgeTerminationPolicy: Redirect
```

Note `spec.host`: if left unset, OpenShift generates one automatically from the cluster's own wildcard domain (`<route-name>-<namespace>.apps.<cluster-domain>`) — the `*.apps.<cluster-domain>` wildcard DNS record every IPI installation provisions automatically is exactly what makes this "just works with no DNS request" behavior possible, a genuinely different default experience from a vanilla-Kubernetes `Ingress`, which has no equivalent automatic hostname provisioning at all.

## TLS Termination Types: Edge, Passthrough, and Re-Encrypt

A Route's `tls.termination` field is one of the more consequential decisions a team makes when exposing a service, and the three options trade off differently on where encryption actually happens:

```mermaid
flowchart LR
    subgraph Edge["Edge Termination"]
        C1["Client"] -->|TLS| R1["Router"]
        R1 -->|Plain HTTP| P1["Pod"]
    end
    subgraph Passthrough["Passthrough Termination"]
        C2["Client"] -->|TLS, unmodified| R2["Router"]
        R2 -->|Same TLS session| P2["Pod"]
    end
    subgraph Reencrypt["Re-encrypt Termination"]
        C3["Client"] -->|TLS #1| R3["Router"]
        R3 -->|TLS #2, re-encrypted| P3["Pod"]
    end
```

| Termination type | Where TLS ends | Router-to-Pod traffic | Fits when |
|---|---|---|---|
| Edge | At the router | Plain HTTP | Simplest option — the Pod itself doesn't need to handle TLS at all |
| Passthrough | At the Pod | The original, unmodified TLS session | The application needs to see the client's own certificate (mTLS), or handles its own certificate rotation independently |
| Re-encrypt | At the router, then again at the Pod | A second, separate TLS session | Defense-in-depth — encrypted in transit end to end, while the router still gets to inspect/route on the decrypted request (host-based routing, metrics) |
| (No TLS at all) | Never | Plain HTTP throughout | Internal-only traffic already inside a trusted network boundary — rare for anything genuinely public-facing |

Edge termination's `insecureEdgeTerminationPolicy: Redirect` (used in the example above) is worth calling out specifically: it automatically redirects any plain-HTTP request to HTTPS, rather than either serving insecure HTTP silently or rejecting it outright — the sensible default for nearly every public-facing Route, and a one-line fix for the common "site works over HTTPS but plain HTTP requests fail confusingly" complaint.

> **From the Trenches:** A team migrating a legacy application to OpenShift chose passthrough termination by habit, assuming it was "more secure" without a specific mTLS requirement driving the choice. The consequence: the router could no longer perform host-based routing decisions using the request's actual hostname (since it never decrypts the traffic at all), and several unrelated routing and metrics features the team expected "for free" from the router simply didn't work for that specific Route. Re-encrypt termination — chosen once the team named their actual requirement (end-to-end encryption, without needing client-certificate inspection) — restored full router-level routing and metrics visibility while keeping the Pod-to-router hop encrypted, the correct fit once the actual requirement was made explicit instead of defaulting to whichever option sounded most secure in the abstract.

## A Worked Example: Creating and Securing a Route

The fastest path from a Service to a secured, externally-reachable Route:

```bash
oc expose service web --hostname=payments.apps.prod-east.example.com

oc patch route web -p '{"spec":{"tls":{"termination":"edge","insecureEdgeTerminationPolicy":"Redirect"}}}'

oc get route web -o jsonpath='{.spec.host}{"\n"}{.status.ingress[0].conditions}'
```

`status.ingress[].conditions` is worth reading directly rather than assuming the Route is live the moment it's created: a Route can be accepted by the API but not yet **admitted** by every router that should be serving it (relevant once router sharding, covered next, is in play), and the condition array names exactly which router has and hasn't admitted it yet.

### Route-Specific Annotations Worth Knowing

Beyond `spec.tls`, a handful of Route annotations cover common production requirements a raw Kubernetes `Ingress` typically needs a controller-specific annotation (with no portability guarantee) to express at all:

| Annotation | What it does |
|---|---|
| `haproxy.router.openshift.io/timeout` | Overrides the router's default backend timeout for this specific Route |
| `haproxy.router.openshift.io/timeout-tunnel` | A separate, typically longer timeout specifically for long-lived WebSocket/passthrough tunnels |
| `haproxy.router.openshift.io/balance` | Selects the load-balancing algorithm (`roundrobin`, `source`, `leastconn`) across the Route's backend Pods |
| `haproxy.router.openshift.io/disable_cookies` | Disables session-affinity cookie insertion, useful for genuinely stateless backends where sticky sessions add no value |
| `haproxy.router.openshift.io/rate-limit-connections` | Enables per-client connection rate limiting at the router |
| `haproxy.router.openshift.io/ip_whitelist` | Restricts a Route to a specific space-separated list of allowed client CIDRs |
| `route.openshift.io/termination` | Alternative to `spec.tls.termination` for tooling that generates Routes without a full TLS block |
| `haproxy.router.openshift.io/hsts_header` | Sets an HTTP Strict Transport Security header, instructing browsers to never attempt plain HTTP for this host again |
| `router.openshift.io/cookie_name` | Sets a custom session-affinity cookie name, useful when a specific name is required for compatibility with existing client tooling |

```yaml
metadata:
  annotations:
    haproxy.router.openshift.io/timeout: "30s"
    haproxy.router.openshift.io/balance: "leastconn"
```

`leastconn` in particular is worth knowing as a deliberate alternative to the default round-robin balancing: for backends with meaningfully uneven per-request processing time (a mix of fast read endpoints and slow write endpoints behind the same Service), round-robin can leave some backend Pods overloaded while others sit idle, whereas least-connections actively accounts for each backend's current in-flight request count.

`source`-based balancing is the third option worth naming explicitly: it hashes the client's source IP to consistently route the same client to the same backend Pod, a lightweight session-affinity mechanism that doesn't require the cookie insertion `disable_cookies` turns off — useful when session affinity is wanted but cookie-based affinity specifically isn't, such as for a non-HTTP protocol passthrough Route where HAProxy has no cookie to insert in the first place.

### Weighted Routing Across Multiple Backends

A Route can split traffic across more than one backend Service by weight — the mechanism most commonly used for a manual canary rollout before a full GitOps-driven progressive delivery pipeline (Part 4 covers this) is in place:

```yaml
apiVersion: route.openshift.io/v1
kind: Route
metadata:
  name: web
spec:
  host: payments.apps.prod-east.example.com
  to:
    kind: Service
    name: web-stable
    weight: 90
  alternateBackends:
    - kind: Service
      name: web-canary
      weight: 10
```

This sends roughly 10% of traffic to `web-canary` and 90% to `web-stable`, adjustable by editing the weights directly — a simple, immediately-available mechanism worth knowing before reaching for a heavier tool, though it lacks the automated analysis and rollback a dedicated progressive-delivery controller provides. Part 4's coverage of OpenShift GitOps and progressive delivery revisits this exact mechanism as the low-level primitive a higher-level automated canary controller ultimately drives on a team's behalf, rather than something to abandon once that automation is in place.

## The Ingress Operator and the Default Router

Every Route ultimately gets served by a **router** — an HAProxy-based load balancer, managed by the `ingress` Cluster Operator (Part 1's roster), that watches every Route object cluster-wide and continuously regenerates its HAProxy configuration to match. The default `IngressController` object (`oc get ingresscontroller default -n openshift-ingress-operator`) is what the Ingress Operator manages, and it's directly configurable for the properties that matter operationally: replica count, exposure strategy (a cloud load balancer, a `NodePort`, or host networking), and TLS security profile.

```yaml
apiVersion: operator.openshift.io/v1
kind: IngressController
metadata:
  name: default
  namespace: openshift-ingress-operator
spec:
  domain: apps.prod-east.example.com
  replicas: 3
  endpointPublishingStrategy:
    type: LoadBalancerService
```

Bumping `replicas` beyond the default is a common, real Day-2 tuning decision for any cluster serving meaningful external traffic — the router is a genuine potential bottleneck and single point of contention if under-scaled, and unlike most Kubernetes workloads, it's not something a HorizontalPodAutoscaler manages automatically by default; capacity planning for it is a deliberate, manual sizing decision tied to expected request volume.

### Endpoint Publishing Strategies

`endpointPublishingStrategy.type` decides how the router itself becomes reachable, and the right choice depends entirely on the underlying infrastructure and network model:

| Strategy | How it works | Fits when |
|---|---|---|
| `LoadBalancerService` | Provisions a cloud load balancer in front of the router | Standard cloud deployments (AWS/Azure/GCP) — the common default |
| `NodePortService` | Exposes the router on a static port on every node | Environments without a cloud load balancer integration, fronted by an external hardware/software load balancer instead |
| `HostNetwork` | Binds the router directly to the host's network namespace on specific nodes | Bare-metal deployments needing the router reachable on the host's own IP directly, without any load-balancer layer |
| `Private` | No external exposure at all — internal cluster traffic only | An internal-only `IngressController` shard serving Routes with no external reachability requirement |

Bare-metal and disconnected environments (Part 1's install-method coverage) commonly land on `NodePortService` or `HostNetwork`, since a cloud provider's own load-balancer API — what `LoadBalancerService` depends on — simply doesn't exist in those environments.

## Router Sharding

A single default router serving every Route in the cluster is the common case, but a real, supported requirement — isolating one Project's ingress traffic from another's at the router level, or dedicating a router to a specific set of high-traffic public-facing Routes — is solved by **router sharding**: running multiple `IngressController` objects, each scoped to a subset of Routes via a `namespaceSelector` or `routeSelector`.

```yaml
apiVersion: operator.openshift.io/v1
kind: IngressController
metadata:
  name: internal-only
  namespace: openshift-ingress-operator
spec:
  domain: internal.prod-east.example.com
  namespaceSelector:
    matchLabels:
      network-tier: internal
  endpointPublishingStrategy:
    type: HostNetwork
```

| Sharding key | How it selects Routes | Fits when |
|---|---|---|
| `namespaceSelector` | Every Route in a matching, labeled namespace | Isolating traffic by team/Project ownership |
| `routeSelector` | Individual Routes matching a label, regardless of namespace | Isolating a specific subset of Routes cutting across multiple Projects |
| Neither (default router only) | Every Route not claimed by a sharded router | The common case for clusters with no isolation requirement between Route groups |

A subtlety worth internalizing: the **default** `IngressController` continues serving every Route not explicitly excluded from it, even after additional sharded routers are introduced — a common misconfiguration is assuming a new sharded router automatically "removes" its matched Routes from the default router's own scope, when in fact the default router needs its own `namespaceSelector`/`routeSelector` exclusion configured explicitly if truly exclusive routing between shards is the goal; each shard also needs its own DNS record pointed at its own router, since nothing wires that up automatically once more than one router exists.

## Kubernetes Ingress on OpenShift — Interop, Not Replacement

A raw Kubernetes `Ingress` object still works on OpenShift — the `route-controller-manager` component (part of the `ingress` Cluster Operator) watches `Ingress` objects and **automatically generates a corresponding `Route`** for each one, translating `Ingress`'s TLS Secret references into the equivalent Route TLS configuration. This is precisely what makes OpenShift's Kubernetes conformance claim from Part 1 hold at the ingress layer too: a Helm chart authored purely against upstream `Ingress` deploys and works on OpenShift without modification, gaining the underlying Route mechanism transparently.

| | Direct `Route` | Kubernetes `Ingress` (auto-converted) |
|---|---|---|
| TLS termination types | All three (edge, passthrough, re-encrypt) directly configurable | Limited to what `Ingress`'s annotation-driven model can express, translated by the controller |
| Portability | OpenShift-specific | Fully portable to any conformant Kubernetes cluster |
| Automatic wildcard hostname | Yes, if `host` is left unset | No — `Ingress` requires an explicit host |
| Ownership model | Directly authored and owned by whoever creates it | Auto-generated and owned by the source `Ingress`, never hand-edited directly |
| Fine-grained router behavior (weighting, custom headers) | Directly supported via Route-specific fields/annotations | Only what maps cleanly onto the generated Route |

The practical guidance: author a raw `Ingress` when portability to a non-OpenShift cluster genuinely matters (a Helm chart meant to ship to customers running vanilla Kubernetes too), and author a `Route` directly when OpenShift-specific features (passthrough/re-encrypt TLS, fine-grained router tuning) are needed — both coexist on the same cluster without conflict, and nothing about choosing one forecloses using the other elsewhere in the same cluster.

### Watching the Automatic Conversion Happen

```bash
oc apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
  namespace: payments-dev
spec:
  rules:
    - host: web.apps.prod-east.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service: { name: web, port: { number: 8080 } }
EOF

oc get routes -n payments-dev
```

The `oc get routes` output shows a Route named after the `Ingress` object, owned by it (visible in `metadata.ownerReferences`) — deleting the `Ingress` deletes the generated `Route` automatically, since the `route-controller-manager` treats the `Route` as a fully owned, derived resource rather than an independent object a team should hand-edit directly. Editing the generated `Route` directly is a common early mistake: any manual change is overwritten the next time the controller reconciles it against the source `Ingress`, the same "edit the source of truth, not the derived object" lesson Part 1's Machine Config Operator and Part 2's default ClusterRole reconciliation both taught in their own layers.

## Multus — Attaching Additional Networks to a Pod

Every Pod's primary network interface (`eth0`) is attached to the cluster's default OVN-Kubernetes network — sufficient for the overwhelming majority of workloads. **Multus** is a meta-CNI plugin that lets a Pod attach one or more **additional** network interfaces (`net1`, `net2`, ...) beyond that default, without replacing it — a genuine, if specialized, requirement for network functions virtualization (NFV) workloads, high-throughput data-plane applications needing direct access to a specific physical NIC, or any workload needing to participate in more than one distinct network segment simultaneously.

```mermaid
flowchart LR
    Pod["Pod"]
    Pod -->|eth0 - default network| CNI0["OVN-Kubernetes"]
    Pod -->|net1 - additional network| CNI1["Multus-attached CNI\n(e.g. macvlan, SR-IOV)"]

    classDef default fill:#1d4ed8,color:#fff,stroke:#1e40af
    classDef extra fill:#b45309,color:#fff,stroke:#92400e
    class CNI0 default
    class CNI1 extra
```

Multus itself doesn't implement any network — it's a wrapper that invokes whichever real CNI plugin (`macvlan`, `ipvlan`, an SR-IOV device plugin) a `NetworkAttachmentDefinition` names, purely responsible for attaching the additional interface(s) a Pod's annotation requests, then handing control back to the default network for everything else.

## A Worked Example: A NetworkAttachmentDefinition

Defining an additional macvlan-based network, then attaching a Pod to it:

```yaml
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  name: high-speed-net
  namespace: telco-workloads
spec:
  config: '{
    "cniVersion": "0.4.0",
    "type": "macvlan",
    "master": "eth1",
    "mode": "bridge",
    "ipam": { "type": "whereabouts", "range": "192.168.100.0/24" }
  }'
```

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: nfv-workload
  namespace: telco-workloads
  annotations:
    k8s.v1.cni.cncf.io/networks: high-speed-net
spec:
  containers:
    - name: nfv-workload
      image: quay.io/myorg/nfv-workload:v1
```

The Pod's annotation is the only wiring needed — Multus reads it, invokes the named `NetworkAttachmentDefinition`'s configured CNI plugin, and the Pod ends up with `eth0` on the default cluster network plus `net1` on the dedicated `high-speed-net` segment, both fully functional simultaneously. This is squarely a specialized-workload feature — telco/NFV, certain ML data-pipeline workloads with dedicated storage-network requirements — and not something an ordinary application team should reach for without a specific, concrete networking requirement Routes/Services can't already satisfy.

### Choosing an Underlying CNI Plugin for a Multus Network

`NetworkAttachmentDefinition.spec.config` names the actual CNI plugin Multus should invoke, and the choice matters:

| CNI plugin | What it provides | Fits when |
|---|---|---|
| `macvlan` | Each Pod gets its own MAC address directly on the physical network segment | The Pod needs to appear as its own distinct device on the physical LAN |
| `ipvlan` | Pods share the host's MAC address but get distinct IPs | Environments where the switch/network restricts the number of MAC addresses per port |
| `ipvlan` L3 mode | Like the default L2 mode, but routes rather than bridges traffic between the additional interfaces | Environments needing L3 routing semantics on the additional network rather than L2 bridging |
| SR-IOV device plugin | Direct hardware-level access to a virtual function on an SR-IOV-capable NIC | Line-rate throughput requirements (NFV data planes, high-frequency trading) where even macvlan's overhead is unacceptable |
| `bridge` | A simple Linux bridge shared across Pods on the same node | The lightest-weight option when cross-node reachability on the additional network isn't required |
| `host-device` | Moves a specific physical NIC directly into the Pod's own network namespace | The rare case where a Pod needs exclusive, direct ownership of one physical device |

SR-IOV in particular requires the underlying node hardware and a dedicated `SriovNetworkNodePolicy` (managed by the SR-IOV Network Operator, installed via OLM from Part 1) to actually expose virtual functions as schedulable node resources — a meaningfully deeper hardware dependency than macvlan/ipvlan, and worth confirming node hardware support before committing a design to it.

### Multi-Cluster Networking: Submariner

Everything in this chapter assumes traffic within a single cluster. Organizations running multiple clusters (Part 1's ROSA/ARO/OKD family, or simply several self-managed clusters across regions) that need Pods in one cluster to reach Services in another directly — rather than only through public ingress — reach for **Submariner**, a separate CNCF project OpenShift supports installing via OLM, which establishes secure tunnels between clusters and extends Service discovery across the cluster boundary. It's mentioned here by name specifically because it's the natural next question once a team internalizes this chapter's single-cluster networking model and then encounters a genuine multi-cluster connectivity requirement — out of scope for this series' depth, but worth recognizing rather than assuming multi-cluster Pod-to-Pod connectivity requires routing everything through public Routes.

## Cluster DNS: CoreDNS and the DNS Operator

Every Service's cluster-internal DNS name (`web.payments-dev.svc.cluster.local`) is resolved by **CoreDNS**, managed by the `dns` Cluster Operator (Part 1's roster) — the same CoreDNS project used across virtually every Kubernetes distribution, configured here through a `DNS` custom resource rather than a hand-edited `Corefile` directly:

```yaml
apiVersion: operator.openshift.io/v1
kind: DNS
metadata:
  name: default
spec:
  servers:
    - name: internal-corp
      zones: ["corp.internal"]
      forwardPlugin:
        upstreams: ["10.10.0.53"]
```

This pattern — forwarding a specific internal zone to a corporate DNS server while every other query resolves normally through the cluster's own CoreDNS — is the standard way to make an existing internal DNS namespace resolvable from inside the cluster, without forwarding *every* query externally and losing the performance and isolation benefits of in-cluster DNS resolution for the cluster's own Service names.

### Troubleshooting DNS Resolution Directly

When a Pod reports a hostname it can't resolve, checking resolution directly from inside the cluster network — rather than guessing from the application's own error message — is the fastest way to isolate whether the problem is DNS itself or something further downstream:

```bash
oc run dns-debug --image=registry.redhat.io/rhel9/support-tools:latest --restart=Never -it -- \
  dig web.payments-dev.svc.cluster.local

oc get pods -n openshift-dns -o wide
oc logs -n openshift-dns -l dns.operator.openshift.io/daemonset-dns --tail=50
```

A resolution failure that reproduces from this debug Pod but not from a Pod in a namespace with an egress-restricting `NetworkPolicy` is a strong signal pointing straight back at this chapter's own DNS-egress gotcha, rather than an actual CoreDNS problem — worth checking before escalating to the DNS Operator's own component logs.

## OpenShift Service Mesh — When Routes and NetworkPolicy Aren't Enough

Routes solve external ingress; NetworkPolicy solves coarse-grained allow/deny between Pods. Neither solves a different, more specific class of requirement that emerges once an application decomposes into enough services talking to each other: **mutual TLS between every service automatically** (not just at the cluster edge), **fine-grained traffic shaping** (canary releases by percentage, retries and circuit breaking per service-to-service call), and **deep observability into service-to-service call patterns** without instrumenting every application individually. **OpenShift Service Mesh**, built on **Istio**, exists specifically for that gap.

```mermaid
flowchart TB
    subgraph MeshNS["Namespace in the mesh"]
        App1["App container"]
        Proxy1["Envoy sidecar"]
        App1 <-->|localhost| Proxy1
    end
    subgraph MeshNS2["Another namespace in the mesh"]
        App2["App container"]
        Proxy2["Envoy sidecar"]
        App2 <-->|localhost| Proxy2
    end
    Proxy1 <-->|mTLS, automatically| Proxy2
    ControlPlane["Istio control plane\n(istiod)"] -.pushes config/certs.-> Proxy1
    ControlPlane -.pushes config/certs.-> Proxy2

    classDef app fill:#047857,color:#fff,stroke:#065f46
    classDef proxy fill:#b45309,color:#fff,stroke:#92400e
    classDef cp fill:#1d4ed8,color:#fff,stroke:#1e40af
    class App1,App2 app
    class Proxy1,Proxy2 proxy
    class ControlPlane cp
```

Every application container in the mesh gets a transparent Envoy proxy sidecar injected alongside it; the application itself talks to `localhost` exactly as if calling the destination service directly, while the sidecar transparently intercepts that traffic, wraps it in mutual TLS to the destination's own sidecar, and reports detailed telemetry (latency, error rate, retry counts) back to the mesh's observability stack — all without a single line of application code aware the mesh exists.

| Concern | NetworkPolicy | Service Mesh |
|---|---|---|
| Enforcement point | OVN's own logical flows (kernel/OVS level) | Application-layer Envoy sidecars |
| Portability of authored policy | Fully portable — works identically on any conformant Kubernetes cluster | Tied to the mesh's own CRDs (Istio's, specifically) |
| Granularity | Allow/deny between Pod/namespace selectors | Per-request routing, retries, circuit breaking, percentage-based traffic splitting |
| Encryption | Not provided by NetworkPolicy itself | Automatic mutual TLS between every sidecar, cluster-wide |
| Observability | None built in | Deep per-service-call metrics, distributed tracing, and a dependency graph, without instrumenting applications |
| Adoption cost | Low — a YAML object per namespace | Higher — sidecar injection, its own control plane, and genuine operational learning curve |
| Resource overhead per Pod | None | An additional Envoy container per Pod (or a shared per-node proxy under ambient mode) |

The honest trade-off, matching the same opinionation-vs-overhead framing this series applies throughout: a service mesh is a real, ongoing operational commitment (another control plane to run, upgrade, and understand) that pays off specifically once an application's actual service-to-service complexity (dozens of services, a real need for mTLS everywhere, canary rollouts by traffic percentage) outgrows what Routes and NetworkPolicy can express — adopting it preemptively, before that complexity is real, adds the same kind of unjustified overhead this series has warned against at every other layer.

## The Sail Operator and Istio's Control Plane on OpenShift

OpenShift Service Mesh 3.x is installed and managed through the **Sail Operator**, a from-scratch rewrite of how OpenShift packages Istio that replaced the older `ServiceMeshControlPlane`-based installation model with a much thinner wrapper around upstream Istio's own `Istio` custom resource — a deliberate convergence with the community Istio project rather than a Red-Hat-specific control plane API, making skills and configuration substantially more portable to a non-OpenShift Istio deployment than the earlier model was.

```yaml
apiVersion: sailoperator.io/v1
kind: Istio
metadata:
  name: default
spec:
  version: v1.26.0
  namespace: istio-system
```

```yaml
apiVersion: v1
kind: Namespace
metadata:
  name: payments-dev
  labels:
    istio-injection: enabled
```

Labeling a namespace `istio-injection: enabled` is the actual, minimal step that brings its workloads into the mesh — the sidecar-injection webhook then automatically adds the Envoy proxy container to every Pod created in that namespace afterward, with zero change required to the applications' own Deployment manifests.

### Traffic Management: VirtualService and DestinationRule

Once services are in the mesh, Istio's own traffic-management CRDs express exactly the kind of fine-grained routing this chapter's earlier Route-weighting example only approximated:

```yaml
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: web
  namespace: payments-dev
spec:
  hosts: ["web"]
  http:
    - route:
        - destination: { host: web, subset: stable }
          weight: 90
        - destination: { host: web, subset: canary }
          weight: 10
---
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: web
  namespace: payments-dev
spec:
  host: web
  subsets:
    - name: stable
      labels: { version: stable }
    - name: canary
      labels: { version: canary }
```

The meaningful upgrade over a Route's own weighted backends: a `VirtualService` can route on request headers, retry automatically on specific error classes, and inject deliberate fault delays for chaos-testing purposes — none of which a Route's simpler weight-based model expresses at all, since a Route operates purely at the ingress edge while `VirtualService`/`DestinationRule` govern every hop inside the mesh, not just the initial entry point.

### Observability: Kiali and Distributed Tracing

The telemetry every Envoy sidecar reports feeds two tools worth knowing by name: **Kiali** renders the mesh's actual service-to-service call graph visually, derived from real observed traffic rather than a diagram someone drew and hoped stayed current, making it the fastest way to answer "what does this service actually call, right now" for a system too complex to reason about from source code alone; and a distributed tracing backend (commonly Jaeger, or Red Hat's OpenTelemetry-based Tempo stack) reconstructs one request's full path across every service hop it touched, with per-hop latency — the tool that turns "the request was slow somewhere in that chain of six services" into a specific answer naming which one.

Istio 1.26's **ambient mode** (referenced as a tech-preview feature in current OpenShift Service Mesh releases) is worth knowing by name as an emerging alternative: it removes the per-Pod sidecar entirely in favor of a shared per-node proxy, trading some of the sidecar model's per-Pod isolation for meaningfully lower resource overhead — a space actively evolving, and worth checking current release notes against before committing to one mode for a new mesh adoption.

### NetworkPolicy and the Mesh — Layered, Not Replaced

A common point of confusion once a namespace joins the mesh: `NetworkPolicy` does not stop being enforced. OVN-Kubernetes still evaluates every `NetworkPolicy` targeting a meshed Pod exactly as before — the mesh's mTLS and traffic-management layer sits **on top of**, not instead of, the CNI-level enforcement this chapter covered earlier. A request that a `NetworkPolicy` would deny is still denied, regardless of Istio's own `AuthorizationPolicy` rules (Istio's mesh-native equivalent of an allow/deny policy, enforced at the sidecar rather than the CNI level) permitting it — the two layers are independent, and both must permit a request for it to succeed, the same "every layer in the chain must agree" model this chapter's opening sequence diagram laid out.

| Layer | Enforced by | Independent of the other? |
|---|---|---|
| `NetworkPolicy` | OVN-Kubernetes, at the CNI level | Yes — evaluated regardless of mesh membership |
| Istio `AuthorizationPolicy` | The Envoy sidecar | Yes — evaluated regardless of NetworkPolicy |
| Combined effect | N/A | Both must permit a request — neither layer alone is sufficient to reason about actual reachability |

A team migrating a namespace into the mesh and expecting Istio's `AuthorizationPolicy` to become the *only* access-control layer going forward is a common early mistake — any pre-existing `NetworkPolicy` in that namespace keeps applying in full, and the two need to be reconciled deliberately rather than assumed redundant with each other.

## Quick Reference: Key Terms From This Chapter

| Term | What it is |
|---|---|
| OVN-Kubernetes | OpenShift's default CNI, built on Open Virtual Network and Open vSwitch |
| Geneve | The overlay encapsulation protocol OVN-Kubernetes uses between nodes |
| Shared / local gateway mode | The two cluster-wide egress/ingress path configurations OVN-Kubernetes supports |
| `ovnkube-trace` | The diagnostic tool simulating a packet's path through OVN's logical flows |
| `NetworkPolicy` | The Kubernetes-native allow-list mechanism governing Pod-to-Pod traffic |
| EgressIP | Assigns a stable, allow-listable external source IP to a Project's outbound traffic |
| `EgressFirewall` | Restricts which external destinations a Project's Pods may reach |
| Egress router | Routes a Project's traffic to one destination through a dedicated, stable-IP Pod |
| Route | OpenShift's native ingress object, richer than Kubernetes `Ingress` |
| Edge / passthrough / re-encrypt | The three Route TLS termination types |
| Ingress Operator | Manages the `IngressController`(s) that actually serve Routes |
| Router sharding | Running multiple `IngressController`s, each scoped to a Route subset |
| Multus | The meta-CNI attaching additional network interfaces beyond the default |
| `NetworkAttachmentDefinition` | The CRD defining one Multus-attached additional network |
| Submariner | The multi-cluster networking project connecting Pods across separate clusters |
| Sail Operator | Manages Istio's control plane for OpenShift Service Mesh 3.x |
| Envoy sidecar | The transparent per-Pod proxy a service mesh injects for mTLS and traffic management |
| Ambient mode | Istio's sidecar-less mesh mode, using a shared per-node proxy instead |
| `VirtualService` / `DestinationRule` | Istio's request-level routing and subset-definition CRDs |
| Kiali | The mesh's observed service-to-service call-graph visualization tool |
| Istio `AuthorizationPolicy` | The mesh-native allow/deny policy, enforced at the sidecar, independent of `NetworkPolicy` |
| `route-controller-manager` | The component auto-generating a Route from any Kubernetes `Ingress` object |

## Common Mistakes and Interview Traps

| Mistake or claim | Why it is wrong | Better answer |
|---|---|---|
| "OVN-Kubernetes is just another name for the same thing as `containerd`'s networking." | CRI-O/containerd handle the container runtime; OVN-Kubernetes is a completely separate layer implementing the cluster's own Pod network. | Name them as answering different questions: runtime (how a container starts) vs. CNI (how it's networked). |
| "A `NetworkPolicy` restricting egress only affects application traffic, not DNS." | DNS resolution is ordinary UDP/TCP traffic subject to the same default-deny once a Pod is selected by an egress policy. | Always add an explicit DNS-egress rule (typically to `kube-system`/`openshift-dns`) alongside any egress-restricting policy. |
| "Passthrough TLS termination is always the most secure choice since the router never sees the traffic." | It also disables router-level host-based routing and several router-provided features for that specific Route. | Choose the termination type based on the actual requirement (client-cert inspection needs passthrough; most cases don't). |
| "Adding a new sharded IngressController automatically removes its matched Routes from the default router." | The default IngressController keeps serving every Route not explicitly excluded from it. | Configure an explicit exclusion on the default router if truly exclusive sharding is the goal. |
| "A Kubernetes `Ingress` object doesn't work on OpenShift since Routes are the OpenShift-native mechanism." | The route-controller-manager automatically generates a corresponding Route from any `Ingress` object. | Both coexist; `Ingress` favors portability, direct `Route` favors OpenShift-specific features. |
| "It's fine to hand-edit a Route that was generated automatically from an `Ingress` object, as long as the edit looks correct." | The route-controller-manager treats the generated Route as owned and derived; any manual edit is overwritten on the next reconciliation. | Edit the source `Ingress` object, never the Route it generated, the same reconciliation lesson from the MCO and default ClusterRoles. |
| "Multus is a general-purpose way to give every Pod better networking performance." | It's a specialized mechanism for a narrow class of workloads needing genuinely separate network segments, not a general performance optimization. | Reach for it only when a concrete requirement (NFV, dedicated storage network, SR-IOV) exists, not by default. |
| "Adopting a service mesh is a good default hardening step for any multi-service application." | It's a real, ongoing operational cost that pays off specifically once NetworkPolicy/Routes' capabilities are genuinely insufficient. | Adopt it when a concrete requirement (universal mTLS, percentage-based canary routing, deep call-graph observability) is real, not preemptively. |
| "Router replica count autoscales automatically like most OpenShift workloads." | The default router is not autoscaled by default — capacity planning for it is a deliberate, manual sizing decision. | Size and monitor router replica count explicitly against expected external traffic volume. |
| "A Route's weighted `alternateBackends` is functionally equivalent to a service mesh's traffic splitting." | Route weighting only splits by percentage at the ingress edge; it can't route on headers, retry selectively, or inject faults for testing. | Use Route weighting for a simple ingress-level canary; reach for `VirtualService`/`DestinationRule` when routing logic needs to live inside the mesh, not just at the edge. |
| "Changing OVN-Kubernetes's gateway mode is a simple configuration toggle." | It's a disruptive, cluster-wide change affecting every node's traffic path, not a lightweight setting. | Treat a gateway-mode change as a significant, carefully-planned Day-2 operation, not a quick fix for an unrelated symptom. |
| "Joining a namespace to the service mesh means Istio's `AuthorizationPolicy` replaces `NetworkPolicy` as the access-control mechanism." | Both layers are enforced independently — a pre-existing `NetworkPolicy` keeps applying in full regardless of mesh membership. | Reconcile the two layers deliberately when migrating a namespace into the mesh, rather than assuming one supersedes the other. |
| "EgressIP and EgressFirewall are two names for the same feature." | EgressIP controls the source IP outbound traffic presents; EgressFirewall controls which destinations are reachable at all — genuinely different concerns, often needed together. | Name each mechanism's specific job separately, and check whether a given requirement (stable source IP vs. destination restriction) needs one, the other, or both. |

## Worked Practice Problems

### 1. A team adds a default-deny `NetworkPolicy` to a namespace, tests every known internal service dependency, and ships it. The next day, Pods in that namespace can't pull images from an internal registry hostname, though direct-IP connections still work. What's the most likely cause, and what's the fix?

The most likely cause is exactly this chapter's DNS-egress gotcha: once a Pod is selected by an egress-covering `NetworkPolicy`, DNS resolution itself is subject to the same default-deny unless an explicit rule permits it, and the team's dependency testing evidently didn't specifically probe hostname-based resolution the way an image pull (which resolves the registry's hostname before connecting) does. The fix is adding an explicit egress rule allowing UDP/TCP port 53 traffic to the cluster's DNS Pods (typically selected via a `namespaceSelector` targeting `openshift-dns`), alongside the existing application-specific egress rules, rather than reverting the policy entirely.

### 2. A public-facing application needs end-to-end encryption for compliance reasons, but the team also wants the router to perform host-based routing and collect its own request metrics — a passthrough Route breaks both. What's the right TLS termination type, and why?

Re-encrypt termination is the right fit: it terminates the client's TLS session at the router (satisfying the compliance requirement that traffic is encrypted from the client all the way to the router), then establishes a second, separate TLS session from the router to the Pod (satisfying "end-to-end encryption" in the sense of never traversing the network in plaintext), while still letting the router see the decrypted request in between — which is exactly what host-based routing and router-level metrics collection require. Passthrough would satisfy the encryption requirement but sacrifice the routing/metrics requirement entirely, since the router never decrypts passthrough traffic at all; re-encrypt is the specific option designed for exactly this combination of requirements.

### 3. A platform team wants to isolate a set of untrusted, multi-tenant-facing Routes onto their own router, separate from the cluster's internal Routes, without accidentally leaving those Routes also served by the default router. What steps does this actually require?

Beyond creating the new sharded `IngressController` with a `namespaceSelector`/`routeSelector` matching the untrusted Routes, the team must also explicitly configure the **default** `IngressController` to exclude that same selector — otherwise, per this chapter's sharding section, the default router continues serving those Routes in addition to the new sharded one, defeating the isolation goal entirely. The team also needs a separate DNS record pointing specifically at the new sharded router's own exposure endpoint, since DNS routing between multiple routers isn't wired up automatically the moment a second `IngressController` is created.

### 4. An application team wants universal mutual TLS between a dozen internal microservices and asks whether NetworkPolicy can provide it. What's the honest answer, and what's the actual right tool?

NetworkPolicy cannot provide this — it's an allow/deny mechanism operating on IP/port-level reachability between Pod and namespace selectors, with no concept of encryption or certificate-based identity at all; a NetworkPolicy allowing traffic between two Pods says nothing about whether that traffic is encrypted or authenticated in any cryptographic sense. OpenShift Service Mesh is the right tool for exactly this requirement: enrolling the relevant namespaces in the mesh (via the `istio-injection: enabled` label) gives every enrolled service automatic, transparent mutual TLS to every other enrolled service through their Envoy sidecars, with no application code changes — the honest trade-off to name alongside recommending it is the added operational cost of running and understanding a service mesh control plane, which is worth being upfront about rather than presenting the adoption as free.

### 5. A cluster's default router is showing elevated latency during peak traffic hours, but every backend Pod's own metrics look healthy. What's the first capacity-related hypothesis to check, and why might it be easy to overlook?

The first hypothesis worth checking is that the router itself — not any backend Pod — is under-provisioned for current traffic volume, since this chapter's own coverage of the Ingress Operator names it explicitly as a component that is not autoscaled by default and requires deliberate, manual replica sizing. It's easy to overlook precisely because most other OpenShift workloads either autoscale automatically or are sized once and rarely revisited, so a platform team's usual capacity-monitoring habits may simply never have been pointed at the router's own replica count and per-instance resource usage — checking `oc get ingresscontroller default -n openshift-ingress-operator -o yaml` for the current replica count against the cluster's actual peak external request volume is the concrete next step, before investigating any backend-side explanation further.

### 6. Two services in the same mesh-enrolled namespace can't reach each other. Istio's `AuthorizationPolicy` for that namespace looks correct and permissive. What's the next layer to check, and why might a team miss it?

The next layer to check is `NetworkPolicy` at the CNI level — since Istio's `AuthorizationPolicy` and Kubernetes' `NetworkPolicy` are enforced completely independently (per this chapter's layering section), a correct and permissive `AuthorizationPolicy` says nothing about whether an existing `NetworkPolicy` in that namespace is silently denying the same traffic. A team is likely to miss this specifically because joining the mesh is often mentally framed as "now Istio handles access control here," leading to `NetworkPolicy` being overlooked entirely once the mesh is in place — `oc get networkpolicy -n <namespace> -o yaml` and tracing the specific denied path with `ovnkube-trace` from earlier in this chapter is the concrete next diagnostic step, treating the mesh's own policy layer as necessary but not sufficient to explain the failure.

## Summary and What's Next

This chapter moved through four distinct networking layers in the order traffic actually experiences them: OVN-Kubernetes as the CNI making Pod-to-Pod connectivity physically possible cluster-wide via a Geneve-encapsulated overlay; NetworkPolicy as the allow-list layer governing which of those physically-possible paths are actually permitted, including the easy-to-miss requirement that DNS itself needs an explicit egress rule; Routes (and their EgressIP/Egress Firewall counterparts for outbound traffic) as OpenShift's opinionated, richer-than-`Ingress` answer to external traffic reaching a Service, with three TLS termination types trading off differently between security posture and router-level functionality; and OpenShift Service Mesh as the optional, higher-cost layer for the specific subset of applications whose service-to-service complexity has genuinely outgrown what the first three layers can express. Multus and cluster DNS configuration rounded out the specialized and supporting mechanisms most clusters touch rarely, but need to recognize by name when a real requirement surfaces.

The five-hop request trace this chapter opened with — Route/Ingress, mesh sidecar, NetworkPolicy, CNI, destination Pod — is worth keeping as a standing mental model for any future networking incident on this platform: naming which specific layer actually failed, rather than describing the symptom generically as "networking is broken," is what turns an open-ended investigation into a targeted one, and every tool this chapter introduced (`ovnkube-trace`, `oc adm policy scc-subject-review`'s RBAC-layer cousin from Part 2, Route status conditions, Kiali's call graph) exists to answer exactly one layer of that question directly rather than requiring it to be inferred.

Part 4 moves from how traffic reaches a running workload to how that workload's container image gets built and delivered in the first place: BuildConfigs and Source-to-Image as OpenShift's built-in build mechanism, ImageStreams and the integrated internal registry, and OpenShift Pipelines (Tekton) paired with OpenShift GitOps (the ArgoCD Operator) as the modern, Kubernetes-native way most teams now assemble their actual delivery pipeline on top of those primitives.

Cross-reference: this catalog's Kubernetes Deep Dive series covers CNI, `Service`, and `Ingress` from a vanilla-Kubernetes vantage point in its own Networking (CNI) & Storage (CSI) chapter and its Gateway API chapters — worth revisiting in parallel with this chapter for the parts of the networking stack (Services, DNS, CNI fundamentals) that are genuinely unmodified between the two platforms, rather than re-deriving them here.

Sources consulted for this chapter: Red Hat's OpenShift Container Platform Networking documentation (OVN-Kubernetes, NetworkPolicy, EgressIP, Egress Firewall, Ingress sharding, Multus/multiple networks, DNS Operator), and Red Hat's OpenShift Service Mesh 3.x and Sail Operator documentation and release notes, current as of OpenShift 4.19/4.20 and OpenShift Service Mesh 3.1.

Confirm these version references against the cluster's own `oc get clusterversion` and `oc get csv -n openshift-operators` output before relying on any specific version number in this chapter for a real deployment decision — both OpenShift and OpenShift Service Mesh ship new minor versions on an ongoing cadence, per Part 1's release-cadence coverage.
