Skip to content

feat(installer): immutable-tag pin + signed sub-script manifest for the curl|bash bootstrap (RFC-0001 R8)#281

Merged
saadqbal merged 2 commits into
developfrom
sec/rfc-0001-r8-bootstrap-supply-chain
Jun 25, 2026
Merged

feat(installer): immutable-tag pin + signed sub-script manifest for the curl|bash bootstrap (RFC-0001 R8)#281
saadqbal merged 2 commits into
developfrom
sec/rfc-0001-r8-bootstrap-supply-chain

Conversation

@saadqbal

@saadqbal saadqbal commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

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.sh pulled ~15 sub-scripts from a mutable branch ref (BRANCH, default main) over raw.githubusercontent.com with no checksum and no signature. Those sub-scripts are the most privileged code in the product: lib/provision.sh mints the machine credential and writes it to disk; lib/install-client-helm.sh runs 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.sh now:

  1. Pins to an immutable release tag (DEFAULT_REF, stamped by the release pipeline). A branch / non-vX.Y.Z ref is refused unless TRACEBLOC_ALLOW_UNVERIFIED=1 (loud, dev-only).
  2. Downloads a cosign-signed manifest.sha256 (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.
  3. Authenticates the manifest with cosign keyless (same Sigstore chain as the CLI binary); bootstraps a pinned, checksum-verified cosign if absent, and fails closed if it can't — never degrades to a same-channel checksum.
  4. Preserves the corporate-proxy / custom-CA path (fix(installer): corporate-proxy hardening (auth proxy, NO_PROXY, 0.0.0.0 detect, Windows parity) #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 → new sign-installer-manifest job: generate + cosign sign-blob (keyless) + stamp the tag into the published install.sh + attach install.sh / manifest.sha256 / .sig / .cert to the release.
  • installer-tests.yaml → manifest-drift gate (gen-manifest.sh --check) + shellcheck.
  • scripts/tests/install-bootstrap.bats8 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 in common.bats/install-client-helm.bats, unrelated — they fail identically on pristine develop).
  • 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)

  1. First signed release: confirm cosign sign-blob keyless works on a real release: published event (this repo's release job didn't sign before). (release eng)
  2. Stamp the entrypoint: point https://tracebloc.io/i.sh (and the README one-liner) at the stamped release asset releases/latest/download/install.sh, not raw main (the in-repo copy carries the __TRACEBLOC_RELEASE_REF__ placeholder and fails closed by design). (web/infra)
  3. Tighten the cert-identity regex to the exact release-helm-chart.yaml identity once the first signed release proves the string. (security)
  4. Branch-protect the release workflow + the gen-manifest.sh FILES list + v* tag/release publishing. (repo admin)
  5. Document the cosign-bootstrap trust posture for hardened sites (pre-install/mirror cosign). (security — drafted in SUPPLY_CHAIN.md §6 + SECURITY.md §8.9)

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-manifest CI 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 | bash bootstrap so privileged installer sub-scripts are no longer fetched from a mutable branch without verification.

scripts/install.sh now pins to an immutable vX.Y.Z tag (release-stamped DEFAULT_REF), refuses un-stamped placeholders and mutable refs unless TRACEBLOC_ALLOW_UNVERIFIED=1, and adds path-traversal guards on the ref before building fetch URLs. After download it verifies every listed sub-script against manifest.sha256, authenticates the manifest with cosign keyless (bootstrap pinned cosign if missing), and fails closed before install-k8s.sh / Helm run.

New scripts/gen-manifest.sh generates the manifest and cross-checks its FILES list with install.sh; scripts/manifest.sha256 is committed. CI adds gen-manifest.sh --check and shellcheck coverage; sign-installer-manifest on release generates/signs the manifest, stamps the tag into published install.sh (safe awk substitution), and attaches release assets. install-bootstrap.bats exercises 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.

…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 saadqbal self-assigned this Jun 25, 2026
@saadqbal

Copy link
Copy Markdown
Contributor Author

Independent security review — 1 must-fix before merge

Reviewed 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:

  • 🔴 Immutable-pin traversal bypass (scripts/install.sh, tag-validation regex): the trailing ([.-].+)? admits / and .., so REF=v1.2.3-../../heads/main passes as a "tag" and curl collapses it to the mutable main branch — verified live (HTTP 200, byte-identical to main), with no TRACEBLOC_ALLOW_UNVERIFIED opt-in and no warning. Not RCE today (the manifest fetch 404s → fail-closed), but R8's immutable-pin layer is defeated. Fix: ^v[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9.]+)?$ + reject */*/*..*.

🟠 Should-fix — GHA tag injection (.github/workflows/release-helm-chart.yaml): ${{ release.tag_name }} interpolated into a single-quoted shell line in the sign-installer-manifest job (id-token: write) → a tag with ' = command injection. Pass via env: + "$TAG".

Follow-ups (not blocking): cert-identity regex is over-broad (accepts any tracebloc/client workflow) → tighten once the first signed release proves the string (infra #3); SHA-pin the signing-job actions (infra #4).

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>
@saadqbal saadqbal merged commit 06d8078 into develop Jun 25, 2026
26 of 27 checks passed
@saadqbal saadqbal deleted the sec/rfc-0001-r8-bootstrap-supply-chain branch June 25, 2026 11:31
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants