PSI DNS Standards

MANDATORY before publishing a web app to Azure — agents and engineers

If you are publishing, deploying, or changing DNS / networking for any web app on Azure, you MUST read this page first. Do not improvise DNS or “figure it out as you go” — DNS is the #1 cause of failed PSI deploys. Every internal app needs app + app.scm records in both zones (Azure Private DNS and the DCs); follow the new-app DNS checklist and verify before you deploy.

Canonical reference for DNS at PSI. DNS is the single biggest cause of failed deploys and “works-locally-but-not-in-prod” incidents in the web fleet. Read this before adding or changing DNS for any app. The command-level walkthroughs live in deploy-to-azure §5 and §10; this page is the rules and the checklist.


TL;DR — the five rules that bite people

  1. Network shares use DFS, never server names. \\ad.ptihome.com\DFS\…, never \\fs1\… or \\PS-GR-FS01\….
  2. Internal app A records go in two places — the Azure public DNS zone and the AD domain controllers. Skipping one half is the classic “resolves for some people, not others.”
  3. Privatelink resolution needs records in two zones — the Azure Private DNS zone and an AD-integrated primary zone on the DCs. Add both the app record and the app.scm record. Missing .scm is the #1 cause of deploy 403s.
  4. App Service → on-prem must use the FQDN (ps-proxy.ad.ptihome.com), never the short hostname (ps-proxy). Linux App Service containers don’t have the ad.ptihome.com search domain.
  5. Cisco Umbrella intercepts workstation DNS even when you pass -Server. A workstation nslookup is not a reliable internal-DNS diagnostic — test from a DC or VNet machine.

The resolvers and zones

DNS servers

ServerIPRole
PS-AZ-DC0110.160.0.5Primary DNS + Azure domain controller; authoritative for the privatelink primary zones
On-prem DC192.9.200.110Secondary DNS (PSI HQ)
Azure wireserver168.63.129.16Azure-internal resolver — avoid pinning apps to it; it can’t see internal AD zones and resolves ad.ptihome.com to wrong public IPs
Cisco Umbrellaendpoint clientIntercepts DNS on PSI workstations below the Windows resolver (see Umbrella gotcha)

Zones

ZoneHosted onPurpose
progressivesurface.comAzure DNS (RG PS-RG-01)Public/authoritative records: app A records (→ private endpoint IPs), asuid.* domain-verification TXT, api → PS-PROXY
ad.ptihome.com (+ legacy ptihome.com)AD DCsInternal AD domain. PSI no longer owns ptihome.com publicly — never let an internal name leak to public DNS (see the api.progressivesurface.com CNAME incident below)
privatelink.azurewebsites.netAzure Private DNS (PS-RG-01) and AD primary zone (PS-AZ-DC01)App Service / Function App private endpoints
privatelink.database.windows.netbothAzure SQL private endpoints
privatelink.vaultcore.azure.netbothKey Vault private endpoints
privatelink.servicebus.windows.net / privatelink.azure-devices.netbothService Bus / IoT Hub
DFS namespace \\ad.ptihome.com\DFS\*ADAll file shares — never use \\fs1, \\PS-GR-FS01, etc.

Why every privatelink zone exists twice (Azure + DC) — and who uses which. The two zones serve two different resolver paths, so a new app needs the records in both; neither is redundant:

  • The deploy runner (ps-cicd-runner) resolves through a systemd-resolved drop-in that routes privatelink.* to Azure DNS (168.63.129.16) → it reads the Azure Private DNS zone. This is the record that unblocks deploys. (Verified 2026-06-25: the runner resolved a brand-new app from the Azure zone alone, while the DCs still returned NXDOMAIN.)
  • Every other VNet/VPN client (Windows machines, App Proxy connector VMs — no drop-in) resolves the app.azurewebsites.net hostname via the DCs → it reads the DC AD-integrated primary zone.

The DC must hold an authoritative primary zone, not a conditional forwarder: when a client resolves app.azurewebsites.net, public DNS returns the complete CNAME chain to a public IP in one answer and Windows DNS trusts it — a conditional forwarder for privatelink.azurewebsites.net is never consulted. Being authoritative, the DC resolves the privatelink CNAME against its own records instead of following the public chain.


✅ Adding DNS for a new internal app

For an internal App Service myapp with custom domain myapp.progressivesurface.com and private-endpoint IP 10.160.140.X (next free IP is tracked in azure-resources / deploy-to-azure):

#RecordZoneWhere
1asuid.myapp TXT = customDomainVerificationIdprogressivesurface.comAzure DNS
2myapp A → 10.160.140.Xprogressivesurface.comAzure DNS
3myapp A → 10.160.140.X and myapp.scm A → 10.160.140.Xprivatelink.azurewebsites.netAzure Private DNS (PS-RG-01)
4myapp A → 10.160.140.X and myapp.scm A → 10.160.140.Xprivatelink.azurewebsites.netDC primary zone (PS-AZ-DC01, forest-replicated)
5(deployment slots only) per-app primary zones myapp-<slot>.azurewebsites.net + .scm apex → slot PE IPnew primary zoneDC

Steps 3 and 4 are not redundant: step 3 (Azure zone) is what the deploy runner reads; step 4 (DC zone) is what every other VNet/VPN client reads. Do both. End users reach the app by its custom domain (steps 1–2), which is why a missing privatelink record breaks deploys/tooling but not the user-facing URL.

# Step 4 — on PS-AZ-DC01 (or any DC; AD-integrated zone replicates)
Add-DnsServerResourceRecordA -ZoneName "privatelink.azurewebsites.net" -Name "myapp"     -IPv4Address "10.160.140.X"
Add-DnsServerResourceRecordA -ZoneName "privatelink.azurewebsites.net" -Name "myapp.scm" -IPv4Address "10.160.140.X"
# Step 3 — Azure Private DNS zone
az network private-dns record-set a add-record -g PS-RG-01 -z privatelink.azurewebsites.net -n myapp     -a 10.160.140.X
az network private-dns record-set a add-record -g PS-RG-01 -z privatelink.azurewebsites.net -n myapp.scm -a 10.160.140.X

Verify (from a VNet machine — not an Umbrella workstation)

nslookup myapp.azurewebsites.net 10.160.0.5            # → 10.160.140.X (private), NOT a public Azure IP
nslookup myapp.scm.azurewebsites.net 10.160.0.5        # → 10.160.140.X
nslookup myapp.progressivesurface.com 10.160.0.5       # → 10.160.140.X

AD replication: ~15 min within a site, up to 3 h cross-site. Force with repadmin /syncall /AeP.

If you create the first private endpoint for a new Azure service (Blob → privatelink.blob.core.windows.net, Key Vault → privatelink.vaultcore.azure.net, …):

  1. Create the Azure Private DNS zone in PS-RG-01 and link it to PS-VNMAIN.
  2. Create a matching AD-integrated primary zone on PS-AZ-DC01 (forest-replicated).
  3. Add A records to both for every endpoint.

Without step 2 the same Windows CNAME-chain problem applies and the DC follows the public chain.


Deployment slots need an extra DNS layer

A slot has no custom domain, so the public CNAME chain for myapp-staging.azurewebsites.net does not reliably end at privatelink. A laptop on the VPN then gets the public IP and hits 403 x-ms-forbidden-ip. Fix: a per-app primary zone on the DC that short-circuits straight to the private IP (apex @ record), in addition to the privatelink records. Full step-by-step in deploy-to-azure §4b.


App Service → on-prem: always use the FQDN

Azure Linux App Service containers don’t have the ad.ptihome.com DNS search domain, so a bare hostname resolves against public DNS and fails (ENOTFOUND / fetch failed).

WrongCorrect
http://ps-proxy:3100/mcphttp://ps-proxy.ad.ptihome.com:3100/mcp
http://aftec-server:5000http://aftec-server.ad.ptihome.com:5000

Applies to every App Service env var / connection string that names an on-prem server. Local dev and the self-hosted runner can use short names (they have the search domain).

The self-hosted runner has two systemd-resolved drop-ins in /etc/systemd/resolved.conf.d/: ad-domain.conf pins ad.ptihome.com / ptihome.com to the DCs (so AD names never fall back to Azure DNS and resolve to wrong public IPs), and privatelink.conf routes privatelink.* to Azure DNS (168.63.129.16) — which is why the runner reads the Azure private DNS zone, not the DC zone. See deploy-to-azure §10.


Workstation gotcha: Cisco Umbrella

PSI workstations run the Cisco Umbrella endpoint client, which intercepts DNS below the Windows resolver — even queries that explicitly target an internal DNS server with -Server. Symptom: the same query returns the private IP from a DC but the public CNAME chain from a workstation.

  • Verify it’s Umbrella: stop the Umbrella service (Stop-Service "Umbrella RC"), re-run, get the right answer.
  • Fix (don’t blanket-disable): add an Umbrella internal-domain entry for the hostname, or test from a VNet machine (CI runner, App Service console, an Azure VM), or use the PE IP directly during testing.

Production traffic from operators / handhelds isn’t affected the same way — this is a diagnostic surprise, not a runtime issue.


SymptomLikely DNS cause
Deploy 403 Forbidden / NXDOMAIN from *.scm.azurewebsites.netMissing app.scm record in the Azure private DNS zone (the runner resolves via that zone through its drop-in)
Browser 403 x-ms-forbidden-ip from PSI office / VPNPublic DNS returned a public IP — missing per-app primary zone (slots) or missing public A/privatelink record
ENOTFOUND / fetch failed from App Service to on-premShort hostname in an env var — use the FQDN
nslookup differs on workstation vs DCCisco Umbrella interception on the workstation
Intermittent TLS failures on the runnersystemd-resolved fell back to Azure DNS for ad.ptihome.com — re-pin to the DCs
Internal name resolves to a hostile external hostAn internal CNAME leaked to public DNS (PSI no longer owns ptihome.com) — use a direct A record, not a CNAME to an *.ptihome.com name

Incident reference (2026-02-23): api.progressivesurface.com was a CNAME to ps-proxy.ad.ptihome.com. Because PSI no longer owns ptihome.com publicly, the name leaked to a hostile wildcard. Fixed by replacing the CNAME with a direct A record (→ 192.9.201.217). Lesson: don’t CNAME public records to *.ptihome.com.


Maintenance rules

  • Two zones, always. Every new App Service private endpoint = records in the Azure Private DNS zone (the deploy runner reads this) and the DC primary zone (all other VNet/VPN clients read this), and both app + app.scm in each.
  • Assign PE IPs sequentially from PS-ProdData (10.160.140.0/24). The live allocation + next-free IP is in Private Endpoint Subnet Allocation.
  • Public records live in Azure DNS (progressivesurface.com, PS-RG-01); privatelink + AD records live on the DCs.
  • After any DNS change, verify from a VNet machine, then from a VPN laptop (catches the Umbrella / slot-403 classes).


Last updated: 2026-06-25 (canonical DNS standards page; resolver-path model verified live on ps-cicd-runner — runner reads the Azure zone, other clients read the DC zone).