feat(cve-gate): hardened gate image, CVE budgets and OpenVEX #74
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/cve-gate-dhi-registry"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Ships the CVE budget gate and its dependencies. Cut from
main, so it also carries the digest-pin commit that PR #71 missed.Releases exactly one image:
cve-gate. No existing image directory is touched — verified:git diff --name-only main...HEAD | grep '^ops/docker/'returns onlyops/docker/cve-gate. The OCI-label repoint that touches all 17 Dockerfiles is deliberately held back for a separate, batched PR, becauseon_source_changebuilds one matrix entry per changed dir and 17 concurrent builds would land onfringe-workstation.What's in it
ops/docker/cve-gate— the gate, and the worked example for ADR-0006. DHIalpine-basewith the-devvariant as a build stage only; grype copied fromdhi.io/grypeinstead ofcurl | shof an install script from a mutable branch; non-root 65532; read-only rootfs; build-time assertions that run as the nonroot user.ops/security/cve-budgets.yaml— per-image ceilings that ratchet. New images start inwarn, the pipeline measures them, the budget is set at the observed number, then it only goes down.ci-runner(8C/136H) andagent-runner(1C/51H) seeded from measured Trivy Operator data.cve-gateis the only entry starting atenforce0/0.ops/vex/— OpenVEX as the only sanctioned suppression mechanism: hand-authored, PR-reviewed, justified from the closed vocabulary. Harbor's project-widecve_allowliststays empty..forgejo/actions/cve-gate— complete but not yet called. Wiring the call site in the same change that first builds the image would makecve-gate's own release depend on an image that doesn't exist. Step 2 restores it.cyclonedx-json@1.6— unpinned it emits 1.7; Dependency-Track tops out at 1.6 and returned400on every upload since run 133 (~50 releases), fail-soft, so nobody saw it.::error::. Loud, still non-fatal.ops/kyverno/deleted — it targetedghcr.io/webgrip/*with a Fulcio identity: a registry we no longer publish to, verified by a mechanism we no longer use. The live policy inhomelab-clusteris correct; this copy was fiction that read as a guarantee.Build history — four real failures, all caught by assertions
401from dhi.ioREGISTRY_DHIdefaults to the Harbor proxy/out/libemptylddwrites to stderr;2>/dev/nulldiscarded itRun 188's real lesson: this repo's pattern is default to the public registry, override to Harbor via
--build-arg. That is only safe because docker.io / ghcr.io / mcr.microsoft.com serve anonymously — a missing override just loses the cache. dhi.io refuses anonymous pulls, so the same pattern turns a missing flag into a hard failure. And it would keep being missing:dockerVerifyGate()in@webgrip/semantic-release-confighardcodes its own three--build-argflags in another repo, and run 188 died in exactly that path. Inverting the default fixes both build paths at once.Run 194's real lesson: an empty result was accepted as an answer. "ldd returned nothing" and "this binary needs nothing" behaved identically, so the stage reported success while doing nothing — the same silent-success shape as the DT 400 and the masked
| teeexit code. It now fails loudly, and the emptiness check runs on the unfiltered list so a libc-only binary doesn't false-FATAL.Verified against live infrastructure
dhiHarbor endpoint:status=healthy,cred=webgrip— the Docker Hub credential authenticates dhi.io (sameservice="registry.docker.io"). No new account, no builder login.dhiprojectpublic=true; anonymous manifest pull returns200— the builder needs no credential.-devtransposition./grype-src/usr/bin/grype, reports0.116.1;jq-1.8.2andyq v4.53.3install from apk in the-devvariant;/bin/shexists in the runtime variant.Not verified
No successful build yet. The last unproven step is
cve-gatereaching its own argument validation in the runtime assertion. If/out/libis empty again it now fails at the staging step with the rawlddoutput attached, rather than 40 relocation errors two stages later.Merge order
cve-gate 0.1.0lands in Harbor..forgejo/actions/cosign-sign-attest(currently a commented block with its reasoning).ALLOW_MISSING_GATEexists as a loud, self-limiting bootstrap escape but should not be needed — the call site is deferred precisely so the happy path never touches it.Refs VIK-631 (the techdocs GitHub-reference purge this deliberately leaves alone).
Run 194 staged ZERO shared libraries and failed the runtime assertion with the same jq relocation errors as run 192. The trace shows why: + ldd /usr/bin/jq + awk '...' + sort -u + read -r lib <- one trace: the loop body never executed extra shared libraries staged for the runtime stage: <- ls printed nothing musl's ldd writes its dependency list to STDERR, not stdout. My `2>/dev/null` discarded precisely the output the stage exists to read. `2>&1` instead. The deeper bug was not the redirect, it was that an empty result was accepted as an answer. "ldd returned nothing" and "this binary needs nothing" produced identical behaviour, so the stage reported success while doing nothing at all — the same silent-success shape as the Dependency-Track 400 and the `| tee` exit code. It now distinguishes three outcomes explicitly: - "Not a valid dynamic program" -> static, stage nothing, fine - paths resolved -> stage everything except libc and the loader - nothing at all -> FATAL, printing the raw ldd output The emptiness check deliberately runs on the UNFILTERED list. Checking after filtering would false-FATAL on a binary that legitimately needs only libc. Also replaces the `while read` subshell with a plain `for` loop, so a failing `install` aborts the build instead of being swallowed by the pipeline. All four outcome shapes tested against real musl output formats before pushing: dynamic-with-libs, static, dynamic-libc-only, and the run-194 empty case — which now fails loudly at this stage rather than 40 relocation errors later. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>