On 8 October 2026, two hijacked maintainer accounts pushed a workflow named security-audit.yml into more than 340 repositories they could write to. GitGuardian ties it to the GhostAction campaign, and its 13 October update counts more than 30,000 targeted repositories across about 3,000 users and organizations in the October wave. In the earlier wave, from 31 August to 30 September, the workflow simply hardcoded the names of secrets used by each repository's legitimate workflows and posted their values to a bare IP address.
That detail is the whole lesson. Anyone who can push a workflow file can ask for any secret the repository can reach. So the real question after GhostAction is how far each push credential reaches, and this guide maps it.
What's affected
GitHub's documentation says "GitHub Actions can only read a secret if you explicitly include the secret in a workflow." An attacker with push access writes that workflow. What they get depends on how your secrets are scoped:
- Organization secrets have a visibility of
all,privateorselected. A secret set toallis readable from every repository's workflows. - Repository secrets are readable by any workflow in that repository that references them.
- Environment secrets are "only available to workflow jobs that reference the environment," and if the environment requires approval, the job can't read them until a reviewer approves.
- The
GITHUB_TOKENcarries your organization's default permissions. GitHub says new organizations start on the restricted, read only setting, but older ones may have been switched to read and write.
Plan limits apply. GitHub notes that on Free, Pro and Team plans, required reviewers are only available for public repositories, while deployment branch and tag rules also cover private repositories on Pro and Team.
How to check if you're exposed
1. List organization secrets and their reach.
ORG=your-org
gh secret list --org "$ORG" --json name,visibility,numSelectedRepos
gh api "orgs/$ORG/actions/secrets/DEPLOY_KEY/repositories" --paginate \
--jq '.repositories[].full_name'
Anything with visibility set to all or private is in the blast radius of every matching repository.
2. Map what each repository can read. The repository organization secrets endpoint shows exactly which org secrets are shared with a repository, so you don't have to work it out from visibility rules.
gh repo list "$ORG" --limit 1000 --json nameWithOwner --jq '.[].nameWithOwner' > repos.txt
while read -r REPO; do
O=$(gh api "repos/$REPO/actions/organization-secrets?per_page=100" --jq '[.secrets[].name] | join(",")')
R=$(gh api "repos/$REPO/actions/secrets?per_page=100" --jq '[.secrets[].name] | join(",")')
printf '%s\torg=%s\trepo=%s\n' "$REPO" "$O" "$R"
done < repos.txt > secret-map.tsv
3. Check how each environment is guarded.
gh api "repos/$REPO/environments" --jq '.environments[] |
[.name, ([.protection_rules[]?.type] | join(",")),
(.deployment_branch_policy | tostring)] | @tsv'
gh secret list --env production -R "$REPO"
An environment with no required_reviewers rule and a null branch policy gives its secrets to any branch that names it.
4. See what your workflows actually use. Pull workflow files through the contents API and extract secret references:
gh api -H "Accept: application/vnd.github.raw+json" "repos/$REPO/contents/.github/workflows/release.yml" |
grep -oE 'secrets\.[A-Za-z0-9_]+' | sort -u
Secrets a repository can reach but never uses are pure exposure. Remove its access to them.
5. Read the default token permissions.
gh api "orgs/$ORG/actions/permissions/workflow"
If default_workflow_permissions is write, an injected workflow also gets write access to the repository through its token.
What to do now
-
Rotate first if GhostAction touched you, and revoke the pushing credential. GitGuardian is explicit: "Rotating the secrets exfiltrated by the malicious workflow is not enough." The GitHub credential used for the injection must be revoked too.
-
Narrow org secrets while you rotate. Setting the new value is the moment to change scope:
gh secret set DEPLOY_KEY --org "$ORG" --visibility selected --repos api,webLater changes to the list go through
PUT /orgs/ORG/actions/secrets/SECRET_NAME/repositorieswithselected_repository_ids. -
Move deploy credentials into protected environments. Add a reviewer team and limit the environment to
main:gh api -X PUT "repos/$REPO/environments/production" \ -F 'reviewers[][type]=Team' -F "reviewers[][id]=$TEAM_ID" \ -F prevent_self_review=true \ -F 'deployment_branch_policy[protected_branches]=false' \ -F 'deployment_branch_policy[custom_branch_policies]=true' gh api -X POST "repos/$REPO/environments/production/deployment-branch-policies" \ -f name=main -f type=branchGet
TEAM_IDfromgh api orgs/$ORG/teams/TEAM_SLUG --jq .id. A workflow pushed to a feature branch can no longer read these secrets, and one onmainwaits for a human. -
Set the default
GITHUB_TOKENto read.gh api -X PUT "orgs/$ORG/actions/permissions/workflow" \ -f default_workflow_permissions=read -F can_approve_pull_request_reviews=falseGrant write per job with a
permissions:block where a job needs it. -
Replace stored cloud keys with OIDC. For AWS, GitHub's guide uses a trust policy condition on
token.actions.githubusercontent.com:sub. Tie it to the environment, not the repository wildcard:"Condition": { "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com", "token.actions.githubusercontent.com:sub": "repo:octo-org/octo-repo:environment:production" } }The job needs
id-token: writeand then usesaws-actions/configure-aws-credentialswithrole-to-assume. Check your subject format: GitHub says repositories created after 15 July 2026 use an immutablesubthat includes owner and repository IDs.
Why it matters
GhostAction didn't exploit a bug in GitHub Actions. It used stolen credentials and relied on the fact that secrets follow the repository, not the reviewed code path. The blast radius of one compromised maintainer is the union of every secret reachable from every repository they can push to.
Environments and OIDC change that math. An environment secret behind a branch policy and a reviewer isn't reachable from a drive-by commit, and an OIDC role scoped to that environment leaves no long-lived key in the repository to steal.
Try it yourself
Run step 2 for your five busiest repositories. For each one, count the secrets it can read and the secrets its workflows actually reference. The difference between those two numbers is what an injected workflow gets for free.
Click through it at your own pace, or open it full screen.
Quick check
Can a workflow pushed to a feature branch read repository secrets?
Yes. Repository secrets are available to any workflow that references them. Only environment secrets can be limited by branch policy and reviewer approval.
Why change org secret visibility during rotation rather than after?
Moving to selected visibility means setting the secret again, so doing it with the new value saves a second change and closes the exposure at the same moment.
What should an AWS trust policy for GitHub OIDC check besides the audience?
The sub claim. Pin it to a specific repository and environment, using the immutable format if your repository uses it.
Sources
- GitGuardian: GhostAction returns
- The Hacker News: Credential-stealing GitHub Actions workflows
- GitHub Docs: Secrets
- GitHub Docs: REST API endpoints for GitHub Actions secrets
- GitHub Docs: Deployments and environments reference
- GitHub Docs: REST API endpoints for deployment environments
- GitHub Docs: REST API endpoints for deployment branch policies
- GitHub Docs: REST API endpoints for GitHub Actions permissions
- GitHub Docs: Configuring OpenID Connect in Amazon Web Services
- GitHub Docs: OpenID Connect reference
- GitHub CLI manual: gh secret list
- GitHub CLI manual: gh secret set