GitHub Actions makes it easy to automate builds, tests, deployments, and repository management. However, every automated workflow also introduces a security consideration: what happens if a workflow or one of its dependencies is compromised?
A workflow may have access to repository contents, pull requests, issues, packages, deployment environments, and external services. If it receives more permissions than necessary, an attacker who manages to execute code inside that workflow may gain access to resources that the workflow never needed.
This is why the principle of least privilege is important when designing GitHub Actions workflows.
Instead of granting every workflow broad access, developers should explicitly define which permissions each workflow and job requires.
In this article, we will explore how GitHub Actions permissions work, how to configure the GITHUB_TOKEN, how to apply permissions at the job level, and how to secure workflows that build, test, and deploy applications.
Understanding GITHUB_TOKEN
GitHub automatically creates a GITHUB_TOKEN for each workflow job.
It is a temporary authentication token that allows a workflow to interact with GitHub resources through supported APIs and actions.
For example, a workflow may need to:
- Read repository source code.
- Create or update a pull request.
- Add labels to an issue.
- Publish a package.
- Create a GitHub release.
- Upload code scanning results.
The permissions assigned to GITHUB_TOKEN determine which operations the workflow can perform.
Consider this workflow:
name: Build Application
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v4
- name: Build application
run: echo "Building application"
The workflow uses actions/checkout to retrieve the repository source code.
It does not need permission to create releases, modify issues, or update pull requests.
Granting unnecessary write permissions to this workflow would increase its security risk without providing any additional benefit.
Understanding GitHub Actions Permission Scopes
GitHub provides granular permissions for GITHUB_TOKEN.
Each permission controls access to a particular category of GitHub resources.
| Permission | Purpose |
| actions | Manage workflow runs and related actions |
| attestations | Manage artifact attestations |
| checks | Manage check runs and check suites |
| contents | Read or modify repository contents |
| deployments | Manage deployment records |
| issues | Read or modify issues |
| packages | Publish or manage packages |
| pull-requests | Manage pull requests |
| security-events | Manage code scanning results |
| statuses | Manage commit statuses |
| id-token | Request an OpenID Connect identity token |
Depending on the permission, GitHub supports read, write, or none access.
For example:
permissions:
contents: read
pull-requests: write
This allows the workflow to read repository contents and modify pull requests.
An important behavior is that when you explicitly specify permissions, any permission not included in the list is set to none.
This makes explicit permission configuration useful for limiting unnecessary access.
Set Default Permissions at the Workflow Level
You can configure permissions at the top level of a workflow.
This applies the same permission configuration to all jobs unless a job defines its own permissions.
For a workflow that only builds and tests an application, read-only access is usually sufficient.
name: Build and Test
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v4
- name: Build
run: echo "Build application"
- name: Test
run: echo "Run application tests"
The workflow receives only the repository content permission specified above.
This is a good starting point for workflows that do not need to modify GitHub resources.
You can also use:
permissions: {}
This disables all GITHUB_TOKEN permissions.
Individual jobs can then receive only the permissions they actually need.
Configure Permissions at the Job Level
Not every job in a workflow performs the same operation.
Consider a pipeline with three jobs:
- Build the application.
- Run automated tests.
- Publish a release.
The build and test jobs only need repository read access. The release job needs permission to create a release.
Instead of granting write access to the entire workflow, configure permissions separately for each job.
name: Application Pipeline
on:
push:
branches:
- main
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Build application
run: echo "Building application"
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Run tests
run: echo "Running tests"
release:
needs: [build, test]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Create release
run: echo "Release creation goes here"
The release job receives write access to repository contents, while the other jobs retain read-only access.
The important advantage is that a problem in the build or test job does not automatically provide the same write permissions available to the release job.
Keep in mind that permissions apply to all steps within a job. If a job executes several third-party actions, those actions may also be able to access the job's GITHUB_TOKEN.
For sensitive operations, separate jobs are often a better security boundary than placing every operation inside one large job.
Secure Pull Request Workflows
Pull request workflows require additional attention because they may execute code contributed by developers outside your organization.
For example, a public repository may receive a pull request from a fork.
That pull request could contain changes to source code, scripts, build configuration, or other files executed during the workflow.
A safer approach is to use the pull_request event with read-only permissions for untrusted contributions.
name: Pull Request Validation
on:
pull_request:
permissions:
contents: read
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v4
- name: Run validation
run: |
echo "Validating pull request"
echo "Running automated checks"
Workflows triggered by pull requests from forks normally receive restricted permissions. Write permissions are not generally granted to these runs.
Avoid using pull_request_target as a simple replacement for pull_request.
The pull_request_target event runs in the context of the base repository and can receive elevated permissions. Checking out and executing untrusted pull request code in such a workflow can expose repository secrets and write-capable tokens.
If you need to use pull_request_target for tasks such as adding labels or commenting on pull requests, keep the privileged workflow separate from the execution of untrusted code.
Set Repository and Organization Permission Defaults
Workflow YAML is only one part of GitHub Actions security.
Organization and repository administrators can also configure the default permissions granted to GITHUB_TOKEN.
To review these settings:
- Open the repository or organization settings.
- Navigate to Actions.
- Open the General settings.
- Locate the Workflow permissions section.
- Select the restricted read-only default where appropriate.
- Save the configuration.
For organizations, administrators can establish a default policy across repositories.
This helps prevent newly created workflows from unintentionally receiving broad permissions.
However, workflow-level and job-level permissions can still modify the effective token permissions, subject to organization and repository restrictions.
Therefore, administrators should combine secure defaults with workflow reviews and repository policies.
Avoid Giving Every Workflow Write Access
One common mistake is configuring all workflows with broad permissions.
For example:
permissions: write-all
This grants write access across the supported permission scopes.
A build workflow that only needs to compile an application should not receive the same permissions as a workflow that publishes releases.
A better approach is to begin with restricted permissions and add only what the workflow requires.
For example:
| Workflow | Required permissions |
| Build application | contents: read |
| Run tests | contents: read |
| Add pull request labels | contents: read, pull-requests: write |
| Publish GitHub package | contents: read, packages: write |
| Create release | contents: write |
| Upload code scanning results | security-events: write |
The exact permissions depend on the actions and API operations used by your workflow.
If a workflow fails because it lacks a permission, identify the operation that failed and add the corresponding permission instead of granting write access to everything.
Review Third-Party Actions
Another important security consideration is third-party actions.
A workflow may use several external actions for checkout, deployment, notifications, artifact management, or code analysis.
An action can access GITHUB_TOKEN through the GitHub context even if you do not explicitly pass the token as an input.
This means the permissions granted to a job should account for every action running inside that job.
Follow these practices:
- Review the source code of third-party actions.
- Use trusted publishers.
- Pin actions to full commit SHAs for stronger version control.
- Avoid giving untrusted actions access to write-capable tokens.
- Keep actions updated through a controlled dependency update process.
- Separate privileged deployment operations from general build steps.
A small action used for formatting or notifications should not automatically receive access to sensitive deployment credentials.
Common Permission Problems and Troubleshooting
Restricting permissions can cause existing workflows to fail. This is expected when a workflow previously depended on access that was not explicitly documented.
Some common errors include:
| Error or behavior | What to investigate |
| Resource not accessible by integration | Check the relevant GITHUB_TOKEN permission |
| HTTP 403 response | Verify the token has the required access |
| Cannot create a release | Check contents: write |
| Cannot add a pull request label | Check pull-requests: write |
| Cannot publish a package | Check packages: write |
| Cannot upload code scanning results | Check security-events: write |
For example, if a workflow needs to create an issue, it requires the issues permission.
permissions:
contents: read
issues: write
Do not respond to every 403 error by granting write-all permissions.
Instead, identify the API endpoint or action that failed, check its required permission, and update the workflow accordingly.
Best Practices for Secure GitHub Actions Permissions
A reliable permission strategy should be part of the workflow design process.
Use these practices when creating or reviewing GitHub Actions pipelines:
- Start with contents: read or permissions: {}.
- Grant write access only when a job needs to modify GitHub resources.
- Prefer job-level permissions for workflows with different responsibilities.
- Keep privileged deployment jobs separate from build and test jobs.
- Treat pull request code from forks as untrusted input.
- Avoid executing untrusted code inside privileged workflows.
- Review permissions required by third-party actions.
- Configure organization-level defaults to restrict unnecessary access.
- Investigate permission failures instead of granting broad access.
- Review workflow changes as part of normal code review.
Summary
GitHub Actions permissions control what automated workflows can do inside a repository. Granting excessive access increases the potential impact of a compromised workflow, an unsafe third-party action, or an application vulnerability.
The GITHUB_TOKEN permission model allows developers to define access at the workflow and job levels. By starting with read-only permissions and granting additional access only where required, teams can reduce unnecessary privileges.
For larger pipelines, separate build, test, and deployment jobs so that each operation receives only the permissions it needs.
Least-privilege permissions are not just an administrative setting. They are an important part of designing secure and maintainable CI/CD pipelines.
Join the conversation! Your thoughts help the community grow.