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.scmrecords 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
- Network shares use DFS, never server names.
\\ad.ptihome.com\DFS\…, never\\fs1\…or\\PS-GR-FS01\…. - 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.”
- Privatelink resolution needs records in two zones — the Azure Private DNS zone and an AD-integrated primary zone on the DCs. Add both the
apprecord and theapp.scmrecord. Missing.scmis the #1 cause of deploy 403s. - 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 thead.ptihome.comsearch domain. - Cisco Umbrella intercepts workstation DNS even when you pass
-Server. A workstationnslookupis not a reliable internal-DNS diagnostic — test from a DC or VNet machine.
The resolvers and zones
DNS servers
| Server | IP | Role |
|---|---|---|
| PS-AZ-DC01 | 10.160.0.5 | Primary DNS + Azure domain controller; authoritative for the privatelink primary zones |
| On-prem DC | 192.9.200.110 | Secondary DNS (PSI HQ) |
| Azure wireserver | 168.63.129.16 | Azure-internal resolver — avoid pinning apps to it; it can’t see internal AD zones and resolves ad.ptihome.com to wrong public IPs |
| Cisco Umbrella | endpoint client | Intercepts DNS on PSI workstations below the Windows resolver (see Umbrella gotcha) |
Zones
| Zone | Hosted on | Purpose |
|---|---|---|
progressivesurface.com | Azure 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 DCs | Internal 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.net | Azure Private DNS (PS-RG-01) and AD primary zone (PS-AZ-DC01) | App Service / Function App private endpoints |
privatelink.database.windows.net | both | Azure SQL private endpoints |
privatelink.vaultcore.azure.net | both | Key Vault private endpoints |
privatelink.servicebus.windows.net / privatelink.azure-devices.net | both | Service Bus / IoT Hub |
DFS namespace \\ad.ptihome.com\DFS\* | AD | All 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 asystemd-resolveddrop-in that routesprivatelink.*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 returnedNXDOMAIN.)- Every other VNet/VPN client (Windows machines, App Proxy connector VMs — no drop-in) resolves the
app.azurewebsites.nethostname 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 forprivatelink.azurewebsites.netis 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):
| # | Record | Zone | Where |
|---|---|---|---|
| 1 | asuid.myapp TXT = customDomainVerificationId | progressivesurface.com | Azure DNS |
| 2 | myapp A → 10.160.140.X | progressivesurface.com | Azure DNS |
| 3 | myapp A → 10.160.140.X and myapp.scm A → 10.160.140.X | privatelink.azurewebsites.net | Azure Private DNS (PS-RG-01) |
| 4 | myapp A → 10.160.140.X and myapp.scm A → 10.160.140.X | privatelink.azurewebsites.net | DC primary zone (PS-AZ-DC01, forest-replicated) |
| 5 | (deployment slots only) per-app primary zones myapp-<slot>.azurewebsites.net + .scm apex → slot PE IP | new primary zone | DC |
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.XVerify (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.XAD replication: ~15 min within a site, up to 3 h cross-site. Force with
repadmin /syncall /AeP.
Adding a new privatelink service type
If you create the first private endpoint for a new Azure service (Blob → privatelink.blob.core.windows.net, Key Vault → privatelink.vaultcore.azure.net, …):
- Create the Azure Private DNS zone in PS-RG-01 and link it to
PS-VNMAIN. - Create a matching AD-integrated primary zone on PS-AZ-DC01 (forest-replicated).
- 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).
| Wrong | Correct |
|---|---|
http://ps-proxy:3100/mcp | http://ps-proxy.ad.ptihome.com:3100/mcp |
http://aftec-server:5000 | http://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.
DNS-related failure → cause
| Symptom | Likely DNS cause |
|---|---|
Deploy 403 Forbidden / NXDOMAIN from *.scm.azurewebsites.net | Missing 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 / VPN | Public 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-prem | Short hostname in an env var — use the FQDN |
nslookup differs on workstation vs DC | Cisco Umbrella interception on the workstation |
| Intermittent TLS failures on the runner | systemd-resolved fell back to Azure DNS for ad.ptihome.com — re-pin to the DCs |
| Internal name resolves to a hostile external host | An 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.comwas a CNAME tops-proxy.ad.ptihome.com. Because PSI no longer ownsptihome.compublicly, the name leaked to a hostile wildcard. Fixed by replacing the CNAME with a directArecord (→ 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.scmin 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).
Related Pages
- webapp-compliance-standard — the canonical web-app baseline (Networking requirement points here)
- deploy-to-azure — command-level DNS how-to (§5 private endpoints, §10 runner + privatelink zones, §4b slots)
- azure-resources — private-endpoint IP allocation, zones, DNS servers
- azure-security — current network/auth posture and exceptions
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).