GhostAction Asks for Your CI Secrets by Name

Written by the Rafter Team

As of October 14, 2026.
In a public repository, the value of a CI secret is hidden and its name is not.
VPS_SSH_KEY sits in plain text in the workflow file, and GhostAction reads that file as a shopping list.
Anyone who can push a workflow to your repository can ask for every repository secret by that name, so the account that can push matters as much as the secret it reaches.
What happened
GitGuardian named GhostAction in September 2025, when it compromised 817 public repositories and exfiltrated at least 3,325 secrets. Its October 7 write-up, updated October 9 and October 13, shows the campaign never stopped.
The method has not changed.
With what appear to be stolen credentials, the attacker commits a workflow such as github_actions_security.yml under the victim's own identity.
In GitGuardian's words, "the workflow hardcodes the names of the secrets the attacker found referenced in the repository's legitimate workflows," then sends their values in one curl POST to the attacker's server.
The numbers grew in three steps:
- August 31 to September 30. The workflow landed in 772 public repositories belonging to 373 users and organizations, targeting 2,577 secrets. SSH keys and deployment server credentials led the list with 446, then Azure credentials with 218.
- October 8 and 9. GitGuardian's October 13 update counts more than 30,000 targeted repositories across 3,000 users and organizations, reached by compromising about 1,900 GitHub accounts. The workflow tried to pull 3,174 secrets from CI.
- October 10 and 12. Two more waves ran the same payload, but GitHub had started blocking the malicious workflow on October 9, and those waves extracted nothing.
The October payload, a file named security-audit.yml, also searched the checked-out code and the full git log with 11 secret patterns, covering AWS key IDs, GitHub and GitLab tokens, and AI-related secrets.
GitGuardian reads that step as aimed at private repositories, since it adds nothing for a public one.
Why rotation alone fails
Two details in GitGuardian's data explain why the campaign keeps finding the same victims.
First, old injections stay put. In 92 cases the attacker did not add a workflow at all; it edited one planted in an earlier wave and never removed, and pointed it at a new server. By October 5, only 124 of the 772 September repositories (16%) had been cleaned in their public history.
Second, the pushing credential outlives the cleanup. Some October victims had been hit in September 2025 and August 2026, and GitGuardian says the attacker is "likely relying in part on unrotated secrets compromised months or years ago." It also found 13 victim repositories used by at least four unrelated cryptomining campaigns, which suggests the stolen GitHub credentials are traded rather than held by one operator. Its conclusion: "The GitHub credential that allowed the injection in the first place must be found and revoked too, or someone else will use it again."
What stopped the earlier wave
Of 3,669 September runs GitGuardian collected, GitHub held most for approval, and only 499 executed, in 32 repositories. That gate did not hold in October, when the runs executed until GitHub began blocking the workflow itself.
You can put your own gate in front of the secrets that matter.
A repository secret goes to any workflow in the repository, including one an attacker pushed a minute ago.
An environment secret is different: GitHub's docs say that if the environment requires approval, "a job cannot access environment secrets until one of the required reviewers approves it."
Add a deployment branch rule so only main can use the environment, and a workflow pushed to a side branch never sees the secret.
The gate has a limit. On a personal repository the stolen account is often the only owner, and by default administrators can bypass environment protection rules. Reviewers help most when the account that can push is not the account that approves.
What to do now
- Search for the workflow. Look in every repository you own or administer for
github_actions_security.yml,security-check.ymlandsecurity-audit.yml, and for commits titled "Add Github Actions Security workflow" or "Update Github Actions Security workflow". Check the Actions run history, not only the current tree, and look for outbound calls to193.32.204.199. - Find the pushing credential and revoke it. The commits carry the victim's own identity. Review the account's security log, then revoke personal access tokens, OAuth and GitHub App authorizations, SSH keys and sessions you cannot account for. Change the password and turn on two-factor authentication if it is off.
- Rotate every secret the workflow named. The injected file lists the names it asked for. Rotate those, then anything else the repository's workflows could read. Our leaked key runbook covers the order.
- Assume committed secrets are gone. The October payload grepped the working tree and full history. A key that was ever committed, even if deleted later, belongs on the rotation list. GitHub's own scanning has gaps here, so run a scanner across history too.
- Move high-value secrets into environments. Deploy keys, cloud credentials and registry tokens go in an environment with required reviewers and a branch rule. Enable "prevent self-review" where more than one person maintains the repository.
- Delete leftovers from older waves. A workflow planted in 2025 and never removed is a standing request for your secrets. Remove it, then rotate whatever it could reach.
Our CI/CD security guide covers the wider workflow hardening, and the Shai-Hulud retrospective traces the same injection technique through npm, where GitGuardian says it was later reused.
Where Rafter fits
Rafter does not watch GitHub Actions runs or account activity, so steps 1 and 2 are yours.
It helps with step 4: rafter secrets . scans a working tree locally with no account, and rafter agent install-hook adds a commit hook that stops a new secret before it reaches history.
The less a repository holds, the less the next grep finds.