Docker audit fix

How to Detect & Upgrade EOL Docker Base Images on VPS

An end-of-life base image stops receiving security patches, so every CVE published after its EOL date stays open in your containers forever. Rebuild on a currently supported base tag and check the real support window at endoflife.date.

Hiren KalariyaLast reviewed: Oct 12, 2026Check eol-base-image
High
severity
Yes
needs root or sudo
3
results it can return
6 of 8
checks in this audit

Every threshold on this page is transcribed from the docker-security-image-vulnerabilities audit script that CtrlOps runs, and a build check fails if the two ever disagree. See all 8 Image Vulnerabilities checks, or how the audit runs.

High severityNeeds sudo

What this check reads

The check pulls every tagged local image with docker images --format '{{.Repository}}:{{.Tag}}', drops the <none> entries, and runs the resulting repo:tag strings through one fixed regex. That pattern covers ubuntu:14.04|16.04|18.04 and the trusty, xenial and bionic codenames, debian:7|8|9 and wheezy, jessie and stretch, centos:6|7|8, alpine:3.9-3.12, node:10|12|14, and python:2.7|3.5|3.6. A single matching tag is enough to FAIL, with the first 10 hits named in the message; no match anywhere in the list is the PASS, and no tagged images at all is the SKIP.

When it applies

Needs the docker binary and daemon access; otherwise SKIP. Unlike the other image checks it examines every tagged local image, not a sample, but reports at most 10 matches. Matching is a single fixed regex over the repo:tag string - it never inspects the image contents - so it only finds distributions named in the tag. An image whose tag hides the base (myapp:1.4.2 built FROM debian:9) is invisible, as is any runtime not in the list.

Known accuracy issue (verified August 2026): the pattern is a hard-coded snapshot and is now well out of date. It covers Ubuntu 14.04/16.04/18.04, Debian 7/8/9, CentOS 6/7/8, Alpine 3.9 - 3.12, Node 10/12/14 and Python 2.7/3.5/3.6. It therefore misses everything that has since reached end of life, including PHP 8.1, Node.js 16/18/20, MySQL 5.7 and 8.0, PostgreSQL 12 and 13, MongoDB 5.0 and 6.0, Redis 6.x and 7.0/7.2, Ubuntu 20.04, Debian 10, Alpine 3.13 - 3.20 and Python 3.7 - 3.9. A PASS here means "no tag matched a 2022-era list", not "everything is supported". This is the known issue flagged at the top of this page.

What each result means

Result thresholds this check applies
ResultWhenWhat it means
SKIPDocker not installed, daemon unreachable, or no local imagesNothing was verified. No image list reached the pattern, so no statement was made about the support status of anything you run.
FAILAny tag matches the EOL listAt least one image is tagged with a distribution or runtime version that has stopped receiving security patches, and the message names it.
PASSNone match (with a reminder to verify manually)No tag matched this frozen list, which is much weaker than being supported. Anything that reached end of life after the pattern was written passes here.

Why it matters

An EOL base image receives no security updates, so every CVE found after the EOL date stays open forever, and scanners often cannot even report them (the distro stops publishing advisories). CIS Docker 4.2/4.4; NIST SP 800-190 4.1.1; OWASP Top 10 A06:2021.

Why it fails, and when it is wrong

  • The EOL list is incomplete (verified August 2026). Not flagged but EOL as of this date: ubuntu:20.04 (standard support ended 31 May 2025; ESM only), debian:10 (June 2024), debian:11 (LTS ended 31 August 2026), centos:stream8 (May 2024), alpine:3.13 through 3.20 (Alpine supports each release for about two years; 3.20 ended April 2026), node:16, node:18 (April 2025), node:20 (April 2026), python:3.7, python:3.8 (October 2024), python:3.9 (October 2025), php:7.*, php:8.0, php:8.1, ruby:3.0/3.1, golang older minors, openjdk:8/11 (OpenJDK project images deprecated entirely). Consider generating the pattern from endoflife.date at release time, or using docker scout recommendations / Trivy's --scanners vuln EOSL warning ("OSEndOfLife" / "This OS version is no longer supported").
  • Only the tag string is examined. An image built FROM ubuntu:18.04 but tagged myapp:2.1 passes. Trivy and Scout detect the OS from the layers (trivy image prints "Detected OS" and an EOSL warning).
  • centos:7 containers are still common for legacy apps; CentOS 7 ended 30 June 2024.
  • Images of a current distro with a stale minor are not EOL, just out of date (covered by the vulnerability scan).

How to fix it

Find the real base of every image

A tag such as myapp:2.1 hides the distribution underneath, so read it out of the image.

for i in $(docker images --format '{{.Repository}}:{{.Tag}}' | grep -v '<none>'); do
  printf '%s -> %s\n' "$i" "$(docker run --rm --entrypoint sh "$i" -c 'grep PRETTY_NAME /etc/os-release' 2>/dev/null)"
done

Check each one against endoflife.date

Check the distribution and the language runtime, since either can run out of support first.

curl -s https://endoflife.date/api/debian.json | head -40
curl -s https://endoflife.date/api/nodejs.json | head -40

Rebuild on a supported base

FROM debian:bookworm-slim

Or ubuntu:24.04, alpine:3.22, node:22-slim, python:3.13-slim, or a distroless or Chainguard equivalent. Package names and library versions move with the base, so run the test suite before shipping. In CI, add trivy image --exit-code 1 --severity CRITICAL and treat trivy's unsupported-OS warning as a failure.

Remove the old image

docker compose up -d --build
docker rmi <old-image>:<tag>

The check reads the local image store, so an old tag left behind keeps the FAIL alive after every container has moved on.

Verify the fix

# Reproduce the check's own match:
docker images --format '{{.Repository}}:{{.Tag}}' | grep -v '<none>' | \
  grep -E 'ubuntu:(14|16|18)\.04|ubuntu:(trusty|xenial|bionic)|debian:(7|8|9)($|-)|debian:(wheezy|jessie|stretch)|centos:(6|7|8)($|-)?|centos:centos(6|7|8)|alpine:3\.(9|10|11|12)($|\.)|node:(10|12|14)($|-)|python:(2\.7|3\.5|3\.6)($|-)'

# Find the real base of every image, including ones whose tag hides it:
for i in $(docker images --format '{{.Repository}}:{{.Tag}}' | grep -v '<none>'); do
  printf '%s -> %s\n' "$i" "$(docker run --rm --entrypoint sh "$i" -c 'cat /etc/os-release 2>/dev/null | grep PRETTY_NAME' 2>/dev/null)"
done

# Check each against the current lifecycle data rather than the frozen list:
curl -s https://endoflife.date/api/ubuntu.json  | head -40
curl -s https://endoflife.date/api/nodejs.json  | head -40

Debugging

Three different cases share this status. 'Docker not installed' is correct on a host with no Docker. 'Cannot query Docker daemon' means the audit account could not reach the daemon: check systemctl is-active docker, then docker ps as that user and sudo docker ps, and run the audit as root or with sudo if only the second works. 'No local images found' means the daemon held no tagged images, which is normal on a host that pulls at deploy time; check with docker images -a. It is also what you get when the audit has root but the daemon is down, because an empty answer and an empty image list look the same to the script. The Docker Inventory check covers the unreachable case in more detail.

The central issue with this check. The list is frozen and misses every runtime that reached end of life after it was written. Use the os-release loop and endoflife.date above to check what you actually run; do not read a PASS as "supported".

A vendor image stuck on an old base is a supply-chain decision, not a config one. Escalate it with the vendor, isolate the container tightly (non-root, read-only, dropped capabilities, no socket), and record the exception.

Old base images linger in the local store. docker image prune -a removes what nothing references; confirm with docker ps -a --format '{{.Image}}' first.

The check can only read tags. Inspect the base with the os-release loop, or docker history <image> to see the build lineage.

The old image is still in the local store under its old tag. Remove it (docker rmi <old>) and confirm running containers use the new one.

Sources

How the script reads this

Next

Re-run the Image Vulnerabilities audit after applying the fix and confirm this check moves to PASS. CtrlOps runs all 8 checks over your existing SSH connection and scores the result, so the change is visible without reading another config file.

All 8 Image Vulnerabilities fixes
Audit your fleet

Find every one of these on every server, in one click

CtrlOps runs this audit over your existing SSH connection - no agents, no scripts to manage. $7/user/month after a 1 month free trial - no credit card required.

Windows

✓ Start instantly·✓ No credit card·✓ No sneaky autorenewals