Granting sensitive write permissions at the workflow level applies those permissions to all jobs in the workflow by default. This violates the principle of least privilege, as jobs that don’t require sensitive permissions will still inherit them unnecessarily.
When sensitive permissions are granted at the workflow level, all jobs inherit these permissions regardless of whether they need them. This creates several security risks:
If a job that only requires read access is compromised but has inherited contents: write permissions from the workflow level, an
attacker could modify repository contents, create unauthorized releases, or manipulate the repository history.
Jobs with unnecessary packages: write permissions could be exploited to publish malicious packages or tamper with existing packages,
potentially compromising downstream consumers of your packages.
Jobs that inherit attestations: write or security-events: write permissions could create fraudulent security attestations
or manipulate security event logs, undermining security monitoring and compliance efforts.
Move sensitive permissions from the workflow level to individual job levels. Only grant permissions at the job level to jobs that actually require them. This ensures that each job receives only the minimum permissions necessary for its function.
The following workflow grants contents: write permission at the workflow level, which applies to all jobs:
name: Example
on:
workflow_dispatch:
permissions:
contents: write # Noncompliant
jobs:
commit-change:
runs-on: ubuntu-latest
steps:
- name: Create file via GitHub API
run: |
# Uses contents: write permission
checkout-main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # Does not need contents: write permission
with:
ref: main
Grant permissions only at the job level to jobs that require them:
name: Example
on:
workflow_dispatch:
jobs:
commit-change:
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- name: Create file via GitHub API
run: |
# Uses contents: write permission
checkout-main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # Does not need contents: write permission
with:
ref: main
The key difference lies in how GitHub Actions applies permissions at different levels:
By placing permissions at the job level, you ensure that each job receives only the permissions it explicitly declares, preventing unnecessary permission inheritance and reducing the attack surface of your workflows.