Skip to content

DEP-91 (5/5): harden image release integrity - #1689

Open
racerxdl wants to merge 5 commits into
dep-91-weeklyfrom
dep-91-integrity
Open

DEP-91 (5/5): harden image release integrity#1689
racerxdl wants to merge 5 commits into
dep-91-weeklyfrom
dep-91-integrity

Conversation

@racerxdl

@racerxdl racerxdl commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Stack (DEP-91)

Split from #1662 for reviewability, then rescoped: main has since landed #1681 (dependency/toolchain CVEs) and #1693 (hoophq/hoopagent), which superseded two PRs of the original stack.

Merge in order, bottom-up. Each PR targets the one below it.

The remaining work is what main still lacks: the fat image's kernel-header CVEs, and any vulnerability gate, SBOM, or scheduled OS patching for hoophq/hoopagent.


What

Hardens how images and release artifacts are published:

  • Verified, reviewed checksums for hand-installed binaries (fails closed on missing, duplicate, or mismatched digests)
  • Immutable candidate promotion — gate before push, so a partial multiarch manifest can never be promoted
  • Digest-pinned actions and scanner images
  • Bundled-tool versions read from immutable image labels rather than re-parsed at release time
  • Second govulncheck job for hsh-tunneld, which ships from its own Go module and is not reachable from the client module's package graph

Why

Final layer: the supply chain should fail closed rather than publish something unverified.

Risk

Highest-surface PR in the stack (release workflow), which is why it merges last — everything below is green by then.

How to test

python3 -m unittest scripts/ci/test_generate_sbom_manifest.py
nix-shell -p actionlint --run 'actionlint .github/workflows/release.yml'

Expected: tests pass. Remaining actionlint findings (GitHub-Linux-Arm-Runner label, merge-archive) are pre-existing on main on lines this PR does not touch.

Part of DEP-91.

Automated by MisterMal

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

DEP-91 (7/7): Harden image release integrity end-to-end

✨ Enhancement ⚙️ Configuration changes 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Verifies hand-installed binary archives (kubectl, awscli, sqlcmd) against reviewed SHA-256 pins,
 failing closed on any mismatch.
• Pins all GitHub Actions and Trivy invocations to immutable commit SHAs/digests instead of mutable
 tags.
• Restructures hoopdev-minimal weekly rebuild and release publishing to gate each per-arch candidate
 on Trivy before any push, avoiding partial multiarch manifests.
• Adds a GitHub Release-asset-based checksum trust anchor for the weekly minimal-image rebuild,
 falling back to reviewed compatibility pins for legacy releases.
• Makes the SBOM/manifest generator fail closed for required images/platforms, retries release-asset
 uploads, and reads bundled-tool versions from OCI labels on the exact shipped digest instead of the
 Dockerfile recipe.
• Adds report-agent-tools-versions.py to probe and pin the exact agent-tools base digest (with
 locked-down container) consumed by Dockerfile.dev builds.
• Adds unit tests for the SBOM generator's subprocess handling and wires them into pull-request CI.
Diagram

graph TD
  A["Dockerfile.tools build"] --> B["verify-manual-download.py"] --> C["agent-tools image"]
  C --> D["report-agent-tools-versions.py"] --> E["Dockerfile.dev build"]
  E --> F{"Trivy gate per arch"}
  F -->|pass| G["Push candidate tag"] --> H["Compose multiarch manifest"]
  F -->|fail| I["Abort: no push"]
  H --> J["generate-sbom-manifest.py"] --> K["GitHub Release assets"]
  L["minimal-image-rebuild.yml"] --> M["GitHub Release digest / legacy pin fallback"] --> E

  subgraph Legend
    direction LR
    _svc([Process]) ~~~ _dec{Decision} ~~~ _ext[[Artifact/Store]]
  end
Loading
High-Level Assessment

The PR's fail-closed, digest-pinned approach with per-arch gate-before-push is the standard and appropriate pattern for supply-chain hardening of container release pipelines; no meaningfully different architecture was worth considering given this is the final layer of an already-designed 7-part stack (DEP-91).

Files changed (16) +857 / -288

Enhancement (5) +475 / -260
minimal-image-rebuild.ymlRestructure weekly rebuild to trust GitHub Release digests and gate-before-push per arch +152/-56

Restructure weekly rebuild to trust GitHub Release digests and gate-before-push per arch

• Replaces the releases.hoop.dev checksum flow with GitHub Release asset digests as the primary trust anchor (falling back to reviewed compatibility pins for pre-DEP-91 releases), moves Docker Hub login after both arch candidates pass the OS-vuln gate, and composes/pushes multiarch tags from per-run candidate tags instead of rebuilding at push time.

.github/workflows/minimal-image-rebuild.yml

pullrequest.ymlRun new CI helper tests and add govulncheck for hsh-tunneld +15/-4

Run new CI helper tests and add govulncheck for hsh-tunneld

• Adds a unittest run for the SBOM generator helper script, pins checkout/setup-go actions by digest, disables credential persistence, and adds a govulncheck step scanning the separately shipped tunnel module.

.github/workflows/pullrequest.yml

release.ymlGate hoopdev-minimal per-arch, pin actions, and harden SBOM/release-asset steps +159/-133

Gate hoopdev-minimal per-arch, pin actions, and harden SBOM/release-asset steps

• Reworks hoopdev-minimal publish jobs to build-load-scan-then-push per architecture and only compose the multiarch manifest when both arches published; pins all actions/Trivy by digest; probes and labels the exact agent-tools base for hoopdev builds; makes SBOM generation and release-asset upload required/retried instead of continue-on-error; publishes Linux release archives as GitHub Release assets for the rebuild workflow's trust anchor.

.github/workflows/release.yml

Dockerfile.toolsVerify manual downloads against reviewed checksums and add provenance labels +55/-16

Verify manual downloads against reviewed checksums and add provenance labels

• Adds OCI labels recording bundled tool versions, copies the checksum manifest and verifier script into the build, and rewrites kubectl/awscli/sqlcmd download steps to verify each archive against reviewed SHA-256 pins before installing; cleans up the build-integrity helper files afterward.

Dockerfile.tools

generate-sbom-manifest.pyMake SBOM generation fail closed for required images and read tool versions from image labels +94/-51

Make SBOM generation fail closed for required images and read tool versions from image labels

• Removes Trivy Docker Hub credentials, requires gateway/fat-agent images and both platforms to be present or fails the job, reads bundled-tool versions from OCI labels on the exact scanned digest instead of parsing Dockerfile.tools, and enforces checked subprocess calls for Trivy.

scripts/ci/generate-sbom-manifest.py

Tests (1) +54 / -0
test_generate_sbom_manifest.pyAdd unit tests for SBOM generator subprocess handling +54/-0

Add unit tests for SBOM generator subprocess handling

• New test module verifying that the trivy() helper invokes subprocess.run with check=True and that the generic _run() helper defaults to check=False.

scripts/ci/test_generate_sbom_manifest.py

Documentation (2) +3 / -1
custom-agent-image.mdUpdate release-tag mutability documentation +1/-1

Update release-tag mutability documentation

• Clarifies that the ':<release>' tag can move on a release-workflow rerun rather than being permanently immutable, and recommends digest pinning for guaranteed byte stability.

docs/custom-agent-image.md

manual-binaries.tomlCross-reference new checksum manifest +2/-0

Cross-reference new checksum manifest

• Adds a comment pointing to the new agent-tools-checksums.sha256 file as the source of independent archive pins.

licenses/manual-binaries.toml

Other (8) +325 / -27
agent-tools-build.ymlPin actions/scanners by digest and defer Docker Hub login until after scan +27/-19

Pin actions/scanners by digest and defer Docker Hub login until after scan

• Replaces mutable action tags with digest-pinned versions, adds persist-credentials: false, pins Trivy by image digest, and reorders the arm64 job so Docker Hub login happens after the license/vuln scans instead of before.

.github/workflows/agent-tools-build.yml

minimal-image-build.ymlPin PR-smoke build actions and Trivy scanner +8/-6

Pin PR-smoke build actions and Trivy scanner

• Pins checkout, QEMU, buildx, build-push-action and Trivy to immutable digests and disables credential persistence on checkout.

.github/workflows/minimal-image-build.yml

pullrequest-release.ymlPin Trivy scanner image by digest +1/-1

Pin Trivy scanner image by digest

• Switches the license-scan Trivy invocation from a mutable tag to a pinned image digest.

.github/workflows/pullrequest-release.yml

Dockerfile.minimalPin base image by digest +1/-1

Pin base image by digest

• Adds a SHA-256 digest pin alongside the ubuntu:noble-20251013 tag.

Dockerfile.minimal

agent-tools-checksums.sha256Add reviewed checksum manifest for hand-installed tool archives +16/-0

Add reviewed checksum manifest for hand-installed tool archives

• New file listing reviewed SHA-256 digests for versioned kubectl, awscli, and sqlcmd archives across architectures, consumed by the Dockerfile.tools verification step.

licenses/agent-tools-checksums.sha256

legacy-release-checksums.sha256Add reviewed bootstrap checksum pins for pre-DEP-91 releases +5/-0

Add reviewed bootstrap checksum pins for pre-DEP-91 releases

• New compatibility manifest with reviewed digests for the two Linux release archives predating GitHub Release asset uploads, used only as a fallback in the weekly rebuild.

scripts/ci/legacy-release-checksums.sha256

report-agent-tools-versions.pyAdd script to probe and pin exact agent-tools base image metadata +190/-0

Add script to probe and pin exact agent-tools base image metadata

• New CLI that either reports bundled-tool versions from inside an agent-tools image or, with --prepare-build, resolves the pinned tag to an exact digest, runs the probe inside a locked-down container, and writes the digest-bound build arg and OCI labels to GITHUB_OUTPUT.

scripts/ci/report-agent-tools-versions.py

verify-manual-download.pyAdd checksum verification script for manually downloaded binaries +77/-0

Add checksum verification script for manually downloaded binaries

• New CLI that loads a reviewed SHA-256 manifest, computes the actual digest of a downloaded artifact, and fails closed on missing entries or mismatches using constant-time comparison.

scripts/ci/verify-manual-download.py

@qodo-code-review

qodo-code-review Bot commented Aug 6, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. No retries on downloads 🐞 Bug ☼ Reliability
Description
Dockerfile.tools switched kubectl/aws/sqlcmd downloads to fail-closed curl --fail, but the new
download commands have no bounded retry policy, so transient CDN/network failures will now fail the
Docker build immediately and can cause avoidable CI/release flakiness.
Code

Dockerfile.tools[R129-132]

+    && curl --fail --location --silent --show-error \
+      --proto '=https' --tlsv1.2 \
+      "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${dpkgArch}/kubectl" \
+      --output "$artifact" \
Relevance

●●● Strong

Bounded curl retries reduce CI flakiness; similar Dockerfile.tools reliability hardening suggestions
were accepted.

PR-#1629

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new kubectl download uses curl --fail ... but no --retry, and the same pattern is used for
other manual downloads (AWS CLI zip and sqlcmd tarball). In contrast, the weekly rebuild workflow
already uses curl --retry 3 --retry-all-errors for integrity-critical downloads, showing an
established repo pattern for bounded resilience.

Dockerfile.tools[123-155]
Dockerfile.tools[195-215]
.github/workflows/minimal-image-rebuild.yml[121-134]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`Dockerfile.tools` now downloads kubectl/AWS CLI/sqlcmd using `curl --fail --location --silent --show-error` (good: fail-closed), but without `--retry` this makes the build brittle to transient network/CDN issues.

### Issue Context
This PR hardened integrity checks for manually downloaded binaries. To keep builds reliable, the download step should also be resilient. Other integrity-critical download paths in CI already use bounded retries.

### Fix Focus Areas
- Dockerfile.tools[127-155]
- Dockerfile.tools[205-213]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context
✅ Compliance rules (platform): 39 rules
✅ Cross-repo context
  Explored: repo: hoophq/infra (sha: 801e2c0a)
  Not relevant to this PR: hoophq/libhoop
  Not relevant to this PR: hoophq/documentation
  Not relevant to this PR: hoophq/changelog

Grey Divider

Tip of the day
💡 Did you know, you can enable the Remediation agent and Qodo fixes findings in a dedicated fix PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread Dockerfile.tools
Comment on lines +129 to +132
&& curl --fail --location --silent --show-error \
--proto '=https' --tlsv1.2 \
"https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${dpkgArch}/kubectl" \
--output "$artifact" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. No retries on downloads 🐞 Bug ☼ Reliability

Dockerfile.tools switched kubectl/aws/sqlcmd downloads to fail-closed curl --fail, but the new
download commands have no bounded retry policy, so transient CDN/network failures will now fail the
Docker build immediately and can cause avoidable CI/release flakiness.
Agent Prompt
### Issue description
`Dockerfile.tools` now downloads kubectl/AWS CLI/sqlcmd using `curl --fail --location --silent --show-error` (good: fail-closed), but without `--retry` this makes the build brittle to transient network/CDN issues.

### Issue Context
This PR hardened integrity checks for manually downloaded binaries. To keep builds reliable, the download step should also be resilient. Other integrity-critical download paths in CI already use bounded retries.

### Fix Focus Areas
- Dockerfile.tools[127-155]
- Dockerfile.tools[205-213]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@github-actions

Copy link
Copy Markdown
Contributor

Migration Safety Analysis

No database migrations were changed in this PR. Safe to deploy to sandbox.

@racerxdl racerxdl changed the title DEP-91 (7/7): harden image release integrity DEP-91 (5/5): harden image release integrity Aug 10, 2026
@sandromello

Copy link
Copy Markdown
Contributor

✅ Build Completed with Success, Version=1689.0.0-gf73d1b1

racerxdl and others added 5 commits August 12, 2026 14:52
Verify downloaded tool archives and release rebuild inputs before publishing.

Closes #DEP-91
Closes #1643

 🤖 Generated with Mister Maluco

Co-Authored-By: MisterMal <teskeslab@lucasteske.dev>
Allow Trivy invocations to fail closed without duplicate check arguments, and exercise both runner modes in pull request CI.

Fixes #DEP-91

 🤖 Generated with Mister Maluco

Co-Authored-By: MisterMal <teskeslab@lucasteske.dev>
Four fixes to the release path, all cases where a control reported more
assurance than it delivered:

- The slim-agent CVE gate scanned amd64 candidates and then rebuilt
  before pushing, so arm64 was never scanned and the published bytes
  were not the scanned ones. Build each flavour/arch once, gate all
  four, and assemble the release tags from those exact images.
- The SSM plugin (from a mutable latest/ path), the legacy MongoDB
  archive and the Oracle client were installed or executed unverified.
  Pin and verify them, and add a static gate so a future unverified
  download fails the PR that adds it. Oracle is pinned per arch: arm64
  tracked a moving pointer, and converging it on the amd64 version would
  have downgraded arm64 agents.
- The MongoDB and Google apt signing keys were trusted as downloaded.
  Assert their fingerprints and scope them with signed-by. The Microsoft
  key is dropped: no Microsoft repository is configured, so it granted
  archive-wide signing authority for nothing.
- SBOM generation ran after the release was already public, so a failure
  left the release with none. Generate it first and make publishing
  depend on it.

Also: a missing hoop_rs is the expected state in the slim images, so log
it as info instead of an error, and refuse to render a defaultAgent
sidecar pointed at a slim flavour, which cannot run its startup script.

 🤖 Generated with Mister Maluco

Co-Authored-By: MisterMal <teskeslab@lucasteske.dev>
The gate matched curl and its URL on one physical line and ended an
instruction at any comment, so it saw 1 of the 8 download instructions
in Dockerfile.tools — including none of the ones it was written to
protect. Removing a verifier from the real file did not fail it.

Flatten continuations into one logical instruction, skip comments
without closing it, and judge exemptions over every URL the instruction
names rather than per line, since the signing-key block passes its URLs
as shell-function arguments. apt source declarations are configuration,
not fetches, so they are excluded.

Tests now assert against the real file's layout: that every download is
seen, and that deleting a verifier from it is caught.

Also distinguish a missing hoop_rs from an unusable one: only
ErrNotFound is the expected slim-image case, so a configured but
non-executable binary is reported as the error it is.

 🤖 Generated with Mister Maluco

Co-Authored-By: MisterMal <teskeslab@lucasteske.dev>
Independent review found the download gate certified recipes that
install before they verify. It only checked that the verifier's name
appeared somewhere in the RUN, so `curl x && dpkg -i x && verify x` and
`curl x && verify y && dpkg -i x` both passed, and an exemption could be
claimed by putting a trusted host's name in an attacker URL's path.

Walk the commands in execution order, track which downloaded file each
verifier actually covers, and fail when a consumer runs first. Match
exemptions on parsed host and path, not substring.

Two more from the same review:

- The Helm defaultAgent guard matched a repository suffix, so it
  rejected a working custom sidecar at <registry>/hoophq/hoopagent while
  letting a digest-pinned official slim image through. Match the
  official repository spellings exactly and read the tag with any
  @sha256 pin removed.
- fetch_key accepted the first fingerprint in the file, so a response
  appending an attacker key passed and apt trusted both. Require exactly
  one primary key.

Also stop the SBOM comment claiming more than it delivers: it gates the
GitHub Release object, not the registry, because the image tags are
pushed by earlier jobs. The optional-image branches it describes are
gone, so the code and comments now say the same thing.

 🤖 Generated with Mister Maluco

Co-Authored-By: MisterMal <teskeslab@lucasteske.dev>
@sandromello

Copy link
Copy Markdown
Contributor

✅ Build Completed with Success, Version=1689.0.0-g42c1f19

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-release Merges without publishing a release (docs, CI, refactors, etc.)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants