Tomorrow, November 10, 2026, .NET 8 and .NET 9 both reach end of support. .NET 8 was the LTS release and .NET 9 the STS one, and they end on the same day. After that, neither gets security patches. .NET 10 is the LTS to move to, supported until November 14, 2028.
If you run .NET on Kubernetes, the first question is a boring one, and it's harder than it sounds: which of the containers running right now are actually on 8 or 9?
I spent some time on this while building runtime checks for Runtimez, and the image tag turned out to be the least useful place to look.
Why the tag doesn't help
mcr.microsoft.com/dotnet/aspnet:8.0 is easy. The version is right there. But almost nobody runs that image directly. You build your own:
FROM mcr.microsoft.com/dotnet/aspnet:8.0 COPY publish/ /app ENTRYPOINT ["dotnet", "/app/Orders.Api.dll"] and push it as registry.example.com/orders-api:1.42. Now the tag is your app's version, and the .NET version is gone from everything kubectl get pods shows you.
The usual next move is kubectl exec and dotnet --list-runtimes. That works until you hit a chiseled image, which has no shell. It also means exec-ing into production pods to answer an inventory question, which I'd rather avoid.
The version is still in the image config
Microsoft's official Dockerfiles set the runtime version as an environment variable in the image. The 8.0 runtime images, chiseled ones included, have ENV DOTNET_VERSION=8.0.33. The aspnet images add ENV ASPNET_VERSION=8.0.33. ENV is inherited, so your orders-api:1.42 carries the same values from its base image.
You can read them from the registry without pulling or running anything. First, list every image running in the cluster, init containers included:
kubectl get pods -A -o json \ | jq -r '.items[].spec | (.containers + (.initContainers // []))[].image' \ | sort -u > images.txt Then read each image's config with crane (from go-containerregistry) and keep the two version variables:
while read -r img; do v=$(crane config "$img" \ | jq -r '.config.Env[] | select(test("^(DOTNET|ASPNET)_VERSION="))' \ | tr '\n' ' ') [ -n "$v" ] && echo "$img $v" done < images.txt The output looks like this:
registry.example.com/orders-api:1.42 DOTNET_VERSION=8.0.33 ASPNET_VERSION=8.0.33 Anything starting with 8. or 9. is on a runtime that stops getting patches tomorrow. Both commands only read. If you'd rather work from images you've already pulled, docker image inspect shows the same Env list.
Where this misses
Self-contained apps are the gap. If you publish with --self-contained and build on the runtime-deps image, the runtime ships inside your app folder. runtime-deps sets no DOTNET_VERSION, so the env check finds nothing.
For those, the image's SBOM still knows. The runtime shows up as a Microsoft.NETCore.App.Runtime.* NuGet package, and Trivy will list it:
trivy image --format cyclonedx --quiet "$img" \ | jq -r '.components[] | select(.name | startswith("Microsoft.NETCore.App.Runtime")) | "\(.name) \(.version)"' which prints something like Microsoft.NETCore.App.Runtime.linux-x64 8.0.33.
That's slower than reading the config, since Trivy has to fetch and unpack the layers. I'd run the env check across everything first and only use the SBOM for images where it came back empty but you know a .NET service lives there.
What to do with the list
Not everything on it is equally urgent. I'd start with services that take traffic from the internet, then anything handling auth or payments, then internal jobs and batch work.
For most services the change itself is small. Bump the base image to 10.0, change TargetFramework to net10.0, rebuild and run your tests. Read Microsoft's "Breaking changes in .NET 10" page before you assume the bump is free, though. It's grouped by area, so you can skip the parts you don't use.
If a service can't move this month, write that down with an owner and a date. An unpatched runtime you know about is a decision you made. One you don't know about turns into a surprise the day a CVE lands for it.
How did you find your .NET 8 services, and did any of them hide behind a tag that didn't say so?
