The ultimate guide to GitHub Actions authentication

05 Oct 2021 in Infrastructure

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/[email protected]
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/[email protected] will run at any point in time (if you do want guarantees, check out pin-github-action).

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/[email protected]
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/[email protected]
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/[email protected]
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.

Available `permissions`

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 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. 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.

Creating a PAT

To create a personal access token, you can visit the PAT page 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 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 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).

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 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. Hereis the list of endpoints accessible with the checks permission. It can be quite hard to read, so let me break it down for you.

Available `check` permissions

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 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 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/[email protected]
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-applic[email protected]
with:
application_id: ${{ secrets.APPLICATION_ID }}
application_private_key: ${{ secrets.APPLICATION_PRIVATE_KEY }}
- name: Use Application Token to create a release
uses: actions/[email protected]
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 is merged

yaml
jobs:
permission-demo:
runs-on: ubuntu-latest
steps:
- name: Get Token
id: get_workflow_token
uses: peter-murray/[email protected]
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 and I’d love to hear them.