GhostAction planted fake security workflows in more than 30,000 repos to steal CI secrets. Here is how to make sure your Google Cloud deploy has no key to steal.

Most GitHub Actions pipelines that deploy to Google Cloud still start the same way. Someone downloads a service account key, pastes the JSON into a repository secret called GCP_SA_KEY, and forgets about it.
That secret is exactly what this month’s attack was built to collect.
I removed it from a test pipeline end to end, on a real Google Cloud project, and recorded every result. The new setup has no key anywhere. GitHub gets a one-hour token instead, and only from the branch you allow.
What GhostAction takes
GhostAction is a campaign that hijacks GitHub accounts, usually with stolen personal access tokens, and pushes a workflow file into their repositories [1].
The file is named security-audit.yml or github_actions_security.yml, so it looks like tooling. When it runs, it collects the repository's named secrets and scans the full git history for credentials, then sends them to a hard-coded IP address [2].
The October wave hit about 1,900 accounts and more than 30,000 repositories, targeting 3,174 secrets [1]. Google Cloud credentials are on its list [2]. In the August–September wave alone, GitGuardian counted 80 Google Cloud and Firebase credentials among the targets [3].
A credentials_json secret is a perfect prize. Google's own action says these keys are "long-lived credentials and must be treated like a password", and by default they never expire [5].
Step 1: Check for GhostAction and for keys
First, check whether the malicious workflow ever landed in your repo:
git log --all --oneline -- \
.github/workflows/security-audit.yml \
.github/workflows/github_actions_security.yml
No output means it was never committed on any branch you have locally, so run git fetch –all first. If you get a commit back, treat every secret in that repo as stolen.
Next, look for a key in your history. My demo repo shop-api committed a key file, then "fixed" it twenty minutes later by moving the key into a GitHub secret.

A plain grep finds nothing, because the file is gone. A history scan with trufflehog3 (pip install trufflehog3) finds the key in both commits, in 0.29 seconds.
Google is blunt about what that means. Having the private key “is similar to knowing a user’s password” [4].
Step 2: Create the identity pool and provider
Workload Identity Federation lets Google Cloud trust GitHub’s own short-lived identity token instead of a stored key. GitHub signs a token for each job, and Google swaps it for a Google Cloud access token.
Two resources do the work: a pool, and a provider that trusts GitHub’s token issuer.
gcloud iam workload-identity-pools create github \
--location=global --display-name="GitHub Actions"
gcloud iam workload-identity-pools providers create-oidc shop-api \
--location=global --workload-identity-pool=github \
--issuer-uri="https://token.actions.githubusercontent.com" \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.repository_owner_id=assertion.repository_owner_id,attribute.ref=assertion.ref" \
--attribute-condition="assertion.repository_owner_id == '10562869' && assertion.ref == 'refs/heads/main'"
The attribute condition is the important line. Every GitHub repo in the world gets tokens from the same issuer, so Google says you “must use an attribute condition” that limits access to your own organization [6].
Use the numeric owner ID, not the name. Mine is 10562869, so swap in yours. Google warns that names can be squatted, while the _id fields "are unique and can't be reused" [6]. You can find yours at https://api.github.com/users/<name>.
I also limited it to main. A token from any other branch is refused at the token exchange.

Step 3: Grant the repo, not a service account
Many Google Cloud APIs don’t need a service account at all. Google recommends granting roles straight to the federated identity, which it calls direct resource access [6].
gcloud projects add-iam-policy-binding wif-demo-1018-7061 \
--role=roles/browser \
--member="principalSet://iam.googleapis.com/projects/196243513032/locations/global/workloadIdentityPools/github/attribute.repository/SaqibInTech/wif-demo"
That principal means “any job from this one repository”. I gave it roles/browser, a read-only role, so the test shows exactly what it can and can't do.
Afterwards, gcloud iam service-accounts list on the project returned Listed 0 items. No service account, so no key to leak.
Step 4: Swap the workflow
Here is the before. The key lives in a secret, and nothing pins the actions:
- uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
And the after:
permissions: {}
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read # checkout the code
id-token: write # mint an OIDC token for Workload Identity Federation
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
with:
project_id: wif-demo-1018-7061
workload_identity_provider: projects/196243513032/locations/global/workloadIdentityPools/github/providers/shop-api
- uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1
- run: gcloud projects describe wif-demo-1018-7061
id-token: write is the only new permission. It lets the job ask GitHub for its identity token. Direct federation is the method the auth action marks as preferred [5].
I pinned each action to a commit hash too. Attacks like this target the code that runs in your CI. A tag can be moved to point at different code, but a commit hash can’t.
The workflow linter zizmor agreed. The old file got 9 findings, including 3 high-severity unpinned actions. The new one got “No findings to report”.
Step 5: Watch it work, and fail
I pushed the workflow to main. The job went green in 27 seconds, and gcloud auth list showed who it was:
ACTIVE ACCOUNT
* principal://iam.googleapis.com/projects/196243513032/locations/global/workloadIdentityPools/github/subject/repo:SaqibInTech@10562869/wif-demo@1424286513:ref:refs/heads/main
No service account and no key. The subject names the owner and the repo, each with its numeric ID, and the branch.
It could read the project. When the same job tried gcloud iam service-accounts list, IAM refused: "Permission 'iam.serviceAccounts.list' denied". That is the read-only role doing its job.
The auth step’s own log showed the token lifetime, access_token_lifetime: 3600s. When the job finished, the action deleted its credentials file from the runner.
Then I created a branch called feature-x. The same workflow ran on it and failed in 5 seconds:

“The given credential is rejected by the attribute condition.” GitHub’s token was valid. Google refused it anyway, because it didn’t come from main.
Step 6: Kill the old key and block new ones
Now delete the old setup:
- Delete the GCP_SA_KEY secret from the repo.
- Disable the key in Google Cloud and watch for a week, then delete it.
- Delete the service account if nothing else uses it.
Then stop it coming back. The organization policy constraints/iam.managed.disableServiceAccountKeyCreation blocks new key creation [7]. Google enforces it by default for organizations created on or after 3 May 2024 [7], so older organizations are the ones most likely to be missing it.

What keyless doesn’t fix
Let’s be honest about this one. Workload Identity Federation removes the stored key. It doesn’t stop someone who can push to main.
GhostAction pushed its workflow with the victim’s own identity. If an attacker can commit a workflow to your allowed branch, that workflow can ask for a token too.
The difference is what they walk away with. With a key, they get a password that never expires and works from anywhere. With federation, they get a one-hour token, minted only while their workflow runs in your repo, with your logs pointing at the exact commit.
So keep doing the boring parts:
- Protect main, and require review for anything under .github/workflows/.
- Turn on MFA and use short-lived tokens for every account with write access.
- Tighten the condition if you need to. GitHub’s token also names the workflow file that is running (the job_workflow_ref claim), and your attribute condition can check it.
The 15-minute version
Check for the GhostAction files. Scan your history for old keys. Create a pool and a provider with an ID-based condition. Grant the repo directly, switch the workflow to id-token: write, then delete the key.
The best secret is the one that doesn’t exist.
Frequently asked questions
Do I still need a service account? Often not. Grant roles to the repository’s principal directly. Google suggests service account impersonation only for APIs with federation limitations [6].
How long does the token last? In my test run, the auth action requested a 3,600-second (one-hour) access token, and removed its credentials file when the job ended.
Does this cost anything? My test project had no billing account attached, and every step in this article ran.
Can I lock it to one repo instead of my whole account? Yes. Add assertion.repository_id == '<id>' to the condition, or grant the role only to attribute.repository/<owner>/<repo>, as above.
Sources
[1] The Hacker News, “GhostAction Compromises About 1,900 GitHub Accounts, Targets Over 30,000 Repositories,” 15 October 2026. https://thehackernews.com/2026/10/ghostaction-compromises-about-1900.html
[2] The Hacker News, “Credential-Stealing GitHub Actions Workflows Planted in Tens of Thousands of Repositories,” 9 October 2026, updated 15 October. https://thehackernews.com/2026/10/credential-stealing-github-actions.html
[3] GitGuardian, “GhostAction Returns: 772 Repos Hit in New GitHub Actions Wave.” https://dev.to/gitguardian/ghostaction-returns-772-repos-hit-in-new-github-actions-wave-576o
[4] Google Cloud, “Best practices for managing service account keys.” https://docs.cloud.google.com/iam/docs/best-practices-for-managing-service-account-keys
[5] google-github-actions/auth (README). https://github.com/google-github-actions/auth
[6] Google Cloud, “Configure Workload Identity Federation with deployment pipelines.” https://docs.cloud.google.com/iam/docs/workload-identity-federation-with-deployment-pipelines
[7] Google Cloud, “Manage Google Cloud security baseline constraints.” https://docs.cloud.google.com/resource-manager/docs/secure-by-default-organizations
Your GitHub Actions Don’t Need a Google Cloud Key. Delete It in 15 Minutes was originally published in Google Cloud – Community on Medium, where people are continuing the conversation by highlighting and responding to this story.
Source Credit: https://medium.com/google-cloud/your-github-actions-dont-need-a-google-cloud-key-delete-it-in-15-minutes-a9e3f0796922?source=rss—-e52cf94d98af—4
