Skip to main content

The GDS Way and its content is intended for internal use by the GDS Product Group community.

Using GitHub Actions and workflows

GitHub repositories can be configured to use GitHub Actions for CI/CD jobs, for example running unit tests or deploying a static site.

Security

Triggers

Do not use these risky triggers: pull_request_target, workflow_run, issue_comment, issues, discussion_comment, discussion, fork, watch unless subjected to rigorous security review - see “Pwn Request” vulnerability.

Permissions

Set the workflow permissions: to only what’s needed.

For example at the top level of your workflow give it just read-only access to the repo contents:

permissions:
  contents: read

Any further permissions can be added to the individual Job that needs it - see: granular permissions in the workflow YAML

Secrets

The scope of your secret should be minimized. Prefer Environment secrets to Repo secrets to Organisation secrets.

Avoid storing long-lived credentials in a secret where possible. For connecting to AWS, DockerHub and other external services, see: “Authenticating with AWS and other external services”.

Authenticating with AWS and other external services

If your workflow interacts with another service (for example AWS or DockerHub), consider setting up a dedicated account or role in that service, with permissions limited to the scope of the action.

When accessing AWS you must authenticate using OpenID Connect (OIDC) and not using an IAM User’s access key and secret access key.

Your IAM Role must specify your repo and should specify the branch you expect to be deploying from (for example, main), to make sure code cannot be deployed from untrusted branches.

Script injections - interpolating untrusted user content

Look out for script injection when referencing user-submitted content.

The risk is when you have interpolation ${{ github.something }} of ‘something’ that came from a user e.g. ${{ github.event.pull_request.title }}, and that is executed in a run:, script or similar.

The simplest fix is to use an intermediate environment variable - see Good practices for mitigating script injection attacks.

Bad:

- name: Print PR Title
  run: echo "The title is ${{ github.event.pull_request.title }}"

If the title is "; rm -rf / #, the runner will execute that command.

Good:

- name: Print PR Title
  env:
    PR_TITLE: ${{ github.event.pull_request.title }}
  run: echo "The title is $PR_TITLE"

User permissions

If your repository has external contributors, ensure they do not have permissions to add workflows or trigger workflow runs.

Consider protecting the .github/workflows folder by using a CODEOWNERS file and requiring review from CODEOWNERS for merges into protected branches.

Third party actions

Make use of a Third-party action only if:

  • The provider is verified by GitHub (for example, aws-actions)
  • The action is complex enough that you cannot write your own local action
  • You have fully reviewed the code in the version of the third-party action you will be using
  • The third-party action is actively maintained, well-documented and tested (follow the guidance on third party dependencies).

Prefer actions owned by your team, GDS or GitHub.

You can enforce this in the repo settings: under Actions -> ‘Allow enterprise, and select non-enterprise, actions and reusable workflows’ select ‘Allow actions created by GitHub’ and ‘Allow actions by Marketplace verified creators’ as required.

Pinning Actions

You must pin third-party Actions to full-length Git commit SHA, for example:

- uses: actions/checkout@01aecccf739ca6ff86c0539fbc67a7a5007bbc81 # v2.1.0

Include the semver version in a comment next to the SHA, like this example, to help humans to understand the version.

When the SHA is updated, please double-check that the commented version is also updated, as Dependabot does not have perfect capability at either identifying the magnitude of the upgrade, or necessarily updating the commented pin.

If the SHA is updated by an externally contributed pull request, be suspicious.

Pinned actions are even more important to have a process to keep the version up to date - see: Updating.

Updating

Keep Actions up to date by configuring Dependabot or other automated dependency management tool.

Output

Note that for public repositories, the output of workflow runs is visible to everyone. Do not use workflows if this output could be considered sensitive.

Authorizing GitHub Actions

GitHub Actions need authorization in order to access things in GitHub. Choose the right approach for authorizing your GitHub Actions for your context.

Using a GitHub App

Use a GitHub App to authenticate your GitHub Actions if they need to:

  • undertake privileged tasks outside a single repository
  • have persistent authentication
  • use finer-grained permissions
  • act as an integration rather than a user or workflow

There’s more information about using GitHub Apps to run automated tasks in the GitHub guidance.

Examples of when to use a GitHub App

  • Needing cross-repository or organization-wide automation for example a bot that manages issues across multiple repositories in an organization.
  • Requiring fine-grained permissions for example an integration that only needs read access to issues but write access to pull requests.

Using the built-in GITHUB_TOKEN

If your GitHub Action only needs to carry out tasks within a single repository, use the built-in GITHUB_TOKEN for that repository.

Personal Access Tokens

Warning Do not use Personal Access Tokens to authorize GitHub Actions

Personal Access Tokens:

  • rely on an individual person’s GitHub account, causing GitHub Actions to break when people move teams, leave GDS, or the token expires
  • are intended for individual use, and can’t be centrally managed and audited

Only use PATs to authorize calls to the GitHub API you’re making yourself as part of development.

Classic Personal Access Tokens must not be used to take actions in GDS repositories. Their use may be disabled altogether in future. Classic PATs have overly broad permissions, can be configured never to expire, and have access to all the repositories your account has.

Configuring a GitHub App

Follow the guidance here to set up a GitHub App in your developer account:

You will need to provide your App permissions to access GitHub resources. Ensure the app only has the permissions it needs.

Storing your Secrets Safely

Store the client_id, client_secret and private_key for this application safely. The client_secret and private_key are sensitive and must be stored in a secrets manager. Choose from one of the following recommended approaches, depending on the scope of your app:

App Scope Recommendation
Single Repository Local Repository Secret
Multi-Repository (i.e. a Team or Pod) Organisation Secret Value or AWS Secrets Manager (Build account)
Organisation-Wide Organisation Secret Value

Rotating your GitHub App’s Secrets

You should include your GitHub App’s secrets in your periodic secrets audit.

Private keys do not expire and must be manually revoked. For more information about how to revoke or delete a private key, see Deleting private keys.

Naming Convention

To allow the use of Shared GitHub Actions and tooling, use the following naming convention for secrets:

  • GH_APP_{APP_NAME_ABBREVIATION}_CLIENT_ID
  • GH_APP_{APP_NAME_ABBREVIATION}_CLIENT_SECRET
  • GH_APP_{APP_NAME_ABBREVIATION}_PRIVATE_KEY
  • GH_APP_{APP_NAME_ABBREVIATION}_PRIVATE_KEY_B64 - if required to encode in base64

Installing your App

Once configured, you can request that the app is installed in the organisation.

You can also request a transfer from your personal account to the organisation. To do this, once the App is production ready, change the ownership of the App to the GitHub organisation by visiting Settings -> Developer Settings -> GitHub Apps -> <your app> -> Advanced -> Transfer

You will need a GitHub Org Admin to approve either of these installation approaches.

Using your App

GitHub Actions

If you are using the App in a GitHub Workflow, GitHub publishes a common github action which allows you to generate a new short-lived access token for your app, to undertake GitHub API requests. This should be your first port of call before implementing anything custom.

Scripting / External Implementations

To enable the App to make REST API requests you will need to use the App ID and Private key to generate a Json Web Token (JWT), then use the JWT to generate a Token. Once you have the token you can make the request.

The lifetime of the JWT/Token is limited. Create them in a workflow. See an example workflow which expects an App Client ID and Private Key to be provided.

See the GitHub documentation for further guidance:

This page was last reviewed on 4 August 2025. It needs to be reviewed again on 4 February 2026 by the page owner #gds-way .