fix: scope per-callback permissions to the caller job (least-privilege)#204
Merged
Conversation
Per-callback permissions: maps were accepted, scope-validated, and carried onto the generator graph node, but never rendered on the uses: caller job, so they were a validated no-op. GitHub Actions allows a permissions: key on a job that calls a reusable workflow (jobs.<id>.uses); only runs-on, the concurrency group key, environment, timeout-minutes, steps, env, container, services, and defaults are forbidden there. Render the callback Permissions map as a sorted job-level permissions: block at every caller-job emission site (orchestrate jobs and their retry shims, promote deploy/prod/external/rollback jobs, rollback deploys, hotfix builds). runs-on and concurrency stay omitted on callbacks, matching existing behavior. Rendering the map enables least-privilege per callback, id-token: write for cloud OIDC, and attestations: write for build provenance. Closes #182 Signed-off-by: Joshua Temple <joshua.temple@stablekernel.com>
Stop unioning callback permissions into the workflow top-level block. Each callback's declared scopes are now rendered only on its own reusable-workflow caller job, so the GITHUB_TOKEN is scoped to least privilege per callback and the OIDC blast radius stays confined to the one job that needs id-token: write. The top-level permissions block now carries only cascade's own orchestration scopes. Job-level permissions replace the workflow default rather than merging, so a callback's map is the complete set for that job; cascade emits exactly what is declared. Signed-off-by: Joshua Temple <joshua.temple@stablekernel.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.
Problem
mainalready grants per-callbackpermissions:(e.g.id-token: writefor OIDC) by unioning every callback's permission map into the top-levelpermissions:block of the generated workflow. That works, but it over-grants: every job in the workflow receives the elevated scopes, not just the one caller job that needs them. For OIDC this widens the blast radius well beyond the callback that requestedid-token: write.Fix
Scope each callback's declared permissions to its own reusable-workflow caller job instead of the top level:
permissions:block now carries only cascade's own orchestration scopes (the historical base). Callback scopes are no longer merged in, so thecollectCallbackPermissionsunion helper and thecallbackUnionparameter ofwriteTopLevelPermissionsare removed (no dead code).permissions:map gets a job-levelpermissions:block with exactly the declared scopes, sorted for deterministic output. A callback that declares nothing gets no job-level block and inherits the top-level base, so existing manifests are byte-identical.permissions:on a reusable-workflowuses:caller job, and actionlint accepts it. The earlier wording claiming GitHub forbids it has been corrected.Semantics
Job-level permissions replace the workflow default rather than merging. A callback's
permissions:map is therefore the complete permission set for that job. cascade emits exactly what is declared and never injects an implicit scope (no autocontents: read). The configuration docs note this so users includecontents: readfor checkout andid-token: writefor OIDC.Verification
go build ./...,go test ./...(1402 passing),golangci-lint run ./...all green.callback-permissions-oidc.yamlscenario rewritten to assert the job-level block carries the declared scopes and the top-level block excludes them.permissions:block.Closes #182