Skip to content

fix(ci): resolve what a command actually executes before matching it in the publication-order guard #2873

Description

@devantler

🤖 Generated by the Agentic Engineer

Evidence

Two P1s raised by Codex against #2848 at head d6401f9c, both confirmed. They are one family:
the guard resolves a command by the word written, not by what bash will actually execute.

1. hash -p rebinds a required tool. A signing block can run
hash -p /usr/bin/true cosign before an otherwise valid
cosign sign --yes --recursive "${REF}". Bash's help hash documents -p pathname as using that
pathname for the named command, so the "signing" call executes true, returns success, and
reconciliation proceeds with no signature published. The alias vector is already hardened here;
the command hash table is the same bypass through a different mechanism and is currently untracked.

2. An interpreter-wrapped workload operation is invisible. If a step invokes the wrapper as
bash ./scripts/run-ksail-prod-with-pull-auth.sh workload push, bash runs that script with those
arguments, but the matcher reads the executable as bash and omits the operation entirely. An
existing direct push then satisfies the pre-evidence marker while a second, interpreter-wrapped
push of modified manifests
happens after signing and attestation — and the recognised reconcile
releases it. That is the ordering guarantee inverted while every check passes.

Why these two together

Both are "the token in the source is not the operation that runs". Fixing them separately would
restate the same missing predicate twice, exactly as the four value-assertion findings in #2870 do one
level up. The natural shape is a single command-resolution step used by every matcher: resolve what a
command word will actually execute (rejecting rebinding of a required tool) and unwrap known
interpreters (bash/sh + script path) before matching an operation.

Scope note

#2848 closes the ordering axis, the two shell-parsing bypasses, and (at 7376dfba) nameref aliasing
of a protected variable. This is the command-resolution axis and is tracked here rather than
extending a PR already well past a dozen review rounds. Sibling: #2870 (value assertions).

Acceptance criteria

  • Rebinding a required executable via the command hash table is rejected, with a test using
    hash -p before the signing call.
  • A workload operation invoked as bash <script> workload <op> (and sh) is recognised as that
    operation, with a test proving a post-evidence interpreter-wrapped push is caught.
  • One shared command-resolution helper, rather than a check per matcher.
  • Over-tightening controls: .github/actions/deploy-prod/action.yml still validates clean, and a
    legitimate bash -c that is not a workload operation is not misread as one.
  • Each rejection test proven to fail when its own assertion is removed.

Size: medium — one resolution helper, matcher call sites, and their tests.

Part of #2627. Raised on #2848.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    Status
    ✅ Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions