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

AreaRequirement
Deployment pathProduction deployments must run through GitHub Actions.
Deployment authProduction App Service deploys must be identity-based (az login --identity on the PSI runner, or Entra federated credentials).
Publishing credentialsbasicPublishingCredentialsPolicies/scm=false and basicPublishingCredentialsPolicies/ftp=false unless a documented exception exists.
Legacy deploy methodsLocal-git and publish-profile deployment are not compliant for production App Services.
SecretsSensitive 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 VaultStandard production vault is ps-certificates-kv; app identity must have only the access it needs.
NetworkingInternal web apps must use private endpoints and have public access disabled.
DNSFollow 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 exposurePublic/anonymous apps require an explicit documented reason and must be treated as exceptions, not defaults.
AuthenticationEach app must use an approved auth profile and have that profile documented.
ValidationUse existing tests/builds, then run a real deployment validation and verify the custom-domain behavior.
Health gatesHealth/reachability gates must treat 200, 401, and 403 according to the intended auth posture of the endpoint.
Controlled rolloutProduction changes should be applied one app at a time with validation between changes.
DocumentationAny auth, deployment, secrets, networking, or exception change must be reflected in the relevant wiki pages.

Approved auth profiles

ProfileWhen to use itRequired guardrailsCurrent examples
Client/app-layer authSPA + API apps where auth is enforced in the app and anonymous root/health behavior may be neededRoute expectations documented, app-layer validation present, no accidental EasyAuth enforcement left behindbom-explorer-web, ps-project-explorer, redbook-web, psi-portal, prgjsmes-prod, erp-migration-api
EasyAuth + app-layerApps that intentionally require platform auth in front of the appEntra-only provider, unused providers disabled, custom-domain behavior validated, deploy health gates allow 401/403 when expectedps-progressive-view, ps-redbook-dashboard
Public / anonymous exceptionAsset or utility apps that must be broadly reachableBusiness reason documented, no unnecessary secrets exposed, public posture explicitly tracked as an exceptionpsredbookphotos, ps-shipphotos

AuthsettingsV2 guardrail: disabling only platform.enabled is not enough if globalValidation.requireAuthentication=true remains set. Anonymous/app-layer apps must also set requireAuthentication=false and AllowAnonymous.


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 localStorage MSAL cache when users are not sharing browser profiles and smoother pass-through sign-in is the goal.
  • Use sessionStorage only when per-session sign-in loss is intentional.
  • On app startup:
    1. handle the redirect promise first,
    2. set the active account from the redirect result or auth events,
    3. verify that the cached account can still silently acquire the app token,
    4. 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

  1. Read this page first, then read the app page plus deploy-to-azure, dns-standards, azure-resources, and azure-security.
  2. Identify whether the app is internal/private, EasyAuth-enforced, or a documented public exception.
  3. Confirm the deployment path is GitHub Actions and does not depend on local-git, publish profiles, or basic publishing credentials.
  4. Move new sensitive settings to Key Vault references instead of plain-text app settings.
  5. Preserve or explicitly update the app’s approved auth profile.
  6. Run the repo’s existing test/build steps, then trigger a real deploy validation (workflow_dispatch when available).
  7. Validate the custom domain and relevant endpoints (/, /health, /api/health, or app-specific health endpoints).
  8. Interpret 200, 401, and 403 against the intended auth posture; do not treat every 401 as a failure without checking the expected profile.
  9. 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:

RequirementRule
NetworkPrivate only — no public exposure. Bind/firewall to private ranges; do not open the listener port to Any. A public FQDN requires a tracked exception.
AuthenticationEnforce 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.
DiscoveryServe 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 closedIf auth config is missing, reject all protected requests rather than running open.
CORSNo wildcard Access-Control-Allow-Origin: *. MCP clients are not browsers; use an explicit allowlist or omit CORS.
App registrationOne 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.com is 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.com currently returns 401 anonymously by design under EasyAuth.
  • psredbookphotos and ps-shipphotos are intentionally public.

Supporting reference pages

  • shared-libraries@psi/* SDK packages; @psi/auth ships 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.)