Home › Cloud › GCP Impersonation Denied: Token Creator Fix
Intermediate 5 min · September 23, 2026

GCP Impersonation Denied: Token Creator Fix

Fix GCP impersonation denied by granting Token Creator on the service account: check policy, bind role, then verify access..

N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Drawn from code that ran under real load.

Follow
✓ Production
production tested
September 26, 2026
last updated
2,085
articles · all by Naren
Before you start⏱ 12 min
  • ✓A GCP project with a service account you administer
  • ✓Google Cloud SDK installed with gcloud auth login done
  • ✓Basic familiarity with IAM roles and bindings
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • Impersonation denied means the caller lacks Token Creator on the target account: no grant, no token, no data touched
  • Confirm the caller with gcloud auth list, then read the SA policy for your member under the Token Creator role
  • Fix with one binding on the service account resource itself, then verify with impersonated print-access-token
  • Check org policy only when bindings are perfect yet denials continue
✦ Definition~90s read
What is GCP Permission Denied on Service Account Impersonation?

Service account impersonation lets a principal — your user account, another service account, or a federated CI identity — obtain short-lived credentials for a target service account without ever handling its keys. The request hits the GenerateAccessToken (or signBlob/signJwt) endpoint, which first checks whether the caller holds iam.serviceAccounts.getAccessToken on the target.

★
Think of impersonation as borrowing a coworker's badge to open a door.

That permission ships inside roles/iam.serviceAccountTokenCreator. Denied there, the call returns PERMISSION_DENIED and no token is minted.

Two roles frame every impersonation question. Service Account User (roles/iam.serviceAccountUser) authorizes attaching an account to a resource — running a VM or Cloud Run service as the account. It does not authorize minting the account's tokens. Service Account Token Creator authorizes exactly that: creating tokens and signatures as the account.

Local development with --impersonate-service-account, CI deployers that act as a release account, and cross-service minting all require Token Creator on the target.

The denial has four standard sources. A missing binding: nobody granted Token Creator to this caller on this account. A mis-scoped grant: the role sits at project level while the check demands it on the account . A wrong member: the binding names an email that isn't the authenticated caller.

And a higher-level block: organization or folder policy constraining credential issuance overrules even perfect bindings.

Every attempt — allowed or denied — lands in the Cloud Audit Logs as a GenerateAccessToken entry with caller, target, and status. That log is the ground truth for debugging and security alike. Done right, impersonation is keyless, short-lived, narrowly granted, and fully audited — the denial is simply the system holding that line.

Plain-English First

Think of impersonation as borrowing a coworker's badge to open a door. GCP's rule is strict: you may only borrow badges explicitly lent to you (the Token Creator grant), and every borrow is logged. Permission denied means nobody lent you that badge — maybe it was lent to your old email, or to the department but not to you. Waving your own badge harder never helps. The fix is getting the loan recorded: the right person, the right badge, at the right desk.

Your deploy script worked yesterday. Today it dies with permission denied on iam.serviceAccounts.getAccessToken, and nothing in the script changed. Nobody rotated a key because there is no key — the script impersonates a service account, and GCP just refused to mint the token. The whole pipeline halts over one missing IAM binding.

Impersonation denials are GCP's keyless auth doing its job: no Token Creator grant, no token. The error fires before your code touches any data, which makes it feel infrastructural when it's really one IAM line. The usual triggers are painfully ordinary — a new team member, a recreated service account, a binding applied at the wrong resource level — and each produces the identical denial.

These failures cluster around onboarding, CI migrations, and security cleanups, because all three reshape who may act as whom. The audit log records every attempt, yet teams rarely look there first.

This guide shows the exact order: confirm your identity, read the service account's policy, grant Token Creator at the right level, verify with a one-line impersonation, and check org policy when bindings look right. You'll see a real incident where a cleanup script revoked a CI binding, plus the governance pattern that keeps impersonation both usable and safe.

How Impersonation Works and Where It Refuses

Service account impersonation lets one identity act as another without handling its keys: your user account (or a CI service account) asks GCP to mint a short-lived token for a target service account, then uses that token. The minting call is GenerateAccessToken, and it checks one permission first: iam.serviceAccounts.getAccessToken on the target account, normally granted through roles/iam.serviceAccountTokenCreator. Fail that check and you get PERMISSION_DENIED before any bucket, dataset, or API is touched.

This ordering is the whole diagnostic story. Because the denial precedes data access, no application log, no data metric, and no retry will illuminate it — the request never reaches the code that would log something useful. Teams accustomed to debugging data permissions keep auditing buckets and datasets while the actual gate sits one layer earlier, at token issuance. Read the error literally: it names the permission and the account, and both are IAM facts you can query in seconds.

The secure shape of impersonation is worth internalizing now. Short lifetimes (minutes, not hours) bound leaked-token damage. Narrow grants (specific humans or workloads on specific accounts) bound who can borrow what. Audit coverage on every GenerateAccessToken call records who borrowed which badge and when. Denials are the system working — the incident is only when legitimate work can't get its grant.

📊 Production Insight
One org dashboards GenerateAccessToken denials per service account and treats sustained spikes as security events. Twice those spikes were attackers probing with phished credentials; three times they were broken automation. The same chart catches both — denied attempts deserve attention either way.
🎯 Key Takeaway
GenerateAccessToken checks getAccessToken first — denials precede data access, so debug IAM, not data.

Granting Token Creator at the Right Scope

The Token Creator role is the standard grant because it bundles exactly the impersonation permissions: getAccessToken for access tokens, signBlob and signJwt for signed payloads, and impersonate for the general capability. Describing the role shows the bundle explicitly, which ends debates about whether some narrower permission would suffice — for token minting, getAccessToken is the load-bearing one and Token Creator is its supported package.

Scope the grant to the service account resource, not the project. A project-level Token Creator grant lets the holder impersonate every service account in the project — convenient, and a breach waiting for a reason. The SA-level binding from the snippet limits borrowing to the one account the workload needs. When someone asks for project-wide impersonation, that request is really asking to skip access reviews for every future account — route it through security, not around them.

Verify from the shell before touching code. The impersonated print-access-token call mints a real token with the real binding chain, succeeding or failing exactly as your application would. A 300-second lifetime keeps the test token nearly harmless. Make this the first and last step of every impersonation change: reproduce with one line, fix, verify with the same line.

grant-token-creator.shBASH
1
2
3
4
5
6
7
8
9
10
# What permissions does Token Creator actually bundle?
gcloud iam roles describe roles/iam.serviceAccountTokenCreator --format="json(name, includedPermissions)"

# Grant impersonation on THIS service account only (least privilege)
gcloud iam service-accounts add-iam-policy-binding <SA_EMAIL> \
  --member=user:<DEV_EMAIL> \
  --role=roles/iam.serviceAccountTokenCreator

# Verify with a real (short-lived) impersonated token
 gcloud auth print-access-token --impersonate-service-account=<SA_EMAIL> --lifetime=300s | cut -c1-20
📊 Production Insight
A startup once granted Token Creator project-wide so contractors could deploy, then forgot. A year later every ex-contractor account could still mint deployer tokens. Quarterly binding reviews now expire stale grants automatically — least privilege decays without maintenance.
🎯 Key Takeaway
SA-level Token Creator plus a one-line impersonation test — grant narrow, verify fast.

Member Strings: the Wrong Email Looks Right

Member strings cause a disproportionate share of these denials because they look right while being wrong. user:dev@company.com, group:eng@company.com, and serviceAccount:ci@project.iam.gserviceaccount.com are three different principals, and the binding must name the one actually authenticating. The classic failure pairs a binding for a human with a workload authenticating as a service account, or a personal email with a contractor-domain login. The console shows a healthy-looking binding; the check fails because the caller isn't that member.

Groups add a propagation wrinkle: adding someone to a granted group can take minutes to take effect, so freshly onboarded engineers fail impersonation until propagation completes. Service accounts in groups behave the same way. When onboarding and impersonation coincide, wait, retry, and check timestamps before redesigning grants that are actually fine.

The discipline is comparing exact strings. Dump the SA policy, dump the active auth identity, and diff them character by character — domains, dots, and service-account suffixes included. The snippet below performs both dumps. Most member mysteries resolve the moment both strings sit side by side, because the difference is usually visible to anyone who looks at both at once.

compare-impersonation-members.shBASH
1
2
3
4
5
6
7
8
# Who are you right now? The ACTIVE line is the caller
 gcloud auth list

# Who may impersonate this service account? Compare member strings exactly
gcloud iam service-accounts get-iam-policy <SA_EMAIL> --format="json(bindings)"

# For CI callers: which account does the runner authenticate as?
gcloud auth print-identity-token --impersonate-service-account=<SA_EMAIL> 2>&1 | head -c 200; echo
📊 Production Insight
A CI migration once switched runners from user credentials to a service account while every binding still named the human. Monday's pipelines failed identically across repos — the member diff showed it in one command, but only after someone thought to compare both strings instead of staring at one.
🎯 Key Takeaway
Diff the SA policy member against the active auth identity character by character.

When Org Policy Overrides Your Perfect Binding

When bindings are correct and denials persist, look up the hierarchy: organization and folder policies can restrict credential issuance regardless of IAM grants. The symptom is distinctive — get-iam-policy shows a perfect binding, the one-line impersonation test still fails, and the audit log records denials citing policy rather than missing permission. No grant you add at the project level overrides a higher-level deny.

List the effective policies for the project and walk upward to folder and organization when something looks restrictive. Changes here need security-team partnership: the policy exists because someone decided impersonation should be limited, and your workload needs an exception with a documented scope, not a quiet deletion. Bring the audit-log evidence showing legitimate blocked work — policy owners respond to denied-business-impact faster than to grant requests.

Test policy changes in a sandbox project first. Organization policy edits propagate across everything beneath them, so a fix for one workload can open (or close) impersonation for hundreds. Sandbox validation plus a staged rollout keeps the exception tight. Record the exception next to the binding in infrastructure code so the next auditor sees intent, not accident.

check-impersonation-policy.shBASH
1
2
3
4
5
6
# Effective org policies on this project (walk up to folder/org if restrictive)
gcloud resource-manager org-policies list --project=<PROJECT_ID> --format="table(constraint, listPolicy.allowedValues)"

# Denials citing policy, not missing permission: pull the exact entries
 gcloud logging read 'protoPayload.methodName="GenerateAccessToken" AND protoPayload.status.code=7' \
  --project=<PROJECT_ID> --limit=20 --format="table(protoPayload.authenticationInfo.principalEmail, protoPayload.resourceName)"
📊 Production Insight
A financial-services org once blocked impersonation project-wide during an audit and broke twelve pipelines at once. The recovery pattern stuck: every impersonation dependency is now registered in a central list, and policy changes roll out against that list instead of into the dark.
🎯 Key Takeaway
Perfect bindings plus continued denials means policy above you — bring audit evidence to security.

Service-account-to-service-account chains multiply both power and confusion. CI runs as account A, impersonates deployer B, which impersonates data-loader C — each hop needing Token Creator on the next. A denial at hop two looks identical to a denial at hop one unless you test each link separately with the one-line impersonation command, authenticating as each intermediate. Test the chain link by link instead of end to end.

Keep chains short and lifetimes shorter. Every hop adds token-exchange latency and another binding an attacker can exploit. Two hops cover nearly every legitimate pattern (human to CI to workload); three hops deserve a design review asking why the middle account exists. Set explicit short lifetimes per hop so a token intercepted mid-chain expires before it's useful.

Prefer federation where the chain exists only to cross boundaries. Workload Identity Federation lets external CI (GitHub Actions, for example) mint short-lived GCP credentials directly, removing the first hop entirely. Fewer hops mean fewer bindings, fewer denials, and fewer 2 AM pages about the middle link. Chains are a tool for residual cases, not the default architecture. Document the chain shape in the runbook so the next debugger tests links instead of re-reading the whole pipeline.

test-impersonation-chain.shBASH
1
2
3
4
5
6
7
8
9
# Test each hop separately: authenticate as A, mint for B
 gcloud auth print-access-token --impersonate-service-account=<B_SA> --lifetime=300s >/dev/null && echo "hop A->B OK"

# Then as B (via impersonation) mint for C: chains need every link
gcloud auth print-access-token --impersonate-service-account=<B_SA>,<C_SA> --lifetime=300s >/dev/null && echo "chain A->B->C OK"

# Who impersonated whom recently? Audit the chain links
 gcloud logging read 'protoPayload.methodName="GenerateAccessToken"' --project=<PROJECT_ID> --limit=30 \
  --format="table(timestamp, protoPayload.authenticationInfo.principalEmail, protoPayload.resourceName)"
📊 Production Insight
A three-hop deploy chain once failed only on alternate Tuesdays — the middle account's group membership synced on a schedule nobody knew existed. Collapsing to one hop via federation deleted the flakiness and the on-call dread that came with it.
🎯 Key Takeaway
Test each hop separately; prefer federation to eliminate hops entirely.

Governing Impersonation So It Stays Safe

Durable prevention treats impersonation as governed infrastructure. Define each service account and its Token Creator bindings in the same infrastructure module, with members resolved from groups rather than hardcoded emails. A CI check diffs live SA policies against that desired state nightly; drift pages the owning team, not the next deploy's victim. Manual console grants become the exception that expires, not the rule that lingers.

Layer audit coverage proportional to power. Every GenerateAccessToken call on production data accounts logs to a monitored sink with alerts on both anomalies: denials spiking (attacks or breakage) and successes from unexpected callers (compromise or scope creep). Break-glass bindings carry short expiries and require incident-linked justification. Reviews happen quarterly against the live policy dumps, not against someone's memory of the intent.

Onboard people and workloads through the same front door: request the binding, record the reason, set the expiry, verify with the one-line test. When the next denial lands, the responder checks live state against declared state and finds the gap in minutes — because the declared state exists. Impersonation stays both usable and safe exactly to the extent that its grants are written down, watched, and expired.

⚠ Never Open Impersonation to the Public
Never grant Token Creator to allUsers or allAuthenticatedUsers, even temporarily for debugging. Either value lets anyone on the internet mint tokens as the account, and scanners find exposed grants within hours. Debug with a single test member, then remove even that when done.
📊 Production Insight
Orgs that review SA bindings quarterly report the same surprise every time: grants nobody remembers approving, for people who left months ago. The review pays for itself the first time it removes a departed contractor's path to production data — before anyone tests whether it still works.
🎯 Key Takeaway
Bindings in code, drift alerts on, audit sinking everything, break-glass expiring.
● Production incidentPOST-MORTEMseverity: high

The Cleanup Script That Revoked the Deploys

Symptom
All four deploy pipelines failed at the authentication step with PERMISSION_DENIED on iam.serviceAccounts.getAccessToken. No workload code executed, no resources changed, and the error was identical across services — the signature of a shared identity dependency, not four simultaneous code bugs.
Assumption
The team assumed GCP was having an IAM outage because four pipelines failed simultaneously with identical denials. Then they assumed the service account had been deleted and recreated it — which changed nothing except adding a second account to the confusion. Each theory was tested by rerunning pipelines, burning forty minutes per cycle.
Root cause
The cleanup removed the roles/iam.serviceAccountTokenCreator binding that let the CI service account impersonate the deployer account. Without it, every GenerateAccessToken call was denied before any deployment step ran. The binding had been granted manually months earlier, so no infrastructure code restored it.
Fix
The fix was one binding: gcloud iam service-accounts add-iam-policy-binding <DEPLOY_SA> --member=serviceAccount:<CI_SA> --role=roles/iam.serviceAccountTokenCreator. The pipeline went green on the next run. The lasting fixes: the cleanup script now skips bindings referenced by CI configs, all SA bindings moved into Terraform with CI-resolved members, and a nightly drift check alerts when a live SA policy differs from desired state.
Key lesson
  • Cleanup scripts must understand references, not just age. A binding untouched for 90 days can still be load-bearing — the script deleted by timestamp what it should have kept by dependency.
  • Recreating identities during an incident makes things worse. The new service account had a new unique ID, so even restoring the binding by email needed care — never rebuild identity while debugging access.
  • One-line IAM reproduction beats pipeline reruns. The gcloud impersonation test would have isolated the missing binding in a minute instead of forty-minute pipeline cycles.
Production debug guideFive checks, in order, from caller identity to org policy.5 entries
Symptom · 01
You don't know who the caller is or what the SA policy holds
→
Fix
Run gcloud auth list and note the ACTIVE account — that's the caller. Then run gcloud iam service-accounts get-iam-policy <SA_EMAIL> --format=json and search for that exact member under roles/iam.serviceAccountTokenCreator. Absent binding equals the denial; stop here and grant it.
Symptom · 02
You need a one-line reproduction without application code
→
Fix
Run gcloud auth print-access-token --impersonate-service-account=<SA_EMAIL> --lifetime=300s. Success prints a token and proves the binding. PERMISSION_DENIED reproduces the failure in one line — faster than any app redeploy and safe to run repeatedly while fixing.
Symptom · 03
The role seems granted yet the denial persists
→
Fix
Run gcloud projects get-iam-policy <PROJECT> --flatten=bindings --filter=bindings.role:serviceAccountTokenCreator --format=table(bindings.members) and compare with the SA-level policy. A project-level binding for a different member explains the confusion: close, but not the grant this SA checks.
Symptom · 04
Two engineers disagree about what was denied and for whom
→
Fix
Run gcloud logging read 'protoPayload.methodName="GenerateAccessToken" AND protoPayload.resourceName:<SA_ID>' --project=<PROJECT> --limit=20 --format=json and read status and caller per entry. The log names the exact missing permission and the true caller identity, settling member-string disputes instantly.
Symptom · 05
Bindings are perfect and impersonation still fails
→
Fix
Run gcloud resource-manager org-policies list --project=<PROJECT> (or the organization/folder equivalent) and review policies restricting credential issuance. If bindings are perfect and denials continue, a higher-level policy is overriding them — escalate with the audit-log evidence attached.
Service Account Impersonation Denied Compared
Root CauseHow to ConfirmFixPrevention
Missing Token Creator bindingget-iam-policy on the SA shows no binding for your member; GenerateAccessToken denied in audit logsadd-iam-policy-binding with roles/iam.serviceAccountTokenCreator on the SAGrant impersonation in IaC next to the SA definition
Binding on the wrong resource levelProject policy has the role but the SA policy doesn't; denial persistsMove the grant to the service account resource itselfStandardize on SA-level grants; lint for project-level impersonation
Wrong member identity stringBinding exists for a different email than gcloud auth list shows activeRebind to the exact authenticated principalResolve member emails from identity groups dynamically
Org policy or constraint blockingBindings correct yet denials continue; org-policy audit shows a deny ruleAdjust the policy exception with security approvalDocument impersonation allowlists; test in a sandbox project first
⚙ Quick Reference
4 commands from this guide
FileCommand / CodePurpose
grant-token-creator.shgcloud iam roles describe roles/iam.serviceAccountTokenCreator --format="json(na...Granting Token Creator at the Right Scope
compare-impersonation-members.shgcloud auth listMember Strings
check-impersonation-policy.shgcloud resource-manager org-policies list --project=<PROJECT_ID> --format="table...When Org Policy Overrides Your Perfect Binding
test-impersonation-chain.shgcloud auth print-access-token --impersonate-service-account=<B_SA> --lifetime=3...Chained Impersonation

Key takeaways

1
getAccessToken denials mean no Token Creator grant
check the SA policy before touching code.
2
Grant roles/iam.serviceAccountTokenCreator on the service account resource, not vaguely at project level.
3
Confirm the exact member string against gcloud auth list
wrong-email bindings fool everyone.
4
Test with gcloud auth print-access-token --impersonate-service-account in seconds, codeless.
5
Read GenerateAccessToken audit entries for every denial; alert on unexpected successes.
6
Keep lifetimes short, chains shorter, and production bindings break-glass with expiry.

Common mistakes to avoid

5 patterns
×

Granting the role on the project instead of the service account

Symptom
The developer holds Token Creator somewhere in the project policy, yet impersonation fails. Project-level grants on the wrong resource don't satisfy service-account-level checks, and the policy dump looks correct to tired eyes.
Fix
Grant at the service-account resource level: gcloud iam service-accounts add-iam-policy-binding <SA> --member=user:<DEV> --role=roles/iam.serviceAccountTokenCreator. Then confirm with get-iam-policy on the service account itself, not the project.
×

Binding the role to the wrong member string

Symptom
The binding exists for dev@company.com while the engineer authenticates as dev@contractor.com. Everything looks granted in the console, but the authenticated principal and the bound member are two different people.
Fix
Use user:, group:, or serviceAccount: principals with the exact authenticated email. Dump the binding with get-iam-policy and compare member strings against gcloud auth list output before redeploying anything.
×

Handing out Token Creator on production data accounts to everyone

Symptom
Every engineer can mint tokens for the account that reads customer data. The first leaked laptop or rogue script becomes a full data breach, and the audit log can't distinguish legitimate use from abuse.
Fix
Give developers Token Creator on dedicated CI/deploy service accounts only, and require human elevation for production data accounts. Short lifetimes plus narrow scope keep the blast radius small when any one credential leaks.
×

Mixing downloaded keys and impersonation in the same workflow

Symptom
Half the team uses keys, half impersonates, and failures alternate between expired keys and denied impersonation. Debugging means asking which auth path each script took, because the error differs by path.
Fix
Keep one keyless path per workload: local dev impersonates through gcloud, CI uses Workforce/Workload Identity Federation. Delete uploaded keys with gcloud iam service-accounts keys delete and alert on keys older than the rotation policy.
×

Ignoring GenerateAccessToken denials in the audit log

Symptom
Attackers probe impersonation paths for weeks while the denials pile up unread. When a mis-scoped grant finally lets one through, nobody notices the success buried in months of ignored failures.
Fix
Query the audit log for GenerateAccessToken denials on a schedule and alert on spikes. Denials that never succeed are either brute force or broken automation — both deserve a human look within the hour, not next quarter.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Impersonation fails with permission denied on getAccessToken. What does ...
Q02JUNIOR
When do you need Token Creator vs Service Account User?
Q03SENIOR
Walk me through debugging a denied impersonation end to end.
Q04SENIOR
Why keep impersonated token lifetimes short?
Q05SENIOR
Design least-privilege impersonation governance for a whole org.
Q01 of 05JUNIOR

Impersonation fails with permission denied on getAccessToken. What does that mean?

ANSWER
The caller lacks iam.serviceAccounts.getAccessToken (or the broader Token Creator role) on the target account. GCP refuses to mint the token before any data is touched. I'd confirm with get-iam-policy on the service account and fix with add-iam-policy-binding for roles/iam.serviceAccountTokenCreator on that SA.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Is roles/iam.serviceAccountTokenCreator safe to grant broadly?
02
Token Creator vs Service Account User: which do I need?
03
How do I check my impersonation permission directly?
04
Can I test impersonation without writing code?
05
Can service accounts impersonate other service accounts?
06
Where do impersonation attempts show up in logs?
N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Drawn from code that ran under real load.

Follow
✓ Verified
production tested
September 26, 2026
last updated
2,085
articles · all by Naren
🔥

That's GCP. Mark it forged?

5 min read · try the examples if you haven't

←
Previous
Azure Function Timed Out After 230 Seconds
1 / 3 · GCP
Next
GCP Cloud Run Container Failed to Start and Listen on PORT
→