Skip to content

Commit 9c784e4

Browse files
authored
fix: pin runtime base image digests (#396)
Pin shipped runtime container base images to reviewed multi-platform manifest digests, enforce complete SHA-256 pins in CI, and document the refresh procedure. Closes #386
1 parent 57440da commit 9c784e4

8 files changed

Lines changed: 82 additions & 7 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@ jobs:
154154
run: npm run build:node
155155
working-directory: worker
156156

157+
- name: Check runtime base image pins
158+
run: node scripts/check-docker-base-images.mjs
159+
157160
- name: Build Node runtime image
158161
run: docker build -f worker/Dockerfile.node worker
159162

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
### Fixed
1111

12+
- Pinned shipped runtime container base images to reviewed multi-platform digests and enforced the pins in CI. Thanks @coygeek.
1213
- Redacted manage-only WebVNC bridge commands and egress session details from `use` share viewers. Thanks @coygeek.
1314
- Created run downloads, captures, proofs, and failure bundles with private POSIX permissions. Thanks @coygeek.
1415
- Rejected broker-supplied GitHub login URLs that do not use the expected HTTPS GitHub authorization endpoint.

docs/operations.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,16 @@ docker run --rm -p 8080:8080 \
169169
crabbox-coordinator:local
170170
```
171171

172+
The checked-in runtime Dockerfiles keep readable base-image tags but pin them to
173+
multi-platform manifest digests. When refreshing a base image, resolve the
174+
current manifest-list digest, update the tag and digest together, build the
175+
affected image, and run the repository check:
176+
177+
```sh
178+
docker buildx imagetools inspect <image>:<tag> --format '{{.Manifest.Digest}}'
179+
node scripts/check-docker-base-images.mjs
180+
```
181+
172182
The service creates PostgreSQL schemas `crabbox` and `crabbox_jobs`. Use
173183
`GET /v1/health` for liveness and `GET /v1/ready` for database readiness.
174184
`SIGTERM` and `SIGINT` stop new requests, drain active HTTP/WebSocket and
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { readFile } from "node:fs/promises";
2+
import { pathToFileURL } from "node:url";
3+
4+
const runtimeDockerfiles = [
5+
"worker/cloudflare-container.Dockerfile",
6+
"worker/azure-dynamic-sessions.Dockerfile",
7+
"worker/Dockerfile.node",
8+
];
9+
10+
export function unpinnedBaseImages(source, file = "Dockerfile") {
11+
const findings = [];
12+
for (const [index, line] of source.split(/\r?\n/).entries()) {
13+
const match = /^\s*FROM\s+(?:--platform=\S+\s+)?(\S+)/i.exec(line);
14+
if (match && !/@sha256:[0-9a-f]{64}$/i.test(match[1])) {
15+
findings.push(`${file}:${index + 1}: base image lacks a valid SHA-256 digest: ${match[1]}`);
16+
}
17+
}
18+
return findings;
19+
}
20+
21+
async function main(files = runtimeDockerfiles) {
22+
const findings = [];
23+
for (const file of files) {
24+
findings.push(...unpinnedBaseImages(await readFile(file, "utf8"), file));
25+
}
26+
if (findings.length > 0) {
27+
console.error(findings.join("\n"));
28+
process.exitCode = 1;
29+
}
30+
}
31+
32+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
33+
await main(process.argv.slice(2).length > 0 ? process.argv.slice(2) : runtimeDockerfiles);
34+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import assert from "node:assert/strict";
2+
import test from "node:test";
3+
4+
import { unpinnedBaseImages } from "./check-docker-base-images.mjs";
5+
6+
test("accepts digest-pinned base images with readable tags", () => {
7+
const digest = "a".repeat(64);
8+
assert.deepEqual(
9+
unpinnedBaseImages(
10+
`FROM --platform=$BUILDPLATFORM node:24-bookworm@sha256:${digest} AS build\nFROM node:24-bookworm@sha256:${digest}\n`,
11+
),
12+
[],
13+
);
14+
});
15+
16+
test("reports unpinned and malformed base-image digests", () => {
17+
assert.deepEqual(
18+
unpinnedBaseImages(
19+
"FROM node:24-bookworm AS build\nFROM node:24-bookworm@sha256:abc\n",
20+
"Dockerfile",
21+
),
22+
[
23+
"Dockerfile:1: base image lacks a valid SHA-256 digest: node:24-bookworm",
24+
"Dockerfile:2: base image lacks a valid SHA-256 digest: node:24-bookworm@sha256:abc",
25+
],
26+
);
27+
});

worker/Dockerfile.node

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# syntax=docker/dockerfile:1.7
22

3-
FROM node:22-bookworm-slim AS build
3+
FROM node:22-bookworm-slim@sha256:e21fc383b50d5347dc7a9f1cae45b8f4e2f0d39f7ade28e4eef7d2934522b752 AS build
44

55
WORKDIR /app
66
COPY package.json package-lock.json ./
@@ -12,7 +12,7 @@ COPY src ./src
1212
COPY tsconfig.json tsconfig.node.json ./
1313
RUN npm run build:node && npm prune --omit=dev
1414

15-
FROM node:22-bookworm-slim
15+
FROM node:22-bookworm-slim@sha256:e21fc383b50d5347dc7a9f1cae45b8f4e2f0d39f7ade28e4eef7d2934522b752
1616

1717
ENV NODE_ENV=production
1818
WORKDIR /app

worker/azure-dynamic-sessions.Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/devcontainers/go:1.26-bookworm AS runner-build
1+
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/devcontainers/go:1.26-bookworm@sha256:de99286e746c99e359edddaf0c47cbfbed34f4d6a3cd6310c99c87fa27c3c341 AS runner-build
22

33
ARG TARGETOS=linux
44
ARG TARGETARCH
@@ -7,7 +7,7 @@ COPY cloudflare-container-runner/go.mod cloudflare-container-runner/main.go ./
77
RUN target_arch="${TARGETARCH:-$(go env GOARCH)}" \
88
&& CGO_ENABLED=0 GOOS=$TARGETOS GOARCH="$target_arch" go build -trimpath -ldflags="-s -w" -o /out/crabbox-container-runner .
99

10-
FROM mcr.microsoft.com/dotnet/runtime-deps:9.0-bookworm-slim
10+
FROM mcr.microsoft.com/dotnet/runtime-deps:9.0-bookworm-slim@sha256:608b519f61bce1ad7496a2544041d6c1538a5c48d056adf55af7fdc35f924283
1111

1212
RUN apt-get update \
1313
&& apt-get install -y --no-install-recommends bash ca-certificates curl git jq ripgrep tar \

worker/cloudflare-container.Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-bookworm AS runner-build
1+
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-bookworm@sha256:5f68ec6805843bd3981a951ffada82a26a0bd2631045c8f7dba483fa868f5ec5 AS runner-build
22

33
ARG TARGETOS=linux
44
ARG TARGETARCH=amd64
55
WORKDIR /src
66
COPY cloudflare-container-runner/go.mod cloudflare-container-runner/main.go ./
77
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -ldflags="-s -w" -o /out/crabbox-cloudflare-container-runner .
88

9-
FROM docker.io/library/golang:1.26-bookworm AS go-runtime
9+
FROM docker.io/library/golang:1.26-bookworm@sha256:5f68ec6805843bd3981a951ffada82a26a0bd2631045c8f7dba483fa868f5ec5 AS go-runtime
1010

11-
FROM docker.io/library/node:24-bookworm
11+
FROM docker.io/library/node:24-bookworm@sha256:40ad9f3064e67d6860b4bc3fe1880b2953934fd6320ada990e45fe0efa6badd7
1212

1313
ARG TARGETARCH=amd64
1414
ARG GH_VERSION=2.92.0

0 commit comments

Comments
 (0)