Skip to content
Let’s talk
GREY WING SECURITY
All field notes
Application security

GitHub Actions Hardening: A Checklist After GhostAction

GhostAction stole 3,325 secrets with fake security workflows. Ten GitHub Actions controls to close that door, and the evidence each one leaves.

10 min read

GhostAction is back. GitGuardian reports that between August 31 and September 30, 2026, the same campaign hit 772 more public GitHub repos and targeted 2,577 secrets. An October 13 update says that from October 8 to 12 the actor targeted more than 30,000 repos across 3,000 users and orgs, and that GitHub started blocking the malicious workflow on October 9.

The first wave came in September 2025. A workflow called "Github Actions Security" showed up in hundreds of repos. It did nothing for security. It read the repo's secrets and posted them to a server the attacker ran. GitGuardian's original report counted 327 GitHub users, 817 repos, and 3,325 stolen secrets, including PyPI, npm, DockerHub, GitHub, and AWS keys.

Our view, from the readiness side: most teams treat CI as plumbing, but your pipeline holds the keys to your packages, your cloud, and your customers. Anyone who can push a workflow file can read those keys. Code scanning matters, and the harder question is this one: who can make our pipeline run new code with our secrets, and would we notice?

How GhostAction worked

The attack was simple. That is the point.

  1. The attacker got control of a GitHub account with write access. GitGuardian says the first way in is still unknown.
  2. They read the repo's real workflow files to learn the secret names.
  3. They pushed one new file to .github/workflows/. It ran on every push. It sent the named secrets out with one curl command.

There was no exploit. A trusted account pushed one file, and the pipeline handed its secrets to the workflow that asked.

A note on the numbers. GitGuardian sells secret detection, so they have a reason to find big numbers. We still think the core facts hold, because their post shows the workflow code, the commit message, and the endpoint. Treat the counts as one vendor's finding, not a census.

It keeps happening

Six months before the first wave, in March 2025, attackers changed the tj-actions/changed-files action. They moved its version tags to point at a bad commit. That commit printed secrets into build logs. The GitHub advisory for CVE-2025-30066 says over 23,000 repos used the action. CISA added it to its Known Exploited Vulnerabilities list. Teams that used the tag @v45 got the bad code without changing a line.

Two attacks, two doors. GhostAction came in through a person who could push. tj-actions came in through code you pulled from someone else. A good checklist closes both.

The checklist

Each item names the GitHub feature and the evidence it leaves behind. Use "evidence" in the plain sense: a record you can show later.

1. Make the default token read-only

Every workflow gets a GITHUB_TOKEN. Set the org or repo default to read-only. Then grant more, job by job, with the permissions key in the workflow file. See GitHub's token permissions docs.

Evidence: the setting, plus a permissions block in each workflow.

2. Pin third-party actions to a full commit SHA

Tags can move. That is how tj-actions spread. GitHub's security hardening guide tells teams to pin to a full-length commit SHA, and now names a workflow dependency lockfile as the stronger option where you can adopt one. Lockfiles are in public preview, so full-SHA pinning is still the baseline. Keep a comment with the version so humans can read it. Turn on Dependabot version updates for the github-actions ecosystem so it proposes new SHAs. Set an allowed-actions policy at the org or repo level so only actions you approve can run.

Evidence: no uses: line that points at a tag or branch for actions you do not own, the Dependabot config, and the allowed-actions setting.

3. Put workflow files behind an owner

Add .github/workflows/ to CODEOWNERS and require code owner review on your main branch. Now a workflow change into main needs a named person to approve it.

Be clear on the limit. CODEOWNERS works on pull requests. GhostAction's workflow ran on push, so a push to any branch could fire it. CODEOWNERS and a PR rule on main do not stop that. That is why items 4 and 5 matter more than this one.

If your plan supports push rulesets, you can go further. GitHub's push rulesets can block any push that changes files under .github/workflows/, on any branch, for everyone not on the bypass list. They work on private and internal repos only, so check your plan and repo type first.

Evidence: the CODEOWNERS file, the branch ruleset that requires pull requests and code owner review on main, and the push ruleset if you use one.

4. Move real secrets into environments

Repo secrets, and org secrets shared with the repo, are open to any workflow run in that repo. Anyone with write access can push a workflow to any branch and read them. Environment secrets are different. A job only gets them when it names the environment and passes its rules. You can limit which branches and tags may deploy to it, and require a reviewer to approve the run.

Put your publish tokens and deploy keys there. Limit them to your main branch. Add a required reviewer for production. On private repos, required reviewers need GitHub Enterprise. A rogue workflow on a side branch then cannot use that environment, so it cannot read those secrets. Be precise about what this covers. The rogue workflow can just skip the environment and read every repo and org secret instead. So the win only holds if the high-value secrets have actually moved out of repo and org scope.

Evidence: environment settings, deployment branch rules, the deployment history for each environment, and the approval record for each deploy. Also a list showing which secrets still live at repo or org level.

5. Replace long-lived cloud keys with OIDC

GhostAction took AWS keys that lived in repo secrets. With OpenID Connect, the workflow asks your cloud for a short-lived token at run time. There is no stored key to steal. The trust policy is the part that matters. Pin it to the token's subject claim for one repo plus one environment (or one branch), not just your org. Then a workflow on a side branch cannot assume the role. See GitHub's example subject claims.

Evidence: the cloud trust policy with its subject condition, and the absence of static cloud keys in secrets.

6. Use rulesets, and keep the bypass list short

Rulesets let you protect branches across many repos at once, with a named bypass list. Require pull requests and status checks on main. Review who is on the bypass list. Admins should not be there by habit. Rulesets on private repos need a paid plan (Pro, Team, or Enterprise Cloud), and plan support for org-wide rulesets varies, so check what yours includes.

Evidence: the ruleset export, its history, and a dated review of the bypass list.

7. Treat pull_request_target as a loaded tool

This trigger runs in the context of your base branch, with access to secrets and a write token. If it checks out and runs code from a fork, an outsider can run code with your secrets. GitHub's hardening guide covers this risk. Use plain pull_request for untrusted code. If you must use the target version, never check out, build, or run the fork's code in it.

Two neighbors of this risk. First, never paste untrusted values such as ${{ github.event.pull_request.title }} or branch names straight into a run step. Pass them through an env variable, as the secure use reference shows. Second, require approval before fork pull request workflows run. On public repos, the default only covers first-time contributors, so choose "Require approval for all external contributors." On private repos, turn on "Require approval for fork pull request workflows." Keep self-hosted runners off public repos, and make them ephemeral where you can.

Evidence: a list of workflows that use the trigger, each with a reason, plus the fork approval and runner settings.

8. Turn on push protection for secrets

Push protection blocks a push that contains a known secret type at push time. It is not a status check. By default, anyone with write access can bypass it with a reason. The bypass raises an alert and lands in the audit log. On private repos it needs GitHub Secret Protection. It will not stop GhostAction, which never wrote a secret into code. It does cut the number of secrets lying around to steal, and that matters more now: GitGuardian says the October 2026 payload also scans git history for secrets.

Evidence: the setting and a review of bypasses.

9. Set up alerts on workflow changes

Use the audit log to set up alerts when a secret, environment, or ruleset changes, or when someone joins a bypass list. A new file in .github/workflows/ is a code push, so catch it with push webhooks or your own tooling. GhostAction used the same file name and commit message across repos. A simple alert on new workflow files would have flagged it.

Evidence: the alert rule and a record of who handled each alert.

10. Have a rotation runbook ready

If a workflow leaks, speed matters. GitGuardian's advice for victims was to revoke and rotate every secret that may have been exposed, then check commit history and published packages. Write that down now: which secrets exist, who owns each one, and how to rotate it.

Evidence: the runbook and the date of its last test.

Illustrative math

Illustrative example, not client data. Say a team has 40 repos and 6 secrets per repo, all as repo secrets. One stolen account with write access can reach up to 240 secrets. Move publish and deploy secrets into branch-limited environments, and swap cloud keys for OIDC. If that leaves 1 low-value secret per repo, the same account now reaches 40. The exact numbers will differ for you. The shape will not.

A practical operating model

You do not need all ten items this week. Do it in three passes.

  • Week one: read-only default token, push protection on, alerts on workflow changes. These are settings, not projects.
  • Month one: pin third-party actions, add CODEOWNERS for workflows, tighten the ruleset bypass list.
  • Quarter one: move secrets to environments, move cloud access to OIDC, write and test the rotation runbook.

Review the bypass lists and the workflow inventory each quarter. Keep the evidence in one place.

If you are preparing for SOC 2, these controls also produce the kind of change and access records an auditor may ask about. Your auditor decides what meets the criteria. We help teams get ready; we are not the auditor. For how this fits with code review, see our post on AI code review and SOC 2.

If you want a second set of eyes on your pipeline, Let’s talk.


Sources

Not sure where to start?

Tell us about your systems and deadline. We’ll reply within one business day.

Keep reading

More on application security

All field notes