![GitHub Actions Security Best Practices [cheat sheet included]](https://hacktech.info/wp-content/plugins/trx_addons/components/lazy-load/images/placeholder.png)
“GitHub Actions keep me up at night. I worry that a malicious actor will use GitHub Actions to inject code into one of my repositories unbeknownst to me.”
GitHub Actions is an increasingly popular CI/CD platform. They allow to automate almost all the tasks of the development cycle while remaining easy to access. However, since they often use external code, they require some security measures to be applied. We have tried to gather the main tips to secure your GitHub Actions in this cheat sheet:

What are GitHub Actions?
GitHub Actions is GitHub’s CI/Cd service. It’s the mechanism used to run workflows from development to production systems. Actions are triggered by GitHub events (a pull request is submitted, an issue opened, a PR is merged, etc…) and can execute pretty much any command. For instance, they can be used to format the code, format the PR, sync an issue comments with another ticketing system’s comments, add the appropriate labels to a new issue, or trigger a full-scale cloud deployment.
A workflow is made of one or more jobs, which are run inside their own virtual machine or container (a runner), that execute one or more steps. A step can be a shell script or an action, which is a reusable piece of code specially packaged for the GitHub CI ecosystem.
Because GitHub is hosting millions of open-source projects that can be forked and contributed to through pull requests, GitHub Actions security is paramount to prevent supply-chain attacks.
This cheat sheet is here to help you mind the risks posed by some GitHub Action workflows, no matter if you are maintaining open-source projects or not.
Let’s dive into the best practices:
Set minimum scope for credentials
This is a general security principle for all the credentials used by your workflow, but let’s focus on a GitHub-specific one: the GITHUB_TOKEN.
This token is granting each runner privileges to interact with the repository. It is temporary, meaning its validity start and ends with the workflow.
By default, the token’s permissions are either “permissive” (read/write for most of the scopes) or “restricted” (no permission by default in most scopes). In either case, forked repos only have, at most, a read-access. Whether you choose one option or the other, the GITHUB_TOKEN should always be granted the minimum required permissions to execute a workflow/job.
You should make use of the ‘permission’ key in your workflows to configure the minimum required permissions for a workflow or job. This will allow fine-grained control over the privileges of your GitHub Actions. The set of permissions required to call each endpoint of the GitHub API is extensively documented, and you should verify what the default permissions are to match and adjust them.
💡
This principle applies to environment variables as well. To limit the scope of environment variables, you should always declare them at the step level, so they won’t be accessible to other stages. In contrast, defining them at the job level will make them available to all stages, including potentially compromised code (more on that later).
Typically, when people make their own workflows on GitHub, they use Actions made by someone else. Almost all workflows begin with a step like the one below:
- name: Check out repository
uses: actions/checkout@v3
Most people probably think:
“Well yeah, that just fetches my code. What could possibly be dangerous about that?”
The important part to consider is how it checks out your code. That line that starts with “uses” means that there’s some work going on behind the scenes to get the code from your GitHub repo to the server that is running your workflow. For the “actions/checkout” action, that behind-the-scenes stuff lives in its own repo. If you read the source code, there’s actually a lot going on that you wouldn’t have known about if you didn’t take a look! More than that though, you don’t maintain the code that is running in that Action.
When you think about it, there is some risk in blindly trusting all these actions. Third-party actions are interacting with your code and possibly running on a server you own, but do you ever look at what they are actually doing under the hood? Are you monitoring changes that are made to the action when the author publishes an update? For just about everyone, the answer to both of those questions is probably no.
💡
Consider this threat scenario: you are using a third-party action that runs a linter on your code to check for formatting issues. Rather than install, configure, and run a linter yourself, you decide to use an action from the GitHub Actions Marketplace that does what you need. You give it a test run, and it works! Since it does what you want, you set up a workflow in your repo that uses it:
– name: Lint code
uses: someperson/lint-action@v1
After months of using this action, you suddenly start having issues with your API keys stolen and abused. After some investigating, you find out that the author of the third party linter action recently pushed an update to the GitHub Marketplace. You go to the source repo for the action and see that code was recently added to the linter action to send environment variables to some random web address.
In that hypothetical scenario, the author of the third party action (or someone who hijacked their account) added malicious code to the action and re-tagged it as “v1”. Everyone using “someperson/linter-action@v1” was now running the malicious code in their workflows. Now that we see the threat from this scenario, how do we protect ourselves from it?
No one has the time to watch for updates on every third-party action they use, but luckily GitHub gives us a way to prevent updates from altering the actions we use. Rather than running an action with a tag from the repo, you can use a commit hash. For example, when I automatically push container images to Docker Hub, I use the following step in my workflows to authenticate:
- name: Log in to the container registry
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
By specifying exactly what commit I want to use when I authenticate to Docker Hub, I never have to worry about the action changing or behaving differently. You can do the same thing with any action that you use in your workflows.
Don’t use plain-text secrets
This one is a little more obvious, but it still needs to be said. Source code isn’t the only place where it’s a bad idea to store API keys and passwords in plain text. In fact, it’s probably better stated that there is no place where it’s okay to do