Skip to content

feat(sc.sh): opt-in install of branch-preview tarballs via TRUST_PREVIEW_BRANCH (+SHA pin)#278

Merged
Cre-eD merged 2 commits into
mainfrom
fix/sc-sh-allow-preview-tarballs
May 20, 2026
Merged

feat(sc.sh): opt-in install of branch-preview tarballs via TRUST_PREVIEW_BRANCH (+SHA pin)#278
Cre-eD merged 2 commits into
mainfrom
fix/sc-sh-allow-preview-tarballs

Conversation

@Cre-eD
Copy link
Copy Markdown
Contributor

@Cre-eD Cre-eD commented May 19, 2026

Why

Production users testing a feature-branch SC build via sc.sh are blocked by the Phase 2c failgate: the cert-identity regex passed to cosign verify-blob is hard-pinned to push.yaml@refs/heads/main. Preview tarballs published by branch-preview.yaml are legitimately Sigstore-signed but cannot install. The only workarounds today are curl + tar -xz (bypasses signature verification entirely) or fork sc.sh locally. Both are worse than a documented, narrowly-scoped opt-in.

Discovered while validating the CloudTrail security alerts plugin (PR #277) against a preview build of that branch.

What

sc.sh — branch-pinned (and optionally SHA-pinned) opt-in

# Minimum (branch-pinned):
SIMPLE_CONTAINER_TRUST_PREVIEW_BRANCH=feat/your-branch \
SIMPLE_CONTAINER_VERSION=YYYY.M.D-pre.<sha>-preview.<sha> \
  bash <(curl -Ls https://dist.simple-container.com/sc.sh)

# Recommended for CI (branch + commit SHA pin):
SIMPLE_CONTAINER_TRUST_PREVIEW_BRANCH=feat/your-branch \
SIMPLE_CONTAINER_TRUST_PREVIEW_SHA=4cc1a03ca5c259a428e07d4f0bb8eb9120a6e2b7 \
SIMPLE_CONTAINER_VERSION=YYYY.M.D-pre.<sha>-preview.<sha> \
  bash <(curl -Ls https://dist.simple-container.com/sc.sh)

verify_sc_tarball widens the accepted identity regex to ALSO accept branch-preview.yaml@refs/heads/<that exact branch>. The branch name is allowlist-validated (^[A-Za-z0-9._/-]+$, no .. / leading-trailing-slash / .lock-suffix) and the . regex metachar is escaped before interpolation, so a value like evil.* or feat.foo cannot re-open the permissive trust set.

When _SHA is set, cosign is given --certificate-github-workflow-sha which verifies against the Sigstore certificate's GitHub OIDC workflow_sha claim (OID 1.3.6.1.4.1.57264.1.3) — pinning verification to a specific commit, not the mutable branch head.

sc.sh — rejects the over-permissive =1 form outright

The prior commit had SIMPLE_CONTAINER_ALLOW_PREVIEW=1 (any branch). Codex + Gemini both flagged that as a meaningful trust expansion — anyone with push access to any branch in the repo could ship a tarball that the regex would accept once a user opted in for "their" branch. Now =1 fails closed with a pointer to _BRANCH.

sc.sh — loud stderr warning + smarter error messages

  • Every preview install emits a multi-line stderr warning listing the trusted branch + whether SHA pin is in effect. Mitigates the "persistent export in shell rc silently relaxes trust" footgun.
  • On verification failure in strict mode, the error message detects the preview-signed case, parses the signer branch out of cosign's got subjects [...] line, and offers it back as a copy-paste env-var value.

docs/SECURITY.md — documentation

New subsection under "Verifying tarballs" documenting:

  • SIMPLE_CONTAINER_TRUST_PREVIEW_BRANCH + SIMPLE_CONTAINER_TRUST_PREVIEW_SHA usage
  • Why "trust all preview" is intentionally not supported
  • Manual cosign verify-blob equivalent for preview tarballs (branch + SHA pin)

Security analysis

Threats addressed:

Threat Fix
Any-branch trust _BRANCH required; =1 rejected; branch allowlist + regex escape
Workflow file content not pinned Optional _SHA via --certificate-github-workflow-sha (verifies OID 1.3.6.1.4.1.57264.1.3)
Persistent env var = ambient policy Loud stderr warning on every preview install; _BRANCH value must change per preview, narrowing the persistence window
CDN-account compromise serves different-branch tarball Branch pin neutralizes — different branch's tarball at the requested URL fails verification
CDN serves old commit of the same branch SHA pin (recommended) neutralizes
Regex injection / identity shadowing Branch allowlist + escape of . metachar

Threats explicitly out of scope (separate concerns, documented in commit message):

  • Same CDN namespace (publish side, not verify side)
  • Installed sc --version vs requested interlock (defense-in-depth, separate hardening pass)
  • Path-traversal-safe extraction (pre-existing concern in extract logic)

Testing

8 manual test cases against the live preview tarball v2026.5.26-pre.4cc1a03-preview.4cc1a03:

# Setup Expected Result
A no env vars strict reject + auto-extracted hint
B ALLOW_PREVIEW=1 fail-fast before cosign
C _BRANCH=evil.* validation reject
D _BRANCH=feat/wrong (valid format, doesn't match signer) cosign mismatch
E correct _BRANCH only success + warning
F correct _BRANCH + correct _SHA success + SHA-pinned warning
G correct _BRANCH + wrong _SHA (40-hex zeros) cosign rejects
H correct _BRANCH + invalid _SHA (not-a-sha) validation reject

Semgrep shell-curl-pipe-to-shell rule: 0 findings (curl|sh example was removed from error text).

Test plan

  • Manual: all 8 paths verified locally
  • Semgrep CI clean (was failing on the previous commit — fixed by removing the curl|sh example string from error text)
  • CI: branch-preview.yaml run on this fix branch confirms the rebuilt sc.sh still works for production installs
  • After merge + a push.yaml release: downstream consumers (integrail/devops install-sc, PAY-SPACE wrappers) gain the opt-in path automatically; PR feat(cloudtrail-alerts): per-detector exclusions + 8 new detectors + SSO MFA fix #277's preview build becomes installable via _BRANCH + _SHA pin

Refs

…TAINER_ALLOW_PREVIEW

Why: production users testing a feature-branch SC build (e.g. before merging
an SC API PR that affects downstream consumers) currently can't use sc.sh —
the Phase 2c cert-identity regex is hard-pinned to push.yaml@refs/heads/main,
so every preview tarball trips cosign verification even though it's a
legitimately signed Sigstore bundle. Today the only workaround is to bypass
sc.sh entirely (`curl tarball + tar -xz`), which loses the signature check
the failgate was built to provide. The opt-in path documented here gives
preview testing back without weakening the production strict-mode default.

What:
- sc.sh: when SIMPLE_CONTAINER_ALLOW_PREVIEW=1 is set, widen the cert-identity
  regex passed to `cosign verify-blob` to also accept
  branch-preview.yaml@refs/heads/*. Default (env var unset / not "1") is
  unchanged — only the production push.yaml@main identity is accepted.
  Signature, Rekor log entry, and OIDC issuer are still verified end-to-end;
  the broader regex is the only thing that changes.
- sc.sh: on signature failure where cosign reports a branch-preview signer,
  surface a precise next-step ("rerun with SIMPLE_CONTAINER_ALLOW_PREVIEW=1
  SIMPLE_CONTAINER_VERSION=...") instead of the generic compromise message,
  so a user who knows they're installing a preview build gets a copy-paste
  unblock instead of having to read the script.
- docs/SECURITY.md: document the opt-in env var alongside the existing
  manual `cosign verify-blob` commands, and update the comment in
  Verifying tarballs that wrongly claimed preview tarballs don't land at
  the CDN (they do — branch-preview.yaml publishes them to the same bucket).

Why this is safe to relax:
1. The regex still anchors to simple-container-com/api workflows only; an
   attacker cannot publish a malicious tarball under a different repo's
   workflow identity.
2. The OIDC issuer is still pinned to GitHub's token endpoint.
3. Rekor log entry, Sigstore bundle, and tarball SHA-256 sidecar are all
   still verified.
4. Production users default to strict. Picking up a preview build requires
   explicit acknowledgement via env var — there's no implicit promotion of
   any feature-branch identity into the production trust set.

Testing:
- Without env var: preview tarball is rejected with the new helpful message
  pointing at SIMPLE_CONTAINER_ALLOW_PREVIEW=1 (verified against the
  v2026.5.26-pre.4cc1a03-preview.4cc1a03 tarball published 2026-05-19).
- With env var: same tarball verifies and installs cleanly.

Refs PR #277 (the trigger for this fix — needed to validate the new
CloudTrail security alerts plugin schema end-to-end against a preview SC).

Signed-off-by: Dmitrii Creed <creeed22@gmail.com>
@Cre-eD Cre-eD requested a review from smecsia as a code owner May 19, 2026 19:51
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 19, 2026

Semgrep Scan Results

Repository: api | Commit: e26e231

Check Status Details
⚠️ Semgrep Warning 10 warning(s), 10 total

Scanned at 2026-05-20 08:17 UTC

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 19, 2026

Security Scan Results

Repository: api | Commit: e26e231

Check Status Details
✅ Secret Scan Pass No secrets detected
✅ Dependencies (Trivy) Pass 2 total (no critical/high)
✅ Dependencies (Grype) Pass 2 total (no critical/high)
📦 SBOM Generated 509 components (CycloneDX)

Scanned at 2026-05-20 08:17 UTC

… review

Codex + Gemini security review of the original commit (cross-referenced against
GitHub Actions OIDC docs and sigstore/cosign source) surfaced material gaps that
would have shipped if the original `=1` opt-in landed as-is. Each fix is paired
with a test that confirms the behavior.

## What changed (and why)

1. Reject `SIMPLE_CONTAINER_ALLOW_PREVIEW=1` entirely.

   The original "=1 widens to any branch" form is gone. It trusted
   `branch-preview.yaml@refs/heads/.+` — anyone with push access to ANY branch
   in the repo could ship a tarball that the regex would accept, once the user
   opted in for "their" branch. That's a much broader trust radius than picking
   up an unreviewed feature branch you actually want to test (push to main is
   gated by branch protection + required reviews; push to a feature branch is
   not). The `=1` form now fails closed with a pointer to `_BRANCH`.

2. Require `SIMPLE_CONTAINER_TRUST_PREVIEW_BRANCH=<branch>` for preview installs.

   Pins cosign verification to one specific branch's branch-preview.yaml
   signature. A different branch's tarball at the CDN URL — whether served
   deliberately by a CDN-account compromise (T4) or accidentally cached —
   fails verification.

   Branch name is validated against a conservative allowlist
   (`^[A-Za-z0-9._/-]+$`) BEFORE interpolation into the cosign regex, plus
   explicit `..` / leading-/trailing-`/` / `.lock`-suffix rejections. This
   neutralizes the regex-injection / "identity shadowing" path Gemini
   flagged: a branch name like `evil.*` would otherwise re-open the
   permissive trust set, and `feat.foo` would shadow `feat/foo`. The single
   regex metachar that the allowlist still permits (`.`) is escaped to `\.`
   in the generated regex.

3. Optional `SIMPLE_CONTAINER_TRUST_PREVIEW_SHA=<40-hex>` to pin the commit.

   Maps to cosign's `--certificate-github-workflow-sha`, which verifies
   against the Sigstore certificate's GitHub OIDC `workflow_sha` claim
   (OID 1.3.6.1.4.1.57264.1.3). Closes T2 (workflow content not pinned at
   the identity layer) and the residual T4 (CDN can serve an old tarball
   signed from the same branch with a different SHA). Recommended for CI;
   optional for ad-hoc developer installs.

   SHA value is validated as 40-char lowercase hex before being passed to
   cosign so an invalid input fails the install with a clear message,
   not a confusing cosign error.

4. Loud stderr warning on every preview install.

   Mitigates T3 (persistent `export` in shell rc silently relaxes trust).
   The warning is unconditional and visible on every invocation when
   _BRANCH is set, listing the exact branch trusted and whether the SHA
   pin is in effect.

5. Smarter error message: extract signer branch from cosign output.

   When verification fails because the tarball is preview-signed but no
   _BRANCH is set, parse the signer ARN out of cosign's "got subjects [...]"
   line and offer it back to the user as the env-var value to copy-paste.
   Removes the curl|sh example string that tripped the org's semgrep
   `shell-curl-pipe-to-shell` rule — the user already knows how they
   invoked us, no need to demo it.

## Threats explicitly preserved as out-of-scope

- "Same CDN namespace" (preview artifacts share production object keys) —
  this is a publish-side concern, belongs in branch-preview.yaml not sc.sh.
- "Verify installed `sc --version` matches requested" — sc.sh already prints
  the post-install version; explicit interlock is a separate hardening pass.
- "Path-traversal-safe extraction" — pre-existing concern in extract logic,
  orthogonal to verification regex.

## Verified end-to-end

8 manual test cases against the live preview tarball
v2026.5.26-pre.4cc1a03-preview.4cc1a03 (signed by branch-preview.yaml@feat/
cloudtrail-alerts-exclusions-and-new-detectors with SHA
4cc1a03):

A. Strict mode (no env vars): rejected with auto-extracted branch hint
B. Deprecated `=1`: rejected BEFORE cosign runs (fail-fast)
C. Invalid branch (`evil.*`): rejected at validation
D. Wrong branch (valid format, doesn't match signer): cosign mismatch
E. Correct branch: success + warning
F. Correct branch + correct SHA pin: success + SHA-pinned warning
G. Correct branch + wrong SHA: cosign rejects (`workflow SHA not found`)
H. Invalid SHA format: rejected at validation

Semgrep `shell-curl-pipe-to-shell` rule passes on the revised script
(simple-container-com/actions semgrep-scan/rules/shell.yml).

Refs codex+gemini cross-review of the original sc.sh patch.

Signed-off-by: Dmitrii Creed <creeed22@gmail.com>
@Cre-eD
Copy link
Copy Markdown
Contributor Author

Cre-eD commented May 20, 2026

Round 2: threat-model-driven hardening

Codex + Gemini parallel security review of the original commit (each fact-checked against AWS docs, GitHub Actions OIDC docs, sigstore/cosign source). Both concluded the original =1 form was a real security regression. Followup commit 915a713 addresses every must-fix finding.

Threat triage

# Threat Fixed how Source
T1 .+ regex trusts any branch — any push-writer can sign Replaced with SIMPLE_CONTAINER_TRUST_PREVIEW_BRANCH=<exact-branch>, no any-branch opt-in both
T2 Workflow file SHA isn't pinned at identity layer — tampered branch-preview.yaml on a branch still produces a valid signature New optional SIMPLE_CONTAINER_TRUST_PREVIEW_SHA=<40-hex> maps to cosign --certificate-github-workflow-sha, pins OID 1.3.6.1.4.1.57264.1.3 codex
T3 Persistent export in shell rc silently relaxes trust Loud stderr warning on every preview install + _BRANCH value must change per-preview, naturally narrowing the window both
T4 CDN-account compromise serves a tarball from a different (legitimately signed) branch at the requested URL Branch pin neutralizes the "different branch" path; SHA pin neutralizes the "same branch, old commit" path codex
Regex injection Branch name evil.* or feat.foo (shadows feat/foo) could re-open trust Branch name validated against ^[A-Za-z0-9._/-]+$ allowlist before interpolation; . is escaped to \. in the generated regex gemini
Misleading wording Generic "tampered in transit / CDN compromised" copy for any verification failure Detection of preview-signed-but-strict-mode case + auto-extraction of the signer branch from cosign output → offered back as copy-paste env-var value (my own observation during testing)
Semgrep blocker bash <(curl ...) example in error message tripped shell-curl-pipe-to-shell rule Replaced with export ENV=value; no `curl shstring insc.sh`

Wontfix (with explicit reasoning)

Finding Source Why deferred
SIMPLE_CONTAINER_DANGEROUS_TRUST_PREVIEW_BRANCH (uglier env var name) gemini Hostile UX with no material security gain — the loud warning + _BRANCH requirement + branch validation already make trust expansion explicit
"Compare installed sc --version vs requested" codex sc.sh already prints the post-install version; explicit interlock is a separate hardening pass
"Path-traversal-safe tarball extraction" codex Pre-existing concern in extract logic, orthogonal to verification regex
"Same CDN namespace — preview shouldn't share production object keys" codex Publish-side concern (belongs in branch-preview.yaml), not verify-side
Add userIdentity.type = "Unauthenticated" to anonymous-probe variant gemini Not a documented userIdentity.type per AWS — likely hallucination

Tests (8 paths, all passing locally against v2026.5.26-pre.4cc1a03-preview.4cc1a03)

# Setup Expected Actual
A no env vars strict reject + auto-extracted branch hint
B ALLOW_PREVIEW=1 (deprecated) fail-fast BEFORE cosign
C TRUST_PREVIEW_BRANCH=evil.* validation reject
D TRUST_PREVIEW_BRANCH=feat/other (valid, doesn't match signer) cosign mismatch
E TRUST_PREVIEW_BRANCH=<correct> success + warning
F TRUST_PREVIEW_BRANCH=<correct> + TRUST_PREVIEW_SHA=<correct> success + SHA-pinned warning
G correct branch + wrong SHA (40-hex zeros) cosign rejects (workflow SHA mismatch)
H correct branch + TRUST_PREVIEW_SHA=not-a-sha validation reject

Semgrep shell-curl-pipe-to-shell rule: 0 findings. go vet and existing test suite unaffected (sc.sh is bash, not Go).

Cert inspection confirms SHA flag works

Sigstore cert for the live preview tarball contains the workflow SHA at OID 1.3.6.1.4.1.57264.1.3 and 1.3.6.1.4.1.57264.1.13. Cosign's --certificate-github-workflow-sha enforces against .1.3 — test G (wrong SHA) failed with expected GitHub Workflow SHA not found in certificate, test F (correct SHA) succeeded.

@Cre-eD Cre-eD changed the title feat(sc.sh): opt-in install of branch-preview tarballs via SIMPLE_CONTAINER_ALLOW_PREVIEW feat(sc.sh): opt-in install of branch-preview tarballs via TRUST_PREVIEW_BRANCH (+SHA pin) May 20, 2026
@Cre-eD Cre-eD merged commit 4e7ce33 into main May 20, 2026
20 checks passed
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.

2 participants