Skip to content

hardening: lock GITHUB_TOKEN to contents: read across all CI jobs - #337

Open
Keengfk wants to merge 1 commit into
stellar-vortex-protocol:mainfrom
Keengfk:hardening/ci-minimum-token-permissions
Open

hardening: lock GITHUB_TOKEN to contents: read across all CI jobs#337
Keengfk wants to merge 1 commit into
stellar-vortex-protocol:mainfrom
Keengfk:hardening/ci-minimum-token-permissions

Conversation

@Keengfk

@Keengfk Keengfk commented Aug 31, 2026

Copy link
Copy Markdown

Without an explicit permissions: block, every job in ci.yml ran with GitHub's
default token scopes — which include contents: write on push events from the
same repository. None of the six jobs here (formatting, tests, wasm-size check,
proptest, dependency audit, mutation testing) need write access to anything. A
compromised third-party action (Swatinem/rust-cache, dtolnay/rust-toolchain) or
a malicious step in a PR-triggered run would have had unnecessary repository
write privilege available to abuse.

What changed

.github/workflows/ci.yml

Added a top-level permissions: contents: read block immediately after the on:
trigger. The block header explains:

  • why contents: read is sufficient for all current jobs
  • why GITHUB_STEP_SUMMARY writes (used by the wasm-size job) are local runner
    file writes and don't require a GitHub API permission
  • that for push events from the same repo, this overrides the otherwise-default
    contents: write; for fork pull_request events GitHub already enforces
    read-only, so this covers both trigger types consistently
  • how to add a narrowly-scoped job-level permissions: override for any future
    job that genuinely needs more (e.g. pull-requests: write to post a PR comment)
    — without widening the workflow-level block

Each of the six jobs also got an inline # Needs: comment documenting its
specific requirements so the rationale is visible at the job level, not just at
the top of the file.

CONTRIBUTING.md

Added a "GITHUB_TOKEN permission model" section to the Maintainer Guide with:

  • a per-job audit table confirming each job's minimum required scope
  • the rationale for the explicit block (default scopes, both trigger types)
  • a concrete example of how to add a job-level override for a future job that
    needs elevated scope

No logic changed

All six CI jobs run identically with the narrowed token. None of them call any
GitHub API beyond checkout (actions/checkout uses contents: read to clone the
repository). The change is purely additive — one YAML key and documentation.

Audit summary

┌───────────┬──────────────────────────────────────────────┬────────────────┐
│ Job │ Operations │ Scope needed │
├───────────┼──────────────────────────────────────────────┼────────────────┤
│ fmt │ checkout, cargo fmt --check │ contents: read │
├───────────┼──────────────────────────────────────────────┼────────────────┤
│ contract │ checkout, clippy, test │ contents: read │
├───────────┼──────────────────────────────────────────────┼────────────────┤
│ wasm-size │ checkout, build, write to │ contents: read │
│ │ $GITHUB_STEP_SUMMARY (local file) │ │
├───────────┼──────────────────────────────────────────────┼────────────────┤
│ proptest │ checkout, cargo test │ contents: read │
├───────────┼──────────────────────────────────────────────┼────────────────┤
│ audit │ checkout, cargo audit (RustSec over HTTPS) │ contents: read │
├───────────┼──────────────────────────────────────────────┼────────────────┤
│ mutants │ checkout, cargo mutants (runner-local temp │ contents: read │
│ │ copy) │ │
└───────────┴──────────────────────────────────────────────┴────────────────┘

closes #278

Add an explicit top-level permissions block to ci.yml and document the
per-job audit that justifies the chosen scopes.

Problem: no permissions: block meant every job ran with the GitHub
default, which includes contents: write on push events. A compromised
third-party action (Swatinem/rust-cache, dtolnay/rust-toolchain) or a
malicious PR step would have had repository write access — more than
any job here actually needs.

Changes:
  .github/workflows/ci.yml
  - Add top-level permissions: contents: read
  - Inline per-job audit comment on all 6 jobs (fmt, contract,
    wasm-size, proptest, audit, mutants)
  - Block header documents: why only read, trigger coverage
    (push vs pull_request), and guidance for future jobs that
    need elevated scope

  CONTRIBUTING.md
  - New 'GITHUB_TOKEN permission model' section in the Maintainer
    Guide: per-job audit table, rationale, and example of how to
    add a narrowly-scoped job-level override for any future job
    that needs to write (e.g. posting a PR comment)

No build logic changed. All existing CI jobs run identically with the
narrowed token — none of them call any GitHub API beyond checkout.
@drips-wave

drips-wave Bot commented Aug 31, 2026

Copy link
Copy Markdown

@Keengfk Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[High] Add explicit least-privilege permissions: blocks to .github/workflows/ci.yml

1 participant