How to Fix GCP IAM AccessDenied Errors

An IAM AccessDenied error means Google Cloud authenticated the caller successfully (it knows who you are) but the IAM policy on the target resource denied the requested action. The caller’s identity is confirmed. The permission is missing, conditional, or granted in the wrong place.

This page covers IAM-specific access failures only. If the error is caused by a disabled API, wrong credentials, an org policy constraint, or a network issue, see the broader Permission Denied guide instead. Here you will learn how to identify the calling principal, find the missing or misconfigured binding, and fix the eight most common IAM causes. Start by identifying three things: the principal email, the target resource, and the exact permission being denied.

Simple explanation

Think of IAM like a building security desk. Authentication is showing your ID at the door. GCP confirmed your identity. IAM policy evaluation is checking the access list for the floor you want to enter. AccessDenied means your ID is valid, but your name is not on the list for that floor.

The key distinction: identity confirmed, permission denied. The system knows exactly who is making the request. It just will not allow the specific action on the specific resource. The fix is always about updating the access list (IAM policy), not about proving who you are.

When to use this page

Use this page when:

  • Authentication succeeded but IAM policy evaluation denied the action
  • The error message mentions a specific permission, resource, and caller email
  • The error says “IAM condition not satisfied”
  • A service account or user has roles but still cannot access a resource

Do NOT use this page when:

IAM AccessDenied vs other GCP access errors

GCP returns several different access errors that look similar but have different root causes. Use this table to confirm you are on the right page.

Error typeWhat it meansFastest first checkWhere to go
IAM AccessDeniedIdentity authenticated, IAM policy denied the actionCheck the caller email and whether the binding exists on the target resourceThis page
Permission Denied (broader 403)Umbrella category that covers IAM, org policy, and disabled API errorsRead the full error message to identify which subcategoryPermission Denied Errors
Authentication errorsGCP cannot verify who is calling (bad credentials, expired token, missing ADC)Check gcloud auth list or ADC configurationAuthentication Errors
API not enabled / SERVICE_DISABLEDThe API has not been enabled on the target projectgcloud services list —project=PROJECT_IDEnabling APIs
Org policy / constraint blockAn organisation-level constraint overrides IAM. The action is not allowed regardless of rolesCheck if the error mentions a constraint nameOrganisation Policies

5-minute diagnosis checklist

Run through these steps in order. Most IAM AccessDenied errors resolve at step 2 or 3.

  1. Read the exact principal identity from the error or logs. The error message or Cloud Audit Logs show the principalEmail. Do not assume. Confirm which identity actually made the call.

  2. Confirm the exact resource and permission being denied. The error message includes the permission name (e.g. storage.objects.get) and the full resource path. Write both down.

  3. Check whether the binding exists on the target resource or project.gcloud projects get-iam-policy PROJECT_ID —flatten=“bindings[].members” —filter=“bindings.members:SERVICE_ACCOUNT_EMAIL” —format=“table(bindings.role)”
  4. Check cross-project access. If the resource is in a different project from the caller, the binding must exist in the resource’s project, not the caller’s project.

  5. Check IAM conditions. View the full policy with gcloud projects get-iam-policy PROJECT_ID —format=yaml and look for condition: blocks on the relevant binding.

  6. Check for disabled or deleted service accounts.gcloud iam service-accounts describe SERVICE_ACCOUNT_EMAIL —format=“value(disabled)”
  7. Check impersonation and WIF mappings. If the workload uses impersonation or Workload Identity Federation, verify the mapping and the impersonated account’s permissions.

  8. Use Cloud Audit Logs and Policy Troubleshooter.gcloud policy-troubleshoot iam //cloudresourcemanager.googleapis.com/projects/PROJECT_ID —principal-email=SERVICE_ACCOUNT_EMAIL —permission=PERMISSION_NAME
Tip

Steps 1 and 2 solve the majority of cases. If you skip straight to granting roles without confirming the principal and permission first, you risk granting the right role to the wrong identity or in the wrong project.

How IAM AccessDenied happens

Understanding the flow helps you pinpoint where things go wrong. Here is what happens on every GCP API call:

  1. The caller authenticates. GCP verifies the credentials: an OAuth token, a service account key, or a federated identity token.
  2. GCP identifies the principal. The token resolves to a specific email: a user account, a service account, or a federated identity.
  3. The target resource’s IAM policy is evaluated. GCP checks every binding on the resource, its parent project, folder, and organisation for a binding that grants the required permission to that principal.
  4. Conditions are checked. If a matching binding has a condition (time window, resource attribute, IP range), the condition must evaluate to true for the binding to apply.
  5. Access is granted or denied. If at least one valid binding grants the permission, access is allowed. Otherwise, the request fails with AccessDenied.
Note

Think of this like a chain of locks. Authentication (step 1) is the first lock. IAM policy evaluation (steps 3-4) is the second lock. AccessDenied means you got past the first lock but not the second. Every troubleshooting cause below maps to a reason the second lock stays closed.

Common causes and how to fix them

1. The workload is using the wrong identity

The most common cause of IAM AccessDenied. You granted a role to one service account, but the workload is running as a different one, often the default Compute Engine service account (PROJECT_NUMBER-compute@developer.gserviceaccount.com).

Each GCP compute service attaches a service account to the workload. If you created a purpose-specific service account and granted it roles, but the workload still runs as the default account, those roles do not apply.

Warning

The default Compute Engine service account has the Editor role on many older projects, which masks permission problems during development. When you deploy to a project without that legacy grant, or switch to a custom service account with only the roles you assigned, previously-hidden missing permissions surface as AccessDenied.

# Check which service account a Cloud Run service uses
gcloud run services describe RESOURCE_NAME \
  --region=REGION \
  --format="value(spec.template.spec.serviceAccountName)"

# Check which service account a Compute Engine VM uses
gcloud compute instances describe RESOURCE_NAME \
  --zone=ZONE \
  --format="value(serviceAccounts[0].email)"

# Check which service account a Cloud Function uses
gcloud functions describe RESOURCE_NAME \
  --region=REGION \
  --format="value(serviceAccountEmail)"

# Switch a Cloud Run service to the correct service account
gcloud run services update RESOURCE_NAME \
  --region=REGION \
  --service-account=SERVICE_ACCOUNT_EMAIL

For GKE pods, the identity depends on whether Workload Identity is configured. Without it, pods use the node’s service account, which is usually the default Compute Engine account.

2. The correct identity is missing the required role

The workload is running as the right service account, but that account does not have a role that includes the required permission. The fix depends on where the target resource lives.

Roles granted at the project level apply to all resources in that project. Roles granted on a specific resource (a bucket, a dataset, a topic) apply only to that resource. The binding must exist where the resource is, not just anywhere in the project hierarchy.

# List all roles a service account has on a project
gcloud projects get-iam-policy PROJECT_ID \
  --flatten="bindings[].members" \
  --filter="bindings.members:serviceAccount:SERVICE_ACCOUNT_EMAIL" \
  --format="table(bindings.role)"

# Grant a role at the project level
gcloud projects add-iam-policy-binding PROJECT_ID \
  --member="serviceAccount:SERVICE_ACCOUNT_EMAIL" \
  --role="roles/storage.objectViewer"

Use the IAM Roles reference to find which predefined role includes the specific permission from the error message.

3. Cross-project access is missing

A service account in project A accessing a resource in project B requires an explicit IAM binding in project B (or on the specific resource in project B). Roles granted in project A do not carry over. This is the single most common mistake in multi-project GCP setups.

Common trap

Granting roles/bigquery.dataViewer to a service account in its home project does not let it read BigQuery data in another project. The binding must exist in the project (or on the dataset) where the data lives.

# Grant a service account from project A access to BigQuery in project B
gcloud projects add-iam-policy-binding PROJECT_B_ID \
  --member="serviceAccount:SERVICE_ACCOUNT_EMAIL" \
  --role="roles/bigquery.dataViewer"

For BigQuery, granting at the dataset level is often better than project-level access because it follows least-privilege:

# Grant access at the dataset level (preferred for BigQuery)
bq add-iam-policy-binding \
  --member="serviceAccount:SERVICE_ACCOUNT_EMAIL" \
  --role="roles/bigquery.dataViewer" \
  PROJECT_B_ID:DATASET_NAME

For Cloud Storage, you can grant access at the bucket or object level instead of the project level. See Cloud Storage IAM vs ACLs for when to use each approach.

Note

When accessing requester-pays Cloud Storage buckets from another project, the calling service account also needs roles/serviceusage.serviceUsageConsumer on its home project so its project can be billed for data transfer.

4. IAM Conditions are blocking the binding

IAM conditions restrict when a role binding applies. When a condition evaluates to false, that binding is ignored entirely. If no other unconditional binding grants the permission, the result is AccessDenied. The binding looks present in the IAM policy, which makes this particularly deceptive.

Warning

Conditional bindings are the sneakiest cause of AccessDenied. The role shows up when you list IAM bindings. The principal is correct. Everything looks right. But the binding has a condition that silently prevents it from applying. Always check the full YAML output for condition: blocks.

The three most common conditions that catch people off-guard:

  • Expired time-based conditions. A binding valid only until a certain date expires silently. Callers that previously had access suddenly receive AccessDenied with no visible policy change.
  • Resource tag conditions. A binding that requires a specific resource tag denies access to resources missing that tag, even within the same project.
  • IP address conditions. Access allowed only from certain IP ranges blocks requests from Cloud Run, Cloud Functions, or other services using dynamic egress IPs.
# View the full IAM policy including conditions
gcloud projects get-iam-policy PROJECT_ID --format=yaml

# Bindings with conditions look like this in the output:
# condition:
#   expression: request.time < timestamp("2026-01-01T00:00:00Z")
#   title: Temporary access until 2026

If the condition has expired, either remove it, update the timestamp, or create a new unconditional binding with the same role.

5. The service account is disabled, deleted, or not the one you think it is

A disabled service account cannot authenticate at all. A deleted service account leaves orphaned IAM bindings that reference a non-existent principal. These orphaned bindings do nothing. Both produce access errors that look identical to a missing-role error.

# Check whether a service account is disabled
gcloud iam service-accounts describe SERVICE_ACCOUNT_EMAIL \
  --format="value(disabled)"

# Re-enable a disabled service account
gcloud iam service-accounts enable SERVICE_ACCOUNT_EMAIL

# List all service accounts to spot email mismatches
gcloud iam service-accounts list --project=PROJECT_ID
Tip

A single character difference in the service account email creates a binding that matches nothing. Copy the exact email from the audit log, then search for it in the IAM policy. Do not retype it by hand.

6. Service account impersonation is misconfigured

Service account impersonation lets one identity act as another service account. Two things must be true for this to work:

  1. The caller must have roles/iam.serviceAccountTokenCreator on the target service account (this allows impersonation).
  2. The impersonated service account must have the roles needed for the actual resource access.
Two-layer permission model

After impersonation, the impersonated account’s permissions are what matter, not the original caller’s. Think of it like handing someone your building pass. Their access depends on what your pass allows, not what their own pass would allow. If the impersonated account is missing a role, the request fails with AccessDenied even if the original caller has the role.

# Check who can impersonate a service account
gcloud iam service-accounts get-iam-policy SERVICE_ACCOUNT_EMAIL

# Grant impersonation permission
gcloud iam service-accounts add-iam-policy-binding SERVICE_ACCOUNT_EMAIL \
  --member="serviceAccount:CALLER_SERVICE_ACCOUNT_EMAIL" \
  --role="roles/iam.serviceAccountTokenCreator"

7. Workload Identity Federation is mapped incorrectly

Workload Identity Federation lets external identities (GitHub Actions, AWS roles, Azure principals) impersonate GCP service accounts without keys. AccessDenied errors here are usually caused by a mismatch between the external identity’s attributes and the principal mapping in GCP.

The impersonation binding on the service account must match the external identity exactly. The principal format looks like this:

principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/attribute.repository/OWNER/REPO

Exact match required

WIF principal strings are case-sensitive and must match character-for-character. A wrong project number, a wrong pool ID, a different attribute name, or a typo in the repository path all produce AccessDenied with no further explanation in the error message.

# Check the Workload Identity Pool configuration
gcloud iam workload-identity-pools describe POOL_ID \
  --location=global \
  --format=yaml

# Check the provider configuration and attribute mapping
gcloud iam workload-identity-pools providers describe PROVIDER_ID \
  --workload-identity-pool=POOL_ID \
  --location=global \
  --format=yaml

# View bindings on the service account that allow impersonation
gcloud iam service-accounts get-iam-policy SERVICE_ACCOUNT_EMAIL

Common WIF mismatches:

  • Subject mismatch: the token’s sub claim does not match the attribute condition in the provider.
  • Attribute mapping mismatch: the provider maps attribute.repository but the binding uses attribute.actor.
  • Wrong principalSet syntax: using principal instead of principalSet (or the reverse) changes which identities match.

8. Terraform or automation changed IAM unexpectedly

Terraform’s google_project_iam_policy resource is authoritative. It replaces the entire project IAM policy with what is defined in code. If a binding was added manually (or by another Terraform module) and is not in the authoritative resource, the next terraform apply removes it silently.

Warning

Using google_project_iam_policy in Terraform is like overwriting a shared document instead of editing it. Any bindings not in your Terraform code get deleted on the next apply. If access broke right after a Terraform run, check terraform plan output for removed bindings.

Prefer google_project_iam_member (additive, manages a single binding) or google_project_iam_binding (authoritative per role) unless you genuinely need full policy control. See Managing IAM with Terraform for the full breakdown of authoritative vs additive patterns.

Common real-world scenarios

Cloud Run service account access denied

Cloud Run revisions use the default Compute Engine service account unless you specify one. If you granted roles to a custom service account but forgot to attach it to the Cloud Run service, the workload runs as the default account (which lacks those roles). Check with gcloud run services describe and update with —service-account. See Cloud Run Security Model for the full identity setup.

GKE workload identity / pod-to-GCP access denied

GKE pods access GCP APIs through Workload Identity. The Kubernetes service account must be annotated with the GCP service account email, and the GCP service account must have an IAM binding allowing the Kubernetes service account to impersonate it. If either side is missing, pods get AccessDenied. Check the annotation and the roles/iam.workloadIdentityUser binding.

BigQuery cross-project access denied

A service account querying BigQuery in another project needs roles/bigquery.dataViewer on the target project or dataset and roles/bigquery.jobUser on the project where the query runs (usually its home project). Missing either role produces AccessDenied with different permission names. Check the error message carefully to see which permission is missing.

Cloud Storage access denied

Cloud Storage supports both IAM and legacy ACLs. If IAM looks correct but access still fails, check whether uniform bucket-level access is enabled and whether legacy ACLs are interfering. See Cloud Storage IAM vs ACLs for how to check.

GitHub Actions / CI pipeline WIF access denied

CI pipelines using Workload Identity Federation fail most often because the principalSet in the service account binding does not match the token attributes from the CI provider. For GitHub Actions, double-check the repository path, branch filter, and that the Workload Identity Pool’s attribute mapping includes attribute.repository. See Secure CI/CD Pipelines.

Use Cloud Audit Logs and Policy Troubleshooter to confirm the cause

Every failed IAM check is recorded in Cloud Audit Logs. The log entry shows who made the request, what they tried to do, which resource was targeted, and exactly why it was denied. This is the definitive source of truth. Use it to confirm your diagnosis before making changes.

# Find recent PERMISSION_DENIED errors in Cloud Logging
gcloud logging read \
  'protoPayload.status.code=7 AND protoPayload.authorizationInfo.granted=false' \
  --project=PROJECT_ID \
  --limit=20 \
  --format="table(timestamp, protoPayload.authenticationInfo.principalEmail, \
    protoPayload.methodName, protoPayload.authorizationInfo[0].permission)"

In the output, look for the principalEmail (who called), permission (what was denied), and resourceName (the target resource). These three values tell you exactly what binding to create or fix.

The IAM Policy Troubleshooter walks through the full policy hierarchy (resource, project, folder, organisation) and explains which binding grants or denies access, including conditions. Access it in the GCP Console under IAM & Admin → Policy Troubleshooter, or via the CLI:

gcloud policy-troubleshoot iam //cloudresourcemanager.googleapis.com/projects/PROJECT_ID \
  --principal-email=SERVICE_ACCOUNT_EMAIL \
  --permission=PERMISSION_NAME

You can also use Logs Explorer in the console to filter and inspect audit log entries visually.

Tip

Cloud Audit Logs captures Admin Activity logs automatically, but Data Access logs must be explicitly enabled per service. If you cannot find a failed access check in the logs, verify that Data Access audit logging is enabled for the relevant service before assuming no check was made.

Common mistakes

  1. Fixing the wrong principal. Always get the principal email from the error message or audit log before making any changes. Granting roles to a service account you think is being used, without confirming, is the most common wasted effort in access debugging.

  2. Granting roles in the wrong project. Cross-project access requires a binding in the target project, not the caller’s project. roles/bigquery.dataViewer in project A does not allow access to BigQuery in project B.

  3. Missing conditions on existing bindings. If access was working and suddenly stopped with no visible policy change, look for a time-based condition that has expired. The binding is still visible in the IAM policy. That is what makes this easy to miss.

  4. Confusing user permissions with service account permissions. Service accounts are separate identities. Permissions granted to your personal Google account are never inherited by service accounts in the same project.

  5. Forgetting the impersonation or WIF layer. When impersonation or Workload Identity Federation is involved, the permissions that matter are on the impersonated account, not the original caller. Debugging the wrong identity in the chain wastes time.

What to remember

IAM AccessDenied always means: identity confirmed, permission denied. Every fix starts with three facts from the error or audit log: the principal email, the target resource, and the missing permission. Get those first, then check bindings, conditions, cross-project grants, and identity chains. Use the Policy Troubleshooter to confirm before you change anything.

If the error turns out to be something other than an IAM policy evaluation failure (a disabled API, an authentication problem, or an org policy constraint), use the links below to get to the right guide.

Frequently asked questions

What does "IAM condition not satisfied" mean?

The identity has the correct role binding, but the condition attached to that binding evaluates to false at request time. Common conditions include time-based windows (the binding expired), resource attribute conditions (the binding only applies to resources with specific tags or in specific locations), and IP-based conditions. The binding looks present in the IAM policy, but it does not apply. Inspect the condition expression with gcloud projects get-iam-policy PROJECT_ID --format=yaml and check whether the condition still matches.

Why does a service account still get AccessDenied after I granted a role?

Check three things. First, confirm the role was granted on the correct resource (the target project, bucket, or dataset, not a different one). Second, confirm the service account email in the binding exactly matches the one the workload is actually using (check the audit log for the real principal). Third, check whether the service account is disabled or deleted. Also allow up to 60 seconds for IAM propagation before retesting.

How is IAM AccessDenied different from Permission Denied?

IAM AccessDenied is specifically about IAM policy evaluation: the identity authenticated successfully, but the policy on the target resource denied the action. Permission Denied is the broader 403 category that also covers disabled APIs (SERVICE_DISABLED), org policy blocks, and quota issues. If the error message mentions an org constraint or a disabled service, the fix is not an IAM role grant. See the broader Permission Denied guide instead.

Which permissions matter when service account impersonation is involved?

The caller needs roles/iam.serviceAccountTokenCreator on the target service account to impersonate it. Once impersonation is active, the impersonated service account's permissions are what matter for accessing resources, not the original caller's permissions. If the impersonated account is missing a role, the request fails with AccessDenied even though the caller has the role.

Why does cross-project access fail even when the service account already has roles?

Roles granted in one project do not carry over to other projects. A service account in project A with roles/storage.objectViewer on project A cannot read objects in project B. The binding must exist on the target project or resource in project B. This is the single most common cross-project mistake.

Last verified: 27 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.