Back to blog
AzureBest PracticesCloud SecurityNetworkingServerless

Locking Down the Azure App Service SCM (Kudu) Endpoint

The Azure App Service SCM/Kudu site is a public management backdoor by default. Learn why an open SCM endpoint is risky and how to lock it down with CLI, Bicep, and policy.

TL;DR

The SCM (Kudu) endpoint on your Azure App Service is the deployment and management backdoor, and by default it is reachable from the entire internet. Lock it down with IP restrictions or, better yet, inherit your main site's rules so the management plane is never more exposed than the app itself.

When most people think about securing an Azure App Service, they focus on the public-facing site: the routes, the auth, maybe a Web Application Firewall in front. The SCM site rarely comes up. That is exactly why it ends up being a soft spot. The App Service SCM Site Has No Access Restrictions check flags App Services where the Kudu management endpoint is open to any source IP, even when the main application has restrictions in place.

This post walks through what the SCM site actually is, why an open one is a real problem, and how to close the gap with CLI, Bicep, and policy automation.


What this check detects

Every Azure App Service ships with a companion site reachable at https://<app-name>.scm.azurewebsites.net. This is the SCM site, also called Kudu. It handles deployment, and it exposes a surprising amount of functionality:

  • A web-based file explorer and editor for the app's file system
  • A command console and PowerShell terminal that runs inside the app's worker
  • Process explorer, environment variable dumps, and runtime logs
  • Deployment endpoints (Git, ZIP deploy, the publishing API)
  • WebJobs management and the site extension gallery

The Lensix check appservice_noscmaccessrestriction inspects the SCM site's scmIpSecurityRestrictions configuration. If there are no restrictions defined, or the only rule is an allow-all (0.0.0.0/0), the check fails. In other words, the deployment and console plane of your app is sitting on the public internet with no network-level gate.

Note: The main site and the SCM site have separate access restriction rule sets. You can lock down the front door of your app and still leave the SCM site wide open. Azure even has a setting, scmIpSecurityRestrictionsUseMain, specifically to make the SCM site inherit the main site's rules, which tells you how often the two drift apart.


Why it matters

The SCM site is not a read-only status page. It is an administrative interface that can execute arbitrary commands inside your application's runtime. Leaving it open turns a relatively narrow web app attack surface into a full management surface.

The realistic attack path

SCM access requires authentication, but authentication is not the same as isolation. Consider how credentials leak in practice:

  • Publishing profiles (the .PublishSettings file) get committed to a repo, pasted into a CI log, or stored in a shared drive. They contain SCM credentials.
  • A developer reuses a deployment credential that later shows up in a breach dump.
  • Azure AD account compromise via phishing gives an attacker portal and SCM access in one step.

If the SCM site is reachable from anywhere, any of those leaks is immediately exploitable from the attacker's laptop. They open the Kudu console, run commands as the app's identity, read connection strings out of the environment, pull source code, and pivot to the databases and storage the app can reach. If the App Service has a managed identity with broad role assignments, that identity becomes the attacker's identity.

Warning: Managed identity makes this worse, not better. A Kudu console running inside the app can call the local IMDS endpoint and mint tokens for whatever roles the identity holds. If you granted that identity Contributor on a resource group "to keep things simple," an open SCM site quietly extends that blast radius to anyone with deployment credentials.

Restricting the SCM site by network does not replace good credential hygiene, but it adds a layer that does not depend on a secret staying secret. An attacker with valid Kudu credentials but no route to the endpoint cannot do anything with them.


How to fix it

You have two sensible options. The simplest is to make the SCM site inherit the main site's access restrictions. The more flexible is to define dedicated SCM rules, for example allowing only your CI/CD egress IPs and a corporate VPN range.

Option A: Inherit the main site's rules

If your main site already has appropriate restrictions, point the SCM site at them:

az resource update \
  --resource-group my-rg \
  --name my-app \
  --resource-type "Microsoft.Web/sites/config" \
  --set properties.scmIpSecurityRestrictionsUseMain=true \
  --api-version 2023-12-01

After this, any rule on the main site applies to my-app.scm.azurewebsites.net as well. This is the lowest-maintenance choice for most teams.

Option B: Define dedicated SCM restrictions

When the SCM site needs a different policy than the public app, for example when the app is fronted by an Application Gateway but deployments come from a known CI range, set explicit rules. First add the allowed source:

az webapp config access-restriction add \
  --resource-group my-rg \
  --name my-app \
  --scm-site true \
  --rule-name "allow-ci-egress" \
  --action Allow \
  --ip-address 203.0.113.10/32 \
  --priority 100

Danger: Access restrictions are default-deny once a single Allow rule exists. The moment you add the first Allow rule above, every IP not on the list is blocked, including your own deployment pipeline if its egress IP is not listed. Add all required source ranges (CI runners, VPN, jump hosts) before you consider the change complete, or your next deploy will fail with a 403.

You can verify the resulting configuration:

az webapp config access-restriction show \
  --resource-group my-rg \
  --name my-app \
  --query "scmIpSecurityRestrictions"

Fixing it in the portal

  1. Open the App Service in the Azure portal.
  2. Go to Settings → Networking, then under Inbound traffic configuration select Access restriction.
  3. Switch to the Advanced tool site (SCM) tab.
  4. Either tick Use main site rules, or untick it and add explicit Allow rules for your trusted sources.
  5. Confirm the unmatched-rule behavior shows Deny.

Infrastructure as Code (Bicep)

The durable fix is in your templates. Here is a Bicep snippet that defines SCM restrictions and defaults to deny:

resource appConfig 'Microsoft.Web/sites/config@2023-12-01' = {
  parent: webApp
  name: 'web'
  properties: {
    scmIpSecurityRestrictionsUseMain: false
    scmIpSecurityRestrictionsDefaultAction: 'Deny'
    scmIpSecurityRestrictions: [
      {
        ipAddress: '203.0.113.10/32'
        action: 'Allow'
        priority: 100
        name: 'ci-egress'
      }
      {
        ipAddress: '198.51.100.0/24'
        action: 'Allow'
        priority: 110
        name: 'corporate-vpn'
      }
    ]
  }
}

The equivalent in Terraform lives under the scm_ip_restriction block of azurerm_linux_web_app or azurerm_windows_web_app, with scm_use_main_ip_restriction = true as the inherit-from-main shortcut.


How to prevent it from recurring

A one-off fix drifts. New App Services get created without the restriction, and someone eventually toggles a rule off during a debugging session. Catch it earlier.

Enforce with Azure Policy

Azure has built-in policy definitions for App Service access restrictions. Assign them in audit or deny mode at the subscription or management group scope. A custom deny policy that targets the SCM configuration looks like this:

{
  "if": {
    "allOf": [
      { "field": "type", "equals": "Microsoft.Web/sites/config" },
      {
        "field": "Microsoft.Web/sites/config/scmIpSecurityRestrictionsUseMain",
        "notEquals": true
      },
      {
        "count": {
          "field": "Microsoft.Web/sites/config/scmIpSecurityRestrictions[*]",
          "where": {
            "field": "Microsoft.Web/sites/config/scmIpSecurityRestrictions[*].ipAddress",
            "equals": "Any"
          }
        },
        "greater": 0
      }
    ]
  },
  "then": { "effect": "deny" }
}

Tip: Start any deny policy in audit mode for a week before flipping it to deny. You will surface existing non-compliant resources without blocking deployments, which gives teams time to add their CI egress IPs before the gate goes live.

Gate it in CI/CD

If you ship App Services through pipelines, scan the IaC before it reaches Azure. Tools like Checkov and tfsec have rules for App Service IP restrictions, and you can run a Lensix scan against the deployed state as a post-apply check. Fail the build when the SCM site has no restrictions and is not inheriting main-site rules.

Continuous monitoring

Policy and CI catch the create path. Continuous scanning catches the drift path, when someone changes a rule live in the portal. Keep appservice_noscmaccessrestriction in your scheduled Lensix scans so a re-opened SCM site surfaces within a scan cycle rather than at the next audit.


Best practices

  • Inherit by default. Unless you have a specific reason to diverge, set scmIpSecurityRestrictionsUseMain = true. It keeps the management plane no more exposed than the app and removes a whole category of drift.
  • Prefer private access for sensitive apps. For internal workloads, use a private endpoint and disable public network access entirely. The SCM site then becomes reachable only over your VNet.
  • Disable basic auth on SCM. Turn off basic publishing credentials (basicPublishingCredentialsPolicies) and deploy with Azure AD or managed identity. Network restrictions plus identity-based deploy is far stronger than either alone.
  • Scope managed identities tightly. Assume the SCM site could be reached and grant the app's identity only the roles it genuinely needs. That keeps the blast radius small even if the network control fails.
  • Document your allowed source IPs. Keep CI egress ranges and VPN CIDRs in source control next to your IaC. When a deploy gets a 403, the fix should be obvious rather than a scavenger hunt.

The SCM site exists for good reasons, and you should not try to delete it. The goal is simply to make sure its reach matches its risk. A few lines of configuration move it from "anyone on the internet with leaked credentials" to "trusted networks only," and that is one of the highest-leverage App Service hardening steps you can take.