Skip to content

feat(workflow-lint): fail when a job holding a secret installs tooling - #147

Merged
Jaro-c merged 1 commit into
mainfrom
ci/121-tooling-isolation
Aug 25, 2026
Merged

Jaro-c merged 1 commit into
mainfrom
ci/121-tooling-isolation

Conversation

@Jaro-c

@Jaro-c Jaro-c commented Aug 25, 2026

Copy link
Copy Markdown
Member

Enforces the rule added to standards/ci in Glyndor/ai-context#125. Acts on Glyndor/apt#121.

The rule

A job that installs third-party tooling must not hold a secret. cargo install executes build.rs for every crate it resolves; go install, gem install and an unpinned pip install are the same shape.

pip install --require-hashes is exempt — it fixes the exact artifacts, so nothing is resolved. cargo install --locked is not: it pins the tool's version and then builds the tool's own tree.

What it actually defends against

Worth stating precisely, because the obvious framing is wrong and I reached for it first. Secrets are injected per step, not per job — a job with no env: block does not place ${{ secrets.X }} in the environment of a step that does not name it. So a build script at an install step cannot read a key that a later step declares.

What it can do is persist: alter a script or a requirements file in the workspace that the signing step then executes with the key in its environment. Splitting the job removes the shared workspace, and that is the part that carries. The comment above the step says this, so nobody applies the rule to the wrong threat.

Why it lives here

workflow-lint is the reusable that already runs actionlint over the caller's workflows, so the caller is checked out and every consumer gets the rule without per-repo work.

Not breaking anyone today

Gated behind tooling-isolation, type: boolean, default: true — the same shape as check-exec-bit in shell-ci.yml. Three repositories do not meet it: epistle (1), helmly (2), helmly-agent (2). They meet it when they next bump their pin deliberately, which is this organisation's pattern for a new check rather than an accident.

Verified by running it

Against every cloned repository in the organisation:

github-config podup apt homebrew-tap scoop-bucket   exit=0
epistle-panel klyradb unitpm authcore specio        exit=0
epistle                                             exit=1   1 violation
helmly · helmly-agent                               exit=1   2 each

Those findings match, one for one, an independent sweep run before this check existed — same files, same jobs, same lines. Two instruments agreeing is worth more than either alone.

Also exercised on a fixture covering the three edge cases, since a check nobody has watched fail is not a check:

fixture expected result
secret + pip install --require-hashes pass exit 0
secret + pip install cryptography fail exit 1, annotated
cargo install with no secret pass exit 0

The annotation says what to do, not only what is wrong:

job build installs third-party tooling while holding a secret: cargo install cross --locked — see Glyndor/apt#121. Fix by moving the install into a job that holds no secrets, or pin it by hash.

actionlint clean. Reusable docs regenerated.

One implementation note

The script is a heredoc rather than python3 -c. Inside a YAML run: | block, YAML strips the block indentation before bash sees it, so an opener and terminator at the same indentation both arrive at column 0 and the heredoc closes correctly — I verified that specifically, having previously mis-diagnosed the opposite. Multi-line python3 -c would have been the actual trap: automatic dedent of -c source arrived in CPython 3.13 and the runner ships 3.12.

`cargo install` executes `build.rs` for every crate it resolves — installing a
tool is running third-party code. Keeping secrets out of the job that does it
bounds what that code can reach.

The defence is narrower than it first looks, and the comment in the step says
so. Secrets are injected per step, not per job, so a build script cannot read a
key that a later step declares. What it can do is alter a script or a
requirements file in the workspace that the signing step then executes with the
key in its environment. Splitting the job removes the shared workspace.

`pip install --require-hashes` is exempt: it fixes the exact artifacts, so
nothing is resolved. `cargo install --locked` is not — it pins the tool's
version and then builds the tool's own tree.

Gated behind `tooling-isolation`, default true, the same shape as
`check-exec-bit` in shell-ci. Three repositories do not meet it today —
epistle, helmly and helmly-agent — and they meet it when they next bump their
pin deliberately, which is how a new check reaches consumers here.

Verified against every cloned repository in the organisation: the five that
were expected to pass do, and the three known violators produce exactly the
findings an independent sweep had already recorded — one in epistle, two each
in helmly and helmly-agent. Also exercised on a fixture covering the three
edge cases: hash-pinned install with a secret passes, unpinned install with a
secret fails, install without a secret passes.

Refs Glyndor/apt#121

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
@Jaro-c
Jaro-c merged commit ace85e7 into main Aug 25, 2026
9 checks passed
@Jaro-c
Jaro-c deleted the ci/121-tooling-isolation branch August 25, 2026 04:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant