Back to blog
AzureBest PracticesCloud SecurityOperations & ComplianceServerless

App Service Uses Outdated Python: Risks and Remediation

Learn why running an outdated Python runtime on Azure App Service is a security risk, how to upgrade safely with CLI and IaC, and how to prevent regressions.

TL;DR

This check flags Azure App Services running Python versions that have reached end of life, which means no more security patches. Move your app to a supported runtime (currently Python 3.10 or later) using az webapp config set --linux-fx-version.

Python moves fast, and so do the people who hunt for vulnerabilities in old interpreters. When an Azure App Service keeps running on a Python version that Microsoft no longer supports, you stop receiving security fixes for the runtime itself. The appservice_oldpython check exists to catch that drift before it turns into an incident.

This is one of those problems that sneaks up on teams. An app gets deployed on Python 3.8, ships value for two years, nobody touches the runtime, and then one day it is two major versions behind and quietly out of support. Let's break down what the check looks for, why it matters more than it seems, and how to fix it without breaking your app.


What this check detects

The check inspects the runtime configuration of your Linux-based Azure App Service and compares the Python version against the list of versions Microsoft still supports. If your app is pinned to a version that has hit end of life, or is otherwise flagged as deprecated, the check fails.

On Linux App Services, the runtime is set through the linuxFxVersion property, which looks something like PYTHON|3.8 or PYTHON|3.11. The check reads that value and evaluates the version number against current support timelines.

Note: Azure App Service runs Python only on the Linux stack. There is no native Python runtime on the Windows stack, so this check applies to Linux plans. If you somehow run Python on Windows App Service, it is through a custom container or workaround, which falls outside what this check evaluates.

Each Python minor version has a defined support window. As a rough guide:

  • Python follows a roughly five-year support cycle per minor version from the upstream Python team.
  • Azure App Service typically removes or deprecates a runtime around the time upstream support ends.
  • Versions like 3.7 and 3.8 are already past end of life and should not be running anywhere in production.

Why it matters

An outdated runtime is not an abstract compliance nag. There are concrete consequences.

You stop getting security patches

Once a Python version reaches end of life, the upstream team stops issuing security fixes. If a vulnerability is discovered in the interpreter, the standard library, or a bundled component, there is no official patch coming. CVEs in older Python releases do happen, and they range from denial of service bugs in the standard library to issues in modules like ssl, http, and tarfile.

Your application code might be flawless, but if the runtime underneath it has an unpatched parsing bug, an attacker can target that layer directly.

Dependencies start dropping support

Modern libraries drop support for old Python versions quickly. Once you fall behind, you cannot upgrade key dependencies, which means you are now stuck with old versions of those libraries too. The security debt compounds. A single outdated runtime quietly freezes your entire dependency tree.

Azure can force the issue

When Azure retires a runtime, it does not always do so gracefully from your perspective. Apps on retired runtimes can be blocked from configuration changes, scaling operations, or restarts. In the worst case you discover the problem during an outage when you need to redeploy and the platform refuses.

Warning: Do not wait for a retirement deadline email to plan your upgrade. Runtime retirements have hard cutoff dates, and rushing a major version bump under time pressure is how regressions reach production.

Compliance and audit findings

Most security frameworks expect supported software with active patching. Running an end-of-life runtime is a common audit finding under SOC 2, ISO 27001, and PCI DSS. It is the kind of thing that turns a clean report into a remediation item.


How to fix it

The fix is to move your app to a supported Python version. Do it carefully, because moving across major versions can introduce behavior changes in your code and dependencies.

Step 1: Check what you are running

List the current runtime for your app:

az webapp config show \
  --resource-group my-resource-group \
  --name my-app-name \
  --query "linuxFxVersion" \
  --output tsv

This returns something like PYTHON|3.8. That tells you exactly where you stand.

Step 2: See which versions are available

Ask Azure which Python runtimes are currently offered in the platform:

az webapp list-runtimes --os linux | grep -i python

Pick the highest stable version your application supports. Aim for the most recent supported version rather than the minimum, so you buy yourself the longest runway before the next upgrade.

Step 3: Test locally first

Before touching the live app, run your test suite against the target Python version locally or in CI. Major version jumps from 3.8 to 3.11 or 3.12 can surface deprecation removals, changed default behaviors, and dependency incompatibilities. Pin and reinstall your dependencies on the new version:

python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pytest

Step 4: Update the runtime

Danger: Changing the runtime version restarts your App Service and swaps the interpreter underneath your running code. Do this against a deployment slot or during a maintenance window, never blindly against production.

If you have a staging slot, update it first:

az webapp config set \
  --resource-group my-resource-group \
  --name my-app-name \
  --slot staging \
  --linux-fx-version "PYTHON|3.12"

Validate the staging slot, then swap it into production:

az webapp deployment slot swap \
  --resource-group my-resource-group \
  --name my-app-name \
  --slot staging \
  --target-slot production

If you do not use slots, set the runtime directly and confirm the app comes back healthy:

az webapp config set \
  --resource-group my-resource-group \
  --name my-app-name \
  --linux-fx-version "PYTHON|3.12"

Tip: Deployment slots make runtime upgrades close to zero downtime. The swap is near instantaneous and gives you a tested-and-warm slot before traffic shifts. If you are running production App Services without a staging slot, set one up now and thank yourself later.

Step 5: Verify

Confirm the new runtime and check the application logs for startup errors:

az webapp config show \
  --resource-group my-resource-group \
  --name my-app-name \
  --query "linuxFxVersion" \
  --output tsv

az webapp log tail \
  --resource-group my-resource-group \
  --name my-app-name

Fixing it with infrastructure as code

If you manage App Services through IaC, change the runtime there so the next deployment does not silently revert your fix.

Terraform

resource "azurerm_linux_web_app" "example" {
  name                = "my-app-name"
  resource_group_name = azurerm_resource_group.example.name
  location            = azurerm_resource_group.example.location
  service_plan_id     = azurerm_service_plan.example.id

  site_config {
    application_stack {
      python_version = "3.12"
    }
  }
}

Bicep

resource site 'Microsoft.Web/sites@2023-01-01' = {
  name: 'my-app-name'
  location: location
  properties: {
    serverFarmId: appServicePlanId
    siteConfig: {
      linuxFxVersion: 'PYTHON|3.12'
    }
  }
}

How to prevent it from happening again

Upgrading once is easy. Staying current is the actual work. Build the guardrails so this never becomes a surprise.

Enforce supported versions with Azure Policy

Azure Policy can audit or deny App Services configured with disallowed runtimes. Use a policy with the linuxFxVersion field to flag or block end-of-life versions. A deny policy stops new deployments on bad runtimes, while an audit policy gives you visibility without breaking anything.

{
  "if": {
    "allOf": [
      {
        "field": "type",
        "equals": "Microsoft.Web/sites"
      },
      {
        "field": "Microsoft.Web/sites/siteConfig.linuxFxVersion",
        "in": [
          "PYTHON|3.7",
          "PYTHON|3.8",
          "PYTHON|3.9"
        ]
      }
    ]
  },
  "then": {
    "effect": "deny"
  }
}

Gate it in CI/CD

Add a check in your pipeline that fails the build if the IaC defines an outdated Python version. A simple grep against your Terraform or Bicep files catches the obvious cases before merge:

if grep -rE "python_version\s*=\s*\"3\.(7|8|9)\"" ./infra; then
  echo "Outdated Python runtime detected in IaC. Upgrade before merging."
  exit 1
fi

Schedule recurring reviews

Runtime support timelines are predictable. Put a recurring calendar reminder, every quarter, to check the support status of every runtime in your estate. Pair it with continuous scanning so nothing slips between reviews.

Tip: Lensix runs the appservice_oldpython check continuously across your subscriptions, so you find out a runtime has aged out as soon as it crosses the line, not during your next manual audit.


Best practices

  • Always pin an explicit version. Avoid leaving the runtime to platform defaults. Pin PYTHON|3.12 so upgrades are deliberate and reviewable, not implicit.
  • Upgrade to the latest supported version, not the minimum. Jumping to the newest stable release maximizes the time before your next forced upgrade.
  • Keep dependencies current alongside the runtime. A modern interpreter with frozen, ancient libraries still carries risk. Treat the runtime and the dependency tree as one upgrade unit.
  • Use deployment slots for every production app. They turn risky runtime changes into safe, reversible swaps.
  • Test against the target version in CI before deploying. Major version jumps change behavior. Catch it in the pipeline, not in production.
  • Track end-of-life dates. Know when each runtime in your environment is scheduled to retire and plan upgrades a quarter ahead of the deadline.

Outdated runtimes are one of the quietest forms of security debt. They do not throw errors, they do not show up in your dashboards, and they keep serving traffic right up until the day a CVE or a retirement notice forces your hand. A few minutes of upgrade work now beats an emergency migration later.