mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 12:21:31 +00:00
Bundle A: Container & supply-chain hardening — 3 findings closed; All High closed
Closes H-001 + M-012 + M-014 from comprehensive-audit-2026-04-25.
H-001 (CWE-829) — Container base images SHA-pinned
Pre-bundle: 5 FROM lines pulled by tag only — registry-side tag
swap could silently change the build.
Post-bundle: every FROM pinned to immutable digest fetched live
from Docker Hub at audit time:
node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293
golang:1.25-alpine@sha256:5caaf1cca9dc351e13deafbc3879fd4754801acba8653fa9540cea125d01a71f (x2)
alpine:3.19@sha256:6baf43584bcb78f2e5847d1de515f23499913ac9f12bdf834811a3145eb11ca1 (x2)
Dockerfile header comment documents the operator bump procedure
(quarterly cadence; docker manifest inspect or Hub Registry API).
CI step Forbidden bare FROM regression guard (H-001) fails build
if any new FROM lacks @sha256.
M-012 (CWE-250) — Verified-already-clean + USER guard
Recon found both Dockerfile:75 and Dockerfile.agent:59 already
carry USER certctl directives; pre-USER RUN calls are build-setup
steps that legitimately need root, each happening before the
USER drop.
CI step Forbidden missing USER regression guard (M-012) greps
every Dockerfile* for the LAST USER directive; fails build if
missing OR equals root/0. Future Dockerfile additions must
preserve the privilege drop.
M-014 — npm ci explicit retry helper
Pre-bundle Dockerfile:25:
RUN npm ci --include=dev || npm ci --include=dev && \
tsc --version && npm run build
Broken bash precedence: A || (B && C && D) means tsc+build only
ran on success path of the second npm ci. A transient registry
blip silently skipped the production step — build would succeed
with no node_modules + no tsc verification.
Post-bundle: deterministic 3-attempt retry loop with 5s backoff
plus explicit [ -d node_modules ] post-check that fails loudly
if directory wasn't created. Silent failure is now impossible.
Audit deliverables:
audit-report.md: H-001/M-012/M-014 flipped [x] with closure
notes; score 49/55 closed (High 9/9 = 100%; Medium 24/27;
Low 19/19 with L-004 deferred). All High audit findings now
closed for the first time.
findings.yaml: 3 status flips
CHANGELOG.md: Bundle A section
Verification:
Self-test of both new CI guards locally — PASS for current state
(every FROM has @sha256; every Dockerfile drops to non-root).
This commit is contained in:
+40
-4
@@ -1,7 +1,28 @@
|
||||
# Multi-stage build for certctl server
|
||||
#
|
||||
# Bundle A / Audit H-001 (CWE-829): every FROM line is pinned to an
|
||||
# immutable digest in addition to the human-readable tag. The tag is
|
||||
# advisory; the digest is what Docker actually pulls. A registry-side
|
||||
# tag swap (the documented prior-art for tag-only pulls being unsafe)
|
||||
# can no longer change the build.
|
||||
#
|
||||
# Bump procedure (operator):
|
||||
# 1. Quarterly cadence (or sooner if a CVE lands on a base image).
|
||||
# 2. For each FROM:
|
||||
# docker pull <image>:<tag>
|
||||
# docker manifest inspect <image>:<tag> | grep -m1 digest
|
||||
# OR via Docker Hub Registry API:
|
||||
# curl -sSL https://hub.docker.com/v2/repositories/library/<image>/tags/<tag> \
|
||||
# | jq -r .digest
|
||||
# 3. Replace the @sha256:... portion of the FROM line.
|
||||
# 4. Run `docker build` locally + verify CI.
|
||||
# 5. Commit with the bump procedure cited in the message body.
|
||||
#
|
||||
# The CI step "Forbidden bare FROM regression guard (H-001)" rejects
|
||||
# any future commit that lands a FROM without an @sha256 pin.
|
||||
|
||||
# Stage 1: Build frontend
|
||||
FROM node:20-alpine AS frontend
|
||||
FROM node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS frontend
|
||||
|
||||
# Proxy propagation (M-4, Issue #9) — defaulted to empty so un-proxied builds
|
||||
# behave identically to the pre-fix tree. When `HTTP_PROXY`/`HTTPS_PROXY`/
|
||||
@@ -22,12 +43,27 @@ ENV HTTP_PROXY=${HTTP_PROXY} \
|
||||
WORKDIR /app/web
|
||||
|
||||
COPY web/ .
|
||||
RUN npm ci --include=dev || npm ci --include=dev && \
|
||||
# Bundle A / Audit M-014: explicit retry loop for `npm ci`. Pre-bundle
|
||||
# this was `npm ci || npm ci && tsc && build` — the bash precedence is
|
||||
# `A || (B && C && D)` so the second `npm ci` only ran on the failure
|
||||
# path of the first, but the `tsc && build` chain only ran on the
|
||||
# success path of the second. Net effect: a transient registry blip
|
||||
# turned the build into a silent skip of the production step.
|
||||
#
|
||||
# New shape: a deterministic 3-attempt retry with 5-second backoff and
|
||||
# an explicit `[ -d node_modules ]` post-check so a silent failure is
|
||||
# impossible.
|
||||
RUN for i in 1 2 3; do \
|
||||
npm ci --include=dev && break; \
|
||||
echo "npm ci attempt $i failed; sleeping 5s before retry"; \
|
||||
sleep 5; \
|
||||
done && \
|
||||
[ -d node_modules ] || (echo "ERROR: npm ci failed after 3 attempts; node_modules missing" && exit 1) && \
|
||||
node_modules/.bin/tsc --version && \
|
||||
npm run build
|
||||
|
||||
# Stage 2: Build Go binary
|
||||
FROM golang:1.25-alpine AS builder
|
||||
FROM golang:1.25-alpine@sha256:5caaf1cca9dc351e13deafbc3879fd4754801acba8653fa9540cea125d01a71f AS builder
|
||||
|
||||
# Proxy propagation (M-4, Issue #9) — see Stage 1 rationale.
|
||||
ARG HTTP_PROXY=
|
||||
@@ -57,7 +93,7 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build \
|
||||
./cmd/server
|
||||
|
||||
# Stage 3: Runtime
|
||||
FROM alpine:3.19
|
||||
FROM alpine:3.19@sha256:6baf43584bcb78f2e5847d1de515f23499913ac9f12bdf834811a3145eb11ca1
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata curl
|
||||
|
||||
|
||||
Reference in New Issue
Block a user