From 88cbe252427203e9d1b305eb4e3ef2a9b772046a Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Mon, 11 May 2026 19:02:39 +0400 Subject: [PATCH] fix(ci): drop root contents: write to contents: read (CIS GHA 1.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three workflows previously declared `permissions: contents: write` at file scope, granting the broadest blast radius to every job. Only a handful of jobs actually need to write the repo (tag-release, welder deploy, sticky-comment posting); every other job — build-setup, lint, test, build-platforms, build-binaries, docker-build, docker-build-push, build-docs, prepare — is read-only. Per CIS GitHub Actions Benchmark §1.2 and OWASP CICD-SEC-5, this PR drops the root grant to `contents: read` and explicitly grants `contents: write` only on the jobs that need it. | Workflow | Root | Per-job writes | |---|---|---| | branch.yaml | read | finalize (already had it) | | branch-preview.yaml | read | publish-sc-preview, publish-git-tag, finalize | | push.yaml | read | docker-finalize (tag-release), finalize | Verification ============ - All 5 workflows scanned with the SC Semgrep ruleset (`simple-container-com/actions/semgrep-scan/rules/github-actions.yml`): **0 findings** across the existing 19 rules. - Every `actions/checkout` still uses `persist-credentials: false` (preserved from PR #238). - No OIDC `id-token: write` was previously set anywhere — none is needed for the current writes (token-issuing OIDC isn't used). - The `branch.yaml` PPE caveat from PR #238 is deliberately out of scope here: the team-accepted defense-in-depth comment + nosemgrep on that workflow indicate the proper `workflow_run`-gated split is tracked separately. This PR only tightens permissions, not triggers. Frameworks satisfied ==================== - CIS GitHub Actions Benchmark §1.2 - OWASP CICD-SEC-1 (insufficient flow control), CICD-SEC-5 (PBAC) - NIST SP 800-218 PS.1 (protect all forms of code) - OpenSSF Scorecard "Token-Permissions" check Signed-off-by: Dmitrii Creed --- .github/workflows/branch-preview.yaml | 9 ++++++++- .github/workflows/branch.yaml | 5 ++++- .github/workflows/push.yaml | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/branch-preview.yaml b/.github/workflows/branch-preview.yaml index 55a8f1b6..e2a95cbc 100644 --- a/.github/workflows/branch-preview.yaml +++ b/.github/workflows/branch-preview.yaml @@ -7,8 +7,11 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +# Least-privilege root (CIS GHA 1.2). Tag-pushing / deploy jobs grant +# `contents: write` per-job below: `publish-sc-preview` (welder deploy), +# `publish-git-tag` (git push tag), and `finalize` (notify/comment). permissions: - contents: write + contents: read jobs: prepare: @@ -276,6 +279,8 @@ jobs: # Does not need docker-build — SC binary publishing is independent of the Docker image. # Runs in parallel with publish-git-tag. needs: [prepare, build-setup, build-platforms, test] + permissions: + contents: write # welder deploy reads release artifacts + updates dist steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -328,6 +333,8 @@ jobs: # Only needs docker-build — the tag must point to a commit referencing a published Docker image. # Does not need build-platforms or publish-sc-preview. Runs in parallel with publish-sc-preview. needs: [prepare, docker-build] + permissions: + contents: write # commits the release branch + pushes the preview tag # GH_TOKEN must be visible to every step that runs git (checkout, commit, # push) because `gh auth setup-git` installs `gh auth git-credential` as # the credential helper — and that helper reads $GH_TOKEN from the diff --git a/.github/workflows/branch.yaml b/.github/workflows/branch.yaml index 1fc332b6..e6a0cdfe 100644 --- a/.github/workflows/branch.yaml +++ b/.github/workflows/branch.yaml @@ -15,8 +15,11 @@ on: - 'main' - 'staging' +# Least-privilege root: every job inherits read-only unless it explicitly +# grants more (CIS GHA 1.2). Only `finalize` actually needs `contents: write` +# (sticky comment / notify), and it grants that itself further down. permissions: - contents: write + contents: read jobs: build-setup: diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 50261f9d..8016ae72 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -10,8 +10,11 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false +# Least-privilege root (CIS GHA 1.2). Only `docker-finalize` (welder +# tag-release + deploy) and `finalize` (notify) need `contents: write`; +# both grant it per-job below. permissions: - contents: write + contents: read jobs: prepare: @@ -364,6 +367,8 @@ jobs: name: Docker finalize (tag-release, deploy) runs-on: blacksmith-8vcpu-ubuntu-2204 needs: [prepare, build-setup, build-platforms, build-binaries, build-github-actions-staging, test, build-docs, docker-build] + permissions: + contents: write # `welder run tag-release` pushes the release git tag steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: