GhostAction used about 1,900 stolen GitHub logins to plant secret-stealing workflows in 30,000 repos
Maya Okonkwo
Attackers got into roughly 1,900 GitHub accounts and used them to add credential-stealing GitHub Actions workflows to more than 30,000 repositories, The New Stack reports. GitGuardian tracked the operation under the name GhostAction. If one of these workflows ran in your repositories, the incident ends when every secret those jobs could read has been rotated. Deleting the file is the easy first step.
Per GitGuardian, the campaign peaked on October 8 and 9. In that window it reached repositories belonging to about 3,000 users and organizations and tried to pull out 3,174 secrets. The malicious jobs went after CI/CD secrets, cloud access keys and AI provider API keys.
No exploit, just a commit
GhostAction did not depend on a vulnerability in GitHub Actions. The attackers used stolen developer credentials to edit workflows directly. StepSecurity's October 9 analysis names personal access tokens taken from credential dumps as the likely source, though the article notes the initial compromise has not been confirmed.
The mechanics are ordinary GitHub behaviour. A workflow file committed under .github/workflows/ with a push trigger runs on that push. A job in a repository's own workflow can reference any repository or organization secret by name, and the runner injects the value at runtime. GitHub's protection against fork pull requests reading secrets doesn't apply, because the change is a push to the repository itself from an account that has write access. A token with workflow-write permission covers the rest.
The files were named to blend in. The New Stack lists security-audit.yml and github_actions_security.yml among the filenames used. A reviewer skimming a commit list sees a hardening job.
Triage for this week
Start with the two filenames, but don't stop there. They are what has been reported so far, and nothing prevents a copycat from picking another name. A quick sweep of an organization's default branches with the GitHub CLI:
ORG="$YOUR_ORG"
for repo in $(gh repo list "$ORG" --limit 1000 --json nameWithOwner -q '.[].nameWithOwner'); do
for f in security-audit.yml github_actions_security.yml; do
gh api "repos/$repo/contents/.github/workflows/$f" --silent 2>/dev/null \
&& echo "FOUND $repo/.github/workflows/$f"
done
done
That only checks default branches. A malicious workflow pushed to a side branch runs on that push all the same. In repositories where it matters, look at the history of the workflows directory across all branches:
git fetch --all
git log --all --since="<start-of-review-window>" --name-status -- .github/workflows/
Check every workflow added or changed by an author you can't account for. Then pull the Actions run history for those workflows. A single completed run means the secrets it referenced should be treated as exposed.
Rotate in order of blast radius. Cloud access keys come first, then registry and deploy credentials, then the AI provider keys the campaign also targeted, which often sit in repository secrets with no spending cap. Revoke the personal access tokens belonging to any account that pushed one of these files, and make those users reset their credentials before anyone restores their access.
Controls that would have narrowed it
None of these block a stolen token outright. Each one cuts down what a stolen token can do on its own.
- Fine-grained personal access tokens with an expiry date and access to named repositories limit both the time window and the number of repos one dump entry can reach. Organizations can require approval for fine-grained tokens and restrict or block classic ones.
- A
CODEOWNERSentry for.github/workflows/combined with a branch ruleset that requires code-owner review keeps a new workflow off protected branches until someone looks at it. This does not cover side branches, so it pairs with item 3. - Secrets stored on a deployment environment are only handed to jobs that target that environment. Add required reviewers or a deployment-branch rule, and a workflow pushed to a random branch can't read them without a human approving the run.
- Swapping long-lived cloud credentials for OIDC federation means a harvested value is short-lived and expires on its own. It also turns a key-rotation incident into a trust-policy review.
Where this leaves on-call
The attack needed no misconfiguration in Actions. It needed accounts with write access and credentials that still worked, and the attackers controlled about 1,900 such accounts. Until the initial compromise is confirmed, assume any personal access token older than your last rotation could already be in a dump somewhere, and rotate on that basis.
Source: The New Stack (thenewstack.io)