From e2264ee2957aa26bcd1568067713183dde21ed1e Mon Sep 17 00:00:00 2001 From: mptyl Date: Tue, 4 Aug 2026 16:33:39 +0200 Subject: [PATCH] build: harden image build inputs --- .dockerignore | 1 + docker/core.Dockerfile | 13 ++++++----- docker/frontend.Dockerfile | 5 +++-- docker/smoke/frontend-smoke.sh | 10 +-------- scripts/test-container-deployment.sh | 30 ++++++++++++++++++++++++++ scripts/test-vector-migration-image.sh | 8 ++++++- scripts/verify-container-images.sh | 13 ++--------- 7 files changed, 52 insertions(+), 28 deletions(-) diff --git a/.dockerignore b/.dockerignore index 7f116149..73fefc6e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,6 +12,7 @@ **/.env.* !.env.example !deploy/env/*.env.example +deploy/thothii.env deploy/secrets/ harness/workspaces/psd.yaml **/*.log diff --git a/docker/core.Dockerfile b/docker/core.Dockerfile index cd59287e..f22510dc 100644 --- a/docker/core.Dockerfile +++ b/docker/core.Dockerfile @@ -4,8 +4,11 @@ ARG PI_VERSION=0.80.3 ARG IMAGE_VERSION=local +# ---- Pinned Node source for the runtime binary and npm ---- +FROM node:22-bookworm@sha256:7725a5c2c83eed1d36258c66efae14b1ceccd021db9ed1d9559d3335ed3d68ed AS node-runtime + # ---- Stage 0: locked Pi runtime ---- -FROM node:22-bookworm AS pi-runtime-build +FROM node:22-bookworm@sha256:7725a5c2c83eed1d36258c66efae14b1ceccd021db9ed1d9559d3335ed3d68ed AS pi-runtime-build ARG PI_VERSION WORKDIR /opt/pi-runtime COPY docker/pi-runtime/package.json docker/pi-runtime/package-lock.json ./ @@ -13,7 +16,7 @@ RUN npm ci --omit=dev \ && test "$(./node_modules/.bin/pi --version)" = "$PI_VERSION" # ---- Stage 1: backend TypeScript -> dist ---- -FROM node:22-bookworm AS backend-build +FROM node:22-bookworm@sha256:7725a5c2c83eed1d36258c66efae14b1ceccd021db9ed1d9559d3335ed3d68ed AS backend-build WORKDIR /src/backend COPY backend/package*.json ./ RUN npm ci @@ -21,7 +24,7 @@ COPY backend/ ./ RUN npm run build # ---- Stage 2: runtime (Python 3.12 nativo + Node 22 copiato, stesso glibc bookworm) ---- -FROM python:3.12-slim-bookworm AS runtime +FROM python:3.12-slim-bookworm@sha256:d50fb7611f86d04a3b0471b46d7557818d88983fc3136726336b2a4c657aa30b AS runtime ARG PI_VERSION ARG IMAGE_VERSION LABEL org.opencontainers.image.title="thothii-core" \ @@ -36,8 +39,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && ln -s /usr/bin/fdfind /usr/local/bin/fd # Node 22 + npm copiati dall'immagine ufficiale (stesso Debian bookworm → binario compatibile) -COPY --from=node:22-bookworm /usr/local/bin/node /usr/local/bin/node -COPY --from=node:22-bookworm /usr/local/lib/node_modules /usr/local/lib/node_modules +COPY --from=node-runtime /usr/local/bin/node /usr/local/bin/node +COPY --from=node-runtime /usr/local/lib/node_modules /usr/local/lib/node_modules RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \ && ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx diff --git a/docker/frontend.Dockerfile b/docker/frontend.Dockerfile index 483194b4..4f3fa076 100644 --- a/docker/frontend.Dockerfile +++ b/docker/frontend.Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1.7 # thothii-frontend: build Vite (React) + nginx-unprivileged (porta 8080). ARG IMAGE_VERSION=local -FROM node:22-bookworm AS build +FROM node:22-bookworm@sha256:7725a5c2c83eed1d36258c66efae14b1ceccd021db9ed1d9559d3335ed3d68ed AS build WORKDIR /src COPY frontend/package*.json ./ RUN npm ci @@ -9,7 +9,7 @@ COPY frontend/ ./ ENV VITE_BASE=/ VITE_BACKEND_URL=/api RUN npm run build -FROM nginxinc/nginx-unprivileged:1.27-alpine AS runtime +FROM nginxinc/nginx-unprivileged:1.27-alpine@sha256:65e3e85dbaed8ba248841d9d58a899b6197106c23cb0ff1a132b7bfe0547e4c0 AS runtime ARG IMAGE_VERSION LABEL org.opencontainers.image.title="thothii-frontend" \ org.opencontainers.image.version="${IMAGE_VERSION}" \ @@ -18,6 +18,7 @@ COPY --from=build /src/dist /usr/share/nginx/html COPY docker/nginx.conf.template /etc/nginx/templates/default.conf.template COPY --chmod=755 docker/frontend-entrypoint.sh /usr/local/bin/frontend-entrypoint COPY --chmod=755 docker/validate-frontend-api-upstream.sh /usr/local/bin/validate-frontend-api-upstream +COPY --chmod=755 docker/smoke/frontend-smoke.sh /usr/local/bin/frontend-config-smoke ENTRYPOINT ["/usr/local/bin/frontend-entrypoint"] CMD ["nginx", "-g", "daemon off;"] EXPOSE 8080 diff --git a/docker/smoke/frontend-smoke.sh b/docker/smoke/frontend-smoke.sh index 0a159f9b..8b84d1db 100644 --- a/docker/smoke/frontend-smoke.sh +++ b/docker/smoke/frontend-smoke.sh @@ -1,14 +1,6 @@ #!/bin/sh set -eu -assignment=$(sed \ - -e 's/^window\.__THOTHII_CONFIG__ = //' \ - -e 's/;$//' \ - /usr/share/nginx/html/config.js) - -printf '%s\n' "$assignment" \ - | jq -e --arg expected "${BACKEND_BASE_URL-/api}" \ - 'type == "object" and keys == ["backendBaseUrl"] and .backendBaseUrl == $expected' \ - >/dev/null +test "$(cat /usr/share/nginx/html/config.js)" = 'window.__THOTHII_CONFIG__ = {};' printf '%s\n' "frontend runtime config smoke: ok" diff --git a/scripts/test-container-deployment.sh b/scripts/test-container-deployment.sh index 7dd0483a..992f7e66 100755 --- a/scripts/test-container-deployment.sh +++ b/scripts/test-container-deployment.sh @@ -18,6 +18,36 @@ export THT_WORKSPACE_GIT_REMOTE="https://git.example.invalid/thothii/workspaces. export THOTH_CORE_HTTP_PORT=0 export THOTH_HTTP_PORT=0 +context_check="$tmp/build-context" +mkdir -p "$context_check/deploy" +cp .dockerignore "$context_check/.dockerignore" +printf '%s\n' 'task-5-context-sentinel' >"$context_check/deploy/thothii.env" +cat >"$context_check/Dockerfile" <<'EOF' +FROM scratch +COPY deploy/thothii.env /sentinel +EOF +if docker build --quiet -f "$context_check/Dockerfile" "$context_check" >"$tmp/context-check.out" 2>&1; then + echo "deploy/thothii.env entered the Docker build context" >&2 + exit 1 +fi + +if ! awk ' + $1 == "FROM" && $2 !~ /^[^@]+@sha256:[0-9a-f]{64}$/ { + print FILENAME ":" FNR ": unpinned base image: " $0 > "/dev/stderr" + bad = 1 + } + END { exit bad } +' docker/core.Dockerfile docker/frontend.Dockerfile; then + echo "every production FROM reference must use tag@sha256" >&2 + exit 1 +fi + +while IFS= read -r base_image; do + manifest=$(docker buildx imagetools inspect "$base_image") + printf '%s\n' "$manifest" | grep -Eq 'Platform:[[:space:]]+linux/amd64' + printf '%s\n' "$manifest" | grep -Eq 'Platform:[[:space:]]+linux/arm64' +done < <(awk '$1 == "FROM" { print $2 }' docker/core.Dockerfile docker/frontend.Dockerfile | sort -u) + docker compose --project-name "$project" -f compose.yaml -f deploy/compose.local.yaml build --pull core_label=$(docker image inspect --format '{{ index .Config.Labels "io.thothii.pi.version" }}' thothii-core:local) diff --git a/scripts/test-vector-migration-image.sh b/scripts/test-vector-migration-image.sh index cfeffb97..ed8da3e7 100755 --- a/scripts/test-vector-migration-image.sh +++ b/scripts/test-vector-migration-image.sh @@ -3,6 +3,7 @@ set -eu image=${1:?usage: test-vector-migration-image.sh IMAGE [PLATFORM]} platform=${2:-${PLATFORM:-linux/arm64}} +repo_root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) slug=$$ network="thoth-vector-migration-$slug" database="thoth-vector-db-$slug" @@ -36,7 +37,12 @@ status=$(docker run --rm --platform "$platform" --network "$network" \ --entrypoint /opt/venv/bin/tht -e THT_VECTOR_ADMIN_URL="$database_url" \ "$image" vector migrate --status --json) -expected='{"applied": ["001", "002", "003"], "drifted": [], "pending": []}' +expected_versions=$(find "$repo_root/harness/tht/migrations/vector" -type f -name '[0-9][0-9][0-9]_*.sql' \ + | sed 's|.*/||; s|_.*||' \ + | LC_ALL=C sort \ + | awk 'BEGIN { separator = ""; printf "[" } { printf "%s\"%s\"", separator, $0; separator = ", " } END { print "]" }') +test "$expected_versions" != '[]' +expected="{\"applied\": $expected_versions, \"drifted\": [], \"pending\": []}" test "$applied" = "$expected" test "$status" = "$expected" echo "core image vector migration discovery/status smoke passed" diff --git a/scripts/verify-container-images.sh b/scripts/verify-container-images.sh index 3dc7e1be..a277a018 100755 --- a/scripts/verify-container-images.sh +++ b/scripts/verify-container-images.sh @@ -25,17 +25,8 @@ test "$(docker image inspect --format '{{ index .Config.Labels "org.opencontaine docker run --rm --platform "$platform" --entrypoint /app/docker/smoke/core-smoke.sh \ "$core_image" ./scripts/test-vector-migration-image.sh "$core_image" "$platform" -docker run --rm --platform "$platform" -e BACKEND_BASE_URL=/api \ - "$frontend_image" frontend-config-smoke -docker run --rm --platform "$platform" -e BACKEND_BASE_URL= \ - "$frontend_image" frontend-config-smoke -docker run --rm --platform "$platform" --entrypoint frontend-policy-smoke "$frontend_image" - -if docker run --rm --platform "$platform" -e BACKEND_BASE_URL=/backend \ - "$frontend_image" frontend-config-smoke >/dev/null 2>&1; then - echo "frontend accepted an unsupported BACKEND_BASE_URL" >&2 - exit 1 -fi +docker run --rm --platform "$platform" "$frontend_image" frontend-config-smoke +./docker/smoke/frontend-policy-smoke.sh if docker run --rm --platform "$platform" -e THOTH_PUBLIC_EXPOSURE=true -e AUTH_MODE=none \ "$core_image" server >/dev/null 2>&1; then echo "core accepted public exposure without upstream authentication" >&2