Skip to content

chore(ci): trim GITHUB_TOKEN permissions - #777

Merged
olivermeyer merged 1 commit into
mainfrom
chore/trim-github-token-permissions
Nov 13, 2026
Merged

olivermeyer merged 1 commit into
mainfrom
chore/trim-github-token-permissions

Conversation

@olivermeyer

Copy link
Copy Markdown
Collaborator

Why?
Many jobs grant GITHUB_TOKEN scopes that no step uses. A compromised action or dependency in such a job can use an unused write scope to push, release, or comment. This is the Security Day measure "Set least-privilege GITHUB_TOKEN permissions in workflows".

How?
Each job now declares only the scopes that its steps use, and each caller of a reusable workflow grants exactly what the callee declares. The release workflows move their workflow-level block into the job and set permissions: {} at workflow level. Each remaining write line has a comment that names the step that needs it.

Changed scopes

Workflow Job Scope Old New Reason
_lint.yml, _docs.yml, _audit.yml lint, docs, audit id-token write removed no google-github-actions/auth or other OIDC consumer. Steps are checkout, setup-uv, apt, make.
_lint.yml, _docs.yml, _audit.yml lint, docs, audit packages read removed no pull from ghcr.io or GitHub Packages
ci-cd.yml lint, docs, audit (callers) id-token, packages write, read removed trimmed to the callee blocks
_test.yml test attestations write removed no actions/attest-* step
_test.yml test id-token write removed no OIDC consumer. Codecov uses CODECOV_TOKEN (use_oidc not set). SonarQube uses SONAR_TOKEN.
_test.yml test packages read removed no pull from ghcr.io
_test.yml generate-matrix contents read removed ({}) no checkout and no API call. The step only writes $GITHUB_OUTPUT.
ci-cd.yml test (caller) attestations, id-token, packages write, write, read removed trimmed to the callee block
_codeql.yml analyze actions read removed the template comment says it is only necessary in private repositories. This repository is public.
_codeql.yml analyze packages read removed no private CodeQL packs configured
ci-cd.yml, codeql-scheduled.yml codeql (callers) actions, packages read removed trimmed to the callee block
ci-cd.yml sonarcloud pull-requests write removed the job has only a checkout step
ci-cd.yml sonarcloud statuses write removed the job has only a checkout step
_ketryx_report_and_check.yml ketryx_report_and_check contents write read no push or release. The Ketryx action authenticates with KETRYX_API_KEY and has no token input. download-artifact in the same run uses the runtime token.
_ketryx_report_and_check.yml ketryx_report_and_check packages read removed no pull from ghcr.io
ci-cd.yml ketryx_report_and_check (caller) attestations, id-token, packages write, write, read removed not declared by the callee
ci-cd.yml ketryx_report_and_check (caller) contents write read trimmed to the callee block
_package-publish.yml build_native attestations, id-token, packages write, write, read removed no attest step, no OIDC consumer, no ghcr.io pull
_package-publish.yml package_publish packages read removed no pull from ghcr.io. uv publish uses UV_PUBLISH_TOKEN.
ci-cd.yml package_publish (caller) attestations, id-token, packages write, write, read removed not declared by the callee
_build-native-only.yml build_native attestations, id-token, packages write, write, read removed no attest step, no OIDC consumer, no ghcr.io pull
build-native-only.yml only_build_native (caller) attestations, id-token, packages write, write, read removed trimmed to the callee block
build-native-only.yml only_build_native (caller) contents write read the callee declares contents: read
build-native-only.yml check-trigger-condition contents read removed ({}) no checkout and no API call
labels-sync.yml sync id-token write removed no OIDC consumer
labels-sync.yml sync pull-requests write removed the labeler needs issues: write only
_scheduled-audit.yml, scheduled-audit-hourly.yml audit-scheduled id-token write removed no OIDC consumer
_scheduled-test-hourly.yml, _scheduled-test-stress.yml and their callers test id-token write removed no OIDC consumer
_scheduled-test-daily.yml and its two callers test-scheduled-daily attestations, id-token, packages write, write, read removed same steps as _test.yml test
_claude-code.yml claude-code contents write read claude-code-action does all GitHub writes with the Claude App token from the OIDC exchange (log: "Using GITHUB_TOKEN from OIDC").
_claude-code.yml claude-code pull-requests write read same as above. The action gives the workflow token only to its github_ci MCP server, which calls pulls.get.
_claude-code.yml claude-code issues write removed same as above. The github_ci MCP server does not read issues.
claude-code-interactive.yml, claude-code-automation-pr-review.yml, claude-code-automation-operational-excellence-weekly.yml callers all no block callee block a reader now sees the grant where the call is
merge-release.yml, prepare-release.yml, publish-release.yml workflow level contents read {}, moved into the job a job added later starts from no scopes

Kept

  • _package-publish.yml package_publish and the ci-cd.yml caller: contents: write for gh release create and gh api repos/.../dispatches.
  • _codeql.yml analyze and both callers: security-events: write for github/codeql-action/analyze.
  • labels-sync.yml sync: issues: write for crazy-max/ghaction-github-labeler.
  • _claude-code.yml claude-code and its three callers: id-token: write for the OIDC exchange of anthropics/claude-code-action.

Not changed

These are pre-existing linter findings on lines this PR does not touch:

  • zizmor excessive-permissions on line 1 of 25 workflows: there is no workflow-level permissions: block. Every job declares its own block.
  • zizmor findings of other audits (artipacked, anonymous-definition, template-injection and more). The total fell from 221 to 188. No new finding.
  • actionlint: uses: $/.github/actions/run-tests in _test.yml and _scheduled-test-daily.yml has an invalid format.
  • actionlint: inputs.commit_message is not defined in _scheduled-test-daily.yml.
  • actionlint: github.head_ref goes directly into an inline script in _ketryx_report_and_check.yml.

How to check

  • This PR runs lint, docs, audit, test, CodeQL, sonarcloud and Ketryx with the new blocks.
  • package_publish runs only on a v* tag. build-native-only runs only with the build:native:only marker.
  • The Claude workflows do the OIDC exchange only when the workflow file is identical to the one on main. Thus, the first check is the first Claude run after the merge. If that run fails, revert the _claude-code.yml part.

🤖 Generated with Claude Code

Each job now declares only the GITHUB_TOKEN scopes that its steps use.
Remove id-token, attestations and packages grants that no step uses.
Downgrade contents and pull-requests writes where the steps use an
App token or only read. Comment each remaining write with its step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@olivermeyer olivermeyer added the skip:test:long_running Skip long-running tests (≥5min) label Nov 13, 2026
@ox-security

ox-security Bot commented Nov 13, 2026

Copy link
Copy Markdown

OX Security Logo

OX Security reviewed this pull request — nothing to fix.

✔ No issues found

Branch chore/trim-github-token-permissions → main

View scan in OX Security →

@olivermeyer
olivermeyer marked this pull request as ready for review November 13, 2026 13:32
@olivermeyer
olivermeyer requested a review from a team as a code owner November 13, 2026 13:32
@olivermeyer
olivermeyer enabled auto-merge (rebase) November 13, 2026 13:34
@codecov

codecov Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

❌ Your project check has failed because the head coverage (63.78%) is below the target coverage (70.00%). You can increase the head coverage or adjust the target coverage.

❗ There is a different number of reports uploaded between BASE (ad3eb23) and HEAD (48dc636). Click for more details.

HEAD has 2 uploads less than BASE
Flag BASE (ad3eb23) HEAD (48dc636)
3 1

see 17 files with indirect coverage changes

@sonarqubecloud

Copy link
Copy Markdown

@olivermeyer
olivermeyer merged commit d0eee9f into main Nov 13, 2026
27 of 29 checks passed
@olivermeyer
olivermeyer deleted the chore/trim-github-token-permissions branch November 13, 2026 13:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip:test:long_running Skip long-running tests (≥5min)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants