Repository navigation
feat(workflow-lint): fail when a job holding a secret installs tooling - #147
Merged
Merged
Conversation
`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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Enforces the rule added to
standards/ciin Glyndor/ai-context#125. Acts on Glyndor/apt#121.The rule
A job that installs third-party tooling must not hold a secret.
cargo installexecutesbuild.rsfor every crate it resolves;go install,gem installand an unpinnedpip installare the same shape.pip install --require-hashesis exempt — it fixes the exact artifacts, so nothing is resolved.cargo install --lockedis 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-lintis 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 ascheck-exec-bitinshell-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:
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:
pip install --require-hashespip install cryptographycargo installwith no secretThe annotation says what to do, not only what is wrong:
actionlintclean. Reusable docs regenerated.One implementation note
The script is a heredoc rather than
python3 -c. Inside a YAMLrun: |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-linepython3 -cwould have been the actual trap: automatic dedent of-csource arrived in CPython 3.13 and the runner ships 3.12.