diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b57e4b..092fb0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,37 @@ on: pull_request: branches: [main] +# ── GITHUB_TOKEN permission policy ──────────────────────────────────────────── +# +# Hardened to the minimum required by every job in this workflow. +# +# Why contents: read and nothing else: +# All jobs here only need to check out source code (contents: read). +# None of them write commits, create releases, post PR comments, call the +# Packages API, or use any other GitHub API scope. GITHUB_STEP_SUMMARY +# writes are local runner file writes — not a GitHub API permission. +# +# Trigger coverage: +# For `pull_request` events from forks, GitHub already restricts the token +# to read-only. For `push` to `main` from the same repo, the default token +# carries `contents: write`. This explicit block overrides both trigger +# types and removes that unneeded write privilege. +# +# Future jobs that need elevated scope: +# Add an explicit job-level `permissions:` override with only the additional +# scope that job requires (e.g. `pull-requests: write` for a PR comment job). +# Do NOT widen the top-level block. +# +# Per-job audit (all trigger types, checked 2026-08-31): +# fmt — checkout only → contents: read ✓ +# contract — checkout, clippy, test → contents: read ✓ +# wasm-size — checkout, build, STEP_SUMMARY → contents: read ✓ +# proptest — checkout, cargo test → contents: read ✓ +# audit — checkout, cargo audit → contents: read ✓ +# mutants — checkout, cargo mutants → contents: read ✓ +permissions: + contents: read + # Cancel in-progress runs for the same branch/PR concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -34,6 +65,7 @@ jobs: contract: name: Contract (${{ matrix.crate }} / ${{ matrix.toolchain }}) runs-on: ubuntu-latest + # Needs: contents: read (checkout only). Inherits workflow-level minimum. strategy: fail-fast: false matrix: @@ -175,6 +207,7 @@ jobs: proptest: name: Bond-conservation proptest runs-on: ubuntu-latest + # Needs: contents: read (checkout only). Inherits workflow-level minimum. defaults: run: working-directory: intent_settlement @@ -218,6 +251,9 @@ jobs: mutants: name: Mutation testing runs-on: ubuntu-latest + # Needs: contents: read (checkout only). cargo-mutants mutates source in a + # temporary copy on the runner — no GitHub API calls. Inherits workflow-level + # minimum. defaults: run: working-directory: intent_settlement diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 72fa2cf..7fcb845 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -413,6 +413,48 @@ once they are merged: | `WASM size gate` | `ci.yml` (planned) | Blocks merges that grow the wasm by > N KB | | `Coverage` | `coverage.yml` (planned) | Advisory until a baseline is established | +### GITHUB_TOKEN permission model + +All jobs in `ci.yml` run under a **`permissions: contents: read`** top-level +policy. This is the minimum required: every job only needs to check out source +code. + +**Why this matters:** Without an explicit `permissions:` block, GitHub applies +its default token scopes, which include `contents: write` for `push` events on +the same repository. A compromised third-party action (e.g. `Swatinem/rust-cache`, +`dtolnay/rust-toolchain`) or a malicious PR-triggered step would then have +write access to the repository — more privilege than any CI job here actually +needs. + +**Per-job audit (checked 2026-08-31):** + +| Job | What it does | Minimum scope | +|---|---|---| +| `fmt` | Checkout + `cargo fmt --check` | `contents: read` | +| `contract` | Checkout + clippy + test + (optional) fmt | `contents: read` | +| `wasm-size` | Checkout + build + write to `$GITHUB_STEP_SUMMARY` (local runner file, not a GitHub API call) | `contents: read` | +| `proptest` | Checkout + `cargo test` | `contents: read` | +| `audit` | Checkout + `cargo audit` (queries RustSec DB over HTTPS, not the GitHub API) | `contents: read` | +| `mutants` | Checkout + `cargo mutants` (mutates source in a runner-local temp copy) | `contents: read` | + +**Adding a job that needs elevated scope:** + +If a future job needs to post PR comments, push a commit, create a release, or +call any other GitHub API, add a **job-level** `permissions:` override with +only the additional scope that specific job requires. Do not widen the +workflow-level block: + +```yaml +jobs: + my-new-job: + permissions: + contents: read # still needed for checkout + pull-requests: write # needed to post a PR comment — only grant here +``` + +See the [GitHub docs on workflow permissions](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) +for the full list of available scopes. + ### MSRV policy The declared MSRV is **Rust 1.78** (see README.md). CI enforces this via a