Architecture¶
oooi bridges the connectivity gap between KubeVirt worker nodes on an isolated VLAN and their hosted control plane running on the management cluster's pod network — without opening the management cluster to tenant traffic.
The problem¶
OpenShift Hosted Control Planes decouple control planes from workers: hosted
control-plane pods (kube-apiserver, OAuth server, Ignition server,
konnectivity) run in a management-cluster namespace such as
clusters-example-hcp, while worker nodes are KubeVirt VMs. When those VMs are
placed on an isolated VLAN with attachDefaultNetwork: false — desirable for
air-gapped or high-security designs — they lose all direct paths to those
Services.
Conventional workarounds (hosting-cluster Routes for everything, shared load balancers) require tenant traffic to traverse management-cluster ingress infrastructure. That breaks strict network isolation.
The oooi approach¶
oooi deploys the network services the VLAN is missing onto the VLAN itself:
flowchart LR
subgraph vlan["Isolated VLAN · e.g. 192.0.2.0/24"]
direction TB
W["KubeVirt<br/>worker VMs"]
DHCP["DHCPServer · .2"]
DNS["DNSServer (CoreDNS) · .3"]
PRX["ProxyServer (Envoy) · .4"]
VIP["MetalLB VIP · .200"]
W -->|DHCP lease| DHCP
W -->|cluster + upstream DNS| DNS
W -->|"api :6443 · HCP :443<br/>(TLS passthrough by SNI)"| PRX
W -->|"*.apps :80/:443"| VIP
VIP -->|wildcard SNI| PRX
end
subgraph mgmt["Management cluster pod network"]
direction TB
OPS["oooi controller<br/>(oooi-system)"]
HCP["Hosted control plane Services<br/>kube-apiserver :6443<br/>oauth / ignition / konnectivity :443"]
ING["Hosted ingress router<br/>(router-default)"]
end
subgraph pub["Public DNS zone"]
direction TB
EDNS["ExternalDNS"]
ZONE[("A records: api.* · oauth.* · *.apps.*")]
CLI["External clients"]
EDNS -->|writes| ZONE
CLI --> ZONE
end
OPS -.->|reconciles| W & DHCP & DNS & PRX
OPS -->|"installs MetalLB,<br/>creates LB Service"| VIP
PRX -->|L4 to ClusterIPs| HCP
ING -.->|backend of *.apps| VIP
EDNS -.->|watches Services| ZONE
Only two crossing points exist between VLAN and management cluster, both narrowly scoped:
- Envoy forwards specific SNI names to specific control-plane Service ClusterIPs — TCP bytes only, TLS never terminated.
- Envoy wildcard backends forward
*.appsto the MetalLB VIP that fronts the hosted ingress router.
Components¶
The user-facing API has two scopes. Infra describes the shared VLAN stack;
InfraClusterAttachment binds one HostedCluster to that stack and carries its
DNS, control-plane, and optional apps-ingress settings. oooi reconciles the
shared child custom resources in the Infra namespace; each child manages its
own Deployment, Services, ConfigMap, and ServiceAccount. When OpenShift
integration is enabled, the relevant child also manages its component-scoped
SCC RoleBinding. Pods reference the configured NetworkAttachmentDefinition
through Multus annotations; oooi does not create the NAD.
| Component | Child CR | Image | Role |
|---|---|---|---|
InfraReconciler |
— | registry.example.com/oooi |
Creates shared children, aggregates attachment routes, and reports shared status |
InfraClusterAttachmentReconciler |
— | registry.example.com/oooi |
Binds one HostedCluster, manages its control-plane policy, and drives optional apps ingress |
| DHCP | DHCPServer |
Infra-generated: oooi image; standalone API default: HyperDHCP image | Serves leases on the VLAN; discovers KubeVirt VM interfaces to keep leases stable |
| DNS | DNSServer |
oooi image (CoreDNS component) | Split-horizon views; static HCP answers; upstream forwarding |
| Proxy | ProxyServer |
Envoy + oooi xDS sidecar | L4 TLS-passthrough gateway; SNI and source-scoped routing; apps wildcard backends |
| Apps ingress | InfraClusterAttachment |
MetalLB operator | Installs MetalLB into the attached hosted cluster, allocates and advertises its wildcard VIP |
Ownership and garbage collection¶
The three child CRs carry owner references to Infra. Deleting Infra
garbage-collects those child CRs and their namespaced workloads, Services, and
RBAC. The control-plane NetworkPolicy and DHCP ClusterRole/ClusterRoleBinding
cannot carry an Infra owner reference because they are cross-namespace or
cluster-scoped; remove them during cleanup. Apps-ingress resources live in the
hosted cluster and must also be checked and removed separately. See
Uninstall and cleanup for the supported teardown
order.
Traffic flows¶
Control plane (API, OAuth, Ignition, konnectivity)¶
sequenceDiagram
participant VM as Worker VM (VLAN)
participant E as Envoy (VLAN IP)
participant S as HCP Service (ClusterIP)
VM->>E: TCP :6443 / :443, TLS ClientHello (SNI)
Note over E: Route selected by SNI name.<br/>TLS not terminated.
E->>S: Raw TCP to matching ClusterIP
S-->>VM: TLS session end-to-end (hosted cluster certs)
api.<cluster>.<domain>:6443→controlPlaneNamespace/kube-apiserveroauth|ignition|konnectivity.<cluster>.<domain>:443→ matching HCP Services
For a KubeVirt worker, the shared proxy supports these service aliases on
port 443 for SNI-based traffic and on port 6443 for the in-cluster
Kubernetes Service path:
The aliases resolve to the one VLAN proxy address. They are not globally
ambiguous at the proxy: the Infra controller finds KubeVirt NodePools and their
CAPI Machine objects, filters Machine.status.addresses to the shared
networkConfig.cidr, and emits one Envoy backend per attachment with sorted
/32 sourcePrefixRanges. A Machine address outside that CIDR is ignored.
The controller consumes the addresses exposed in Machine status; validate the
CAPK address-selection and refresh behavior for the target release. Until an
in-CIDR address is available, the attachment keeps its fully qualified routes
but has no alias backend; the controller retries discovery.
The 6443 service-alias backend matches the worker source range because that
path connects by IP and does not send SNI. The 443 alias backend retains the
Kubernetes hostname and matches both SNI and source range.
The source address is a routing selector, not an identity mechanism. A client that can spoof another worker's VLAN address can select another attachment, so use CNI, switch, or DHCP anti-spoofing controls when the VLAN is shared by untrusted tenants.
Because Envoy never terminates TLS, certificates remain inside the hosted cluster and the proxy cannot inspect tenant traffic.
On port 443, the generated configuration also includes a legacy konnectivity
fallback when a non-source-scoped konnectivity backend exists. It is intended
for no-SNI clients, but the catch-all filter chain can receive unmatched SNI or
source traffic too; it is not an alias route or an access-control boundary.
Apps ingress (wildcard *.apps)¶
For an attachment with spec.appsIngress.enabled: true:
- oooi waits for a Ready hosted worker (OLM's MetalLB bundle-unpack Job needs schedulable capacity).
- Installs the Red Hat MetalLB operator subscription in the hosted cluster,
then
MetalLB, anIPAddressPool, and anL2Advertisement. - Creates a
LoadBalancerService (default nameoooi-ingress) in the hosted cluster'sopenshift-ingressnamespace, selector fixed to the default IngressController deployment. - Reads the allocated IP or hostname from Service status and publishes it as
InfraClusterAttachment.status.appsIngressStatus.externalIPor.externalHostname. - Adds the
*.apps.<cluster>.<domain>A record to the VLAN DNS view only when an external IP exists and, wheninternalProxyServiceis configured, to the pod-network view; it adds Envoy wildcard backends for either an IP or a hostname endpoint.
MetalLB L2 mode advertises the VIP from a hosted worker itself, so the worker VMs reach the ingress without any external load balancer.
DNS split-horizon¶
flowchart LR
Q{{"query for api.example-hcp.clusters.example.com"}}
subgraph C["CoreDNS (two views)"]
V1["VLAN view<br/>source = networkConfig.cidr"]
V2["Default view<br/>(pod network)"]
end
Q --> C
V1 -->|"static: proxy.serverIP<br/>192.0.2.4"| A1[/"A record"/]
V2 -->|"internalProxyService ClusterIP (if configured)"| A2[/"A record"/]
Q -.->|"non-HCP names"| UP["Upstream resolvers<br/>networkConfig.dnsServers"]
Both views answer identically for names outside the static zones — only the
HCP endpoint, *.apps, and conditional Kubernetes alias names differ per view.
The aliases still resolve to one shared proxy address; source matching happens
after the TCP connection reaches Envoy.
Security model¶
| Aspect | Design |
|---|---|
| TLS | Passthrough only; no keys or secrets on the proxy |
| Exposed ports | VLAN: DHCP/67+68, DNS/53, proxy 443+6443; Envoy admin 9901 stays ClusterIP-only |
| External exposure | Each enabled attachment gets a <attachment>-proxy-external LoadBalancer exposing only the configured ingress port — never admin or backend ports |
| OpenShift SCC | With --enable-openshift=true, scoped SCC RoleBindings are created: privileged for DHCP and Proxy, and anyuid for DNS. Without the flag, grant equivalent permissions through cluster policy. |
| Control-plane policy | An ingress-only allow-infrastructure NetworkPolicy selects all pods in the control-plane namespace and allows traffic from namespaces labeled hostedcluster.densityops.com/network-policy-group=infrastructure |
| Network scope | No general tenant route into the management network; Envoy permits only configured control-plane and apps backends |
Status model¶
Infra.status reports reconciliation state. It does not replace checking the
child Deployments for runtime availability:
status:
conditions: # Ready / ReconciliationSucceeded / Degraded ...
componentStatus: # dhcpReady, dnsReady, proxyReady
# On the corresponding InfraClusterAttachment:
status:
appsIngressStatus:
phase: # Pending | Ready | Degraded
reason: # WaitingForHostedClusterNodes, WaitingForExternalIP, ...
message: # human-readable detail when Degraded
externalIP: # assigned IP, when the endpoint is IP-backed
externalHostname: # assigned hostname, otherwise
See Verification for ready-made queries and expected values.