Repository navigation
Least privilege for the workflow tokens, and a test that holds it - #74
Conversation
Correction: this change broke the FIPS leg, and the fix is in
Before this PR, What that looked like from the outside:
Two checks disagreeing about whether an image exists is what an authorization failure looks like when the API answers "not found" instead of "not allowed". Both jobs now declare This is the argument for the required checks being what they are — it was caught on the branch, not after merge. |
OpenSSF Scorecard scored this repository zero on Token-Permissions. The reason was not subtle: gate.yml and package.yml declared no top-level permissions at all, so both ran with whatever the repository default happened to be, and fips-image.yml granted packages: write at the top level, where it applies to every job the workflow will ever have rather than to the one that pushes an image. All three now declare contents: read at the top. The two jobs that genuinely need to write say so themselves, next to the step that uses it: the attesting job in package.yml, and the image job in fips-image.yml. The test is the point of the commit rather than a companion to it. A top-level write costs nothing visible when it is forgotten: nothing fails, the token is simply broader than the work needs, and the next workflow added inherits the habit. So the property is held by the build. Every file under .github/workflows must declare a top-level permissions block and it must grant no write, with the population read from git ls-files so a workflow added without a thought for it is caught by its own existence. Demonstrated red by removing the block from gate.yml, which failed the test with the message a reader would need. The remaining Scorecard findings are not silently accepted: Code-Review and Branch-Protection both need someone other than the author approving changes, which R22 already tracks; Maintained wants ninety days of history the repository does not have yet; Signed-Releases is slice 101. Signed-off-by: Ayla Croft <aylacroft@proton.me>
…k away The previous commit narrowed every gate job's token to contents: read. Before it, the jobs inherited the repository default, which grants read on every scope, so fips-tag and fips silently depended on packages: read and lost it. The failure did not look like an authorization failure, which is why it is worth the comment now in the file. fips-tag logs in to the registry and polls for the FIPS image; with no packages scope the image is simply invisible to it, so it polled for the full thirty minutes and failed with 'was not published' while the fips-image workflow had already reported the same tag as present and skipped its build. Two checks disagreeing about whether an image exists is what this looks like from the outside. Both jobs now declare contents: read and packages: read. The fips job needs it as much as fips-tag does: the runner pulls its container before the first step runs, so without the scope the job fails with 'manifest unknown' and no output. Caught on the branch by the gate rather than after merge, which is the argument for the required checks being what they are. Signed-off-by: Ayla Croft <aylacroft@proton.me>
f519564 to
cf91290
Compare
OpenSSF Scorecard scored this repository zero on
Token-Permissionsin its first ever successful run. The cause was not subtle.gate.ymlpermissions:at allcontents: readpackage.ymlpermissions:at allcontents: read; the attesting job keeps its ownid-token/attestationswritefips-image.ymlpackages: writeat the top levelcontents: read;packages: writemoved to theimagejobA workflow with no
permissions:block runs with whatever the repository default happens to be. A write granted at the top level is granted to every job the workflow will ever have, including the ones nobody has written yet. Neither is a theoretical problem in a project whose own security model is built on least privilege for effects.The test is the point of this change
A forgotten top-level write costs nothing visible: nothing fails, the token is simply broader than the work needs, and the next workflow added inherits the habit. That is exactly the class of thing this project holds in the build rather than in a reviewer's attention.
test/workflows_test.exsgains a third property beside the two it already held (everything parses; every action pinned to a full commit SHA): every file under.github/workflowsdeclares a top-levelpermissions:block, and it grants no write. The population comes fromgit ls-files, so a workflow added without a thought for it is caught by its own existence.Demonstrated red rather than asserted. With the block removed from
gate.yml:The other Scorecard findings, not silently accepted
docs/06-risk-register.mdR22 already tracks this and states the lift condition.sobelow, which runs on every commit.Evidence
mix gateexit 0; 576 passed, 18 excluded; coverage 80.31%.actionlintclean on all four workflows (two pre-existing shellcheck style notes, untouched by this change).