How to Harden GitHub Actions: The Unofficial Guide
35 comments
·May 6, 2025cyrnel
This has some good advice, but I can't help but notice that none of this solves a core problem with the tj-actions/changed-files issue: The workflow had the CAP_SYS_PTRACE capability when it didn't need it, and it used that permission to steal secrets from the runner process.
You don't need to audit every line of code in your dependencies and their subdependencies if your dependencies are restricted to only doing the thing they are designed to do and nothing more.
There's essentially nothing nefarious changed-files could do if it were limited to merely reading a git diff provided to it on stdin.
Github provides no mechanism to do this, probably because posts like this one never even call out the glaring omission of a sandboxing feature.
esafak
Where can I read about this? I see no reference in its repo: https://github.com/search?q=repo%3Atj-actions%2Fchanged-file...
delusional
What would be outside the sandbox? If you create a sandbox that only allows git diff, the I suppose you fixed this one issue, but what about everything else? If you allow the sandbox to be configurable, then how do you configure it without that just being programming?
The problem with these "microprograms" have always been that once you delegate so much, once you are willing to put in that little effort. You can't guarantee anything.
If you are willing to pull in a third party dependency to run git diff, you will never research which permissions it needs. Doing that research would be more difficult than writing the program yourself.
kylegalbraith
Glad this got posted. It's an excellent article from the Wiz team.
GitHub Actions is particularly vulnerable to a lot of different vectors, and I think a lot of folks reach for the self-hosted option and believe that closes up the majority of them, but it really doesn't. If anything, it might open more vectors and potentially scarier ones (i.e., a persistent runner could be compromised, and if you got your IAM roles wrong, they now have access to your AWS infrastructure).
When we first started building Depot GitHub Actions Runners [0], we designed our entire system to never trust the actual EC2 instance backing the runner. The same way we treat our Docker image builders. Why? They're executing untrusted code that we don't control.
So we launch a GitHub Actions runner for a Depot user in 2-5 seconds, let it run its job with zero permissions at the EC2 level, and then kill the instance from orbit to never be seen again. We explicitly avoid the persistent runner, and the IAM role of the instance is effectively {}.
For folks reading the Wiz article. This is the line that folks should be thinking about when going the self-hosted route:
> Self-hosted runners execute Jobs directly on machines you manage and control. While this flexibility is useful, it introduces significant security risks, as GitHub explicitly warns in their documentation. Runners are non-ephemeral by default, meaning the environment persists between Jobs. If a workflow is compromised, attackers may install background processes, tamper with the environment, or leave behind persistent malware.
> To reduce the attack surface, organizations should isolate runners by trust level, using runner groups to prevent public repositories from sharing infrastructure with private ones. Self-hosted runners should never be used with public repositories. Doing so exposes the runner to untrusted code, including Workflows from forks or pull requests. An attacker could submit a malicious workflow that executes arbitrary code on your infrastructure.
diggan
> Using Third-Party GitHub Actions
Maybe I'm overly pedantic, but this whole section seems to miss the absolutely most obvious way to de-risk using 3rd party Actions, review the code itself? It talks about using popularity, number of contributors and a bunch of other things for "assessing the risk", but it never actually mentions reviewing the action/code itself.
I see this all the time around 3rd party library usage, people pulling in random libraries without even skimming the source code. Is this really that common? I understand for a whole framework you don't have time to review the entire project, but for these small-time GitHub Actions that handle releases, testing and such? Absolute no-brainer to sit down and review it all before you depend on it, rather than looking at the number of stars or other vanity-metrics.
KolmogorovComp
Because reading the code is useless if you can't pin the version, and the article explains well it's hard to do
> However, only hash pinning ensures the same code runs every time. It is important to consider transitive risk: even if you hash pin an Action, if it relies on another Action with weaker pinning, you're still exposed.
ratrocket
Depending on your circumstances (and if the license of the action allows it) it's "easy" to fork the action and use your own fork. Instant "pinning".
carlmr
But how does that solve the issue with the forked action not using pinned versions itself.
You need to recursively fork and modify every version of the GHA and do that to its sub-actions.
You'd need something like a lockgile mechanism to prevent this.
axelfontaine
This is a great article, with many important points.
One nitpick:
> Self-hosted runners should never be used with public repositories.
Public repositories themselves aren't the issue, pull requests are. Any infrastructure or data mutable by a workflow involving pull requests should be burned to the ground after that workflow completes. You can achieve this with ephemeral runners with JIT tokens, where the complete VM is disposed of after the job completes.
As always the principle of least-privilege is your friend.
If you stick to that, ephemeral self-hosted runners on disposable infrastructure are a solid, high-performance, cost-effective choice.
We built exactly this at Sprinters [0] for your own AWS account, but there are many other good solutions out there too if you keep this in mind.
bob1029
> By default, the Workflow Token Permissions were set to read-write prior to February 2023. For security reasons, it's crucial to set this to read-only. Write permissions allow Workflows to inadvertently or maliciously modify your repository and its data, making least-privilege crucial.
> Double-check to ensure this permission is set correctly to read-only in your repository settings.
It sounds to me like the most secure GH Action is one that doesn't need to exist in the first place. Any time the security model gets this complicated you can rest assured that it is going to burn someone. Refer to Amazon S3's byzantine configuration model if you need additional evidence of this.
wallrat
Been tracking this project for a while https://github.com/chains-project/ghasum . It creates a verifiable checksum manifest for all actions - still in development but looks very promising.
Will be a good compliment to Github's Immutable Actions when they arrive.
cedws
I’ve been reviewing the third party Actions we use at work and seen some scary shit, even with pinning! I’ve seen ones that run arbitrary unpinned install scripts from random websites, cloning the HEAD of repos and running code from there, and other stuff. I don’t think even GitHub’s upcoming “Immutable Actions” will help if people think it’s acceptable to pull and run arbitrary code.
Many setup Actions don’t support pinning binaries by checksum either, even though binaries uploaded to GitHub Releases can be replaced at will.
I’ve started building in house alternatives for basically every third party Action we use (not including official GitHub ones) because almost none of them can be trusted not to do stupid shit.
GitHub Actions is a security nightmare.
MillironX
Even with pinning, a common pattern I've seen in one of my orgs is to have a bot (Renovate, I think Dependabot can do this too) automatically update the pinned SHA when a new release comes out. Is that practically any different than just referencing a tag? I'm genuinely curious.
wongarsu
I guess you still have some reproducibility and stability benefits. If you look at an old commit you will always know which version of the action was used. Might be great if you support multiple releases (e.g. if you are on version 1.5.6 but also make new point releases for 1.4.x and 1.3.x). But the security benefits of pinning are entirely negated if you just autoupdate the pin.
MadsRC
Shameless plug, I pushed a small CLI for detecting unpinned dependencies and automatically fix them the other day: https://codeberg.org/madsrc/gh-action-pin
Works great with commit hooks :P
Also working on a feature to recursively scan remote dependencies for lack of pins, although that doesn’t allow for fixing, only detection.
Very much alpha, but it works.
tomrod
I support places that use GH Actions like its going out of style. This article is useful.
I wonder how we get out of the morass of supply chain attacks, realistically.
guappa
We use linux distributions.
tomrod
How do apt, dnf, and apk prevent malicious software from getting into repositories?
liveoneggs
never update
wongarsu
In principle by having the repository maintainer review the code they are packaging. They can't do a full security review of every package and may well be fooled by obfuscated code or deliberately introduced bugs, but the threshold for a successful attack is much higher than on Github Actions or npm.
Arch-TK
The recommendation is not to interpolate certain things into shell scripts. Don't interpolate _anything_ into shell scripts as a rule. Use environment variables.
This combined with people having no clue how to write bash well/safely is a major source of security issues in these things.
cedws
Zizmor has a check for this.
After tj-actions hack, I put together a little tool to go through all of github actions in repository to replace them with commit hash of the version
https://github.com/santrancisco/pmw
It has a few "features" which allowed me to go through a repository quickly:
- It prompts user and recommend the hash, it also provides user the url to the current tag/action to double check the hash value matches and review the code if needed
- Once you accept a change, it will keep that in a json file so future exact vesion of the action will be pinned as well and won't be reprompted.
- It let you also ignore version tag for github actions coming from well-known, reputational organisation (like "actions" belong to github) - as you may want to keep updating them so you receive hotfix if something not backward compatible or security fixes.
This way i have full control of what to pin and what not and then this config file is stored in .github folder so i can go back, rerun it again and repin everything.