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 always
uses 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
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 jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.interfaces[0].ipAddress}{"\n"}{end}'
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
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"
- "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 A record or CNAME that references the LoadBalancer Service's external IP:
-
Get the external IP:
export EXTERNAL_IP=$(oc -n $HCP_NAMESPACE get service ${CLUSTER_NAME}-apps \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}') -
Configure a wildcard
*.apps.<cluster_name>.<base_domain>.DNS entry referencing the IP stored in$EXTERNAL_IP. The DNS record must be routable both from outside the cluster and from inside the guest VMs (see the Troubleshooting section for hairpin issues).For example, for a cluster named
examplewith base domainhypershift.laband external IP192.168.20.30:dig +short test.apps.example.hypershift.lab 192.168.20.30
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.32-192.168.216.122 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
oc get vmi), 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 NodePort 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 DNS hairpin — the VMs are sending traffic to a VIP that routes back to themselves, but the return path is broken. Common fixes include enabling SNAT on the load balancer or configuring split-horizon DNS so that guest VMs resolve*.appsdirectly to their own machineNetwork IPs instead of the external VIP.