feat(installer): immutable-tag pin + signed sub-script manifest for the curl|bash bootstrap (RFC-0001 R8)#281
Merged
Conversation
…ts vs a signed manifest (R8) The curl|bash bootstrap pulled ~15 sub-scripts from a MUTABLE branch ref (BRANCH, default main) with no checksum and no signature. Those sub-scripts are the most privileged code in the product: provision.sh mints the machine credential and writes it to disk, install-client-helm.sh runs Helm as a cluster admin. Whoever could move that ref — or sit on-path — controlled what ran as root on every customer box. The cosign-signed CLI binary covered only the leaf (RFC-0001 R8, backend#889). scripts/install.sh now: - pins to an IMMUTABLE release tag (DEFAULT_REF, stamped by the release pipeline). A branch / non-vX.Y.Z ref is refused unless the developer sets TRACEBLOC_ALLOW_UNVERIFIED=1 (loud, dev-only); - downloads a cosign-signed manifest.sha256 (published as a release asset) and verifies every sub-script's digest against it, ABORTING on any mismatch or any file missing from the manifest — before install-k8s.sh / provision.sh / Helm run; - authenticates the manifest with cosign keyless (same Sigstore chain as the CLI binary); if cosign is absent it bootstraps a pinned, checksum-verified one, and FAILS CLOSED if it can't — never degrading to a same-channel checksum; - preserves the corporate-proxy / custom-CA path (#172/#722) and the retry loop. Supporting pieces: - scripts/gen-manifest.sh generates the manifest from the exact FILES the bootstrap fetches and cross-checks the two lists (no secrets — signing is separate); - release-helm-chart.yaml gains a sign-installer-manifest job: generate + cosign sign-blob (keyless) + stamp the tag into the published install.sh + attach the four assets to the release; - installer-tests.yaml gates manifest drift (gen-manifest.sh --check) and shellchecks gen-manifest.sh; - scripts/tests/install-bootstrap.bats (8 tests) proves fail-closed on a mutable ref, digest mismatch, missing manifest entry, signature failure, and genuinely-absent cosign; - docs/SUPPLY_CHAIN.md (verification model + the release-pipeline / key-mgmt follow-ups a human must wire) and a SECURITY.md §4.8 + residual-risk §8.9. Why cosign keyless over minisign/gpg: it introduces no long-lived signing secret (identity = the release workflow's OIDC cert + Rekor), and the CLI binary already uses it — one mechanism across both repos. See docs/SUPPLY_CHAIN.md §3. Cross-repo companion: tracebloc/cli makes its installer's cosign check mandatory / fail-closed under the same ticket. Refs: backend#889 (RFC-0001 §9, §14 R8, §13) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
Author
Independent security review — 1 must-fix before mergeReviewed independently of the author. The design holds under adversarial testing — verify-before-exec ordering, fail-closed on every failure path, and mandatory cosign all confirmed (bats 8/8). One must-fix:
🟠 Should-fix — GHA tag injection ( Follow-ups (not blocking): cert-identity regex is over-broad (accepts any Fixes are being applied to this branch now. 🤖 Reviewed by Claude Code |
…ion (R8 review) Security-review follow-ups on PR #281 (RFC-0001 R8, backend#889), applied before merge: MUST-FIX — immutable-pin traversal bypass (scripts/install.sh): The tag-validation regex's trailing `([.-].+)?` admitted `/` and `..`, so a ref like `v1.2.3-../../heads/main` passed the gate and curl collapsed the `..` to fetch sub-scripts from the MUTABLE `main` branch — the whole point of the immutable pin — with no TRACEBLOC_ALLOW_UNVERIFIED opt-in and no warning. - Tighten the trailer to `([.-][A-Za-z0-9.]+)?` (accepts v1.2.3, v1.2.3-rc1, v1.2.3.4; rejects any ref containing `/` or `..`). - Belt-and-suspenders: a `case "$REF" in */*|*..*)` guard before REPO_RAW/ REPO_REL are built, independent of which branch let the ref through. - Add two bats regressions asserting a traversal ref (`v1.2.3-../../heads/ main`) and a bare-slash ref are REJECTED without the opt-in (no fetch, no privileged step, non-zero exit). SHOULD-FIX — GHA tag injection (.github/workflows/release-helm-chart.yaml): The sign-installer-manifest job (id-token:write + contents:write — the trust-establishing job) interpolated `${{ github.event.release.tag_name }}` into a single-quoted shell assignment, so a tag with a single quote = shell injection in that job. Pass the tag via the step's `env:` block and reference "$TAG" instead; replace the `sed s|...|...|` stamp with a literal awk find/replace (index()/substr(), replacement read from ENVIRON) so a tag with `/`, `&`, or `\` can't corrupt the stamp. Add a post-stamp placeholder-gone assertion. Out of scope (tracked follow-ups): cert-identity regex breadth; SHA-pinning the signing-job actions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aptracebloc
approved these changes
Jun 25, 2026
saadqbal
added a commit
that referenced
this pull request
Jul 1, 2026
…sion reorder (#284) Promotes #279 (installer reorder: sign-in + provision before Helm), #281 (R8 verified bootstrap + sign-installer-manifest release job), #285 (stale-bootstrap CLI install fix), #286 (jq-free one-client guard). Brings the R8 install.sh + the sign-installer-manifest job to main ahead of the signed installer release. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Security-sensitive — DRAFT for human review. Part of RFC-0001 R8 (Phase-1 blocking security must-have), tracked as tracebloc/backend#889. Cross-repo companion PR lands in tracebloc/cli (the CLI installer's cosign check).
The gap
scripts/install.shpulled ~15 sub-scripts from a mutable branch ref (BRANCH, defaultmain) overraw.githubusercontent.comwith no checksum and no signature. Those sub-scripts are the most privileged code in the product:lib/provision.shmints the machine credential and writes it to disk;lib/install-client-helm.shruns Helm as cluster admin. Whoever could move that ref — or sit on-path — controlled what ran as root on every customer box.The fix (verification side, fully implemented)
scripts/install.shnow:DEFAULT_REF, stamped by the release pipeline). A branch / non-vX.Y.Zref is refused unlessTRACEBLOC_ALLOW_UNVERIFIED=1(loud, dev-only).manifest.sha256(release asset) and verifies every sub-script's digest against it — aborting on any mismatch or any file missing from the manifest, beforeinstall-k8s.sh/provision.sh/ Helm run.Supporting pieces
scripts/gen-manifest.sh— generates the manifest from the exactFILESthe bootstrap fetches and cross-checks the two lists (no secrets; signing is separate).release-helm-chart.yaml→ newsign-installer-manifestjob: generate +cosign sign-blob(keyless) + stamp the tag into the publishedinstall.sh+ attachinstall.sh/manifest.sha256/.sig/.certto the release.installer-tests.yaml→ manifest-drift gate (gen-manifest.sh --check) + shellcheck.scripts/tests/install-bootstrap.bats— 8 tests, network-free: fail-closed on mutable ref, digest mismatch, missing manifest entry, signature failure, genuinely-absent cosign, and the opt-in degrade. 8/8 pass; full suite otherwise green (two pre-existing macOS-local failures incommon.bats/install-client-helm.bats, unrelated — they fail identically on pristinedevelop).docs/SUPPLY_CHAIN.md(model + verify-by-hand recipe + the human follow-ups below),SECURITY.md§4.8 + residual §8.9, README note.Why cosign keyless (not minisign/gpg)
No long-lived signing secret to store/rotate/leak — identity = the release workflow's OIDC cert + Rekor — and the CLI binary already uses it. One mechanism across both repos. See
docs/SUPPLY_CHAIN.md §3.Human follow-ups required to make this fully real (not in this PR — infra/process)
cosign sign-blobkeyless works on a realrelease: publishedevent (this repo's release job didn't sign before). (release eng)https://tracebloc.io/i.sh(and the README one-liner) at the stamped release assetreleases/latest/download/install.sh, not rawmain(the in-repo copy carries the__TRACEBLOC_RELEASE_REF__placeholder and fails closed by design). (web/infra)release-helm-chart.yamlidentity once the first signed release proves the string. (security)gen-manifest.shFILES list +v*tag/release publishing. (repo admin)Not verified
I could not run the installer end-to-end against a real GitHub release (no network to releases here), nor exercise the
sign-installer-manifestCI job — the happy path is proven only via the mocked bats harness. The release-pipeline signing is the infra-dependent half called out above.🤖 Generated with Claude Code
Note
High Risk
Changes the most privileged customer-facing install path (credential mint, Helm); mistakes could brick installs or weaken verification, though fail-closed defaults and bats tests mitigate regression risk.
Overview
Hardens the
curl | bashbootstrap so privileged installer sub-scripts are no longer fetched from a mutable branch without verification.scripts/install.shnow pins to an immutablevX.Y.Ztag (release-stampedDEFAULT_REF), refuses un-stamped placeholders and mutable refs unlessTRACEBLOC_ALLOW_UNVERIFIED=1, and adds path-traversal guards on the ref before building fetch URLs. After download it verifies every listed sub-script againstmanifest.sha256, authenticates the manifest with cosign keyless (bootstrap pinned cosign if missing), and fails closed beforeinstall-k8s.sh/ Helm run.New
scripts/gen-manifest.shgenerates the manifest and cross-checks itsFILESlist withinstall.sh;scripts/manifest.sha256is committed. CI addsgen-manifest.sh --checkand shellcheck coverage;sign-installer-manifeston release generates/signs the manifest, stamps the tag into publishedinstall.sh(safe awk substitution), and attaches release assets.install-bootstrap.batsexercises fail-closed and happy paths offline. Docs:docs/SUPPLY_CHAIN.md, SECURITY.md §4.8/§8.9, README supply-chain note.Reviewed by Cursor Bugbot for commit a265ea7. Bugbot is set up for automated code reviews on this repo. Configure here.