Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 19 additions & 13 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions api/test/image-hardening.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
});
Loading