Uh oh!
There was an error while loading. Please reload this page.
feat(installer): immutable-tag pin + signed sub-script manifest for the curl|bash bootstrap (RFC-0001 R8) - #281
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>
saadqbal
commented
Jun 25, 2026
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>
Uh oh!
There was an error while loading. Please reload this page.
…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>
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.