From 8c1621f28a94763a4e406e2b475a1167cec89ff5 Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:43:54 -0400 Subject: [PATCH] Security: delete setuptools from the image setuptools arrives as an apk dependency of supervisor and carries CVE-2026-59890 in the version Alpine 3.24 packages. apk refuses to remove it, but the dependency is packaging metadata, not a runtime need: supervisor has not required setuptools on Python 3.8 or newer, this image is 3.14, and the Alpine package ships no pkg_resources for anything to import. Delete it in the layer that installs supervisor, since a later layer would leave the files in this one, and prove both halves in the build: python3 can no longer import setuptools, and supervisord still reports its version. --- CHANGELOG.md | 5 +++++ Dockerfile | 32 +++++++++++++++++------------ api/test/image-hardening.test.js | 35 ++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d0177ba..e0445cd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The admin save toast now carries its success or failure as a coloured fill that meets WCAG AA, instead of a thin accent bar on one edge. +### Security + +- The image no longer ships Python setuptools, which arrived as a packaging + dependency of supervisor and nothing in the container used. + ## [1.5.1] - 2026-08-08 ### Fixed diff --git a/Dockerfile b/Dockerfile index de30d45b..5b5e8b2d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,19 +28,25 @@ RUN apk add --no-cache nginx supervisor && \ # Owned by the node user (UID 1000, provided by the base image) so the # API process can write config and uploaded icons without running as root. mkdir -p /data /icons && \ - chown -R node:node /data /icons - -# py3-setuptools stays, and cannot be removed: apk refuses, because supervisor -# depends on it. Docker Scout reports CVE-2026-59890 against it (medium, 6.1, -# fixed in setuptools 83). Nothing in this image runs pip or setuptools, so it is -# a library on disk rather than a code path; the release scan gates on HIGH and -# CRITICAL and does not fail on it. -# -# Checked 2026-08-09: node:24-alpine is Alpine 3.24.1, which carries -# py3-setuptools 82.0.1-r1. Alpine edge has 83.0.0-r0, so this clears itself when -# the base image floats onto the next stable Alpine, which is the reason the FROM -# above is a tag rather than a digest. `apk del py3-setuptools` is the check, and -# it printing "not removed due to: supervisor" is why this note exists. + chown -R node:node /data /icons && \ + # setuptools comes in as an apk dependency of supervisor, and CVE-2026-59890 + # is reported against the version Alpine carries. `apk del` refuses it, but + # the dependency is packaging metadata rather than a runtime need: supervisor + # has not required setuptools on Python 3.8 or newer, this image is 3.14, and + # the Alpine package ships no pkg_resources for anything to import. Nothing + # here runs pip. So the library is deleted outright rather than carried and + # explained. Same layer as the install, or the files survive in this one. + rm -rf /usr/lib/python3*/site-packages/setuptools \ + /usr/lib/python3*/site-packages/setuptools-*.dist-info \ + /usr/lib/python3*/site-packages/_distutils_hack \ + /usr/lib/python3*/site-packages/distutils-precedence.pth && \ + # Both halves proved here: nothing can import it, and supervisord still runs + # without it. A path that stops matching after a base image bump would + # otherwise delete nothing and say nothing. + if python3 -c 'import setuptools' 2>/dev/null; then \ + echo 'setuptools survived removal'; exit 1; \ + fi && \ + supervisord --version # Copy Nginx config — Alpine nginx reads from http.d/ COPY nginx/dashboard.conf /etc/nginx/http.d/dashboard.conf diff --git a/api/test/image-hardening.test.js b/api/test/image-hardening.test.js index 6b9b2c66..1cec2236 100644 --- a/api/test/image-hardening.test.js +++ b/api/test/image-hardening.test.js @@ -68,3 +68,38 @@ test('the API still declares no runtime dependencies', () => { assert.deepEqual(pkg.dependencies ?? {}, {}, 'the API has a runtime dependency, which the image can no longer install'); }); + +/* setuptools arrives as an apk dependency of supervisor and is reported against + CVE-2026-59890. It is deleted rather than accepted: supervisor has not needed + it at runtime since Python 3.8, this image is 3.14, and the Alpine package + ships no pkg_resources for anything to import. + + The deletion has to share the RUN that installs supervisor. In a later layer + the files would still exist in this one, which is where a scanner reads them + from and where the image would still carry them. */ +test('the Dockerfile deletes setuptools', () => { + for (const target of [ + '/usr/lib/python3*/site-packages/setuptools', + '/usr/lib/python3*/site-packages/setuptools-*.dist-info', + '/usr/lib/python3*/site-packages/_distutils_hack', + '/usr/lib/python3*/site-packages/distutils-precedence.pth', + ]) { + assert.ok(dockerfile.includes(target), `${target} is no longer removed`); + } +}); + +test('setuptools is removed in the layer that installs supervisor', () => { + const run = /RUN apk add --no-cache nginx supervisor[\s\S]*?(?=\n(?:#|[A-Z]+ ))/.exec(dockerfile); + assert.ok(run, 'the apk install step has been restructured'); + assert.match(run[0], /rm -rf \/usr\/lib\/python3\*\/site-packages\/setuptools/, + 'the deletion moved out of the install layer, so the files survive in it'); +}); + +/* Deleting the files is only half of it. The build asserts nothing can import + setuptools afterwards, and that supervisord, whose apk dependency brought it + in, still starts without it. */ +test('the build proves the removal and that supervisord survives it', () => { + assert.match(dockerfile, /if python3 -c 'import setuptools' 2>\/dev\/null; then/); + assert.match(dockerfile, /setuptools survived removal/); + assert.match(dockerfile, /supervisord --version/); +});