> **Page index:** https://km.oslo.systems/llms.txt lists every page on this site, with a link to its Markdown version.

# GitHub Actions best practices

## Scorecards

[Scorecards from the Open Source Security Foundation](<https://github.com/ossf/scorecard/blob/main/docs/checks.md>) is a good starting point to learn about security best practices for GitHub Actions.

## Permissions and environments

Use the `permissions` key in workflows to specify least privilege [permissions](<https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token>) for `GITHUB_TOKEN`. [You can find a lot of examples at GitHub](<https://github.com/step-security/secure-workflows/tree/main/knowledge-base/actions>). For example:

```
permissions:
  contents: write
  pull-requests: write
```

Continuous delivery *jobs* (for example running Terraform) should run isolated in its own job with scoped permissions and environments to reduce the risk of [supply-chain attacks](<https://en.wikipedia.org/wiki/Supply_chain_attack>) and adheres to the [principle of least privilege](<https://en.wikipedia.org/wiki/Principle_of_least_privilege>).

🔴 In the example below, both `hashicorp/setup-terraform` and `terraform-linters/setup-tflint` can assume the same AWS IAM role because they originate from a job that has an environment named `production`. This trust condition is checked by the IAM policy.[^/application/github-actions-best-practices/#2]

```
jobs:
  terraform:
    name: Lint and apply Terraform configuration

    permissions:
      id-token: write
      contents: read

    environment: production

    steps:
      - uses: aws-actions/configure-aws-credentials@sha-1
      - uses: terraform-linters/setup-tflint@sha-1
      - uses: hashicorp/setup-terraform@sha-1
```

🟢 Only `aws-actions/configure-aws-credentials` and `hashicorp/setup-terraform` can use the IAM role:

```
jobs:
  tflint:
    name: Lint Terraform configuration

    permissions:
      contents: read

    environment: production

    steps:
      - uses: terraform-linters/setup-tflint@sha-1

  tflint:
    name: Lint and Terraform configuration

    permissions:
      id-token: write
      contents: read

    steps:
      - uses: aws-actions/configure-aws-credentials@sha-1
      - uses: hashicorp/setup-terraform@sha-1
```

## Pin actions

GitHub Actions should be pinned to a commit hash. This is because Git tags are mutable in practice.\[^1\] There's no need to fork an action for security purposes as long as you follow this recommendation. Use [Dependabot](<https://docs.github.com/en/code-security/dependabot>) to keep on top of security updates. When pinning to a SHA-1 hash it's good practice to comment with the corresponding version number:

```
uses: actions/checkout@e2f20e631ae6d7dd3b768f56a5d2af784dd54791 # v2.5.0
```

## Use fine-grained personal access tokens for cross-repository access

🟢 Set up fine-grained personal access tokens (PAT) on a [machine user](<https://docs.github.com/en/developers/overview/managing-deploy-keys#machine-users>) (such as [`okctl-bot`](<https://github.com/okctl-bot>)). Consider making the token available as a organization-level secret, which reduces the need to create duplicate secrets.

🔴 Avoid using PATs attached to private GitHub accounts.

## SSH deploy keys for cross-repository access

🔴 **Don't** use SSH deploy keys unless you have a special reason. Use [fine-grained personal access tokens](<#use-fine-grained-personal-access-tokens-for-cross-repository-access>) instead. This eliminates the need of using a third-party action.

If there is no other option, use [`webfactory/ssh-agent`](<https://github.com/webfactory/ssh-agent>) to load SSH deploy keys in a GitHub Actions workflow.

```
- name: Load golden-path-iac SSH deploy key
  uses: webfactory/ssh-agent@fc49353b67b2b7c1e0e6a600572d01a69f2672dd # v0.5.4
  with:
    ssh-private-key: ${{ secrets.GOLDEN_PATH_IAC_PRIVATE_DEPLOY_KEY }}
```

## Signing commits in workflows with GnuPG

Use [`crazy-max/ghaction-import-gpg`](<https://github.com/crazy-max/ghaction-import-gpg>) to sign commits in a GitHub Actions workflow.

```
- name: Import GPG key
  uses: crazy-max/ghaction-import-gpg@111c56156bcc6918c056dbef52164cfa583dc549 # v5.2.0
  with:
    gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
    passphrase: ${{ secrets.GPG_PASSPHRASE }}

    # Username and email is inferred from GPG key metadata
    git_user_signingkey: true
    git_commit_gpgsign: true
    git_config_global: true
```

## See also

- [`actions/starter-workflows`](<https://github.com/actions/starter-workflows>)
- [GitHub Actions Security Best Practices](<https://blog.gitguardian.com/github-actions-security-cheat-sheet>)
- [The ultimate guide to GitHub Actions authentication](<https://michaelheap.com/ultimate-guide-github-actions-authentication>)
- [Implementing least privilege for secrets in GitHub Actions](<https://github.blog/2021-04-13-implementing-least-privilege-for-secrets-in-github-actions/>)
- [Security hardening for GitHub Actions](<https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions>)

[^/application/github-actions-best-practices/#2]: [Filtering for a specific environment](<https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#filtering-for-a-specific-environment>)

[^/application/github-actions-best-practices/#3]: [Principle\_of\_least\_privilege](<https://en.wikipedia.org/wiki/Principle_of_least_privilege>)
