Skip to content

ExplicitJobPermissions

Permissions should be declared on the job level only.

Defined by ExplicitJobPermissionsRule which supports workflows in the "Default" ruleset along with MissingJobPermissions.

Description

Declaring permissions on the workflow level leads to elevated permissions for all jobs. Even if the workflow has only one job, it is better to declare the permissions on the job level, this improves consistency, copy-paste-ability, and forms habits.

Move the permissions declaration from the workflow level to the job level.

References:

  • Best practice in documentation

    The two workflow[s ...] show the permissions key being used at the job level, as it is best practice to limit the permissions' scope.

  • Explanation of the above

    As a good security practice, you should always make sure that actions only have the minimum access they require by limiting the permissions granted to the GITHUB_TOKEN.

Compliant examples

Compliant example #1

Permissions are explicitly declared on the job level.

example.yml

on: push
jobs:
  example:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - run: echo "Example"

Compliant example #2

All permissions are explicitly forbidden on the workflow level.

example.yml

on: push
permissions: {}
jobs:
  example:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Example"

Non-compliant examples

Non-compliant example #1

Permissions are declared on the workflow level, leading to escalated privileges for all jobs.

There are two jobs: build and comment:

  • The build job needs to access contents to invoke the make command.
  • The comment job needs to write comments to the pull request.

With the permissions: being declared on the workflow, both jobs will have the same permissions. This leads to a larger attack surface:

  • The comment job will be able to read the repository contents. This means that if the publish-comment-action is compromised, it can read/steal the repository contents.
  • The build job will have full access to Pull Requests. This means that if the make command is compromised, it can do anything to PRs.

example.yml

on:
  pull_request:
permissions:
  contents: read
  pull-requests: write
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: make
  comment:
    runs-on: ubuntu-latest
    steps:
      - uses: some/publish-comment-action@v0

  • Line 7: Job[build] should have explicit permissions.
  • Line 12: Job[comment] should have explicit permissions.

Non-compliant example #2

Permissions are declared on the workflow level.

Note: This could be actually acceptable, because the workflow has only one job, but for consistency, copy-paste-ability, and habit-forming, it's better to still flag it to enforce declaring it on the job level.

One exemption from this rule is when the permission list only contains contents: read. This is for practical reasons, as this is quite a common workflow structure.

example.yml

on: push
permissions:
  actions: read
jobs:
  example:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Example"

  • Line 5: Job[example] should have explicit permissions.

Non-compliant example #3

Redundant permissions declared on workflow-level.

When declaring permissions on the workflow-level as well as the job-level, the job-level restricts the workflow-level permissions.

However, it is not necessary to declare permissions on the workflow-level, this can help reduce duplication and maintenance overhead.

See MissingJobPermissions, which help prevent accidental missing permissions.

example.yml

on: push
permissions:
  contents: read
jobs:
  example:
    permissions:
      contents: read
    runs-on: ubuntu-latest
    steps:
      - run: echo "Example"

  • Line 4: Workflow[example] has redundant permissions.