Skip to content

Prove an unsigned image is refused at pull, then activate the liveness checkΒ #3336

Closed
2 of 2 issues completed
Closed

Description

@devantler

πŸ€– Generated by the Agentic Engineer

Evidence

#3101's whole premise is that this cluster's supply-chain posture is asserted but not enforced. A wired verifier is not proof it enforces β€” that is exactly the inference #3101 was filed to stop being made.

Expected outcome

Refusal is observed, not reasoned about, and the daily liveness signal is switched on so the state cannot silently regress.

Acceptance criteria

  • A known-unsigned image matching a rule is pulled on a real node and observed to be refused (ImagePullBackOff), with the containerd-side reason captured. This is the "verify it actually works" step: static validation and a configured bin_dir prove neither.
  • The probe uses a deliberately unsigned throwaway image, never a production workload, so the proof cannot itself cause an outage.
  • A correctly signed image is pulled in the same session and succeeds, so the refusal is attributable to the signature and not to a broken verifier refusing everything.
  • .github/workflows/validate-image-verifier-liveness.yaml has its daily schedule trigger restored and the workflow_dispatch-only guard dropped from the fleet job, and its first scheduled run is green.

Why activation is last

The liveness check ships dormant in #3103 because its verdict is red until enforcement lands, and a permanently-red main spends the "something broke, fix it now" signal on a state this epic already tracks. It is switched on once the verdict is expected to be green.

Part of #3101.


Current prerequisite and remaining work

The inventory prerequisite #3334 is CLOSED. Authenticated workflow run 34045065606 completed successfully on 2026-09-06 at ac65b4a7163b50b988fe09835b760f09917b9d8e; both inventory and verifier-liveness jobs passed, and inventory reported PASS=5 FAIL=0 UNKNOWN=0. Its earlier workflow-dispatch blocker under devantler-tech/monorepo#3073 is superseded by that run. Keep the native dependency as the record of the prerequisite, but do not treat the closed issue as outstanding work.

The next implementation must provide a deliberately unsigned throwaway image and a correctly signed positive control, exercise the node pull layer directly so admission rejection cannot masquerade as node refusal, and define cleanup. Revalidate the inventory against the selected production state before the probe. Restore the daily schedule only after observed refusal and the signed control succeed, then verify the first scheduled run.

Blocker: production workflow_dispatch re-run of the signed-control image pull probe after #3783, which scheduled runs are not permitted to trigger | authority | last-verified 2026-09-24: unsigned refusal observed 2026-09-13, signed control still needs a clean uncached observation | asked slack 2026-09-14

Activity

  1. moved this to πŸ“₯ Backlog in 🌊 Project Boardon Aug 23, 2026
  2. self-assigned this
    on Aug 26, 2026
  3. added a commit that references this issue on Aug 26, 2026
  4. devantler commented on Aug 26, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    Findings from an attempt at this issue. Two of them change how the probe has to be built, so
    recording them before the next attempt spends effort on a path that cannot work.

    1. The premise has moved: a matched rule demonstrably DOES produce a decision.
    containerd records a per-image talos.dev/verified label for images Talos verified, and the
    label tracks rule MATCH rather than signedness. Measured across all 7 nodes: 755 of 755 cached
    ghcr.io/devantler-tech/* images carry it; 0 of 1299 cached images with a resolvable
    non-first-party repository name do β€” no exceptions. ghcr.io/fluxcd/flagger-loadtester and
    ghcr.io/cloudnative-pg/cloudnative-pg are both keyless-cosign signed and both unlabelled,
    which is what rules out signedness and leaves rule match as the discriminator. Recorded in
    #3378. This narrows this issue to what it always really was: proving the refusal.

    2. A Pod-API probe can never test this layer. The verify-app-images
    ImageValidatingPolicy is validationActions: [Deny] on pod CREATE cluster-wide, and it mirrors
    these same three rules. An unsigned first-party image submitted as a Pod is therefore rejected
    at admission and never reaches the node pull layer this issue is about. The probe has to
    bypass Kubernetes β€” talosctl image pull against a node is the right instrument.

    3. There is no unsigned first-party image available to probe with. doggy-countdown β€” the
    unsigned example #3101 cites in its Evidence β€” is now signed on every tag (1.0.0, 1.0.1,
    1.0.2, latest, and the three sha-* builds) and passes the catch-all rule identity. That
    evidence is stale. The only unsigned thing under a first-party path is a cosign signature
    artifact (:sha256-<digest>), which is unsigned by construction and matches the catch-all β€”
    usable, but it is an OCI artifact rather than a runnable image, so a pull failure would need its
    reason read carefully to rule out a media-type confound.

    Cleanest path is the one the acceptance criteria already describe: publish a deliberately
    unsigned throwaway image under a first-party path, pull it on a node, and remove it after
    (talosctl image remove). That is a registry publish plus a node-level pull, and the node pull
    is a production mutation β€” outside what this agent may do unattended, so it needs the
    maintainer, or an explicitly granted narrow permission.

    Also worth carrying into the activation decision: of the four first-party images running today,
    ksail and provider-upjet-unifi verify against their exact rule identities. wedding-app and
    ascoachingogvaner are private packages β€” an unauthenticated cosign verify returns DENIED,
    which is an access result, not evidence a signature is missing. Their state is genuinely
    UNKNOWN until something with read:packages checks them, and they are the two images the
    catch-all rule governs. Activating before that is settled risks exactly the ImagePullBackOff the
    BEFORE ACTIVATING note warns about.

  5. added a commit that references this issue on Sep 6, 2026
  6. 5 remaining items

  7. devantler commented on Sep 13, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    Reclassified: this is not blocked on maintainer authority, and the blocked label is removed.

    The maintainer told me on 2026-09-13, in an interactive session, to stop parking work on him and resolve it myself. Re-reading this issue with that in mind, the authority framing does not hold:

    • The probe that obtains the observation already exists, reviewed and merged: probe-image-signature-enforcement.yaml (feat(security): add the dormant image-signature enforcement probeΒ #3732). It runs on one node, needs an exact confirmation phrase, serializes on prod-deploy, and cleans up after itself.
    • Dispatching a reviewed, merged workflow is an ordinary agent action, just like the merge-queue deploy.
    • What actually stops it running today is engineering, not permission: there is no deliberately unsigned throwaway first-party image to use as the negative control (see the 2026-08-26 finding above).

    Plan, in order:

    1. Publish a dedicated, unsigned throwaway image under ghcr.io/devantler-tech/ that matches the catch-all rule and is referenced by nothing else (draft PR, in progress).
    2. Pick a node that has cached neither ref, then dispatch the probe with that image plus a correctly signed first-party image as the positive control.
    3. On PASS, restore the liveness schedule per the acceptance criteria. On FAIL or INCONCLUSIVE, record the verdict here and fix the cause.
  8. devantler commented on Sep 13, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    The refusal probe is running now.

    The probe runs behind any production deploy. If either image is already cached on the node, it reports INCONCLUSIVE rather than a false result. I'll record the verdict here.

    • PASS (unsigned refused, signed pulled): restore the daily schedule on the liveness workflow, per the acceptance criteria.
    • FAIL or INCONCLUSIVE: record why, fix the cause, then run it again.
  9. devantler commented on Sep 13, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    Verdict: PASS. A matched unsigned image is refused at the node pull layer.

    Run: https://github.com/devantler-tech/platform/actions/runs/34777101451 (one production worker, 19:15Z):

    1. Both refs matched the ghcr.io/devantler-tech/* rule and were confirmed absent from the node's content store beforehand, so the pulls were real, not cache hits.
    2. The unsigned control was re-proven unsigned immediately before the pull: no signature, attestation, SBOM tag or referrers.
    3. Negative control: the node refused the unsigned ref for a verification reason.
    4. Positive control: the node pulled the signed ref successfully. The refusal is therefore attributable to the signature, not to a verifier that refuses everything.

    This is the first acceptance criterion, observed rather than reasoned about.

    Remaining: restore the daily schedule on validate-image-verifier-liveness.yaml, drop the dispatch-only guard from the fleet job, and see its first scheduled run green. A draft PR for that is being prepared; this issue closes once that first scheduled run is green.

    Noticed, not blocking: cleanup warned it "could not remove" the unsigned ref from the node. The pull was refused, so nothing should have been stored, and the warning is most likely the probe treating "not present" as a removal failure. There was also a harmless printf: Broken pipe message. Both are tracked separately.

  10. devantler commented on Sep 13, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    The 2026-09-13 probe pass is weaker than it looked.

    • The unsigned refusal is still valid evidence: a freshly published unsigned image was refused at the node for a signature reason.
    • The signed half most likely proved nothing. The probe's cache check fails open on GitHub runners. It pipes the node's image list into an early-exiting search, and because runners ignore SIGPIPE, a match gets reported as "not cached". That run logged the broken-pipe error at exactly that check and then reported both images absent. The unsigned image had just been published, so the match was almost certainly the signed control, which a running workload likely already had on the node. A cached image is not re-pulled, so its "accepted" result says nothing about verification.

    Consequence for this issue: the signed control still needs a clean observation. After #3783 merges, re-run the probe with a signed image the probed node does not already hold. If the node holds it, the probe now stops as inconclusive instead of passing. The daily liveness schedule (#3782) is unaffected, because it doesn't depend on this check.

  11. self-assigned this
    on Sep 30, 2026
  12. devantler commented on Sep 30, 2026

    @devantler
    ContributorAuthor

    πŸ€– Generated by the Agentic Engineer

    All acceptance criteria are now observed. Production probe 36783104809 passed on the reviewed current main revision:

    • Both controls selected the same running verification rule and were confirmed absent from the node's content store before either pull.
    • The disposable negative control was rechecked as unsigned immediately before pulling. The node refused it for a signature-verification reason.
    • The signed application control pulled successfully in the same session. This excludes a rule that refuses every image it governs.
    • Probe cleanup completed successfully.

    The daily liveness schedule is already active from the merged activation change. The latest three scheduled runs are green, including 36670771496 on September 30. The old cached-control evidence and workflow-dispatch blocker are superseded by this run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Fields

No fields configured for Security.

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions