Ingress and DNS configuration
This guide covers how to configure ingress and DNS for KubeVirt-based Hosted Control Plane (HCP) clusters.
How KubeVirt ingress works
On KubeVirt HCP clusters, the guest cluster's default IngressController defaults
to the NodePortService endpoint publishing strategy. This means the guest
cluster's router pods are exposed through a NodePort Service
(router-nodeport-default in the openshift-ingress namespace) that listens
on dynamically assigned ports on each guest VM's network interface.
There are two modes for routing external traffic to these NodePorts:
| Mode | When it applies | Who manages ingress routing |
|---|---|---|
| baseDomainPassthrough (default) | No baseDomain specified, or baseDomainPassthrough explicitly set to true |
HyperShift (automatic) |
| Custom baseDomain | Explicit baseDomain provided at creation time |
User (manual) |
When baseDomainPassthrough is auto-enabled
When creating a KubeVirt HostedCluster without specifying a baseDomain,
the HyperShift webhook automatically enables baseDomainPassthrough:
- If
spec.dns.baseDomainis empty, the webhook setsspec.platform.kubevirt.baseDomainPassthrough = true - If you provide an explicit
baseDomain, the webhook does not enable baseDomainPassthrough, and you are responsible for configuring ingress manually
Important
baseDomainPassthrough is immutable after HostedCluster creation. If
you create a cluster with a custom baseDomain (and therefore without
baseDomainPassthrough), you cannot enable it later without recreating the
cluster.
Default: baseDomainPassthrough
When baseDomainPassthrough is enabled (the default when no baseDomain is
specified), HyperShift automatically configures all ingress routing
infrastructure on the management cluster. No manual LoadBalancer or DNS setup
is required.
What HyperShift creates automatically
-
A wildcard passthrough Route on the management cluster with
TLSTerminationPassthroughandWildcardPolicySubdomain. This Route matches all*.apps.<guest>.<mgmt-apps-domain>requests and forwards them without terminating TLS. -
A ClusterIP Service on the management cluster with an empty selector (no pod selector). The Service's target port is set to the guest router's HTTPS NodePort.
-
EndpointSlices managed by the Machine controller, pointing to the VM's machineNetwork IPs (not pod IPs) on the correct NodePort. These are automatically updated when VMs are added, removed, or live-migrated.
Resulting DNS domain
The guest cluster's base domain is auto-detected as a subdomain of the
management cluster's *.apps domain. For example:
- Management cluster apps domain:
*.apps.mgmt-cluster.example.com - Guest cluster named
guest:*.apps.guest.apps.mgmt-cluster.example.com
How the default ingress passthrough works
The default ingress passthrough is implemented in the infra cluster namespace where the KubeVirt VMs run with:
- A selector-less
ClusterIPService (default-ingress-passthrough-service-<id>) exposing port443. EndpointSlices for that Service, one per worker VM, pointing at the VM internal IPs and the port where the guest routers listen.- A wildcard passthrough
Route(default-ingress-passthrough-route-<id>) for*.apps.<guest>.<infra base domain>targeting that Service.
The port targeted on the VMs depends on the guest default IngressController
endpointPublishingStrategy:
NodePortService(default for KubeVirt): the HTTPSnodePortof theopenshift-ingress/router-nodeport-defaultService in the guest cluster.-
HostNetwork: thehostNetwork.httpsPort(defaults to443). This strategy can be selected at creation time throughspec.operatorConfiguration.ingressOperator.endpointPublishingStrategyin theHostedCluster, for example:spec: operatorConfiguration: ingressOperator: endpointPublishingStrategy: type: HostNetwork hostNetwork: httpPort: 80 httpsPort: 443 statsPort: 1936 protocol: TCP
Note
With HostNetwork, every running worker VM is added as an endpoint. Only the
nodes where a router pod is scheduled accept connections; the infra cluster
router health checks exclude the other endpoints.
Other endpoint publishing strategies (e.g. LoadBalancerService) are not
supported by the default ingress passthrough; use the custom baseDomain
behavior described below instead.
Prerequisites
The management cluster must allow wildcard DNS routes:
oc patch ingresscontroller -n openshift-ingress-operator default \
--type=json \
-p '[{ "op": "add", "path": "/spec/routeAdmission", "value": {"wildcardPolicy": "WildcardsAllowed"}}]'
Note
When using baseDomainPassthrough, connectivity is limited to HTTPS traffic over port 443. Plain HTTP traffic over port 80 will be rejected. This limitation only applies to the default ingress behavior, not the custom baseDomain configuration described below.
Custom baseDomain (without baseDomainPassthrough)
When you provide an explicit baseDomain at creation time, HyperShift does
not enable baseDomainPassthrough and does not create any ingress
routing infrastructure on the management cluster. You are fully responsible
for configuring:
- A LoadBalancer Service on the management cluster
- An EndpointSlice pointing to the VM machineNetwork IPs
- A wildcard DNS record for
*.apps.<cluster-name>.<baseDomain>
The traffic flow for this configuration is:
Client
└─> *.apps.<cluster>.<baseDomain> (DNS wildcard)
└─> LoadBalancer VIP (MetalLB / external LB)
└─> VM machineNetwork IP (EndpointSlice target)
└─> NodePort (guest router)
└─> guest Route (application)
This process involves four steps:
- Cluster creation
- LoadBalancer and EndpointSlice creation
- Wildcard DNS configuration
- Verification
Step 1 - Deploy the HostedCluster with a custom baseDomain
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
This creates a HostedCluster with ingress wildcard *.apps.example.hypershift.lab.
The HostedCluster will remain in Partial progress until the LoadBalancer and
DNS are configured:
1 2 3 4 | |
Step 2 - Set up the LoadBalancer and EndpointSlice
Warning
Do not use a pod selector (such as kubevirt.io: virt-launcher) on the
LoadBalancer Service. KubeVirt VMs typically have two network interfaces: the
pod network (used by the virt-launcher pod on the management cluster) and
the machineNetwork (the VM's actual network, often on a secondary bridge
interface). The guest router's NodePort only listens on the machineNetwork
IPs, not on the pod network IPs. A pod selector resolves to pod network IPs,
which causes connection refused or http: server gave HTTP response to
HTTPS client errors.
Instead, create a Service with no selector and manually manage an EndpointSlice that points to the VM machineNetwork IPs.
Note
If your cluster is on bare metal you may need MetalLB to be able to provision functional LoadBalancer services. See the Optional MetalLB Configuration Steps section.
1. Retrieve the guest cluster NodePorts
export CLUSTER_KUBECONFIG="${CLUSTER_NAME}-kubeconfig"
hcp create kubeconfig --name $CLUSTER_NAME > $CLUSTER_KUBECONFIG
export HTTP_NODEPORT=$(oc --kubeconfig $CLUSTER_KUBECONFIG get services \
-n openshift-ingress router-nodeport-default \
-o jsonpath='{.spec.ports[?(@.name=="http")].nodePort}')
export HTTPS_NODEPORT=$(oc --kubeconfig $CLUSTER_KUBECONFIG get services \
-n openshift-ingress router-nodeport-default \
-o jsonpath='{.spec.ports[?(@.name=="https")].nodePort}')
echo "HTTP NodePort: $HTTP_NODEPORT"
echo "HTTPS NodePort: $HTTPS_NODEPORT"
2. Retrieve the VM machineNetwork IPs
export HCP_NAMESPACE="clusters-${CLUSTER_NAME}"
oc get vmi -n $HCP_NAMESPACE -o json | \
jq -r '.items[] | "\(.metadata.name)\t\(.status.interfaces[] | select(.name != "default" and .ipAddress != null and .ipAddress != "") | .ipAddress | split("/")[0])"'
This command filters out the pod network interface (default) and strips any
CIDR suffix from the IP address. If your VMs use a different interface layout,
list all interfaces with oc get vmi -n $HCP_NAMESPACE -o yaml and adjust the
filter accordingly.
Save the VM IPs for use in the EndpointSlice below. For example:
example-workers-abc12-xyz34 192.168.216.50
example-workers-abc12-xyz56 192.168.216.51
3. Create the LoadBalancer Service (no selector)
cat << EOF | oc apply -f -
apiVersion: v1
kind: Service
metadata:
labels:
app: ${CLUSTER_NAME}
name: ${CLUSTER_NAME}-apps
namespace: ${HCP_NAMESPACE}
spec:
ports:
- name: https-443
port: 443
protocol: TCP
targetPort: ${HTTPS_NODEPORT}
- name: http-80
port: 80
protocol: TCP
targetPort: ${HTTP_NODEPORT}
type: LoadBalancer
EOF
Note that the Service has no selector field. Traffic routing is handled
by the EndpointSlice created in the next step.
4. Create the EndpointSlice
Replace the IP addresses below with the VM machineNetwork IPs retrieved in step 2:
cat << EOF | oc apply -f -
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: ${CLUSTER_NAME}-apps-endpoints
namespace: ${HCP_NAMESPACE}
labels:
kubernetes.io/service-name: ${CLUSTER_NAME}-apps
endpointslice.kubernetes.io/managed-by: manual
addressType: IPv4
ports:
- name: https-443
port: ${HTTPS_NODEPORT}
protocol: TCP
- name: http-80
port: ${HTTP_NODEPORT}
protocol: TCP
endpoints:
- addresses:
- "192.168.216.50"
- addresses:
- "192.168.216.51"
EOF
Important
The EndpointSlice must be updated manually whenever the guest cluster's VMs change:
- Scaling up: Add new VM machineNetwork IPs to the EndpointSlice
- Scaling down: Remove decommissioned VM IPs
- Live migration: Update IPs if the VM's machineNetwork address changes
Run oc get vmi -n $HCP_NAMESPACE to retrieve the current VM IPs after
any scaling or migration event.
Step 3 - Set up a wildcard DNS record for *.apps
Configure a wildcard DNS record that references the LoadBalancer Service's external address:
-
Get the external address. Depending on the load balancer provider, either
.ip(IP-based, e.g., MetalLB, GCE) or.hostname(DNS-based, e.g., AWS ELB) is populated:export EXTERNAL_IP=$(oc -n $HCP_NAMESPACE get service ${CLUSTER_NAME}-apps \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}') export EXTERNAL_HOSTNAME=$(oc -n $HCP_NAMESPACE get service ${CLUSTER_NAME}-apps \ -o jsonpath='{.status.loadBalancer.ingress[0].hostname}') -
Configure a wildcard
*.apps.<cluster_name>.<base_domain>.DNS entry. The DNS record must be routable both from outside the cluster and from inside the guest VMs (see the Troubleshooting section for hairpin issues).-
If
$EXTERNAL_IPis set, create a wildcard A record:*.apps.example.hypershift.lab. IN A 192.168.20.30 -
If
$EXTERNAL_HOSTNAMEis set instead, create a wildcard CNAME record:*.apps.example.hypershift.lab. IN CNAME a1b2c3-1234.us-east-1.elb.amazonaws.com.
Verify DNS resolves correctly:
dig +short test.apps.example.hypershift.lab -
Step 4 - Verify the HostedCluster status
Once the LoadBalancer and DNS are in place, the HostedCluster progress should
move from Partial to Completed:
1 2 3 4 | |
Optional MetalLB Configuration Steps
LoadBalancer type services are required. If MetalLB is in use, here are some example steps outlining how to configure MetalLB after installing MetalLB using CLI.
-
Create a MetalLB instance:
oc create -f - <<EOF apiVersion: metallb.io/v1beta1 kind: MetalLB metadata: name: metallb namespace: metallb-system EOF -
Create address pool with an available range of IP addresses within the node network:
oc create -f - <<EOF apiVersion: metallb.io/v1beta1 kind: IPAddressPool metadata: name: metallb namespace: metallb-system spec: addresses: - 192.168.216.200-192.168.216.220 EOF -
Advertise the address pool using L2 protocol:
oc create -f - <<EOF apiVersion: metallb.io/v1beta1 kind: L2Advertisement metadata: name: l2advertisement namespace: metallb-system spec: ipAddressPools: - metallb EOF
Troubleshooting
CanaryChecksRepetitiveFailures with custom baseDomain
When using a custom baseDomain (without baseDomainPassthrough), the ingress
operator may report Degraded with errors like:
CanaryChecksRepetitiveFailures: Canary route checks for the default ingress
controller are failing. Last 1 error messages:
error sending canary HTTP request: http: server gave HTTP response to HTTPS client
or:
connection refused
Diagnostic steps
-
Verify DNS resolution from inside the guest VMs. The canary check runs from inside the guest cluster, so DNS must resolve correctly from within the VMs:
oc --kubeconfig $CLUSTER_KUBECONFIG debug node/<any-guest-node> -- \ chroot /host nslookup canary-openshift-ingress-canary.apps.${CLUSTER_NAME}.${BASE_DOMAIN}Compare this IP with the LoadBalancer VIP. If they differ, DNS is misconfigured.
-
Verify the LoadBalancer endpoints use machineNetwork IPs, not pod IPs. Check the EndpointSlice:
oc get endpointslice -n $HCP_NAMESPACE -l kubernetes.io/service-name=${CLUSTER_NAME}-apps -o yamlThe IP addresses in the EndpointSlice must be the VM machineNetwork IPs (the same IPs returned by the
oc get vmi -o json | jqcommand in Step 2), not the virt-launcher pod IPs. If the EndpointSlice contains pod network IPs (typically in a different CIDR than the machineNetwork), the guest router NodePort will not be reachable and connections will be refused. -
Test LoadBalancer VIP reachability from inside the guest. Curl the LoadBalancer VIP from within a guest VM:
oc --kubeconfig $CLUSTER_KUBECONFIG debug node/<any-guest-node> -- \ chroot /host curl -vk --connect-timeout 5 https://<EXTERNAL_IP>:443If this returns
connection refusedbut the same curl works from outside the guest VMs, the issue is VIP return-path routing — the VMs are sending traffic to a VIP that routes back to themselves, but the return path is broken (asymmetric routing). Configure split-horizon DNS so that guest VMs resolve*.appsdirectly to their own machineNetwork IPs instead of the external VIP. See the JSON patch DNS override in the recipe for an automated approach.