PSI Web App Compliance Standard
Canonical source of truth for any human or AI agent implementing, changing, or deploying a PSI web app. Start here before making Azure, auth, deployment, secrets, networking, or production configuration changes.
Agents MUST review the DNS standard before publishing
Before you publish or deploy a web app to Azure, you MUST also read dns-standards and follow its new-app checklist. DNS is the #1 cause of failed PSI deploys — a private endpoint without records in both zones ships broken.
What this page governs
This standard applies to:
- Azure App Service applications
- Azure Static Web Apps
- GitHub Actions deployment workflows
- auth, networking, secrets, and production configuration changes
Use this page for the baseline. Use the linked pages at the bottom for implementation details, current exceptions, resource mapping, and historical evidence.
Mandatory baseline
| Area | Requirement |
|---|---|
| Deployment path | Production deployments must run through GitHub Actions. |
| Deployment auth | Production App Service deploys must be identity-based (az login --identity on the PSI runner, or Entra federated credentials). |
| Publishing credentials | basicPublishingCredentialsPolicies/scm=false and basicPublishingCredentialsPolicies/ftp=false unless a documented exception exists. |
| Legacy deploy methods | Local-git and publish-profile deployment are not compliant for production App Services. |
| Secrets | Sensitive settings must live in Key Vault and be referenced from app settings; never commit secrets or leave production secrets as plain-text app settings when Key Vault references are possible. |
| Key Vault | Standard production vault is ps-certificates-kv; app identity must have only the access it needs. |
| Networking | Internal web apps must use private endpoints and have public access disabled. |
| DNS | Follow dns-standards. Every internal app needs records in two privatelink zones (Azure + the DCs), both app and app.scm; on-prem references from App Service use the FQDN. |
| Public exposure | Public/anonymous apps require an explicit documented reason and must be treated as exceptions, not defaults. |
| Authentication | Each app must use an approved auth profile and have that profile documented. |
| Validation | Use existing tests/builds, then run a real deployment validation and verify the custom-domain behavior. |
| Health gates | Health/reachability gates must treat 200, 401, and 403 according to the intended auth posture of the endpoint. |
| Controlled rollout | Production changes should be applied one app at a time with validation between changes. |
| Documentation | Any auth, deployment, secrets, networking, or exception change must be reflected in the relevant wiki pages. |
Approved auth profiles
| Profile | When to use it | Required guardrails | Current examples |
|---|---|---|---|
| Client/app-layer auth | SPA + API apps where auth is enforced in the app and anonymous root/health behavior may be needed | Route expectations documented, app-layer validation present, no accidental EasyAuth enforcement left behind | bom-explorer-web, ps-project-explorer, redbook-web, psi-portal, prgjsmes-prod, erp-migration-api |
| EasyAuth + app-layer | Apps that intentionally require platform auth in front of the app | Entra-only provider, unused providers disabled, custom-domain behavior validated, deploy health gates allow 401/403 when expected | ps-progressive-view, ps-redbook-dashboard |
| Public / anonymous exception | Asset or utility apps that must be broadly reachable | Business reason documented, no unnecessary secrets exposed, public posture explicitly tracked as an exception | psredbookphotos, ps-shipphotos |
AuthsettingsV2 guardrail: disabling only
platform.enabledis not enough ifglobalValidation.requireAuthentication=trueremains set. Anonymous/app-layer apps must also setrequireAuthentication=falseandAllowAnonymous.
Client/app-layer auth best practice
For internal PSI apps that use app-layer Entra/MSAL auth and are accessed from each user’s own Windows/browser profile:
- Prefer redirect-based auth flows for normal browser use. Do not add popup token fallback unless a specific app has a documented need for it.
- Prefer
localStorageMSAL cache when users are not sharing browser profiles and smoother pass-through sign-in is the goal. - Use
sessionStorageonly when per-session sign-in loss is intentional. - On app startup:
- handle the redirect promise first,
- set the active account from the redirect result or auth events,
- verify that the cached account can still silently acquire the app token,
- if that silent acquisition fails, clear the stale MSAL cache and force a fresh redirect login.
- Do not surface transient redirect/token-refresh states as user-facing feature errors while auth recovery is in progress.
For kiosk or shared-browser scenarios, document the exception and reconsider localStorage.
Agent execution checklist
- Read this page first, then read the app page plus deploy-to-azure, dns-standards, azure-resources, and azure-security.
- Identify whether the app is internal/private, EasyAuth-enforced, or a documented public exception.
- Confirm the deployment path is GitHub Actions and does not depend on local-git, publish profiles, or basic publishing credentials.
- Move new sensitive settings to Key Vault references instead of plain-text app settings.
- Preserve or explicitly update the app’s approved auth profile.
- Run the repo’s existing test/build steps, then trigger a real deploy validation (
workflow_dispatchwhen available). - Validate the custom domain and relevant endpoints (
/,/health,/api/health, or app-specific health endpoints). - Interpret
200,401, and403against the intended auth posture; do not treat every401as a failure without checking the expected profile. - If production posture changed, update the supporting wiki pages and record any exception, incident, or follow-up action.
When an exception is required
If a change cannot meet the baseline, document:
- the rule being excepted
- the app and environment affected
- the business reason
- the owner
- the expiry or next review point
Do not leave compliance deviations implicit.
MCP servers
MCP servers that expose an HTTP transport are subject to this standard. They tend to carry broad, read-heavy access to internal data, so the baseline is private + authenticated by default:
| Requirement | Rule |
|---|---|
| Network | Private only — no public exposure. Bind/firewall to private ranges; do not open the listener port to Any. A public FQDN requires a tracked exception. |
| Authentication | Enforce Entra ID bearer tokens on every route except /health and the OAuth discovery doc. The server is an OAuth 2.1 Resource Server: validate signature (tenant JWKS / RS256), iss, aud, and a required scope/role. No secret is needed server-side — resource-server validation uses public JWKS. |
| Discovery | Serve RFC 9728 Protected Resource Metadata at /.well-known/oauth-protected-resource and a 401 WWW-Authenticate: Bearer resource_metadata="…" so clients (Claude Code, Copilot) can discover the authorization server. |
| Fail closed | If auth config is missing, reject all protected requests rather than running open. |
| CORS | No wildcard Access-Control-Allow-Origin: *. MCP clients are not browsers; use an explicit allowlist or omit CORS. |
| App registration | One Entra app per MCP server (App ID URI api://<id>, exposed scope, e.g. Mcp.Invoke). |
Client connection (Claude Code / Copilot). Either pre-configured OAuth (--client-id + --callback-port,
with authServerMetadataUrl pointing at the Entra tenant — Entra has no Dynamic Client Registration, so
clients must be pre-registered), or a headersHelper that mints a token per connection
(az account get-access-token --resource api://<id> --query "{Authorization:join(' ',['Bearer',accessToken])}" -o json).
The headersHelper path is preferred for internal LAN tooling: non-interactive, auto-refreshing, no stored secret.
stdio MCP servers are exempt from the network/auth rules — they run as a local child process over stdio and never expose a network listener; they inherit the launching user’s identity.
Reference implementation: psi-machine (psi-machine-mcp, runs on PS-PROXY) — Entra bearer auth via
lib/auth.js, app registration PSI Machine MCP (api://0dfb7d0a-b815-4611-93ba-ccdf3213187b, scope Mcp.Invoke),
firewall scoped to private ranges. A consolidated mcp.progressivesurface.com gateway (path-routed, central
TLS/OAuth/PRM) is the future landing zone for any off-LAN access.
Current explicit exceptions and important decisions
erp-migration-api/dmt.progressivesurface.comis intentionally app-layer auth with platform auth disabled so root, health, and MCP endpoints can remain anonymously reachable as required.ps-redbook-dashboard/quality.progressivesurface.comcurrently returns401anonymously by design under EasyAuth.psredbookphotosandps-shipphotosare intentionally public.
Supporting reference pages
- shared-libraries —
@psi/*SDK packages;@psi/authships this standard’s auth defaults by default - deploy-to-azure — Detailed deployment implementation standard
- dns-standards — Canonical DNS rules (public zone, privatelink two-zone rule, slots, Umbrella gotcha)
- azure-security — Current production posture, recommendations, and exceptions
- azure-resources — Resource, app registration, runner, and deployment identity mapping
- azure-remediation-2026-04 — April 2026 hardening rollout, validation evidence, and incident history
- index — App-specific documentation starting point
Last updated: 2026-06-25 — added DNS baseline row + dns-standards links. (June 2026: MCP servers section — private + Entra bearer auth baseline.)