Skip to main content
Gheist: Stolen GitHub Tokens Are Dumping Actions Secrets Past Log Masking. Hunt for ci-sync Branches

Gheist: Stolen GitHub Tokens Are Dumping Actions Secrets Past Log Masking. Hunt for ci-sync Branches

KU
· Founder & Chief Editor
• 8 min read•4 views

An automated tool is using stolen GitHub tokens to plant ci-sync branches whose workflow prints every secret base32-encoded, grabs the log and deletes the run. How to hunt for it across an org, what to rotate, and the controls that stop the next one.

Key takeaways

  • Since at least September 15, 2026, the Gheist tool has used stolen GitHub tokens to plant more than 13,500 ci-sync branches across 850+ accounts.
  • Each runs a workflow that prints every secret base32-encoded, downloads the log and deletes the run.
  • Private and GitHub Enterprise Server repositories are exposed; hunt for the branches, then rotate every reachable secret.

If any token in your GitHub org could have been stolen, search every repository for ci-sync- branches today. An automated tool Wiz calls Gheist pushes a workflow that prints all secrets base32-encoded, so log masking misses them, then downloads the log and deletes the run. Private and GitHub Enterprise Server repositories are the exposed ones.

Verified against

Campaign details, indicators and dates against Wiz Research's Gheist report (October 25, 2026); masking, OIDC subject and audit log behavior against GitHub's Secure use reference, OpenID Connect reference and organization audit log events docs. Checked November 8, 2026.

What happened, in dates

Wiz Research has tracked the activity since September 15, 2026 and published its report on October 25. By then it had recovered more than 13,500 planted branches from more than 850 compromised accounts, from personal projects to private enterprise repositories. The name comes from the gheist user agent the tool sends with its API calls. Where Wiz could trace the tokens, they came from infostealer logs and from tokens exposed in earlier supply-chain compromises such as the March Trivy compromise.

The pattern is the same everywhere. For each repository a token can reach, the tool creates a branch named ci-sync- plus six random characters and commits one file, .github/workflows/sync-check.yml, with the message ci: sync config. The commit is unsigned and authored as the token's owner. The push triggers the workflow, which sets permissions: {} (that limits GITHUB_TOKEN, not the secrets context) and pipes both env and toJSON(secrets) through base32 -w 120. Where a repository has deployment environments, one variant adds a job matrix that runs once per environment, so environment-scoped production secrets print too.

The tool then pulls the run log through the GitHub API and, in Wiz's words, "deletes the run seconds later." There is no command-and-control server: the secrets leave through GitHub itself, so there is no domain or IP to block.

Two dates change the risk picture. Since July 28, 2026, GitHub holds potentially malicious workflow runs in public repositories until a collaborator approves them, and every public Gheist run Wiz checked sat at action_required. That hold does not cover private repositories or GitHub Enterprise Server, where audit logs showed runs completing and being deleted. On October 19 the tool changed its payload, and hours later a sync-host job with runs-on: self-hosted appeared on repositories that had a self-hosted runner registered. It searches the host for 31 token formats, private keys, .env files and Terraform state, reads /proc/*/environ, mounts the host filesystem through the Docker socket, and asks for cloud instance credentials and the Kubernetes service account token. Wiz found the job buggy, for now.

Am I affected? Hunt across the org

Wiz gives the branch pattern as ^ci-sync-[0-9a-z]{6}$ (two variants run in parallel: one uses hex suffixes, the other, first seen September 28, base36; the regex covers both). The first loop uses the matching-refs endpoint, one prefix query per repository instead of paging through every branch:

Bash
# 1. ci-sync branches in every repo of an org (empty repos return 409, hidden by 2>/dev/null)
ORG=your-org
for r in $(gh repo list "$ORG" --limit 5000 --json nameWithOwner -q '.[].nameWithOwner'); do
>   gh api "repos/$r/git/matching-refs/heads/ci-sync-" -q '.[].ref' 2>/dev/null \
>     | sed 's|^refs/heads/||' | grep -E '^ci-sync-[0-9a-z]{6}$' | sed "s|^|$r: |"
> done

# 2. In a local clone: the commit message, and Wiz's published workflow blob SHAs
git fetch --all --prune
git log --all --oneline --grep='^ci: sync config$'
git log --all --oneline --find-object=f3da2b8b357dcda02fd455d314be2a95007516a6

# 3. Deleted runs and the gheist user agent (org owner, GitHub Enterprise Cloud, read:audit_log)
gh api "orgs/$ORG/audit-log?phrase=action:workflows.delete_workflow_run&per_page=100" --paginate \
>   -q '.[] | [(.["@timestamp"]/1000 | todate), .actor, .repo, .user_agent] | @tsv' | grep -i gheist

# 4. AWS: OIDC role assumptions from a ci-sync branch (90 days, current region only)
aws cloudtrail lookup-events --start-time 2026-09-15 \
>   --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity \
>   --query 'Events[].CloudTrailEvent' --output text | grep -o 'ci-sync-[0-9a-z]\{6\}' | sort | uniq -c

The blob SHA in check 2 is the plain variant; Wiz lists three more (d09535857a741bbb976bdc084a9a021713133291, 1dc5af9cce950aad1eb1546ecce8565b117540c4, c665b778b2d822453293003e7ca0bbb2cb08e39d), and the environment-matrix files differ per repository, so the commit message search matters more. Rerun check 4 in every region your runners call STS in.

IndicatorValueWhere to look
Branchci-sync- plus 6 charactersBranches API, local clones
Commitci: sync config, only sync-check.ymlgit log --all
Audit logworkflows.delete_workflow_run, user agent gheistOrg audit log
Runner files.gdmp_pat, .gdmp_patg, .gdmp_sweep, gdmp-pivotSelf-hosted runner hosts, job logs

What to do now

Found a branch? Revoke the token that pushed the commit (the author is the token's owner) and check that person's machines for an infostealer. Then rotate every repository, organization and environment secret the repository could read. The workflows.prepared_workflow_job audit event lists the secrets provided to each job, which tells you exactly what one run received. Deleting the branch or the run does not undo anything: the attacker already has the log.

Check the cloud side. Wiz saw the same enterprise organization's cloud credentials used hours later to enumerate AWS Secrets Manager, and CloudTrail showing workflows on attacker branches assuming AWS roles. Treat any role a ci-sync run assumed as used.

Self-hosted runners. If a ci-sync branch ran on one, treat the host and its instance role as compromised, and rebuild it rather than clean it.

Public repositories. Do not approve a held sync-check run. That approval is the only thing between the payload and your secrets.

Where this breaks

Log masking was never a control against someone who can edit a workflow. GitHub's Secure use reference says "automatic redaction is not guaranteed" and that anyone with write access can read every secret in the repository, so a stolen token with push rights is the whole attack.

The OIDC check has a blind spot. GitHub's default subject is repo:ORG/REPO:ref:refs/heads/BRANCH only when the job has no environment; a job that references an environment gets repo:ORG/REPO:environment:NAME instead, and the branch never appears. Gheist's environment matrix produces exactly those jobs, so for environment-bound roles, correlate role assumptions by time against the deleted runs from check 3. Repositories created, renamed, transferred or opted in after July 15, 2026 use an ID-based subject (repo:OWNER@OWNER-ID/REPO@REPO-ID:...); the branch grep still matches, but trust policies must match it.

The audit log API needs GitHub Enterprise Cloud, the log keeps 180 days, and the REST endpoint defaults to three months without a created: qualifier. Wiz also does not report whether the self-hosted variant ran successfully in any private repository, so absence of evidence is not a clean bill for runner hosts.

Questions people ask

Does GitHub Actions log masking protect my secrets?

Only from accidents. Masking replaces a secret's exact value in log output; base32, base64 or any other transform produces a different string that prints in full. GitHub's own guidance says redaction is not guaranteed and that a transformed value must be registered separately. Anyone who can push a workflow file can read the secrets, which is why the defense is limiting who can push workflows, not masking.

Does deleting the workflow run remove the leaked secrets?

No. Gheist downloads the log before it deletes the run, so by the time the run disappears the encoded secrets are already off GitHub. Deleting the ci-sync branch or the run removes evidence, not exposure. Rotate every secret the repository could read, including organization and environment secrets, and revoke the token that pushed the commit.

Are public GitHub repositories safe from Gheist?

Mostly, so far. Since July 28, 2026, GitHub holds potentially malicious workflow runs in public repositories until a collaborator approves them, and every public Gheist run Wiz checked was stuck at action_required. The risk is someone approving one. Private repositories and GitHub Enterprise Server have no such hold, which is where Wiz saw runs complete.

The decision, concretely

  • Using classic personal access tokens for automation? Move to GitHub Apps or short-lived fine-grained tokens with an expiry; a stolen token that expires in days limits the window.
  • Can anyone with write access change .github/workflows/? On private and internal repositories, a push ruleset with Restrict file paths on .github/workflows/** blocks those pushes for everyone outside the bypass list.
  • Production secrets in environments? Add required reviewers and limit deployment branches to selected branches, since environment secrets are released only after those rules pass. On private repositories, required reviewers need GitHub Enterprise.
  • OIDC roles trusting repo:ORG/*? Pin the subject to ref:refs/heads/main or a named environment, as covered in our OIDC federation guide for AWS.

The call we'd make

Run the four hunt checks today on every org you own, and treat one hit as a full secret rotation for that repository, not a branch cleanup. Then close the door: a push ruleset on workflow paths and environment reviewers stop this tool even with a valid stolen token. The caveat: Gheist is evolving and the self-hosted variant goes after the runner host, so the GitHub Actions hardening steps from the Trivy compromise matter as much as the hunt.

Sources

React
Share this post
Cite this article
Plain text

Kiril Urbonas (2026). Gheist: Stolen GitHub Tokens Are Dumping Actions Secrets Past Log Masking. Hunt for ci-sync Branches. DevOpsNess. https://www.devopsness.com/blog/github-actions-gheist-stolen-tokens-ci-sync-secrets

Markdown

[Gheist: Stolen GitHub Tokens Are Dumping Actions Secrets Past Log Masking. Hunt for ci-sync Branches](https://www.devopsness.com/blog/github-actions-gheist-stolen-tokens-ci-sync-secrets) (Kiril Urbonas, DevOpsNess, 2026-11-08)

KU

Kiril Urbonas

Founder & Chief Editor

590 articles
View all articles by Kiril Urbonas

You might have missed

Evergreen posts worth revisiting.