From 360eaa75bc0966e42399a7c4b2a3977e9e606a1e Mon Sep 17 00:00:00 2001 From: shankar0123 Date: Sat, 16 May 2026 04:31:14 +0000 Subject: [PATCH] =?UTF-8?q?fix(compose):=20DEPL-002=20=E2=80=94=20pin=20al?= =?UTF-8?q?pine/openssl=20+=20postgres:16-alpine=20by=20digest=20+=20H-002?= =?UTF-8?q?=20CI=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sprint 3 unified-master-audit closure. The production-shaped compose (deploy/docker-compose.yml) — explicitly self-described as 'PRODUCTION-SHAPED (Bundle 2)' in its header — pulled two images by floating tag: image: alpine/openssl:latest image: postgres:16-alpine The certctl Dockerfiles have been digest-pinned for two bundles (see Bundle A / H-001 + the digest-validity.sh CI guard). Compose shipped on the lower bar — a registry-side tag swap could change what an operator deploys without their seeing the diff in their infra repo. Fix: - Pin both images by @sha256: (alpine/openssl looked up via Docker Hub tag API on 2026-05-16; postgres:16-alpine the same). - New scripts/ci-guards/H-002-bare-compose-image.sh — analogous to H-001 — fails the build if any 'image:' line in deploy/docker-compose.yml lacks a @sha256 digest. Test compose files (deploy/docker-compose.test.yml + the loadtest stack) and examples/ stay scoped out by design: those are throwaway development-loop tooling where floating tags are intentional. - The existing digest-validity.sh CI guard auto-discovers digests via grep across deploy/ so the new pins get verified on the same run that pulls them, without a separate change. Closes DEPL-002. --- deploy/docker-compose.yml | 15 +++++- scripts/ci-guards/H-002-bare-compose-image.sh | 50 +++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) create mode 100755 scripts/ci-guards/H-002-bare-compose-image.sh diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 4ff4193..584251e 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -62,7 +62,13 @@ services: # handshake. ECDSA-P256 with SHA-256 is universally supported. See # docs/tls.md Pattern 1. certctl-tls-init: - image: alpine/openssl:latest + # DEPL-002 closure (Sprint 3, 2026-05-16): digest-pin so the + # production-shaped compose has the same supply-chain posture as + # the certctl Dockerfiles (which CI guards via digest-validity.sh). + # The :latest tag floats; the digest is captured at the time + # this comment was written. Bump after running the digest- + # validity guard to confirm the new digest is still pullable. + image: alpine/openssl:latest@sha256:41036db23542ed4cc09bc278d8a7e23b3da01690abb4b0e353b1bb87d70520ed container_name: certctl-tls-init restart: "no" entrypoint: /bin/sh @@ -123,7 +129,12 @@ services: # `unhealthy` flap to cascade into certctl-server's `service_healthy` # depends_on, blocking the whole stack. postgres: - image: postgres:16-alpine + # DEPL-002 closure (Sprint 3, 2026-05-16): digest-pin matching the + # alpine/openssl pin above. The `16-alpine` tag is the stable + # major-version stream; the digest snapshots today's image so a + # silent upstream rebuild can't slip into a production deploy + # mid-rollout. Bump alongside dependency reviews. + image: postgres:16-alpine@sha256:890480b08124ce7f79960a9bb16fe39729aa302bd384bfd7c408fee6c8f7adb7 container_name: certctl-postgres environment: POSTGRES_DB: certctl diff --git a/scripts/ci-guards/H-002-bare-compose-image.sh b/scripts/ci-guards/H-002-bare-compose-image.sh new file mode 100755 index 0000000..9c174e1 --- /dev/null +++ b/scripts/ci-guards/H-002-bare-compose-image.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# scripts/ci-guards/H-002-bare-compose-image.sh +# +# DEPL-002 closure (Sprint 3, 2026-05-16). Companion to H-001-bare-from.sh +# (which enforces digest pins on every Dockerfile FROM): every `image:` +# line in the production-shaped compose file MUST carry an @sha256 +# digest. Pre-fix `deploy/docker-compose.yml` had two floating tags +# (alpine/openssl:latest and postgres:16-alpine), so a registry-side +# tag swap could change what an operator deploys without their seeing +# the diff. +# +# Scope is intentionally narrow: only the production-shaped compose +# under deploy/. Test compose files (deploy/docker-compose.test.yml, +# deploy/test/loadtest/docker-compose.yml) and the examples/ directory +# stay free of digest pins because they're development-loop tooling +# whose floating tags are intentional (developer pulls latest, runs +# the loop, throws it away). If a finding ever escalates one of those +# files to "ships in production," extend SCAN_FILES below. + +set -e + +SCAN_FILES=( + "deploy/docker-compose.yml" +) + +failed=0 +for f in "${SCAN_FILES[@]}"; do + if [ ! -f "$f" ]; then + echo "::error::H-002 misconfig: $f not found" + failed=1 + continue + fi + # Match `image: something:tag` (with optional indent / quotes) that + # does NOT contain @sha256. Strip commented lines and YAML anchors. + BAD=$(grep -nE '^\s*image:\s+[^#@]+$' "$f" | grep -v '@sha256' || true) + if [ -n "$BAD" ]; then + echo "::error file=${f}::H-002 regression: compose has bare image (no @sha256 digest pin):" + echo "$BAD" + failed=1 + fi +done + +if [ "$failed" -ne 0 ]; then + echo "" + echo "Pin every production-compose image to an immutable digest." + echo "Look up current digests via:" + echo " curl -sS https://hub.docker.com/v2/repositories///tags/ | jq -r .digest" + exit 1 +fi +echo "H-002 bare-compose-image: clean."