mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 22:41:31 +00:00
12003f5ca5
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).
82 lines
3.0 KiB
Docker
82 lines
3.0 KiB
Docker
# Multi-stage build for certctl agent
|
|
#
|
|
# Bundle A / Audit H-001 (CWE-829): every FROM line is pinned to an
|
|
# immutable digest. See Dockerfile (server) for the bump-procedure
|
|
# operator runbook; the pins here MUST be bumped in the same pass.
|
|
|
|
# Stage 1: Build
|
|
FROM golang:1.25-alpine@sha256:5caaf1cca9dc351e13deafbc3879fd4754801acba8653fa9540cea125d01a71f AS builder
|
|
|
|
# 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`/
|
|
# `NO_PROXY` are forwarded via `docker build --build-arg` (or compose
|
|
# `build.args`), they are re-exported as ENV with both upper- and lower-case
|
|
# names because apk and curl read the lowercase variants while Go reads the
|
|
# uppercase ones.
|
|
ARG HTTP_PROXY=
|
|
ARG HTTPS_PROXY=
|
|
ARG NO_PROXY=
|
|
ENV HTTP_PROXY=${HTTP_PROXY} \
|
|
HTTPS_PROXY=${HTTPS_PROXY} \
|
|
NO_PROXY=${NO_PROXY} \
|
|
http_proxy=${HTTP_PROXY} \
|
|
https_proxy=${HTTPS_PROXY} \
|
|
no_proxy=${NO_PROXY}
|
|
|
|
RUN apk add --no-cache git ca-certificates
|
|
|
|
WORKDIR /app
|
|
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
|
|
COPY . .
|
|
|
|
ARG TARGETARCH=amd64
|
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build \
|
|
-ldflags="-w -s" \
|
|
-o bin/agent \
|
|
./cmd/agent
|
|
|
|
# Stage 2: Runtime
|
|
FROM alpine:3.19@sha256:6baf43584bcb78f2e5847d1de515f23499913ac9f12bdf834811a3145eb11ca1
|
|
|
|
# U-2: `procps` ships pgrep, which the HEALTHCHECK below uses to verify the
|
|
# agent process is alive. Pre-U-2 the deploy/docker-compose.yml agent
|
|
# HEALTHCHECK called `pgrep -f certctl-agent` against this image but
|
|
# pgrep wasn't installed — the compose probe was a latent always-fail.
|
|
# Adding procps here fixes both the new image-level HEALTHCHECK and the
|
|
# pre-existing compose override. Adds ~250KB to the image; acceptable for
|
|
# observability parity with the server image.
|
|
RUN apk add --no-cache ca-certificates curl procps
|
|
|
|
RUN addgroup -g 1000 certctl && \
|
|
adduser -D -u 1000 -G certctl certctl
|
|
|
|
WORKDIR /app
|
|
|
|
COPY --from=builder /app/bin/agent .
|
|
|
|
# Create key storage directory for agent-side keygen
|
|
RUN mkdir -p /var/lib/certctl/keys && \
|
|
chown -R certctl:certctl /app /var/lib/certctl
|
|
|
|
USER certctl
|
|
|
|
# Image-level HEALTHCHECK for bare `docker run` / Docker Swarm / Nomad / ECS.
|
|
#
|
|
# U-2 (P1, cat-u-healthcheck_protocol_mismatch — adjacent fix): the agent
|
|
# has no HTTP listener (it polls the server via outbound HTTPS), so a
|
|
# process-presence check is the correct primitive. Pre-U-2 the agent image
|
|
# shipped with no HEALTHCHECK at all, so bare-`docker run` operators got
|
|
# zero health signal and orchestrators that key off Docker's HEALTHCHECK
|
|
# (Swarm, Nomad, ECS) saw the container reported as `none`. The compose
|
|
# override at deploy/docker-compose.yml:173 used the same `pgrep -f
|
|
# certctl-agent` shape; we mirror it here so the published image has
|
|
# parity with the compose stack and the override on docker-compose.yml
|
|
# becomes redundant-but-correct rather than load-bearing.
|
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
CMD pgrep -f certctl-agent > /dev/null || exit 1
|
|
|
|
ENTRYPOINT ["/app/agent"]
|