security: pin mitmproxy image chain + rtk installer (Phase 1 of #20) - #100
Conversation
|
I'm not sure I understand the whole latest-tag ceremony, it seems quite complex. Can't we pin the mitmproxy version and simply tag with |
|
@adamw yes, you are right. The proposal is overcomplicated. I will simplify it the way you described |
Pin the mitmproxy image chain to an exact version (12.2.3): the public image referenced by generated compose-proxy.yml, and the two derived ghcr secret-provider images. Two-file source of truth (CLI constants + images/mitmproxy.env) enforced by a contract test. Weekly cron becomes a latest-only tracking channel; versioned tags publish only from master pushes and are never overwritten. rtk install script pinned to a commit SHA (script-level freeze; binary pin deferred until rtk has stable releases). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFCiFbv1Cpr7yzCU8ftvzZ
#20) Introduces SCT_MITMPROXY_VERSION="12.2.3" in constants.bash and threads it through the compose-proxy.yml template placeholder and apply_secret_provider's 1password/protonpass ghcr images, replacing all `:latest` tags on the CLI side. devcontainer.bash now sources constants.bash directly (it wasn't transitively sourced through its own dependency chain, only accidentally via caller ordering in production). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFCiFbv1Cpr7yzCU8ftvzZ
…ebuilds latest only (#20) Adds images/mitmproxy.env as the single source of truth for the pinned mitmproxy base image version, consumed by both build workflows and both Dockerfiles, and asserted equal to the CLI-side SCT_MITMPROXY_VERSION by a new contract test. Both Dockerfiles take MITMPROXY_VERSION as a build arg with no default (pin lives in exactly one place) and parameterize their FROM. Both workflows load the pin into $GITHUB_ENV for all non-scheduled triggers, override it to `latest` for the weekly schedule, and reshape their metadata-action tags into three buckets: schedule publishes only `latest` (tracking channel), push/workflow_dispatch publish the pinned version + sha as immutable tags, and pull_request keeps its existing ref tag with no push (unchanged behavior).
The pinned <version> tag was reachable from workflow_dispatch on any ref, so a manual dispatch off a feature branch could overwrite the canonical immutable tag with unmerged code. Gate it to push (already master-only) or workflow_dispatch explicitly on refs/heads/master; off-master dispatch now falls back to a branch-name + sha tag instead, so manual runs are never tagless and never collide with the canonical tag.
Pin the rtk install.sh download URL to a specific commit SHA rather than the master branch, closing the arbitrary-code-via-master attack vector. The script itself downloads a binary internally, so the final binary is not fully pinned until rtk publishes stable releases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFCiFbv1Cpr7yzCU8ftvzZ
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFCiFbv1Cpr7yzCU8ftvzZ
…rkflows The mitmproxy_version.bats contract test is the only sync enforcement between cli/lib/constants.bash and images/mitmproxy.env, but cli-tests.yml only triggered on cli/** — a bump touching only the env file never ran the test, so image workflows could go green while the CLI-side constant silently drifted out of sync. Also add -m1 to both image workflows' grep of MITMPROXY_VERSION so a duplicate key in the env file can't make the workflow (last-wins) and the contract test (first-wins, -m1) resolve to different values. Fixes #20
The comment claimed rtk "has only dev-* pre-releases" so the binary couldn't be pinned — verified false: `gh api repos/rtk-ai/rtk/releases/latest` returns v0.45.0 with prerelease: false, and that release's target commit is the same b34be37 SHA already pinned for install.sh. rtk's install.sh also supports RTK_VERSION=vX.Y.Z pinning and SHA-256-verifies the downloaded archive against the release's checksums.txt. Add RTK_VERSION=v0.45.0 to the install invocation so both the script (commit SHA) and the binary it fetches (checksum-verified release) are pinned. Extend rtk.bats to assert the emitted block contains RTK_VERSION=v so the binary pin can't silently regress. Also update the README's host-side rtk install instructions to use the same pinned commit + RTK_VERSION instead of pointing at master. Fixes #20
cli/README.md's bump procedure was missing the merge-day race: between merging a version bump and the image workflows publishing the new versioned ghcr tags, `docker compose up` fails loudly with `manifest unknown` for the op/pass providers (provider none is unaffected — it resolves straight to the public mitmproxy/mitmproxy Docker Hub tag). Document waiting for both workflows before standing up or upgrading a secret-provider project. Also fix the bump procedure's image name, which referenced a nonexistent `sandcat-mitmproxy` — the published images are `sandcat-mitmproxy-op` and `sandcat-mitmproxy-pass`. Soften "immutable" to reflect that it's a convention, not a guarantee: a dispatch on master can re-push the same versioned tag, and registry tags aren't cryptographically frozen without digest pinning. Fixes #20
These are working documents from the spec-driven-development process, not project documentation — drop them before merge.
The "Note: Cursor agent support..." paragraph sat between the pinning docs and the init-command reference since before this branch, but inserting the mitmproxy bump procedure directly above it made it read as part of the merge-day-race guidance. Move it into the `sandcat init devcontainer` section, where `--agent cursor` is documented and the note actually applies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFCiFbv1Cpr7yzCU8ftvzZ
pass-cli.env sits next to its single consumer's Dockerfile; mitmproxy.env is consumed by BOTH image builds, so it lives in their common parent. Spell out the placement rule so the asymmetry doesn't read as an accident. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFCiFbv1Cpr7yzCU8ftvzZ
Per review: drop the dual-channel design where ghcr `latest` tracked upstream mitmproxy:latest via the weekly cron. That kept the exact supply-chain exposure this PR closes alive for already-generated projects, which reference ghcr `latest`. Now every published image contains the pinned mitmproxy from images/mitmproxy.env, and `latest` simply means the newest master build — so pre-pinning projects get the pinned base for free, and a Dockerfile hotfix reaches them on the next master push instead of the next cron. The weekly cron remains as a build-only canary against upstream mitmproxy:latest: a red run flags a breaking upstream release, and nothing is ever pushed from it. Docs updated to match (cli/README bump procedure, stale `:latest` mention in the upstream-CA section). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
You're right — and your version is actually safer, not just simpler. The dual-channel design kept ghcr Reworked as you suggested: every published image now contains the pinned mitmproxy (from |
9fc01e4 to
762ee63
Compare
Closes #20.
Summary
Removes `latest`-tag supply-chain exposure from sandcat's security-critical image chain. The mitmproxy image sees all agent plaintext and holds real secret values — today every `docker compose pull` silently swaps it for whatever upstream published, and the weekly CI cron overwrites the ghcr `latest` tags that generated projects reference.
What's pinned now (to 12.2.3):
Two-file source of truth, contract-enforced: `SCT_MITMPROXY_VERSION` in `cli/lib/constants.bash` (ships with the CLI) + `MITMPROXY_VERSION` in `images/mitmproxy.env` (consumed by CI). A contract test (`cli/test/compat/mitmproxy_version.bats`) fails CI when they diverge — and `cli-tests.yml` now triggers on `images/mitmproxy.env` too, so a one-sided bump can't slip through. Bump = 2 lines, documented in `cli/README.md`.
CI channel policy (traced across all 5 event buckets in review):
Deliberate design choices (for reviewer visibility)
Merge-day sequencing (important)
The ghcr `12.2.3` tags publish only AFTER this merges (the image workflows trigger on this push). Until both workflows go green (~5-25 min), a freshly generated secret-provider project fails `docker compose up` with a loud, retry-safe `manifest unknown`. Provider `none` is unaffected (the public Docker Hub tag exists). Documented in the bump procedure.
Post-merge checklist: confirm both image workflows published `sandcat-mitmproxy-op:12.2.3` and `-pass:12.2.3`, then smoke-test a 1password-provider project.
Test plan
🤖 Generated with Claude Code
https://claude.ai/code/session_01DFCiFbv1Cpr7yzCU8ftvzZ