GhostAction: stolen accounts, workflows that steal secrets

$ cat blog/ghostaction-github-actions-secret-theft.md
✓ incident
✓ CI/CD
✓ secrets
✓ supply chain
6 min read
$ 

GhostAction is a campaign that takes over real developer accounts and uses them to push GitHub Actions workflows whose only job is to steal the repository's secrets. According to GitGuardian, about 1,900 accounts were compromised and more than 30,000 repositories were targeted, with new waves running into October 2026. The Hacker News has the full report.

What happened

The attackers did not exploit a bug in GitHub. They logged in as legitimate developers, using stolen personal access tokens (PATs) or credentials taken from infostealer logs and dumps, and pushed workflow files with reassuring names such as security-audit.yml and github_actions_security.yml.

According to the report, those workflows went after "CI/CD secrets, cloud credentials, package registry tokens (like npm, PyPI, and DockerHub), and artificial intelligence (AI) provider API keys." Socket and StepSecurity described the payload scanning the repository's git history with regular expressions for AWS key IDs, GitHub and GitLab tokens, AI provider secrets and cloud API keys. A later version captured roughly two lines of context around each AWS key ID, so the matching secret key could be paired with it.

The numbers come from GitGuardian's researchers, Gaetan Ferry and Guillaume Valadon:

  • About 1,900 accounts compromised.
  • More than 30,000 repositories across about 3,000 users and organizations targeted.
  • An attempt to extract 3,174 secrets from the CI of public repositories.

The campaign was first discovered in September 2025. A wave started on August 31, 2026. On October 8–9, 2026, the attackers first hit about 160 previously observed victims and, roughly 20 minutes later, 28,000 new repositories. Waves on October 10 and 12 extracted nothing, because GitHub had started blocking the workflow from October 9.

Some victims were hit in more than one wave. GitGuardian's reading is that the attackers were "banking on unrotated secrets": a key exposed once and never changed is still worth stealing again.

Why it worked

A GitHub Actions workflow is code that GitHub runs for you, with whatever secrets and token permissions the repository grants. Anyone who can push to a branch that triggers a workflow can, in effect, run code next to those secrets. A stolen PAT with write access is enough; no pull request, review or exploit is needed.

Three habits made this cheap for the attackers:

  • Long-lived, broad tokens. A classic PAT often has write access to every repository its owner can reach, and it works until someone revokes it.
  • Repository-wide secrets. Secrets available to every workflow on every branch are available to a workflow the attacker just added.
  • Static cloud keys. An AWS access key stored as a secret keeps working long after the run ends, wherever it is copied to.

Here is an illustrative example, not taken from the campaign. The first job hands a long-lived cloud key to every step, with a default token, and interpolates untrusted event text straight into a shell:

# .github/workflows/deploy.yml  (risky, illustrative)
on: [push, issues]
jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    steps:
      - uses: actions/checkout@v4
      - run: echo "Deploying for ${{ github.event.issue.title }}"
      - run: ./deploy.sh

The safer version narrows the token, swaps the stored key for a short-lived OIDC role, and gates the job behind an environment that requires a reviewer:

# .github/workflows/deploy.yml  (safer, illustrative)
on:
  push:
    branches: [main]
permissions:
  contents: read
  id-token: write   # only for the OIDC exchange
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production   # required reviewers + environment-only secrets
    steps:
      - uses: actions/checkout@<full-commit-sha>
      - uses: aws-actions/configure-aws-credentials@<full-commit-sha>
        with:
          role-to-assume: ${{ vars.DEPLOY_ROLE_ARN }}
          aws-region: eu-central-1
      - run: ./deploy.sh

None of this stops an attacker who controls an account with admin rights. It does mean a workflow pushed to a random branch finds no long-lived cloud key to take, and a production secret only reaches a job someone approved.

attack, as reported where OpenRouting checks 1 Developer PAT stolen by infostealeroutside a code scan 2 Attacker pushes security-audit.yml✓ PR review of workflow files 3 Workflow reads repo secrets✓ CI/CD agent: token scope 4 Payload greps code for keys✓ Secret rules: working tree 5 Secrets sent to attackernot a runtime monitor
The attack as reported, and where an OpenRouting check applies. Illustrative.

What to do now

  1. Audit your workflow files. Look for recently added or changed files under .github/workflows/, especially ones with security-sounding names you don't recognize, and check who pushed them.
  2. Rotate anything a workflow could read. If a suspicious workflow ran, rotate every repository and organization secret it had access to: cloud keys, npm, PyPI and Docker Hub tokens, AI provider keys. Don't rely on "it was only exposed for a few minutes".
  3. Replace classic PATs with fine-grained, short-lived tokens scoped to the repositories and permissions they need, and revoke tokens nobody can account for.
  4. Protect the workflow directory. Add a CODEOWNERS entry for .github/workflows/ and branch protection or rulesets that require review before changes land on protected branches.
  5. Move production secrets into environments with required reviewers, so a workflow on an arbitrary branch can't read them.
  6. Use OIDC for cloud access instead of storing long-lived cloud keys as secrets, and scope the trust policy to the repository and branch.
  7. Set minimal `permissions:` at the top of every workflow (start from contents: read) and widen per job only where needed.
  8. Clean up old leaks. The payload searched past commits too. Rotating a secret that was ever committed is the fix; rewriting history alone isn't. Our guide to secrets in git history covers how.

How OpenRouting helps

OpenRouting scans the repositories you connect and the pull requests you ask it to review. For this class of problem:

  • Semgrep's GitHub Actions rules run on your workflow files. They flag patterns such as untrusted ${{ github.event.* }} values interpolated into a run: step, and pull_request_target workflows that check out pull request code with access to secrets.
  • The [CI/CD pipeline agent](/agents/ci-cd-pipeline) reviews those workflow findings: secrets exposed in logs, untrusted code running with secrets, actions not pinned by commit SHA and over-broad job token permissions. AI triage (Claude, via Amazon Bedrock) gives each finding a verdict, a plain-English explanation and a suggested fix.
  • Semgrep's secret rules flag cloud keys and tokens committed in the files you scan, so fewer secrets sit in the repository for a payload like this to find. Secret values are redacted before any AI call.
  • [PR review](/use-cases/pull-request-review) scans the changed files of a pull request. A new workflow file added in a PR shows up there with its findings, before it merges.

What it won't catch

  • A stolen token pushing directly. GhostAction's workflows were pushed by real accounts. If an attacker pushes to a branch with no review, nothing in a code scan runs first; branch protection and token hygiene are the controls.
  • Old secrets in past commits. Secret rules look at the files in the working tree, not your git history yet. A key committed and later deleted is not flagged.
  • Runtime activity. OpenRouting is not a runtime monitor. It doesn't watch workflow runs, outbound traffic or token use.
  • Findings need a scan. Results appear when you run a scan from the dashboard or review a pull request.

For a wider view of pipeline and agent permissions, see least privilege for AI agents and secrets detection.

Sources

Questions

What is the GhostAction campaign?

GhostAction takes over real developer accounts, using stolen personal access tokens or credentials from infostealer logs and dumps, and pushes GitHub Actions workflows that steal the repository's secrets. According to GitGuardian, about 1,900 accounts were compromised and more than 30,000 repositories were targeted.

Did GhostAction exploit a bug in GitHub?

No. The attackers logged in as legitimate developers and pushed workflow files with reassuring names such as security-audit.yml. Anyone who can push to a branch that triggers a workflow can, in effect, run code next to that repository's secrets.

What should I rotate if a GhostAction workflow ran in my repository?

Rotate every repository and organization secret the workflow had access to: cloud keys, npm, PyPI and Docker Hub tokens, and AI provider keys. Some victims were hit in more than one wave, and GitGuardian's reading is that the attackers were banking on unrotated secrets.

How do I harden GitHub Actions against pushed workflows?

Replace classic PATs with fine-grained, short-lived tokens, protect .github/workflows/ with CODEOWNERS and required review, and move production secrets into environments with required reviewers. Use OIDC instead of long-lived cloud keys and set minimal permissions at the top of every workflow.

Keep reading