Skip to content(if available)orjump to list(if available)

How to harden GitHub Actions

How to harden GitHub Actions

73 comments

·May 6, 2025

ebfe1

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.

loginatnine

This is good, just bear in mind that if you put the hash of an external composite action and that action pulls on another one without a hash, you're still vulnerable on that transitive dependency.

ebfe1

oh damn - that is a great point! thanks matey!

newman314

I don't know if your tool already does this but it would be helpful if there is an option to output the version as a comment of the form

action@commit # semantic version

Makes it easy to quickly determine what version the hash corresponds to. Thanks.

ebfe1

Yeap - that is exactly what it does ;)

Example:

uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 #v1.16.0

And for anything that previously had @master, it becomes the following with the hash on the day it was pinned with "master-{date}" as comment:

uses: ravsamhq/notify-slack-action@b69ef6dd56ba780991d8d48b61d94682c5b92d45 #master-2025-04-04

fartbagxp

I've been using https://github.com/stacklok/frizbee to lock down to commit hash. I wonder how this tool compares to that.

remram

Having control is good, but reading all the code yourself seems unrealistic. We need something like crev or cargo-vet.

ebfe1

Yea hence it prompts for you to check the first time but once you verify the hash for particular version of action, it would automatically apply the hash to that same version of action everywhere. Also you can reuse the same config for all other repos so it is only tedious the first time but after that it is pretty quick to apply to the rest of the org :)

The tool is indeed meant for semi-auto flow to ensure human eye looked at the action being used.

tuananh

renovate can be configured to do that too :)

jquaint

Do you have an example config?

Trying to get the same behavior with renovate :)

cyrnel

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.

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.

abhisek

GitHub Actions by default provide isolated VM with root privilege to a workflow. Don’t think job level privilege isolation is in its threat model currently. Although it does allow job level scopes for the default GitHub token.

Also the secrets are accessible only when a workflow is invoked from trusted trigger ie. not from a forked repo. Not sure what else can be done here to protect against compromised 3rd party action.

cyrnel

People have been running different levels of privileged code together on the same machine ever since the invention of virtual machines. We have lots of lightweight sandboxing technologies that could be used when invoking a particular action such as tj-actions/changed-files that only gives it the permissions it needs.

You may do a "docker build" in a pipeline which does need root access and network access, but when you publish a package on pypi, you certainly don't need root access and you also don't need access to the entire internet, just the pypi API endpoint(s) necessary for publishing.

lmeyerov

Yes, by default things should be sandboxed - no network, no repo writes, ... - and should be easy to add extra caps (ex: safelist dockerhub)

Likewise, similar to modern smart phones asking if they should remove excess unused privs granted to certain apps, GHAs should likewise detect these super common overprovisionings and make it easy for maintainers to flip those configs, e.g., "yes" button

esafak

Where can I read about this? I see no reference in its repo: https://github.com/search?q=repo%3Atj-actions%2Fchanged-file...

cyrnel

Every action gets these permissions by default. The reason we know it had that permission is that the exploit code read from /proc/pid/mem to steal the secrets, which requires some permissions: https://blog.cloudflare.com/diving-into-proc-pid-mem/#access...

Linux processes have tons of default permissions that they don't really need.

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.

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.

[0] https://depot.dev/products/github-actions

colek42

We just built a new version of the witness run action that tracks the who/what/when/where and why of the GitHub actions being used. It provides "Trusted Telemetry" in the form of SLSA and in-toto attestations.

https://github.com/testifysec/witness-run-action/tree/featur...

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.

[0] https://sprinters.sh

maenbalja

Timely article... I recently learned about self-hosted runners and set one up on a Hetzner instance. Pretty smooth experience overall. If your action contains any SSH commands and you'd like to avoid setting up a firewall with 5000+ rules[0], I would recommend self-hosting a runner to help secure your target server's SSH port.

[0] https://api.github.com/meta

woodruffw

FWIW: Self-hosted runners are non-trivial to secure[1]; the defaults GitHub gives you are not necessarily secure ones, particularly if your self-hosted runner executes workflows from public repositories.

(Self-hosted runners are great for many other reasons, not least of which is that they're a lot cheaper. But I've seen a lot of people confuse GitHub Actions' latent security issues with something that self-hosted runners can fix, which is not per se the case.)

[1]: https://docs.github.com/en/actions/security-for-github-actio...

maenbalja

Hm that's good to know, thanks for the link. I'm just using the runner for private solo projects atm so I think my setup will do for now. But I definitely didn't consider the implications of using it on a private project with other contributors yikes.

gose1

> Safely Writing GitHub Workflows

If you are looking for ways to identify common (and uncommon) vulnerabilities in Action workflows, last month GitHub shipped support for workflow security analysis in CodeQL and GitHub Code Scanning (free for public repos): https://github.blog/changelog/2025-04-22-github-actions-work....

The GitHub Security Lab also shared a technical deep dive and details of vulnerabilities that they found while helping develop and test this new static analysis capability: https://github.blog/security/application-security/how-to-sec...

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.

pabs3

Review every single line of source code before use, and bootstrap from source without any binaries.

https://github.com/crev-dev https://bootstrappable.org/ https://lwn.net/Articles/983340/

guappa

We use linux distributions.

tomrod

How do apt, dnf, and apk prevent malicious software from getting into repositories?

guappa

You have a 2nd independent sets of eyes looking at software, rather than "absolutely nobody" like it is if you use npm and friends?

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.

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.

duped

I feel like there was a desire from GH to avoid needing a "build" step for actions so you could use `use: someones/work` or whatever, `git push` and see the action run.

But if you think about it, the entire design is flawed. There should be a `gh lock` command you can run to lock your actions to the checksum of the action(s) your importing, and have it apply transitively, and verify those checksums when your action runs every time it pulls in remote dependencies.

That's how every modern package manager works - because the alternative are gaping security holes.