Skip to content

Scope CI workflow credentials to least privilege #15529

@FredKSchott

Description

@FredKSchott

Describe the problem

Two CI/CD hygiene improvements that follow the principle of least privilege:

1. Persisted git credentials (persist-credentials: true default)

All actions/checkout usages retain the default persist-credentials: true, which writes the GITHUB_TOKEN into .git/config as a base64-encoded HTTP extraheader. This token remains readable by any process for the entire workflow run. The most sensitive instances are:

  • Release workflow (release.yml): Token has contents: write, id-token: write, and pull-requests: write permissions, plus npm publishing access
  • Autofix workflow (autofix-lint.yml): Token has contents: write and is exposed to pnpm install, pnpm format, and code generation steps

After checkout, any subsequent step can extract the token:

grep -A1 'extraheader' .git/config
# AUTHORIZATION: basic <base64-encoded-token>

2. Over-broad secret inheritance in platform tests

The platform-tests-all.yml workflow passes all repository-level secrets to the Vercel tests workflow via secrets: inherit, when only three specific secrets are needed (VERCEL_TOKEN, VERCEL_PROJECT_ID_BASIC, VERCEL_ORG_ID). Any repository-level secrets added in the future will automatically become accessible without explicit review.

# Current
jobs:
  vercel:
    uses: ./.github/workflows/platform-tests-vercel.yml
    secrets: inherit  # passes ALL repository secrets

Describe the proposed solution

For persisted credentials, add persist-credentials: false to all actions/checkout usages:

- uses: actions/checkout@v6
  with:
    persist-credentials: false

For workflows that need to push commits (like autofix-lint.yml), configure git authentication explicitly only for the push step and unset it immediately after:

- name: Push changes
  run: |
    git config --local http.https://github.com/.extraheader \
      "AUTHORIZATION: basic $(echo -n x-access-token:${{ github.token }} | base64)"
    git push origin HEAD
    git config --local --unset http.https://github.com/.extraheader

For secret inheritance, replace secrets: inherit with explicit secret passing:

jobs:
  vercel:
    uses: ./.github/workflows/platform-tests-vercel.yml
    with:
      sha: ${{ inputs.sha }}
    secrets:
      VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
      VERCEL_PROJECT_ID_BASIC: ${{ secrets.VERCEL_PROJECT_ID_BASIC }}
      VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}

Alternatives considered

  • Relying on GitHub's token expiration (tokens expire after the workflow run) — but within a single run, the token is available to all steps regardless of whether they need it.
  • Using environment-scoped secrets exclusively — this helps for the platform tests case but doesn't address the persisted credentials issue.

Importance

would make my life easier

Additional Information

Affected workflows:

  • ci.yml — multiple checkout instances (contents: read, lower risk)
  • autofix-lint.yml — checkout with contents: write
  • release.yml — checkout with contents: write, id-token: write, pull-requests: write
  • platform-tests-vercel.yml — checkout with contents: read
  • audit.yml — checkout with contents: read
  • platform-tests-all.ymlsecrets: inherit to called workflow

These changes complement SHA-pinning of third-party actions as general CI/CD hardening best practices.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions