Skip to content

Security Operations

Protect App Service workloads with layered controls: identity, authentication, transport security, network boundaries, and operational governance. This guide focuses on language-agnostic hardening steps.

Prerequisites

  • Existing Web App and App Service Plan
  • Azure Entra tenant and permissions for identity/auth configuration
  • Security ownership defined for app, platform, and network controls
  • Variables set:
    • RG
    • APP_NAME

When to Use

Procedure

flowchart TD
    A[Security Operations] --> B[Transport]
    A --> C[Identity]
    A --> D[Network]
    A --> E[Application]
    A --> F[Secrets]
    B --> B1[HTTPS-only]
    B --> B2[TLS 1.2+]
    C --> C1[Managed Identity]
    C --> C2[Platform Auth]
    D --> D1[Access Restrictions]
    D --> D2[Private Endpoints]
    E --> E1[CORS]
    E --> E2[Security Headers]
    F --> F1[Key Vault References]
    F --> F2[Secret Rotation]

Security Baseline Checklist

Apply these baseline controls first:

  1. HTTPS-only enabled
  2. Minimum TLS version enforced
  3. Managed identity enabled
  4. Access restrictions configured
  5. Authentication policy chosen (platform and/or app)
  6. Secrets stored outside application code

Enforce HTTPS and TLS Minimum

az webapp update \
  --resource-group $RG \
  --name $APP_NAME \
  --https-only true \
  --output json

az webapp config set \
  --resource-group $RG \
  --name $APP_NAME \
  --min-tls-version 1.2 \
  --output json

Verify settings:

az webapp show \
  --resource-group $RG \
  --name $APP_NAME \
  --query "{httpsOnly:httpsOnly,state:state}" \
  --output json

az webapp config show \
  --resource-group $RG \
  --name $APP_NAME \
  --query "{minTlsVersion:minTlsVersion,ftpsState:ftpsState}" \
  --output json

Enable System-Assigned Managed Identity

az webapp identity assign \
  --resource-group $RG \
  --name $APP_NAME \
  --output json

Retrieve principal ID:

az webapp identity show \
  --resource-group $RG \
  --name $APP_NAME \
  --query "{principalId:principalId,tenantId:tenantId,type:type}" \
  --output json

Sample output (PII-masked):

{
  "principalId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "tenantId": "<tenant-id>",
  "type": "SystemAssigned"
}

Configure Platform Authentication (App Service Auth)

Enable platform authentication with Entra ID:

az webapp auth update \
  --resource-group $RG \
  --name $APP_NAME \
  --enabled true \
  --action LoginWithAzureActiveDirectory \
  --output json

Alternatively configure provider-specific details:

az webapp auth microsoft update \
  --resource-group $RG \
  --name $APP_NAME \
  --client-id "<app-registration-client-id>" \
  --client-secret "<client-secret>" \
  --allowed-audiences "api://<app-registration-client-id>" \
  --output json

Protect client secrets

Never store client secrets in source control or plain-text operational notes. Prefer managed identity and secure secret stores whenever possible.

Portal view: Authentication blade (empty state)

Authentication blade for the Web App with a minimal command bar offering Refresh, Troubleshoot, and Send us your feedback actions. The main content area is an empty state with a large silhouette-and-key avatar icon, a heading "Add an identity provider", a subtitle reading "Choose an identity provider to manage the user identities and authentication flow for your application. Providers include Microsoft, Facebook, Google, and Twitter.", a "Learn more about identity providers" link, and a primary blue "Add identity provider" call-to-action button. The left navigation has the Settings group highlighted.

The Authentication blade in its empty state is the visual representation of the gap the az webapp auth update and az webapp auth microsoft update commands above close. When no provider has been configured, the platform performs no token validation at all — every request reaches the application regardless of identity, which is why this empty state should be considered a security finding for any production app that expects authenticated traffic. The Add identity provider button walks through the same provisioning the CLI performs but additionally surfaces app-registration creation, redirect-URI selection, and token-store toggles that are easy to misconfigure when scripted blind. After running the CLI commands above, return to this blade and confirm the empty state has been replaced by a configured provider row — the platform-authentication layer in the defense-in-depth control list further down depends on this Portal surface no longer reading "Add an identity provider".

Restrict Inbound Access by IP or Private Networking

az webapp config access-restriction add \
  --resource-group $RG \
  --name $APP_NAME \
  --rule-name AllowCorp \
  --action Allow \
  --ip-address 203.0.113.0/24 \
  --priority 100 \
  --output json

az webapp config access-restriction add \
  --resource-group $RG \
  --name $APP_NAME \
  --rule-name DenyAll \
  --action Deny \
  --ip-address 0.0.0.0/0 \
  --priority 2147483647 \
  --output json

Secure Secrets and Configuration

Recommended controls:

  • Store credentials in secure secret store services
  • Use Key Vault references in app settings where possible
  • Rotate secrets on a fixed schedule
  • Audit secret access and failed retrieval events

Set Key Vault reference style app setting:

az webapp config appsettings set \
  --resource-group $RG \
  --name $APP_NAME \
  --settings "DB_PASSWORD=@Microsoft.KeyVault(SecretUri=https://kv-shared.vault.azure.net/secrets/db-password/)" \
  --output json

Harden Publishing and Administrative Surfaces

Disable insecure FTP where policy requires:

az webapp config set \
  --resource-group $RG \
  --name $APP_NAME \
  --ftps-state Disabled \
  --output json

Prefer deployment through secure CI/CD identities and least privilege RBAC.

Configure CORS

Set allowed origins for cross-origin requests:

az webapp cors add \
  --resource-group $RG \
  --name $APP_NAME \
  --allowed-origins "https://frontend.example.com" "https://admin.example.com" \
  --output json

View current CORS configuration:

az webapp cors show \
  --resource-group $RG \
  --name $APP_NAME \
  --output json

Remove a specific origin:

az webapp cors remove \
  --resource-group $RG \
  --name $APP_NAME \
  --allowed-origins "https://old-frontend.example.com" \
  --output json

Avoid wildcard origins with credentials

Setting --allowed-origins "*" allows any origin. When combined with App Service Authentication, this can expose tokens to unauthorized frontends. Always specify explicit origins in production.

Platform CORS vs Application CORS

App Service platform CORS and application-level CORS middleware (e.g., Flask-CORS, Express cors) can conflict. Use one or the other, not both. If the platform handles CORS, disable it in your application code to avoid duplicate headers.

Configure Security Headers

App Service does not set security headers by default. Add them via application code or web.config/custom startup.

Recommended production headers:

Header Recommended Value Purpose
Strict-Transport-Security max-age=31536000; includeSubDomains Enforce HTTPS via HSTS
X-Content-Type-Options nosniff Prevent MIME sniffing
X-Frame-Options DENY Prevent clickjacking
Content-Security-Policy default-src 'self' Prevent XSS and injection
Referrer-Policy strict-origin-when-cross-origin Limit referrer leakage
Permissions-Policy camera=(), microphone=(), geolocation=() Restrict browser features

Verify headers are present:

curl --silent --head "https://$APP_NAME.azurewebsites.net" | grep -iE "(strict-transport|x-content-type|x-frame|content-security|referrer-policy|permissions-policy)"

Where to set headers

On Linux App Service, set headers in your application framework (Flask, Express, Spring, ASP.NET middleware). On Windows, you can also use web.config custom headers. For both, Azure Front Door can inject headers at the edge.

Verification

Authentication and identity:

az webapp auth show \
  --resource-group $RG \
  --name $APP_NAME \
  --output json

az webapp identity show \
  --resource-group $RG \
  --name $APP_NAME \
  --output json

Access restrictions:

az webapp config access-restriction show \
  --resource-group $RG \
  --name $APP_NAME \
  --output json

Transport checks:

curl --silent --show-error --include "http://$APP_NAME.azurewebsites.net"
curl --silent --show-error --include "https://$APP_NAME.azurewebsites.net"

Expected:

  • HTTP redirects to HTTPS
  • TLS meets minimum baseline
  • unauthorized requests challenged or denied by policy

Rollback / Troubleshooting

Authentication redirect loop

  • verify allowed redirect URIs in app registration
  • ensure hostnames match custom domain configuration
  • confirm authentication policy aligns with reverse proxy setup

Managed identity access denied

  • verify role assignments on target resource
  • confirm principal ID used in role assignment is current
  • allow propagation delay after role changes

Unexpected public access

  • review access restriction priorities
  • confirm deny-all rule exists
  • verify private endpoint DNS resolution path

Advanced Topics

Defense-in-Depth Pattern

Combine:

  • private inbound networking
  • authentication at platform layer
  • authorization in application layer
  • managed identity for outbound resource access
  • central policy enforcement

Security Operations Cadence

Run periodic activities:

  • monthly access review
  • quarterly secret rotation verification
  • recurring incident simulation for auth/network outage
  • security baseline drift report

Policy and Compliance at Scale

Use Azure Policy to enforce controls such as:

  • HTTPS-only required
  • minimum TLS version
  • managed identity required
  • diagnostic settings required

Enterprise Considerations

Security posture improves when baseline configuration is enforced by policy and continuously audited, not only documented. Treat configuration drift as a security incident precursor.

Language-Specific Details

For language-specific security patterns and auth integration:

See Also

Sources