Skip to content

ci(security): publish a deliberately unsigned probe image for the enforcement probe - #3775

Merged
devantler merged 4 commits into
mainfrom
claude/unsigned-probe-throwaway-image-3336
Sep 13, 2026
Merged

devantler merged 4 commits into
mainfrom
claude/unsigned-probe-throwaway-image-3336

Conversation

@devantler

Copy link
Copy Markdown
Contributor

🤖 Generated by the Agentic Engineer

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

…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>
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

@devantler I will perform a full review of PR #3775.

⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 53 minutes.

@coderabbitai

coderabbitai Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 56 minutes.

Check out review usage here.

View limit details

Limit 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.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 1b85a8eb-2405-478a-a557-87f77a477fa0

📥 Commits

Reviewing files that changed from the base of the PR and between 5599d0d and f7ba468.

📒 Files selected for processing (9)
  • .github/workflows/ci.yaml
  • .github/workflows/probe-image-signature-enforcement.yaml
  • .github/workflows/publish-unsigned-probe-image.yaml
  • scripts/guard-unsigned-probe-image-publisher.sh
  • scripts/probe-image-signature-enforcement.sh
  • scripts/tests/test-guard-unsigned-probe-image-publisher.sh
  • scripts/tests/test-probe-image-signature-enforcement.sh
  • scripts/tests/test-verify-image-unsigned.sh
  • scripts/verify-image-unsigned.sh

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-13T18:58:41.243271Z f7ba468 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: 022cdf0ba0

ℹ️ 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".

… 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>
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

@devantler I will perform a full review of PR #3775.

⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 23 minutes.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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>
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

@devantler I will perform a full review of PR #3775.

⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 57 minutes.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: f7ba468371

ℹ️ 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".

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Readiness at f7ba46837159a4c133f4b866aa9b39a042d4e473

  • Programmatically tested: CI is green at this head. All three suites pass:

    Suite Result
    Publisher guard 38 of 38
    Probe 35 of 35
    New verify-image-unsigned.sh 22 of 22
    • Guard: it rejects every injected unsafe change with that change's own message. It fails if the workflow adds signing, attestation or id-token, or grants extra permissions. It also fails if the main-only check is removed, reordered or inverted, or if the verification call is commented out, wrapped or not first.
    • Verify script: it passes an unsigned image. A signature, attestation or SBOM tag, or a referrer, makes it fail. An auth failure, an unexpected status or a network error comes back as "unknown", and a malformed digest or a missing credential is a usage error.
    • Probe: a control that has been signed since publication, or whose state can't be established, is INCONCLUSIVE and nothing is pulled.
    • Ablations: one per Codex finding. Each ablation breaks exactly the check it targets.
    • Linters: shellcheck and zizmor are clean. actionlint is clean apart from the pre-existing queue: max warning.
  • Reviewed: Codex reviewed this exact head and found no major issues (comment, Reviewed commit: f7ba468371). CodeRabbit was requested first at this head and refused on its hourly quota, so the round advanced to Codex. All 3 earlier Codex findings are fixed and their threads resolved (3 of 3); there are no review-body findings.

  • Tried and evaluated as a user: both workflows only run from main, so neither can be dispatched before this merges. Running them is the next step on Prove an unsigned image is refused at pull, then activate the liveness check #3336. As the operator, I checked what I'd rely on:

    • The publisher refuses any ref but main, pushes only ghcr.io/devantler-tech/unsigned-probe-throwaway under a per-run tag, and prints the digest reference to hand to the probe.
    • The probe accepts the control only by digest, and re-proves it is unsigned just before the node pull. A control signed in between can no longer be read as broken enforcement.
    • The registry token is passed through a private config file, never on a command line.
  • Scope, stated plainly: besides adding the publisher, this changes the already-merged probe from feat(security): add the dormant image-signature enforcement probe #3732 in three ways: the digest-only rule, the re-check before the pull, and packages: read on the probe job. They ship together because the probe re-uses the publisher's verification script. Splitting them would ship a check with no consumer.

  • Accepted residual: a workflow_dispatch run uses the workflow file from the ref it targets, so someone with write access could dispatch from a branch whose copy deletes the main check. That actor could already push arbitrary code. The publisher holds only packages: write for one throwaway package; the probe is also limited by prod's protected-branches-only policy. Closing the publisher fully needs a repository setting, such as a dedicated main-only environment, which is the maintainer's call.

@devantler
devantler marked this pull request as ready for review September 13, 2026 18:59
@devantler
devantler added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit f08d6da Sep 13, 2026
30 checks passed
@devantler
devantler deleted the claude/unsigned-probe-throwaway-image-3336 branch September 13, 2026 19:12
@github-project-automation github-project-automation Bot moved this from 🫴 Ready to ✅ Done in 🌊 Project Board Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

1 participant