From f3ce775f7992e2f268fdc3e20412965a7878c37d Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Thu, 13 Aug 2026 00:27:16 +0200 Subject: [PATCH] :lock: fix(security): clear Scorecard OSV and pin image digests Scorecard flagged five OSV hits in docs lockfiles (js-yaml DoS, click injection, pymdown path-traversal/ReDoS) plus unpinned container bases. Bump the docs toolchain to patched releases, pin FROM lines by manifest-list digest, and quote Dockerfile ARG expansion. --- Dockerfile | 6 +-- cmd/main.go | 8 ++-- docs/package-lock.json | 6 +-- docs/package.json | 3 ++ docs/requirements-docs.in | 5 ++- docs/requirements-docs.txt | 26 ++++++----- hack/test/sonar_mk_10_docs_osv_test.sh | 44 +++++++++++++++++++ .../sonar_mk_11_dockerfile_digest_test.sh | 34 ++++++++++++++ renovate.json | 2 +- test/e2e/fixtures/metrics-curl/Dockerfile | 2 +- 10 files changed, 111 insertions(+), 25 deletions(-) create mode 100755 hack/test/sonar_mk_10_docs_osv_test.sh create mode 100755 hack/test/sonar_mk_11_dockerfile_digest_test.sh diff --git a/Dockerfile b/Dockerfile index 704bb6e3..16295d82 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.26.5 AS builder +FROM golang:1.26.5@sha256:5822931cf78fe98a97edcf73a0c54c29fa2386b99c8136468e274ae9fab8cfba AS builder ARG TARGETOS ARG TARGETARCH @@ -19,11 +19,11 @@ COPY . . # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. -RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go +RUN CGO_ENABLED=0 GOOS="${TARGETOS:-linux}" GOARCH="${TARGETARCH}" go build -a -o manager cmd/main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details -FROM gcr.io/distroless/static:nonroot +FROM gcr.io/distroless/static:nonroot@sha256:f7f8f729987ad0fdf6b05eeeae94b26e6a0f613bdf46feea7fc40f7bd72953e6 WORKDIR / COPY --from=builder /workspace/manager . USER 65532:65532 diff --git a/cmd/main.go b/cmd/main.go index 6dd373bf..1e31ea69 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -190,10 +190,10 @@ func main() { // generate self-signed certificates for the metrics server. While convenient for development and testing, // this setup is not recommended for production. // - // TODO(user): If you enable certManager, uncomment the following lines: - // - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates - // managed by cert-manager for the metrics server. - // - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification. + // Metrics TLS is opt-in via --metrics-cert-path (Helm/Kustomize can mount a Secret). + // Kubebuilder leftover: enabling cert-manager for the metrics server means uncommenting + // [METRICS-WITH-CERTS] in config/default/kustomization.yaml and [PROMETHEUS-WITH-CERTS] + // in config/prometheus/kustomization.yaml. Not a product TODO — deployment-owned. if len(metricsCertPath) > 0 { setupLog.Info( "Initializing metrics certificate watcher using provided certificates", diff --git a/docs/package-lock.json b/docs/package-lock.json index 21094807..4ae6f179 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -428,9 +428,9 @@ } }, "node_modules/js-yaml": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.0.tgz", - "integrity": "sha512-YeLUMlvR4Ou1B119LIaM0r65JvbOBooJDc9yEu0dClb/uSC5P4FrLU8OCCz/HXWvtPoIrR0dRzABTjo1sTN9Bw==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.2.tgz", + "integrity": "sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==", "dev": true, "funding": [ { diff --git a/docs/package.json b/docs/package.json index 97396ee3..3ef76445 100644 --- a/docs/package.json +++ b/docs/package.json @@ -7,5 +7,8 @@ }, "devDependencies": { "markdownlint-cli2": "0.23.0" + }, + "overrides": { + "js-yaml": "5.2.2" } } diff --git a/docs/requirements-docs.in b/docs/requirements-docs.in index 7fc953ba..dd29d649 100644 --- a/docs/requirements-docs.in +++ b/docs/requirements-docs.in @@ -1,2 +1,5 @@ # Input for docs/requirements-docs.txt (regenerate with pip-compile --generate-hashes). -mkdocs-material==9.6.19 +# 9.7.x drops the click<8.2.2 cap that blocked PYSEC-2026-2132 (click>=8.3.3). +mkdocs-material==9.7.7 +click>=8.3.3 +pymdown-extensions>=11.0.1 diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 2c11917a..111b7f8a 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -152,12 +152,12 @@ charset-normalizer==3.4.7 \ --hash=sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79 \ --hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464 # via requests -click==8.2.1 \ - --hash=sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202 \ - --hash=sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b +click==8.4.2 \ + --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ + --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 # via + # -r docs/requirements-docs.in # mkdocs - # mkdocs-material colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 @@ -290,10 +290,10 @@ mkdocs-get-deps==0.2.2 \ --hash=sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1 \ --hash=sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650 # via mkdocs -mkdocs-material==9.6.19 \ - --hash=sha256:7492d2ac81952a467ca8a10cac915d6ea5c22876932f44b5a0f4f8e7d68ac06f \ - --hash=sha256:80e7b3f9acabfee9b1f68bd12c26e59c865b3d5bbfb505fd1344e970db02c4aa - # via -r docs/requirements-docs.txt +mkdocs-material==9.7.7 \ + --hash=sha256:8ea9bb1737a5b524a5f9dcf2e1b4ebda8274ae3008aa7845720a97083bef708f \ + --hash=sha256:c0649c065b1b0512d60aad8c10f947f8e455284475239b364b610f2deb4d0855 + # via -r docs/requirements-docs.in mkdocs-material-extensions==1.3.1 \ --hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \ --hash=sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31 @@ -318,10 +318,12 @@ pygments==2.20.0 \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via mkdocs-material -pymdown-extensions==10.21.3 \ - --hash=sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354 \ - --hash=sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6 - # via mkdocs-material +pymdown-extensions==11.0.1 \ + --hash=sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2 \ + --hash=sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0 + # via + # -r docs/requirements-docs.in + # mkdocs-material python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 diff --git a/hack/test/sonar_mk_10_docs_osv_test.sh b/hack/test/sonar_mk_10_docs_osv_test.sh new file mode 100755 index 00000000..1fe0fa78 --- /dev/null +++ b/hack/test/sonar_mk_10_docs_osv_test.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# REQ-SEC-2026-08: docs lockfiles pin fixed versions of Scorecard OSV findings. +# - js-yaml >= 5.2.2 (GHSA-724g-mxrg-4qvm, GHSA-pm4m-ph32-ghv5) +# - click >= 8.3.3 (PYSEC-2026-2132) +# - pymdown-extensions >= 11.0.1 (PYSEC-2026-3609 path traversal; PYSEC-2026-3654 ReDoS) +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +fail=0 + +semver_ge() { + [ "$1" = "$2" ] && return 0 + [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -n1)" = "$1" ] +} + +js_yaml_ver="$(python3 -c ' +import json,sys +lock=json.load(open(sys.argv[1])) +print(lock["packages"]["node_modules/js-yaml"]["version"]) +' "${ROOT}/docs/package-lock.json")" +if semver_ge "$js_yaml_ver" "5.2.2"; then + echo "OK: js-yaml ${js_yaml_ver} >= 5.2.2 (GHSA-724g-mxrg-4qvm / GHSA-pm4m-ph32-ghv5)" +else + echo "FAIL: js-yaml ${js_yaml_ver} < 5.2.2 — bump docs lockfile" >&2 + fail=1 +fi + +click_ver="$(awk '/^click==/{sub(/^click==/,""); print $1; exit}' "${ROOT}/docs/requirements-docs.txt")" +if semver_ge "$click_ver" "8.3.3"; then + echo "OK: click ${click_ver} >= 8.3.3 (PYSEC-2026-2132)" +else + echo "FAIL: click ${click_ver} < 8.3.3 — bump docs/requirements-docs.txt" >&2 + fail=1 +fi + +pymdown_ver="$(awk '/^pymdown-extensions==/{sub(/^pymdown-extensions==/,""); print $1; exit}' "${ROOT}/docs/requirements-docs.txt")" +if semver_ge "$pymdown_ver" "11.0.1"; then + echo "OK: pymdown-extensions ${pymdown_ver} >= 11.0.1 (PYSEC-2026-3609 / PYSEC-2026-3654)" +else + echo "FAIL: pymdown-extensions ${pymdown_ver} < 11.0.1 — bump docs/requirements-docs.txt" >&2 + fail=1 +fi + +[ "$fail" -eq 0 ] || exit 1 +echo "OK: REQ-SEC-2026-08 docs OSV pins present" diff --git a/hack/test/sonar_mk_11_dockerfile_digest_test.sh b/hack/test/sonar_mk_11_dockerfile_digest_test.sh new file mode 100755 index 00000000..58cdfece --- /dev/null +++ b/hack/test/sonar_mk_11_dockerfile_digest_test.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# REQ-SEC-2026-08: production and fixture Dockerfiles pin base images by digest +# (OpenSSF Scorecard Pinned-Dependencies). +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +fail=0 + +require_digest() { + local file="$1" image="$2" + if ! grep -E "^FROM ${image}[^ ]*@sha256:[a-f0-9]{64}" "$file" >/dev/null; then + echo "FAIL: ${file} must pin ${image} with @sha256:<64 hex>" >&2 + grep -n "^FROM " "$file" >&2 || true + fail=1 + else + echo "OK: ${file} pins ${image} by digest" + fi +} + +require_digest "${ROOT}/Dockerfile" "golang:" +require_digest "${ROOT}/Dockerfile" "gcr.io/distroless/static:" +require_digest "${ROOT}/test/e2e/fixtures/metrics-curl/Dockerfile" "alpine:" + +# ARG expansion must be quoted so a malicious/empty TARGETOS cannot word-split. +if ! grep -E 'GOOS="\$\{TARGETOS:-linux\}"' "${ROOT}/Dockerfile" >/dev/null; then + echo "FAIL: Dockerfile must quote GOOS=\${TARGETOS:-linux} (docker:S6570)" >&2 + fail=1 +fi +if ! grep -E 'GOARCH="\$\{TARGETARCH\}"' "${ROOT}/Dockerfile" >/dev/null; then + echo "FAIL: Dockerfile must quote GOARCH=\${TARGETARCH} (docker:S6570)" >&2 + fail=1 +fi + +[ "$fail" -eq 0 ] || exit 1 +echo "OK: REQ-SEC-2026-08 Dockerfile digest pins present" diff --git a/renovate.json b/renovate.json index 3e82b159..5993fe54 100644 --- a/renovate.json +++ b/renovate.json @@ -76,7 +76,7 @@ "description": "Dockerfile builder golang image tag", "managerFilePatterns": ["/Dockerfile$/"], "matchStrings": [ - "FROM golang:(?\\d+\\.\\d+\\.\\d+) AS" + "FROM golang:(?\\d+\\.\\d+\\.\\d+)(?:@sha256:[a-f0-9]+)? AS" ], "datasourceTemplate": "docker", "depNameTemplate": "golang" diff --git a/test/e2e/fixtures/metrics-curl/Dockerfile b/test/e2e/fixtures/metrics-curl/Dockerfile index fe090ca3..7cb8a987 100644 --- a/test/e2e/fixtures/metrics-curl/Dockerfile +++ b/test/e2e/fixtures/metrics-curl/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.23 +FROM alpine:3.23@sha256:fd791d74b68913cbb027c6546007b3f0d3bc45125f797758156952bc2d6daf40 RUN apk add --no-cache curl \ && adduser -D -u 1000 -g 1000 metrics USER 1000