The ultimate guide to GitHub Actions authentication
Tags: github, githubactions, security
Published: 2021-10-07 10:39:25

---
title: The ultimate guide to GitHub Actions authentication
published: true
date: 2021-10-05 16:01:58 UTC
tags: github, githubactions, security
canonical_url: https://michaelheap.com/ultimate-guide-github-actions-authentication/
---

If you’ve done any work with GitHub Actions, you’ve probably come across the `GITHUB_TOKEN` secret. This token allows you to interact with the GitHub API and push or pull a repositories contents. GitHub automatically generate a token for you, and it’s only available whilst your workflow jobs are running.

This is awesome for 90% of the things you’d want to do with GitHub Actions, but what about the remaining 10%? The tasks where `GITHUB_TOKEN` doesn’t have the correct permissions, or when you want to trigger another GitHub Action somehow, but the `GITHUB_TOKEN` key won’t let you?

The good news is that you’re not out of luck! You’ve got two options for working with the GitHub API - a personal access token (or `PAT`) and a GitHub App. A PAT is much simpler to set up, but a GitHub App is a lot more powerful. It’s entirely up to you which route you choose.

## Working with `GITHUB_TOKEN`

Before we move on to personal access tokens and GitHub Apps, I want to spend a little time talking about the magical `GITHUB_TOKEN` secret that GitHub provide. It works well enough for the majority of cases, so you probably haven’t thought too much about it.

Even though it’s just _there_, there’s a lot to `GITHUB_TOKEN`.

### Token Permissions

I'm going to lead with this as it’s key but not many people know about it. `GITHUB_TOKEN` allows you to specify which permissions the token is granted.

I’m going to say that one more time for the people in the back. **`GITHUB_TOKEN` allows you to specify which permissions the token is granted.**

This is _huge_, as it means that a rogue action can only perform the actions that you’re expecting a workflow to do.

Imagine that you work on a team where you use labels to mark pull requests as major, minor or patch version changes. If it’s a major change, you want to automatically add a comment explaining why the PR won’t be immediately merged. The workflow file you use would look something like this:


```yaml
on:
  pull_request:
    types:
      - labeled
jobs:
  add-comment:
    runs-on: ubuntu-latest
    steps:
      - uses: evil-author/add-comment@main
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          comment: "Thanks for the PR! I've tagged this for the next major release as it has some backwards compatability breaks"
          label: "semver:major"
```

Your workflow could work great for the first month, six months, or year. However, there’s no guarantees about what code `evil-author/add-comment@main` will run at any point in time (if you **do** want guarantees, check out [pin-github-action](https://github.com/mheap/pin-github-action "Automatically pin your GitHub Actions dependencies at a specific SHA")).

Imagine that you’re running the action on a private repository full of sensitive information. Although you set out to automatically add comments to a pull request, somehow the `add-comment` action has been compromised and the code on `main` is doing something that you're not expecting. Now all your data has been sent off to a third party server by some code that wasn't there when you added the action to your workflow.

You can prevent actions from doing anything except what you expect by using the `permissions` key in your workflow file. If we want the `add-comment` action to _only_ have the ability to add a comment to an issue, we can add the `issues` permission:

```yaml
on:
  pull_request:
    types:
      - labeled

permissions:
  issues: write

jobs:
  add-comment:
    runs-on: ubuntu-latest
    steps:
      - uses: evil-author/add-comment@main
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          comment: "Thanks for the PR! I've tagged this for the next major release as it has some backwards compatability breaks"
          label: "semver:major"
```

If the `add-comment` action is compromised with this workflow, it will be unable to read the repository’s contents using the API as you’ve only provided the `issues` permission. When you set the `permissions` key, all unspecified permissions default to no access.

You can also provide `permissions` at the `job` level instead of, or in addition to, the workflow level. This allows you to provide specific permissions to actions that require them.

```yaml
on:
  pull_request:
    types:
      - labeled

permissions:
  issues: write

jobs:
  add-comment:
    runs-on: ubuntu-latest
    steps:
      - uses: evil-author/add-comment@main
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          comment: "Thanks for the PR! I've tagged this for the next major release as it has some backwards compatability breaks"
          label: "semver:major"
  needs-checks:
    runs-on: ubuntu-latest
    permissions:
      checks: write
    steps:
      - uses: trusted-user/update-check@v1
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          name: merge-this-sprint
          status: fail
```

Be careful! If you don’t specify the `permissions` key, the token will be granted read/write permission for all of the available scopes. If you’d like to change this behaviour, you can head in to your repository settings, choose the _Actions_ section on the left and then select _Read repository contents permission_ at the bottom.

Once that’s done, any `GITHUB_TOKEN` generated in a workflow without a `permissions` section will only be able to read repository contents and metadata - not pull requests, issues, checks or any of the other scopes.

![workflow-permissions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o5gp53ehi263nyaj3hq1.png)
 

Be careful though, as even with this restricted configuration, the `read` permission is granted for your repo contents (and the `contents` permission covers [a lot more](https://docs.github.com/en/rest/reference/permissions-required-for-github-apps#permission-on-contents) than just the files in the repo).

If you’ve unsure which permissions your `GITHUB_TOKEN` has, you can see them listed in the _Set up job_ section of the logs for your run. It’ll look something like this:

```
> GITHUB_TOKEN Permissions
  Actions: write
  Checks: write
  Contents: write
  Deployments: write
  Discussions: write
  Issues: write
  Metadata: read
  Packages: write
  PullRequests: write
  RepositoryProjects: write
  SecurityEvents: write
  Statuses: write
```

### Security

Staying with the security theme, there are a lot of security considerations around `GITHUB_TOKEN` in addition to being able to set granular permissions.

**Expiration**: A new `GITHUB_TOKEN` is generated when each job begins, and the token expires when the job is finished

**Secure**: The `GITHUB_TOKEN` secret will not be shown in logs and can not be extracted from the runner via HTTP (trust me, I’ve tried)

**Available by default**: This is **important**. GitHub Actions that you use can access the GitHub token _even if you don’t pass it in as an input_. They can access it through the `github.token` context, including setting it as a default input in their `action.yml`. You should make sure that you set the minimum permissions required using the `permissions` parameter.

**Scope**: The `GITHUB_TOKEN` is scoped to the repository running the workflow. It cannot be used to make changes to any other repositories.

These security considerations make the `GITHUB_TOKEN` the best choice when interacting with the GitHub API in your actions unless you have specific requirements that require a behaviour that the `GITHUB_TOKEN` can’t achieve.

### When `GITHUB_TOKEN` isn’t enough

The number 1 question that I’ve seen around the `GITHUB_TOKEN` is “_Why doesn’t it trigger new workflow runs?_”. GitHub decided that any actions performed by the GitHub Actions bot should not trigger events, which means that you can’t use a workflow to trigger another workflow whilst using the `GITHUB_TOKEN` secret. This is to prevent circular dependencies that can cause your builds to get stuck in a loop.

The other question I frequently see is “_Can we change the user shown for `GITHUB_TOKEN`?_”. This can be useful when changes are committed to a repository, or a comment is added to an issue or pull request. Rather than showing `github[bot]` as the user, people would like to show a company branded account. This is possible when committing changes by setting the author email address, but not when adding an issue or pull request.

The `GITHUB_TOKEN` is limited to the current repository, which is great in most cases but prevents some workflows. If you want to make a change in another repository when a PR is merged, this is not possible with the default `GITHUB_TOKEN`. An example of this workflow is to automatically update a submodule pointer in repository B when `main` changes in repository A.

Fortunately there are ways to work around all of these limitations. Keep reading to understand the options and the pros and cons of each.

### Bonus: It’s a GitHub App under the hood!

The `GITHUB_TOKEN` provided by actions isn’t something custom built especially for Actions. It leverages the existing GitHub Apps infrastructure, which is why the `permissions` list is a lot closer to how GitHub Apps handles permissions than how user accounts handle scopes.

Whilst the list of permissions is limited today, there may be a day when the full set of applications permissions is available to `GITHUB_TOKEN`. Imagine being able to control your Environments automatically using the provided `GITHUB_TOKEN`.

I think GitHub have made the correct decision today, where there are limited permissions available as people learn about token security with Actions, but I’m interested to see what else they open up in the future.

## Authentication options

Now that we’ve covered `GITHUB_TOKEN` and the limitations you can face when using it, let’s take a look at the alternatives. Most of the workarounds you’ll find online recommend using a Personal Access Token (or PAT). There is another option though - GitHub Apps!

In this section we’ll take a look at both personal access tokens and GitHub Apps to help decide which is the correct option for you.

## Personal Access Token (PAT)

A personal access token (PAT) is an authentication token scoped to a single GitHub user account. The scopes available are the same as the scopes for [OAuth Apps](https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps). These tokens are typically used by an individual to work with their repositories and data, and as such the permissions are quite coarse. Granting the `repo` scope allows access to almost everything

> Grants full access to repositories, including private repositories. That includes read/write access to code, commit statuses, repository and organization projects, invitations, collaborators, adding team memberships, deployment statuses, and repository webhooks for repositories and organizations. Also grants ability to manage user projects.

The only areas that you can’t modify with a `repo` scoped token are repo workflows and GitHub packages, in addition to some organization administration tasks.

If you don’t need total access, there are sub-permissions available, such as `repo:status`, `public_repo` and `repo:invite`. As always, selecting the minimum permissions required to achieve your task is key. If you don’t need access to private repositories, the `public_repo` scope might be the correct one for you. Alternatively, if you only need to read and write commit statuses, the `repo:status` permission may be all you need.

If you’re going to use a PAT for automation purposes, I recommend creating a dedicated GitHub account to use. This allows you to control which repositories that account has access to, and prevents any integrations performing actions as your personal user. This is a common pattern, and even the GitHub docs team use it with their [Octomerger Bot](https://github.com/octomerger).

### Creating a PAT

To create a personal access token, you can visit the [PAT page](https://github.com/settings/tokens) in settings whilst logged in. This will show all of your existing tokens and allows you to create a new token.

> Make sure to add a good note for your new token. In 6 months time when you’re reviewing your tokens (you review regularly, right?) you’ll be thankful that you know why each token exists

Token expiration is a recent feature, which I recommend you take advantage of. Having an expired token cause your workflow to fail isn’t ideal, but it’s better than having a leaked token be valid forever. My recommendation is to set this value as low as possible, with 30 days being my personal maximum validity. Rotating this credential is easy if you use organization secrets to define it once and provide access to multiple repositories. If you’re not using an org and need to update multiple repositories under a user account you may find [github-update-secret](https://github.com/mheap/github-update-secret) useful.

Finally, you have to choose your scopes. As mentioned above, try and minimise the scopes available. My personal workflow is to generate a token with `repo` scope whilst developing a workflow, then revoke that token and build a new token with minimal permissions once the action is working. Unfortunately, there’s no way to provide the `issues` permission without also allowing access to everything else.

### Benefits

The biggest benefit to personal access tokens is how easy they are to get started with. You can switch from using `GITHUB_TOKEN` to a `PAT` in under 5 minutes. Once you’ve switched, all the limitations of `GITHUB_TOKEN` disappear.

You can now trigger a workflow from another workflow. I’ve used this to trigger automatic release generation whenever a pull request was merged to `main`.

You can also make changes across multiple repositories. Going back to an earlier example, you could now raise a pull request in repository B whenever `main` changes in repository A.

Finally, as your personal access token belongs to a user, any comments or commits added will show the username and profile image of your account. This can make the experience a little more personable, even though it’s still being automated.

### Downsides

Although using a PAT makes it easy to get started, there are some downsides compared to using the `GITHUB_TOKEN`.

The biggest downside is that if your token is leaked, it can be used by people other than the GitHub Actions runner. If you set a long expiry time, this could go undetected for a while. If you set a short expiry time, you’ll spend all your time rotating credentials.

The available permissions are also less granular than those offered by the `GITHUB_TOKEN`. There is no way to provide access to just `issues` without also giving access to everything else in the `repo` scope.

> This is changing! If you're interested in granular scopes for personal access tokens, follow [this public roadmap item](https://github.com/github/roadmap/issues/184) from GitHub

Secondly, people join and leave companies. If your PAT is owned by someone that no longer works there, and no longer has access to your repos (with good reason!) all your workflows will stop working.

If you use an automation account, it’s likely to be shared across multiple purposes on your team. This makes it more difficult to track who is making each change as these credentials can be used anywhere, not just within Actions.

Finally, if you work at a company with GitHub enterprise enabled you may start hitting compliance issues. Shared accounts are frowned upon, and it gets even harder when your GitHub login is done through single sign on with your IDP. Suddenly you now have to pay for a few additional (or lots of additional, depending on how they feel about shared accounts) GitHub seats, each of which is mapped to an identity in your IDP. It _is_ possible (I’ve done it!), but the barrier to entry is much higher than the five minutes promised earlier.

## GitHub Apps

If you don’t need to perform actions on behalf of a user, a GitHub Apps might be the right choice for you. Applications are how the majority of GitHub integrations are built, and is how people built custom workflows before Actions existed (usually using [Probot](https://probot.github.io/)).

Applications excel when it comes to security. They give you the fine grained permissions that `GITHUB_TOKEN` provides, time limited tokens (without the need to rotate credentials) and more!

### Creating an application

Creating an application is a little more involved than creating a PAT. There are two types of application - GitHub Apps and OAuth apps. Make sure that you’re on the [GitHub Apps](https://github.com/settings/apps) page in your settings. An application can be owned by an individual user account, or an organization. If you’re using this for work, I’d recommend creating the application under your org.

The GitHub App name and description are usually critical when it comes to onboarding, but as this is intended to be an internal only application you can choose any name and description that you like. You’ll also need to provide a homepage URL, but this isn’t used by a callback workflow at all, so put in any URL you like.

You don’t need to provide a Callback URL or Setup URL, and you’ll want to make sure that webhooks are deactivated.

Finally, we’re on to _Repository permissions_. This is the important part. Each of the permissions shown can be granted with a `none`, `read`, `write` and in some cases, `admin` permission. You can click on the `(i)` next to each permission to learn more about which endpoints are covered by that permission. [Here](https://docs.github.com/en/rest/reference/permissions-required-for-github-apps#permission-on-checks)is the list of endpoints accessible with the `checks` permission. It can be quite hard to read, so let me break it down for you.

![check-permissions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k5cnzf2yi4w1i7hiuf71.png)

If you grant the `checks` permission to your application, you’ll be able to call `POST /repos/:owner/:repo/check-runs` to create a check run, but only if you’ve granted `write` permission. You’ll also be able to call `GET /repos/:owner/:repo/check-runs/:check_run_id` if you have `read` _or_ `write` permission.

It can be hard to match up each endpoint to what you’re trying to achieve, but you can click each request to learn more about each endpoint’s capabilities and parameters.

It’s important to note that the permissions you select when creating an application are the **maximum permissions available to that application**. You can request fewer permissions in the generated token, but you can’t ask for extra permissions. With that in mind, I’d spend some time thinking about what your actions need to achieve and set these permissions appropriately.

Right at the bottom there’s a _User permissions_ section - we can ignore this as we’ll be making server to server requests rather than editing users.

Finally, you need to decide where the application can be installed. I tend to allow it only on my account and create new applications in each organization that needs to use application authentication.

Create your application and save your `App ID` and `Private Key` in a safe place as you’ll need them both in the next step. You’ll also need to [install it on your account or organization](https://docs.github.com/en/developers/apps/managing-github-apps/installing-github-apps#installing-your-private-github-app-on-your-repository) before using it to create authentication tokens.

### Using applications in a GitHub workflow

Now that you’ve created an application, it’s time to update your workflows to generate an authentication token using that app rather than using `GITHUB_TOKEN` or a `PAT`.

There are a couple of actions available, but I like [this one from Peter Murray](https://github.com/peter-murray/workflow-application-token-action) as it works for applications installed at both an organization and a repository level.

To use this action you’ll need to create two secrets in your repo or organization: `APPLICATION_ID` and `APPLICATION_PRIVATE_KEY` using the details you saved when creating an application.

Once that’s done, it’s time to update your workflow! Imagine that you have the following workflow that creates a release using the provided `GITHUB_TOKEN`:

```yaml
jobs:
  create-release:
    runs-on: ubuntu-latest

    steps:
      - name: Use Application Token to create a release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with: ...
```

This works, but you’d like to create a release as your company’s application rather than the GitHub bot. By adding an additional step to generate a token using your application, you can use the token output in the `create-release` action:

```yaml
jobs:
  create-release:
    runs-on: ubuntu-latest

    steps:
      - name: Get Token
        id: get_workflow_token
        uses: peter-murray/workflow-application-token-action@v1
        with:
          application_id: ${{ secrets.APPLICATION_ID }}
          application_private_key: ${{ secrets.APPLICATION_PRIVATE_KEY }}

      - name: Use Application Token to create a release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ steps.get_workflow_token.outputs.token }}
        with: ...
```

This will create the release with your application’s identity, rather than using the GitHub bot.

I mentioned earlier that you can request limited permissions from an application. The example above will return a token that has the maximum permissions available to an application. Here’s an example of a workflow that requests `write` access to `issues` and `read` access to `deployments`:

> Support for permissions is a recent addition and is not available until [this PR](https://github.com/peter-murray/workflow-application-token-action/pull/6) is merged

```yaml
jobs:
  permission-demo:
    runs-on: ubuntu-latest

    steps:
      - name: Get Token
        id: get_workflow_token
        uses: peter-murray/workflow-application-token-action@v1
        with:
          application_id: ${{ secrets.APPLICATION_ID }}
          application_private_key: ${{ secrets.APPLICATION_PRIVATE_KEY }}
          permissions: "issues:write,deployments:read"
```

You’ve now got a solution that has all the benefits of `GITHUB_TOKEN` such as granular permissions and automatic expiry, but none of the downsides. Let’s dig in to the benefits and downsides of using a GitHub App for authentication.

### Benefits

Let’s go back to the number 1 question that people have about `GITHUB_TOKEN`. “_Why does my workflow not trigger other workflows?_”. Using a GitHub App solves this problem, just like using a personal access token does. However, there are other benefits that applications bring over a PAT.

Application tokens are valid for a very short amount of time. Using this action, the token is valid for 60 seconds from the moment it’s created. This means that even if it’s leaked by an action, it will be useless almost immediately.

As this is an application and not an account, there’s no shared account for people to log in to. This means better accountability, and no SSO/IDP issues mentioned above if you’re using GitHub Enterprise.

Finally, the biggest benefit to using applications is how granular the permissions are. Here are all the different permissions that you can allow using applications:

**Repository permissions**

- Actions
- Administration
- Checks
- Content references
- Contents
- Deployments
- Discussions
- Environments
- Issues
- Metadata
- Organization packages
- Packages
- Pages
- Pull requests
- Webhooks
- Projects
- Secret scanning alerts
- Secrets
- Security events
- Single file
- Commit statuses
- Dependabot alerts
- Workflows

**Organization permissions**:

- Members
- Administration
- Events
- Webhooks
- Plan
- Projects
- Secrets
- Self-hosted runners
- Blocking users
- Team discussions

### Downsides

The biggest downside to using a GitHub App is that you need to edit every workflow to add an additional step where you generate a token for use later on. This can make your workflows harder to read for those that aren’t familiar with the application token workflow.

If you need to perform any actions as a user, this authentication mechanism won’t work for you. Whilst GitHub App _do_ support user OAuth, the way we’ve configured it for use with Actions they’re not suitable for managing user profiles.

Finally, understanding all of the available permissions can be tough. Using applications for authentication has a lot of benefits even if you don’t use it to scope permissions to the minimum required, but you’re losing the biggest benefit if you don’t.

## So which is the best?

There are three options for authentication when it comes to GitHub Actions:

1. `GITHUB_TOKEN`
2. Use a Personal Access Token (PAT)
3. Generate credentials with a GitHub App

Each of these options have pros and cons. If you can achieve everything you need using `GITHUB_TOKEN`, I’d stick with that for your workflows. It’s well understood by the community and reduces the number of moving parts in your workflows.

Given the choice between a PAT and a GitHub App, I’d choose a GitHub App _unless_ you need to perform actions as a user. It solves the main problems with `GITHUB_TOKEN` (triggering new workflow runs, posting as an identity other than `github[bot]`) without any of the issues that a `PAT` introduces (long lived tokens, shared accounts).

That concludes the ultimate guide to GitHub Actions authentication. If you’ve got any questions or comments, I’m [@mheap on Twitter](https://twitter.com/mheap/) and I’d love to hear them.