Safeguard
Supply Chain Security•

Map Which Workflows Can Read Which Secrets Before GhostAction Does

GhostAction's October wave pushed a fake security audit workflow to over 30,000 repos. Map which repos can read which secrets, then shrink that reach with environments and OIDC.

Safeguard Security Team
6 min read

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, private or selected. A secret set to all is 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_TOKEN carries 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

  1. 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.

  2. 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,web
    

    Later changes to the list go through PUT /orgs/ORG/actions/secrets/SECRET_NAME/repositories with selected_repository_ids.

  3. 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=branch
    

    Get TEAM_ID from gh api orgs/$ORG/teams/TEAM_SLUG --jq .id. A workflow pushed to a feature branch can no longer read these secrets, and one on main waits for a human.

  4. Set the default GITHUB_TOKEN to read.

    gh api -X PUT "orgs/$ORG/actions/permissions/workflow" \
      -f default_workflow_permissions=read -F can_approve_pull_request_reviews=false
    

    Grant write per job with a permissions: block where a job needs it.

  5. 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: write and then uses aws-actions/configure-aws-credentials with role-to-assume. Check your subject format: GitHub says repositories created after 15 July 2026 use an immutable sub that 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

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.