# Hardening GitHub Actions in Private Repositories

Security measures for GitHub Actions.

- **Author:** Jan Hagelauer
- **Published:** 2021-10-26
- **Updated:** 2021-10-26
- **Tags:** github, actions, security

---

## Introduction

GitHub has recently made changes to GitHub Actions to improve security, and
published a series of blog posts on how to secure Actions and Workflows
([part 1](https://securitylab.github.com/research/github-actions-preventing-pwn-requests/),
[part 2](https://securitylab.github.com/research/github-actions-untrusted-input/),
[part 3](https://securitylab.github.com/research/github-actions-building-blocks/)).
A recurring topic is the securing of GitHub Actions triggered by Pull Requests
of untrusted authors. This is typically not a concern in a private repository,
where only known and trusted collaborators are allowed to submit PRs. However,
there are other security concerns that apply to both public and private
repositories, and even unique security concerns that only apply to private
repositories: to keep the codebase undisclosed.

In the April 2021
[Codecov Bash Uploader incident](https://about.codecov.io/security-update/),
attackers compromised the uploader to exfiltrate credentials and code from
repositories that used the
[Codecov Github Action](https://github.com/codecov/codecov-action) or similar CI
integrations. The attack remained unnoticed and unfixed for 2 months.

Besides third-party Github Actions, other attack vectors for GitHub Workflows
also include dependency updates in the codebase. When using tools like
[Dependabot](https://dependabot.com/), updates for hijacked packages can result
in automatically created PRs that execute malicious code during installation or
testing in GitHub Workflows.

An example for this is the
[2018 eslint-scope incident](https://eslint.org/blog/2018/07/postmortem-for-malicious-package-publishes),
where an attacker gained access to the npm credentials and released package
updates with malicious `postinstall` scripts. In this case, the update was
withdrawn in less than 3 hours.

In response to these growing risks, GitHub has recently made several
security-related changes to GitHub Actions. Since March,
[Workflows initiated by Dependabot run with read-only permissions and no access to secrets](https://github.blog/changelog/2021-02-19-github-actions-workflows-triggered-by-dependabot-prs-will-run-with-read-only-permissions/).
One month later,
[GitHub introduced a more fine-grained permission system for the `GITHUB_TOKEN`](https://github.blog/changelog/2021-04-20-github-actions-control-permissions-for-github_token/).

In this article, we will discuss how to use this new functionality and other
measures to reduce the attack surface of your private GitHub repositories with a
focus on Node.js projects.

## How to harden GitHub Workflows

### Reduce permissions

It is now possible to restrict access to the repository for a GitHub Workflow by
changing the
[permissions for the `GITHUB_TOKEN`](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token).
Keep in mind this does not affect permissions for custom Personal Access Tokens
(PAT).

Permission can be set at different levels — organization- or repository-wide by
[setting the global default permissions](https://github.blog/changelog/2021-04-20-github-actions-control-permissions-for-github_token/#setting-the-default-permissions-for-the-organization-or-repository),
for an entire Workflow or for an individual job inside a Workflow by specifying
permissions inside a GitHub Workflow. Local permissions override the global
defaults — full details on how the effective permissions are calculated can be
found
[in the GitHub documentation](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#how-the-permissions-are-calculated-for-a-workflow-job).

By setting the global Workflow permissions to _"Read repository contents
permission"_, Workflows will only have read access to the codebase — all other
permissions need to be set in the Workflows.

There are currently
[12 permission scopes available](https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#permissions)
inside the Workflows. Unfortunately, there is no documentation we know of
explaining the scopes, so it may take some trial and error to get the right
combination for each Workflow. For one of our favorite "publish" jobs (run
[semantic-release](https://semantic-release.gitbook.io/semantic-release/) to
determine the next version number and publish a release to the repository), we
need write access to the repo content (tag/release), pull-requests (to update
the PR comment after publishing) as well as deployments. The permissions look
like this:

```yaml
jobs:
  release:
    steps:
      - name: Semantic release
        run: npx semantic-release
        permissions:
          contents: write
          deployments: write
          issues: read
          pull-requests: write
```

### Audit dependencies before installing

The [GitHub Advisory Database](https://github.com/advisories) collects
information about known security vulnerabilities of packages for various
ecosystems, including pip and Composer. Since early October,
[the npm advisory database has also been merged into GitHub's database](https://github.blog/changelog/2021-10-03-the-npm-advisory-database-is-now-part-of-the-github-advisory-database/).

npm's [audit](https://docs.npmjs.com/cli/v7/commands/npm-audit) command can be
used to scan project dependencies against the database. This can be done as part
of a GitHub Workflow before installing the dependencies to abort the Workflow
execution if vulnerable dependencies are found.

The audit output can be quite noisy at times, and not all vulnerabilities can be
exploited in each project setup. To reduce the number of false positives that
come up in the audit, it is possible to filter out vulnerabilities with low
severity.

```yaml
jobs:
  test:
    steps:
      - name: Audit Dependencies
        # This step will fail if at least one vulnerability with high severity
        # is found
        run: npm audit --audit-level=high
```

### Avoid executing code during dependency installation

By default, packages installed with npm can execute arbitrary code by defining
[`install` or `postinstall` scripts in their `package.json`](https://docs.npmjs.com/cli/v6/using-npm/scripts#npm-install).

These scripts can be skipped by using the switch `--ignore-scripts`. However,
using this switch will cause issues with packages that rely on these scripts to
perform legitimate installation tasks such as downloading or compiling
platform-specific binaries. In this case, you can manually run the scripts in
the Workflow for the packages that use them:

```yaml
jobs:
  test:
    steps:
      - name: Install Dependencies
        run: npm ci --ignore-scripts

      - name: Install Sharp
        working-directory: node_modules/sharp
        run: npm run install
```

The following one-liner can come in handy to find packages with (post)install
scripts:

```sh
find ./node_modules/ -name 'package.json' | xargs grep -lE '"(post)?install":'
```

### Limit exposure of secrets

Just like the the token permissions, `env` variables with sensitive information
should not be exposed globally in the Workflow, but only for the steps that
actually need them.

```yaml
jobs:
  test:
    env:
      # non-sensitive values are fine here
      NODE_ENV: test

    steps:
      - name: Run Tests
        run: npm test
        env:
          # the API key is only needed for this step
          API_KEY: ${{ secrets.API_KEY }}
```

## Going beyond

Once you hardened your GitHub Workflows as described above, you might ask
yourself what else you can do to secure your codebase.

Here are two ideas:

- **also consider dev environments:** running `npm i` on a PR with compromised
  dependencies locally can expose the development machine — with potentially
  even worse implications than inside a GitHub Action
- **delay dependency updates:** delaying the testing of updates by a few days
  after their release, you increase the chance of compromised updates being
  reported in the Advisory Database and picked up by `npm audit`

If you have any questions or comments, please
[reach out on Bluesky](https://bsky.app/profile/zentered.co) or
[start a discussion on GitHub](https://github.com/zentered/zentered/discussions/new?category=ideas-feedback).

## Further reading

- [Dependabot secrets](https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/managing-encrypted-secrets-for-dependabot)
  can only be used inside `dependabot.yml`, not in the Workflows
- [Suggestions for fixing GitHub Workflows](https://github.com/dependabot/dependabot-core/issues/3253)
- [Feature request for opt-in to allow secrets in Dependabot workflows](https://github.community/t/opt-in-to-allow-secrets-on-dependabot-workflows/183040)
