This check flags Azure App Services running an unsupported or outdated Java runtime, which exposes you to known CVEs and missing security patches. Bump the runtime to a supported LTS version (Java 17 or 21) under your App Service language settings and pin it in your IaC.
Java has one of the longest tails of any runtime in production. Plenty of App Services were spun up years ago on Java 8 or 11, deployed once, and forgotten. The app still serves traffic, the team moved on, and nobody noticed that the runtime hasn't seen a security update in a long while. That is exactly the situation this check exists to surface.
The appservice_oldjava check looks at every Azure App Service in your subscription and reports any that are pinned to a Java major version Microsoft no longer ships current patches for, or a minor version that has fallen behind the supported baseline.
What this check detects
Azure App Service runs your Java workload on a managed runtime stack. You pick a major version (Java 8, 11, 17, 21) and a web container (Tomcat, JBoss EAP, or Java SE), and Azure handles the underlying JDK build and OS patching for that stack.
The catch is that the Java major version is something you choose and pin. Azure will not silently upgrade you from Java 8 to Java 17, because that could break your application. So when a major version reaches end of community support or Microsoft retires it from App Service, any service still pinned to it keeps running on an increasingly stale runtime.
This check fires when an App Service is configured with:
- A Java major version that has reached end of support (for example Java 8 in many contexts, or older).
- A minor JDK build that lags significantly behind the patched build Azure offers for that major version.
- An outdated bundled web container, such as an old Tomcat 8.5 line, paired with the Java runtime.
Note: App Service distinguishes between the OS patching (which Azure manages automatically for the platform) and the Java major version (which you pin). A "patched" platform can still be running a Java major version that upstream no longer supports, so platform patching alone does not clear this finding.
Why it matters
An outdated Java runtime is not a theoretical risk. The JDK and the servlet containers bundled with it have a steady stream of CVEs, and several have been weaponized at scale.
Known, exploitable vulnerabilities
The deserialization and parsing surface in older Java stacks is large. Vulnerabilities in libraries that ship with or commonly run on older JDKs, along with container-level issues in old Tomcat and JBoss builds, give attackers well-documented paths to remote code execution. When you run a retired runtime, you inherit every unpatched CVE in that line with no vendor fix coming.
Missing TLS and crypto updates
Older JDK builds ship outdated cipher suites and root certificate stores. That leads to two problems at once: your outbound TLS connections may negotiate weak ciphers, and your service may fail to validate newer certificate chains, causing intermittent connection failures that are painful to debug.
Compliance exposure
Running software past its end-of-support date is a direct finding under most compliance frameworks. PCI DSS, SOC 2, and HIPAA assessments all expect that production components receive security patches. An unsupported runtime is an automatic flag during an audit.
Warning: The longer a service stays on an old major version, the larger the eventual upgrade jump becomes. Going from Java 8 to 21 across several language and API changes is far more work than staying current one LTS at a time. The risk compounds with delay.
How to fix it
The fix is to move the App Service to a supported LTS Java version. As of now that means Java 17 or Java 21. Test in a staging slot first, since a major version bump can change default behavior, API availability, and library compatibility.
Step 1: Check the current runtime
Find out exactly what the service is pinned to before you change anything.
az webapp config show \
--resource-group my-rg \
--name my-java-app \
--query "{linuxFxVersion: linuxFxVersion, javaVersion: javaVersion, javaContainer: javaContainer, javaContainerVersion: javaContainerVersion}" \
-o json
For Linux App Services, the runtime is encoded in linuxFxVersion (for example JAVA|11-java11 or TOMCAT|9.0-java11). For Windows plans, you read javaVersion, javaContainer, and javaContainerVersion separately.
Step 2: List the supported runtimes
Confirm which versions are available in your region before picking a target.
# Linux
az webapp list-runtimes --os linux | grep -i java
# Windows
az webapp list-runtimes --os windows | grep -i java
Step 3: Deploy to a staging slot and test
Danger: Do not change the runtime on a production slot directly. A major version bump can break startup, and you may not be able to roll back cleanly if the old runtime is already retired. Always validate in a staging slot first, then swap.
# Create a staging slot if you don't have one
az webapp deployment slot create \
--resource-group my-rg \
--name my-java-app \
--slot staging
# Set the new runtime on the staging slot (Linux, Tomcat on Java 17)
az webapp config set \
--resource-group my-rg \
--name my-java-app \
--slot staging \
--linux-fx-version "TOMCAT|10.1-java17"
For a Java SE app (no managed container), the value looks like JAVA|17-java17 instead.
On a Windows plan, set the three values together:
az webapp config set \
--resource-group my-rg \
--name my-java-app \
--slot staging \
--java-version 17 \
--java-container TOMCAT \
--java-container-version 10.1
Step 4: Rebuild against the new version
Update your build to compile and target the new JDK so you catch incompatibilities at build time, not runtime.
<!-- Maven: pom.xml -->
<properties>
<maven.compiler.release>17</maven.compiler.release>
</properties>
Run your full test suite, then deploy the rebuilt artifact to the staging slot and exercise the critical paths, especially anything touching reflection, serialization, or TLS.
Step 5: Swap to production
az webapp deployment slot swap \
--resource-group my-rg \
--name my-java-app \
--slot staging \
--target-slot production
Tip: A slot swap is near instant and reversible. Keep the old slot around for a day after swapping so you can swap back in seconds if a problem surfaces under real traffic.
Fixing it in infrastructure as code
If you manage App Services with Terraform or Bicep, pin the runtime there so the version is reviewed and version controlled rather than clicked through a portal.
Terraform
resource "azurerm_linux_web_app" "java_app" {
name = "my-java-app"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
service_plan_id = azurerm_service_plan.main.id
site_config {
application_stack {
java_version = "17"
java_server = "TOMCAT"
java_server_version = "10.1"
}
}
}
Bicep
resource javaApp 'Microsoft.Web/sites@2023-12-01' = {
name: 'my-java-app'
location: location
properties: {
serverFarmId: appServicePlan.id
siteConfig: {
linuxFxVersion: 'TOMCAT|10.1-java17'
}
}
}
How to prevent it from happening again
The reason this check keeps firing in real environments is that runtime versions get set once and never revisited. Prevention is about making the version an explicit, enforced decision.
Enforce a minimum version with Azure Policy
Azure Policy can audit or deny App Services that use a runtime outside an approved list. Use an audit effect first to measure your fleet, then switch to deny once you are clean.
{
"if": {
"allOf": [
{
"field": "type",
"equals": "Microsoft.Web/sites"
},
{
"field": "Microsoft.Web/sites/siteConfig.linuxFxVersion",
"notIn": [
"JAVA|17-java17",
"JAVA|21-java21",
"TOMCAT|10.1-java17",
"TOMCAT|10.1-java21"
]
}
]
},
"then": {
"effect": "audit"
}
}
Warning: A deny policy will block legitimate deployments that use a version you forgot to add to the allowed list. Keep the list current and treat it as a real change when a new LTS lands, otherwise you will have engineers fighting the policy instead of using it.
Gate it in CI/CD
Catch outdated runtimes before they deploy by scanning your IaC in the pipeline. A simple grep-based check works as a fast first gate, and tools like Checkov or tfsec give you policy-as-code with proper reporting.
# Fail the build if any Terraform file pins an old Java version
if grep -rEn 'java_version\s*=\s*"(8|11)"' ./infra; then
echo "Outdated Java runtime detected in IaC. Use 17 or 21."
exit 1
fi
Schedule a recurring runtime review
Java LTS releases land on a predictable cadence, and older versions retire from App Service on published dates. Put a recurring item on the platform team's calendar to review every App Service runtime against the supported list each quarter. Lensix surfaces this automatically, so wiring the appservice_oldjava finding into your alerting closes the loop without manual checking.
Tip: Subscribe to the Azure App Service language runtime support timeline and the OpenJDK release schedule. Knowing the retirement date for your current version in advance turns an emergency migration into a planned one.
Best practices
- Stay on LTS, one hop at a time. Move to each new LTS (17, then 21) as it stabilizes rather than skipping several at once. Small jumps are cheap, large jumps are projects.
- Pin runtimes in IaC, never the portal. A version set through the portal is invisible to review and easy to forget. In code it gets a pull request and a history.
- Keep the web container current too. Tomcat and JBoss have their own CVE streams. Upgrading the JDK while leaving an old Tomcat in place only fixes half the problem.
- Always migrate through a staging slot. Slot swaps make major version changes low risk and instantly reversible.
- Rebuild against the target JDK. Compiling for the new version catches removed APIs and behavioral changes at build time instead of in production.
- Treat end-of-support as a hard deadline. Once Microsoft retires a runtime, there are no more fixes. Plan migrations to land before the retirement date, not after.
An outdated Java runtime is one of the easier security findings to fix and one of the easiest to ignore, which is why so many App Services drift onto unsupported versions. Pin a supported LTS, enforce it in policy, gate it in CI, and the problem stops recurring.

