Understanding ARO’s Private DNS Zones Setup

Private DNS Zones Created by ARO

When you deploy a private ARO cluster, Azure automatically creates two private DNS zones in the ARO-managed resource group — one for the API server and one for application ingress. You own neither; they are managed by the ARO service, but you are responsible for linking them to every VNet that needs to resolve them.


The Two Zones ARO Creates

ARO creates both zones inside the ARO managed resource group — the resource group whose name starts with aro- that Azure creates automatically alongside your cluster. You cannot modify or delete these zones without breaking the cluster.

Zone 1 — API Server

Zone name: cluster.<unique-id>.<region>.aroapp.io
Example: cluster.a1b2c3d4.eastus.aroapp.io
A records:
api → 10.1.0.8 (private endpoint NIC IP)
Full FQDN: api.cluster.a1b2c3d4.eastus.aroapp.io:6443

Zone 2 — Application Ingress

Zone name: cluster.<unique-id>.<region>.aroapp.io
(same parent zone, different record)
A records:
*.apps → 10.1.1.100 (internal load balancer frontend IP)
Example resolutions:
my-app.apps.cluster.a1b2c3d4.eastus.aroapp.io → 10.1.1.100
console.apps.cluster.a1b2c3d4.eastus.aroapp.io → 10.1.1.100
grafana.apps.cluster.a1b2c3d4.eastus.aroapp.io → 10.1.1.100

Inspecting the Zones After Deployment

# Get the ARO managed resource group
MANAGED_RG=$(az aro show \
--resource-group rg-aro \
--name aro-prod \
--query clusterProfile.resourceGroupId \
--output tsv | xargs basename)
# List all private DNS zones ARO created
az network private-dns zone list \
--resource-group $MANAGED_RG \
--query "[].{Zone:name, Records:numberOfRecordSets}" \
--output table
# Output:
# Zone Records
# ─────────────────────────────────────────── ─────────
# cluster.a1b2c3d4.eastus.aroapp.io 4
# List all A records in the zone
az network private-dns record-set a list \
--resource-group $MANAGED_RG \
--zone-name cluster.a1b2c3d4.eastus.aroapp.io \
--output table
# Output:
# Name TTL ARecords
# ────── ──── ──────────────────────
# api 300 [{'ipv4Address': '10.1.0.8'}]
# *.apps 300 [{'ipv4Address': '10.1.1.100'}]
# List VNet links on the zone
az network private-dns link vnet list \
--resource-group $MANAGED_RG \
--zone-name cluster.a1b2c3d4.eastus.aroapp.io \
--output table
# Output:
# Name VirtualNetwork RegistrationEnabled
# ─────────────────────── ────────────────────── ───────────────────
# aro-spoke-vnet-link aro-spoke-vnet false ← auto-created

The VNet Linking Problem

This is the most common post-deployment mistake. ARO automatically links the private DNS zone to only one VNet — the ARO spoke VNet. Every other VNet that needs to resolve the API server or app routes must be manually linked:

ZONE_NAME="cluster.a1b2c3d4.eastus.aroapp.io"
# Link to hub VNet (required for jump host, Bastion, CI/CD runners)
az network private-dns link vnet create \
--resource-group $MANAGED_RG \
--zone-name $ZONE_NAME \
--name "link-to-hub-vnet" \
--virtual-network $(az network vnet show \
--resource-group rg-hub \
--name hub-vnet \
--query id -o tsv) \
--registration-enabled false
# Link to other spoke VNets if they need to call ARO-hosted APIs
az network private-dns link vnet create \
--resource-group $MANAGED_RG \
--zone-name $ZONE_NAME \
--name "link-to-spoke2-vnet" \
--virtual-network $(az network vnet show \
--resource-group rg-spoke2 \
--name spoke2-vnet \
--query id -o tsv) \
--registration-enabled false

registration-enabled false is correct here — you are linking for resolution only, not to auto-register VM hostnames into the zone.


On-Premises DNS Conditional Forwarding

On-premises DNS servers cannot be linked to Azure private DNS zones directly — they resolve through the DNS Private Resolver inbound endpoint using conditional forwarding:

On-premises Windows DNS Server:
Conditional Forwarder:
Domain: aroapp.io
Forward to: 10.0.5.4 (DNS Private Resolver inbound endpoint)
On-premises BIND (Linux):
zone "aroapp.io" {
type forward;
forwarders { 10.0.5.4; };
};

With this in place, an on-premises developer running oc login gets the full resolution chain:

1. oc login https://api.cluster.a1b2c3d4.eastus.aroapp.io:6443
2. Laptop DNS → corp DNS server
3. Corp DNS: aroapp.io → forward to 10.0.5.4
4. DNS Private Resolver checks linked private DNS zones
5. Finds: api.cluster.a1b2c3d4.eastus.aroapp.io → 10.1.0.8
6. Returns 10.1.0.8 to laptop
7. oc connects to 10.1.0.8:6443 via ExpressRoute / VPN tunnel
8. Login succeeds ✅

OpenShift Console DNS

The OpenShift web console runs as an application on the cluster, so it resolves through the *.apps wildcard record:

# Get console URL
az aro show \
--resource-group rg-aro \
--name aro-prod \
--query consoleProfile.url \
--output tsv
# Output:
# https://console-openshift-console.apps.cluster.a1b2c3d4.eastus.aroapp.io
# DNS resolution:
# console-openshift-console.apps.cluster.a1b2c3d4.eastus.aroapp.io
# → matched by *.apps wildcard A record
# → returns 10.1.1.100 (internal LB)
# → browser connects via VPN/ER or Bastion proxy

Custom Domain — Replacing aroapp.io

If you want your own domain (e.g. openshift.contoso.com) instead of aroapp.io, you create a custom private DNS zone and manage the records yourself:

# Create your own private DNS zone
az network private-dns zone create \
--resource-group rg-aro-network \
--name openshift.contoso.com
# Add API server A record
az network private-dns record-set a add-record \
--resource-group rg-aro-network \
--zone-name openshift.contoso.com \
--record-set-name api \
--ipv4-address 10.1.0.8
# Add wildcard apps A record
az network private-dns record-set a add-record \
--resource-group rg-aro-network \
--zone-name openshift.contoso.com \
--record-set-name "*.apps" \
--ipv4-address 10.1.1.100
# Link to all VNets
az network private-dns link vnet create \
--resource-group rg-aro-network \
--zone-name openshift.contoso.com \
--name link-hub \
--virtual-network /subscriptions/.../hub-vnet \
--registration-enabled false

Then update the ARO cluster to use the custom domain during deployment:

az aro create \
--resource-group rg-aro \
--name aro-prod \
--vnet aro-spoke-vnet \
--master-subnet master-subnet \
--worker-subnet worker-subnet \
--apiserver-visibility Private \
--ingress-visibility Private \
--domain openshift.contoso.com \ # ← custom domain
--pull-secret @pull-secret.txt

With a custom domain the API server becomes api.openshift.contoso.com and apps become *.apps.openshift.contoso.com — owned and managed entirely by you, with no dependency on aroapp.io.


Key Takeaway

ARO automatically creates a private DNS zone under aroapp.io with two critical records — api pointing to the API server private endpoint IP and *.apps pointing to the internal load balancer frontend IP. The zone is auto-linked only to the ARO spoke VNet — you must manually link it to the hub VNet, any other spoke VNets, and configure on-premises DNS conditional forwarding to the DNS Private Resolver for the complete name resolution chain to work end-to-end across your hub and spoke estate.

Leave a comment