Search nomadLab

Debian 11 Dies on 31 August, and It Is Hiding in Your Base Images

The people this deadline catches are not Debian admins. They run python:3.9-slim and have never once thought about what is underneath it. Nine days to find out.

Updated

Debian 11 “bullseye” stops receiving security updates on 31 August 2026. That is nine days from now.

If you administer Debian machines for a living you knew this and finished months ago. The people who get caught are app developers who do not think of themselves as Debian users at all. They run python:3.9-slim, or a ruby:3.0 image, or a vendor container pulled three years ago and never opened. Bullseye is what is underneath. Nothing in the Dockerfile says “debian” anywhere. The exposure is there regardless.

Full support actually ended back in August 2024, when the Debian Security Team handed bullseye to the volunteer LTS team. What runs out this month is that extension. From 1 September, every new CVE in glibc, OpenSSL, systemd, or anything else in the base system is yours to carry, or yours to pay someone to carry.

What breaks on 1 September

Nothing. That is the trap.

Containers keep running, VMs keep booting, there is no license check and no nag screen. What you get instead is slower and more irritating.

apt starts failing. Bullseye packages move off the main mirrors to archive.debian.org, so apt update against an untouched sources.list returns 404s, and once the Release file’s validity window lapses you get expired-signature errors stacked on top. Any Dockerfile with RUN apt-get install in it stops building. That is usually how teams find out.

Then the scanners light up. Trivy, Grype, Snyk, and Docker Scout all read the OS release and match it against advisory feeds. With no fix version published for a bullseye CVE, findings accumulate as unfixable, which is exactly the class that blocks a release gate or turns up in a SOC 2 evidence review. The Amazon Linux 2 holdouts hit the same wall after their EOL.

One detail that catches people: during the LTS phase, bullseye only covered amd64, i386, armhf, and arm64. On ppc64el or s390x your “supported until August 2026” ended two years ago. Worth checking before you assume you had runway.

Finding it in your build graph

Start from the assumption that you do not know where it is. Tag names lie constantly. python:3.9-slim was bullseye-based, python:3.11-slim is bookworm, and they look identical in a Dockerfile.

The cheapest check is one line per image:

docker run --rm --entrypoint sh IMAGE -c 'cat /etc/os-release' | grep -E 'VERSION_CODENAME|PRETTY_NAME'

If you would rather not execute untrusted images, docker inspect will not help, because the codename is not in the metadata. Syft reads the OS out of the layers without running anything:

syft IMAGE -o json | jq '.distro'

Then check the places images hide that are not in your Dockerfiles: GitHub Actions container: and services: blocks, Kubernetes sidecars, docker-compose.yml files in test setups, base images your platform team maintains that everyone else inherits from, and vendor images you pinned by digest years ago. That last category is the worst, because a digest pin means the vendor could have rebuilt on bookworm a year ago and you would still be pulling the old bullseye layer.

Rough triage heuristic: language runtimes released before roughly mid-2023 were built on bullseye, later ones on bookworm, and the current generation is moving to trixie. Do not trust it. It exists to tell you which images to check first.

Bookworm or trixie

Support windows for three Debian releases. Debian 11 LTS ends 31 August 2026. Debian 12 LTS ends 30 June 2028. Debian 13 LTS ends 30 June 2030. Paid extended support for Debian 11 runs from September 2026 to 30 June 2031. How much runway each choice actually buys 31 Aug 2026 Debian 11 Debian 12 LTS ends 30 Jun 2028 Debian 13 LTS ends 30 Jun 2030 Debian 11 ELTS paid, to 30 Jun 2031 2026 2028 2030 2032
Dates from the Debian LTS wiki and Freexian's ELTS page, checked 22 August 2026.

Two live options, and the answer turns on how often you rebuild.

Debian 12 “bookworm” went to the LTS team on 12 July 2026 and is supported until 30 June 2028. Debian 13 “trixie” shipped in August 2025, holds regular security support into August 2028, and LTS until 30 June 2030.

For containers, go to trixie. Two extra years before you do this again, a release that has had a year to settle, and images are cheap to rebuild when something is off. Bookworm buys you 22 months and then you are back here reading this post.

For long-lived VMs and bare metal, bookworm is easier to defend, mostly because you cannot skip. Debian does not support jumping releases, so bullseye to trixie means bullseye to bookworm to trixie, two full dist-upgrade cycles. If a machine is stateful enough that you are upgrading in place rather than replacing it, take the one hop, take the June 2028 date, and plan the trixie hop for a maintenance window that is not nine days wide.

The awkward case is a fleet you rebuild from images but that carries state, like databases and queue brokers. There I would still say bookworm now and trixie next year, because a smaller version jump means fewer simultaneous changes to debug when something behaves oddly at 2am.

The container path

Most of the time it is one line and a green build:

- FROM python:3.9-slim-bullseye
+ FROM python:3.13-slim-trixie

The things that actually break, roughly in order of how often they bite:

OpenSSL 1.1 to 3.x. Bullseye shipped OpenSSL 1.1.1, bookworm moved to 3.0, trixie is on 3.5. Legacy algorithms (RC4, older DES modes, small RSA keys, MD5 signatures) moved into a legacy provider that is not loaded by default. If your app talks to an old vendor API, a legacy SFTP server, or an internal service with a pre-2018 certificate, the handshake fails with an error that reads like a network problem. Anything pinning libssl1.1 in an apt-get install line simply will not resolve.

Python version jumps. Bullseye’s system Python was 3.9, bookworm’s is 3.11, trixie’s is 3.13. If you use the distro Python rather than a python: image, that is two or four minor versions in one move: distutils gone in 3.12, imp gone, several asyncio behaviors changed. Bookworm onward also enforces PEP 668, so pip install into the system Python is refused unless you pass --break-system-packages or use a venv, which surprises a lot of Dockerfiles. The Python 3.10 EOL playbook covers the language side of that jump.

Native modules and wheels. glibc moves from 2.31 to 2.36 on bookworm, 2.41 on trixie. Prebuilt wheels and npm native modules compiled against the older glibc mostly work forward, but anything you built yourself and cached needs recompiling. Clear your build caches: a stale layer holding a bullseye-compiled .so will survive the base image bump and fail at runtime instead of build time, which is a much worse place to find out.

Packages that quietly vanished. Transitional packages get dropped between releases, so an apt-get install line that worked for years fails on one name. Usually a rename, occasionally a real removal.

The VM path

For machines you are upgrading rather than replacing, the sequence has not changed much. Get fully current on bullseye first with apt update && apt full-upgrade, because upgrading from a half-patched state adds variables you do not want. Then inventory every source: apt policy, plus a read through /etc/apt/sources.list and everything in sources.list.d/.

That inventory step is where in-place upgrades go wrong. People run a sed across every file replacing bullseye with bookworm, which also rewrites third-party repos (Docker, PGDG, NodeSource, Grafana) that may not publish a bookworm suite at that path or may need a new signing key. Disable those separately, upgrade the base system, then re-point them one at a time and check each. Backports entries should just be removed, and re-added afterward only if you still need something from them.

Then apt update, apt upgrade --without-new-pkgs, apt full-upgrade, reboot. Snapshot first if your hypervisor or cloud offers snapshots. It is the cheapest insurance in the whole process, and an in-place dist-upgrade is not something you want to unwind by hand.

Watch for held-back packages after apt upgrade. They are usually telling you about a dependency conflict that full-upgrade will resolve by removing something you care about. Read the removal list before you answer yes.

CI is where the stragglers live

Three things hide from image audits.

Pinned apt sources inside CI Dockerfiles that point at deb.debian.org/debian bullseye. Those become 404s on a schedule nobody on your team controls.

Actions jobs with container: node:16 or similar, plus service containers for Postgres and Redis. postgres:13 and postgres:14 were bullseye-based. Those service containers are not network-exposed, but a scanner pointed at your compose file does not care about the nuance and will flag them anyway.

Layer caching that keeps the old base alive. Bumping FROM does not invalidate a cached layer that a registry mirror or a self-hosted runner still holds under the old tag. If you use a mutable tag like python:3.11-slim, force a pull. Same failure shape as the containerd 1.7 migration, where the config says one thing and the running system does another.

When you genuinely cannot make the date

Some systems cannot move in nine days. An appliance with a vendor-certified OS image, a medical or industrial box under recertification, an app whose original build environment no longer exists. That is what extended support exists for.

Freexian’s ELTS picks up exactly where Debian’s LTS stops, 31 August 2026 through 30 June 2031, on amd64, arm64, armhf, and i386, with i386 only partially supported. Coverage is a list of base system packages rather than the whole archive: bash, openssh, openssl, systemd, sudo and their neighbors. Ask for the exclusion list with the quote. TuxCare’s ELS is the other option, backporting CVE fixes as signed packages over apt and patching the kernel you already run rather than moving you forward.

Neither publishes pricing, so both are quote-based per-instance deals and I cannot tell you what you would pay. The honest planning assumption is that for a handful of machines the upgrade is cheaper than the contract. Extended support earns its keep when the blocker is contractual or regulatory rather than technical, or when you have hundreds of instances and need to spread the work across a year without an unpatched gap in the middle.

What it is not is a way to avoid the work. You are buying a schedule, and the meter runs the whole time.

Nine days, spent honestly

The four-week version of this plan had a week of slack in it. This one does not, so the ordering matters more.

Two days on inventory only. Every image, every VM, every CI config, written down and marked rebuild-only or stateful. Resist fixing things while you count, because a partial inventory is how you miss the one that matters.

Four days on the stateless containers. Bump to trixie, run the tests, ship. Most will just work, and the ones that fail will fail on OpenSSL or a native module, which is why you want them found early rather than on the 30th.

The rest on stateful hosts, one at a time, lowest blast radius first, never in parallel across a fleet. The point of going one at a time is that the first failure teaches you something about hosts two through twenty.

If today is all you have, run the codename check across whatever images you can list in ten minutes. The gap between what a team thinks it is running and what is actually in the layers is consistently wider than anyone expects, and that gap is the entire problem here.

Keep reading