Best Practices

Azure Functions Supply Chain Security

Azure Functions hide a surprising amount of supply chain risk — Oryx builds, run-from-package, extension bundles, and the way deployment slots interact with identity.

Shadab Khan
Security Engineer
8 min read

Azure Functions is the service where every supply chain shortcut I warn teams about in their main application code comes back into scope — and usually with less review, because it is "just a function." The service makes deployment easy, which is the point, but the ease of deployment obscures a fairly complex chain between the code you commit and the bytes that run in production. This post walks that chain end to end for a typical Azure Functions v4 app on the consumption plan and calls out the decisions that actually move the needle on supply chain risk.

I am assuming a Node.js or Python function app deployed from GitHub via GitHub Actions or from Azure DevOps via an Azure Resource Manager service connection. The patterns map with minor changes to .NET, Java, and PowerShell.

What Actually Runs When You Deploy a Function

When you push code to an Azure Functions app, one of several things happens depending on how the app is configured. The default for zip-deploy is the Oryx build — a build process that runs on a Kudu worker inside the App Service infrastructure, downloads your dependencies from the public package registries, and produces a deployment artifact that is mounted into the function host. Oryx is open source, but it is a full build system running in Microsoft's environment with your dependencies as input. If a dependency is malicious, it runs there first.

This is the first supply chain decision most teams make without realizing it: "should Azure build my function, or should I build it myself?" The Oryx path is convenient because you push source code and get a working function. The self-build path — build the artifact in your own CI, zip it, and deploy via run-from-package — is more work but eliminates the Kudu build step entirely. For any function that handles sensitive data or has any significant dependency tree, the self-build path is the right choice. The build happens in an environment you control, and the deployed artifact is byte-for-byte what you built.

WEBSITE_RUN_FROM_PACKAGE=1 (or a SAS URL) is the setting that switches the function to read-only package mode. The package is mounted and the function host runs from it; the function's working directory is immutable. This is both a supply chain control (no runtime modification) and a reliability control (no partial deploys). It is the first setting I check on any function app in review.

Extension Bundles Are a Trust Decision

Azure Functions uses extension bundles to package the runtime extensions that provide triggers and bindings — Azure Storage, Service Bus, Event Hubs, Cosmos DB, and so on. Extension bundles are versioned through a range in host.json:

{
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[4.*, 5.0.0)"
  }
}

The default range floats. That is convenient when a binding fixes a bug. It is also a supply chain dependency on Microsoft's ability to publish only the right bytes to the extension bundle feed. The risk is low — Microsoft signs and hosts the bundles — but it is not zero, and for a regulated workload the right answer is to pin the bundle to a specific version and review upgrades explicitly. The version range can be as narrow as [4.14.0, 4.14.1), which pins to a single bundle version.

The more interesting decision is whether to use the extension bundle at all. The alternative is to install extensions as explicit NuGet packages in the function's project file, which moves the version decision into your dependency manifest alongside every other package. For a team that already has an SCA process for NuGet, that is strictly better — one manifest to review rather than two systems of versioning.

Deployment Slots and the Identity Swap

Deployment slots on Azure Functions are a zero-downtime deployment primitive that works by swapping the configuration and content of two slots. A staging slot holds the new version; you warm it up, you swap, and the production slot now has the new code. From a release engineering perspective this is great. From a supply chain perspective it has a subtle gotcha.

The slot swap preserves some settings as "sticky to the slot" — the app settings that are marked as slot-specific stay with the slot they were configured on. Managed identity is one of the settings that does not move in the expected way. A system-assigned managed identity is scoped to the App Service resource, and the resource persists across swaps, so the identity on the "production" resource remains constant. But a user-assigned identity attached only to the staging slot's deployment configuration will, after a swap, be on the production slot. If the staging slot has a broader identity for testing purposes, you just promoted it.

The working pattern is to use user-assigned identities consistently across slots (one identity for prod, one for staging, never overlapping) and to review slot-specific settings before every swap. There is no Azure Policy that catches this automatically; it is a procedural control.

Managed Identity for the Storage Account

Every Azure Functions app has a storage account — the AzureWebJobsStorage account that the runtime uses for internal state, queue triggers, and timer metadata. Historically, the connection to that storage was a connection string with a full account access key. In late 2022, Azure Functions added support for identity-based connections (AzureWebJobsStorage__accountName with a managed identity and the Storage Blob Data Owner role), and by 2024 that is the recommended pattern for any new deployment.

The supply chain relevance is that a storage account connection string stored in app settings is a long-lived credential that any code running in the function can read and exfiltrate. A compromised dependency that can read environment variables is then one HTTP call away from having durable access to the function's entire state. Identity-based connections remove that credential from the function's environment. The managed identity token is scoped, time-limited, and cannot be trivially exfiltrated to a remote attacker for later use.

Migrating a running function to identity-based storage is a two-step rollout: add the identity-based settings alongside the connection string, redeploy, verify the runtime is using the identity, then remove the connection string. Plan for the migration taking an afternoon per function app, not a maintenance window.

Zip Deploy, OneDeploy, and What Ends Up on Disk

The Functions deployment APIs have grown over the years. Zip deploy (/api/zipdeploy) is the original endpoint. OneDeploy (/api/publish) is the newer, preferred endpoint that supports partial deploys and better diagnostics. Run-from-package is a deployment mode rather than an endpoint — the package URL goes into an app setting and the runtime fetches it.

The supply chain distinction is that zip deploy expands the zip onto the function's filesystem; run-from-package does not. A compromised deployment credential (the Kudu publish profile) that can call zip deploy can modify individual files on the running function. The same credential against a run-from-package function can only replace the package URL, which is a much more visible change that can be gated by RBAC on the app setting.

The other control that matters: disable basic authentication (scmBasicAuthPublishingDisabled: true) on the app's SCM endpoint. By default, Functions apps accept publish-profile credentials, which are shared secrets in a file. Disabling them forces all deployments to go through Azure AD, which means deployment identities are Azure AD service principals with auditable role assignments. This setting has been available since 2023 and is a one-line ARM change.

SBOM at the Function Boundary

The function app is the deployment unit; the SBOM should be too. For Node.js functions, generate an SPDX SBOM during the self-build step (npm sbom since npm 10, or Syft) and attach it to the deployment artifact. For Python, generate a CycloneDX SBOM with cyclonedx-py. Store the SBOM alongside the package in the storage container, so the function's provenance is retrievable from the function resource itself without having to trace back to a build system that may have rotated logs.

How Safeguard Helps

Safeguard treats each Azure Functions app as a first-class asset, correlating the deployed package with its build-time SBOM, the extension bundle version, and the managed identity's role assignments. Floating extension bundle ranges, connection-string-based storage access, and unpinned dependencies surface as concrete findings rather than generic warnings. For organizations with dozens of function apps across subscriptions, Safeguard's inventory view makes the "which ones still use publish profiles" question answerable in seconds instead of a PowerShell script run, and the remediation paths are wired into the existing SCM integrations.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.