Repository navigation
ci(security): publish a deliberately unsigned probe image for the enforcement probe - #3775
Conversation
…orcement probe The signature-enforcement probe needs a first-party image that carries no signature, and none exists. Add a dispatch-only workflow that builds a single-file image from scratch and pushes it to ghcr.io/devantler-tech/unsigned-probe-throwaway with a fresh tag per run, without signing it, then checks anonymously that the digest is readable and has no signature or referrers. A guard, run unconditionally in CI, keeps it that way: no id-token permission, no action but runner hardening, no secret but GITHUB_TOKEN, no signing or attestation tooling, dispatch as the only trigger, and no file outside a fixed set referencing the image. Its test covers each check with a negative control asserting that check's own message. Part of #3336 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A newly published GHCR package is private by default, so the anonymous post-push reads would fail on the first run. The probe does not need the package public: nodes pull private first-party images with their registry credential. Read the digest with the job's GITHUB_TOKEN instead; packages: write already grants read, so no permission is added. The check fails closed: a missing digest, a token that cannot read the digest itself, a signature, attestation or SBOM tag, a referrer, or any registry answer other than a clear 404 stops the run. Signature tags are probed directly rather than through a tag list, so a paginated list cannot hide one. The guard now allows only the packages permission and requires that step to exist after the push, read signature tags and referrers, authenticate with GITHUB_TOKEN, and never continue on error. Its check for enabled provenance or SBOM output matches the build flag rather than the bare word, since the verification names the .sbom tag. Each new rule has an injected test case. Part of #3336 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai full review |
|
|
|
Warning Review limit reachedNext included review available in 56 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Repository YAML (base), Organization UI (inherited) Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (9)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. Another round soon, please! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
… but main A workflow_dispatch run can target any branch or tag, so the publisher could build and push from unreviewed code that the pull-request guard never saw. Its first step now refuses to run unless GITHUB_REF is refs/heads/main, before any build, login or push. The workflow checks out no code, so there is no checkout ref to pin. A dispatch runs the workflow file of the ref it targets, so this stops a ref that still carries the reviewed file; limiting who can dispatch covers a ref whose copy deletes the check. The guard now requires that refusal as the first step: a plain run step with no uses, if or continue-on-error, holding the exact != comparison and exit 1. Test cases cover removing it, moving it after the push, inverting the comparison and letting the job continue past it. Writing that check exposed a fail-open in how the guard used yq: combining conditions with `and` inside a single select matched steps it should not. The new check passed with the pin removed, and the existing verification check counted every step, including one without a run, as the verification step. Both checks now read one fact per yq call and decide in bash. The verification check reports a step before the push with its own message, and the removal test asserts the specific "no step reads" message, so neither can pass on another check's output. Part of #3336 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai full review |
|
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 37403f3b9a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ep the token off argv Three review findings on the unsigned probe image. The negative control could go stale between publish and probe: the two are separate runs, so a package writer could retarget the tag or sign the image in between, and the probe would then report broken enforcement. The probe now requires the unsigned ref pinned by digest and, immediately before the node pull, re-checks that the digest still has no signature, attestation or SBOM tag and no referrer. Anything short of confirmed unsigned is INCONCLUSIVE and nothing is pulled. The probe job gains packages: read and a GITHUB_TOKEN credential for that check. The bearer token was expanded into curl's -H argument, where the process table shows it. Both the password and the token now travel only in a curl config file inside a private temporary directory removed by a trap, as the existing inventory script does. The guard matched fragments of the verification step's text, so a step with its checks commented out still passed. The registry verification now lives in scripts/verify-image-unsigned.sh, shared by the publisher and the probe and pinned by a fake-curl suite: unsigned passes; a signature tag, attestation, SBOM or referrer fails; an auth failure, unexpected status, malformed response or transport error is unknown; a malformed digest or missing credential is a usage error; and the credential never appears in curl's argv. The guard now requires the step to run `set -euo pipefail` followed by the exact script call, so a commented-out, ignored or conditional call does not count, and the step may not set if or continue-on-error. Part of #3336 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai full review |
|
|
@codex review |
|
Codex Review: Didn't find any major issues. Keep them coming! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Readiness at
|
Why
The probe that proves the cluster refuses unsigned first-party images cannot run: it needs an unsigned image to try, and every first-party image is signed. Without a negative control, the refusal is still asserted rather than observed.
What
Adds a manual-only workflow that publishes a deliberately unsigned throwaway image (
ghcr.io/devantler-tech/unsigned-probe-throwaway), a fresh tag per run, and confirms after publishing that it is publicly readable and carries no signature. A CI guard keeps it unsigned and unused by anything else, and the probe's instructions now name it. Nothing is published until someone runs the workflow.Part of #3336
🤖 Generated with Claude Code