GCP 403 PERMISSION_DENIED: Fix IAM, API, and Org Policy Errors
A GCP 403 PERMISSION_DENIED error means the calling identity lacks a required permission on the target resource. But that single error code covers at least six different root causes: wrong caller identity, missing IAM role, role granted at the wrong scope, disabled API, organisation policy block, or IAM propagation delay. This guide walks through each cause in priority order with exact commands to diagnose and fix the problem.
Most engineers waste time granting roles before they read the error message. The error itself tells you the caller, the missing permission, and the resource. Start there, and you can usually resolve the issue in under two minutes.
What “permission denied” means in plain English
Think of GCP like a building with a security desk. When you walk in, the guard checks three things: your ID badge (your identity), the room you want to enter (the resource), and the access list for that room (IAM permissions). A 403 PERMISSION_DENIED means the guard recognised your badge but your name is not on the list for that room.
In GCP terms, every API call involves four things: an identity (who is calling), a permission (what action they want), a resource (which thing they want to act on), and a scope (where the access grant exists in the resource hierarchy). A 403 means GCP checked these four things and found a gap.
If you are new to how GCP decides who can do what, read What Is IAM in GCP first. IAM is the system that maps identities to permissions on resources. Every permission denied error traces back to something in that mapping being wrong or missing.
The same 403 error code can come from completely different root causes. A missing IAM role, a disabled API, and an org policy constraint all return 403 PERMISSION_DENIED. That is why you need to read the full error message and work through the causes systematically rather than immediately granting a broader role.
Quick diagnosis checklist
Work through this checklist in order. Most permission denied errors resolve at step 1, 2, or 3.
- Read the full error message. Extract the caller identity, the missing permission, and the target resource. Do not skip this step.
- Identify the caller. Is it your user account, a service account, or a compute default service account? Run
gcloud auth listor check the service account attached to your Cloud Run, GKE, or Cloud Functions workload. - Identify the missing permission. The error tells you the exact permission string (for example,
storage.objects.get). Look up which predefined role includes that permission. - Check the resource scope. Confirm the IAM binding exists at the correct level: resource, project, folder, or organisation. A role granted at the wrong scope will not cover the target resource.
- Confirm the API is enabled. Run
gcloud services list —enabledand check for the relevant API. A disabled API returns a 403 that looks identical to a permission error. - Check org policy constraints. If IAM looks correct and the API is enabled, ask your organisation admin whether a constraint is blocking the action.
- Retry after propagation delay. IAM changes take up to 60 seconds to propagate. If you just granted the role, wait a minute and retry.
- Validate with testIamPermissions. Use the
testIamPermissionsAPI or Cloud Audit Logs to confirm whether the permission is actually granted.
How GCP evaluates permissions
Understanding the evaluation flow helps you pinpoint where the failure occurs. Think of it as a series of locked doors. The request must pass through every door in sequence, and a failure at any door produces a 403.
- Who is calling? GCP extracts the identity from the request credentials. This could be a user account or a service account. If no valid credential is present, the request fails with UNAUTHENTICATED (401), not PERMISSION_DENIED (403).
- Is the API enabled? If the API is not enabled in the target project, the request fails with a 403 that says SERVICE_DISABLED. This happens before IAM is even checked.
- What permission is required? Each API method maps to one or more IAM permissions. For example, reading a Cloud Storage object requires
storage.objects.get. - Which resource is targeted? The permission is checked against the specific resource in the request, not just the project.
- Does an IAM binding exist? GCP walks up the resource hierarchy (resource, project, folder, organisation) looking for a binding that grants the required permission to the caller. If it finds one, the permission check passes. If not, it returns PERMISSION_DENIED. Learn how this hierarchy works in the IAM policies guide.
- Does an org policy block the action? Even after IAM allows the request, organisation policy constraints can still deny it. Org policies override IAM.
- Has the binding propagated? IAM is eventually consistent. A newly added binding may not be visible at every regional endpoint for up to 60 seconds.
When debugging, work backwards through this list. Start by reading the error to identify the caller and the missing permission, then check whether each door in the chain is open.
What the error message tells you
Before touching IAM, read the full error. GCP error messages contain everything you need to act:
Error 403: The caller does not have permission, or the resource may not exist.
Permission 'storage.objects.get' denied on resource
'//storage.googleapis.com/projects/_/buckets/my-bucket/objects/config.json'
for caller 'backend-sa@my-project.iam.gserviceaccount.com'.Extract three pieces of information:
- The permission:
storage.objects.get, which is the exact IAM permission that is missing - The resource: a specific object in a specific bucket
- The caller: the service account that made the call
With these three you know exactly what predefined role to grant, to which identity, and at which scope. You do not need to guess.
If your terminal or SDK truncates the error, find the full version in
Logs Explorer.
Filter on protoPayload.status.code=7 to surface all recent
PERMISSION_DENIED errors across your project. The protoPayload.authorizationInfo
field shows every permission that was checked and the exact outcome.
Root causes in order of frequency
1. Wrong identity making the call
The single most common cause. You grant a role to your personal account, but the code runs as a service account. Or you grant it to the right service account but in the wrong project. Always confirm the calling identity before making any IAM change.
Picture this: you hand a VIP pass to your friend, but your colleague is the one trying to enter the event. It does not matter how good the pass is if the wrong person is holding it.
Symptom: The error message shows a different caller than expected.
How to verify:
# Check which account gcloud is currently using
gcloud auth list
# Check the active project
gcloud config get-value project
# Find the service account a Cloud Run service runs as
gcloud run services describe SERVICE_NAME \
--region=REGION \
--format="value(spec.template.spec.serviceAccountName)"
# Find the service account a Cloud Function runs as
gcloud functions describe FUNCTION_NAME \
--region=REGION \
--format="value(serviceConfig.serviceAccountEmail)"
# Find the service account a GKE pod uses
kubectl get pod POD_NAME -o jsonpath='{.spec.serviceAccountName}'Fix: Grant the role to the identity shown in the error message, not the identity you assumed was making the call. If the wrong identity is being used, fix the configuration at the source and attach the correct service account to the workload.
2. Role missing or granted at the wrong scope
Granting roles/storage.objectViewer at project level gives access to all buckets in the project. Granting it at bucket level gives access only to that bucket. If your error references a specific resource, make sure the grant covers that scope.
Symptom: The caller is correct and has a role, but the role does not include the required permission, or it is granted at a scope that does not cover the target resource.
How to verify:
# List all roles an identity has on the project
gcloud projects get-iam-policy PROJECT_ID \
--flatten="bindings[].members" \
--filter="bindings.members:serviceAccount:backend-sa@my-project.iam.gserviceaccount.com" \
--format="table(bindings.role)"
# Check roles on a specific bucket
gcloud storage buckets get-iam-policy gs://my-bucket \
--format="table(bindings.role, bindings.members)"Fix: Use gcloud IAM commands to grant the correct predefined role at the narrowest scope that covers the target resource. Follow the principle of least privilege and grant access at the resource level, not the project level, when possible.
3. API not enabled
This is the most misleading cause. A disabled API returns a 403 that looks almost identical to a permission error. It tricks you into debugging IAM when the fix is a single command. Always check API enablement before touching IAM.
Symptom: The error includes a mention of SERVICE_DISABLED, or you recently started using a new GCP service in this project.
How to verify:
# Check if a specific API is enabled
gcloud services list --enabled --filter="name:sqladmin.googleapis.com"Fix: Enable the API: gcloud services enable sqladmin.googleapis.com. For a deeper walkthrough, see API not enabled errors. You can also learn how to enable APIs via Console, gcloud, or Terraform.
4. Organisation policy blocking the action
Organisation policies sit above IAM. A constraint can block an action even when the IAM binding is correct. Common culprits:
constraints/iam.allowedPolicyMemberDomainsprevents adding accounts from external domainsconstraints/gcp.resourceLocationsrestricts which regions resources can be created inconstraints/iam.disableServiceAccountKeyCreationblocks service account key creation
Symptom: IAM bindings are correct, the API is enabled, but the action still fails. The error may reference an org constraint directly.
How to verify:
# List effective org policies on a project
gcloud resource-manager org-policies list --project=PROJECT_ID
# Get the effective value of a specific constraint
gcloud resource-manager org-policies describe \
constraints/iam.allowedPolicyMemberDomains --project=PROJECT_IDYou cannot override org policies with IAM grants. No amount of IAM binding will bypass an organisation constraint. Work with your organisation administrator to adjust the constraint, request an exception, or change your approach to comply with the policy.
5. IAM propagation delay
GCP IAM changes are eventually consistent. After you add a binding, it can take up to 60 seconds to propagate to all GCP regions.
Symptom: You just granted the role (within the last minute) and the error appeared immediately after.
Fix: Wait 60 seconds and retry. If the error persists after waiting, the cause is not propagation delay. Go back to step 1 of the checklist.
6. Cross-project resource access
When a service account in Project A needs to access a resource in Project B, the IAM binding must exist in Project B, not Project A. This is a common source of confusion because the service account lives in one project but the permission must be granted in the other.
Think of it like a house key. If you live in House A but need to enter House B, the owner of House B must give you access. Having more keys to House A does not help.
Symptom: The service account has the right role in its home project, but the target resource is in a different project.
How to verify: Check the resource path in the error message. If it references a different project, check IAM bindings in that project.
# Grant a role to a cross-project service account
gcloud projects add-iam-policy-binding TARGET_PROJECT_ID \
--member="serviceAccount:backend-sa@source-project.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer"Fix: Grant the role in the project (or on the resource) where the target resource lives, not in the service account’s home project.
7. Service account impersonation or workload identity mismatch
If you use service account impersonation or Workload Identity Federation, there are two layers of permissions. The caller needs permission to impersonate the target service account, and the target service account needs the actual resource permission.
Symptom: The error shows the impersonating identity lacks iam.serviceAccounts.actAs or iam.serviceAccounts.getAccessToken, or the target service account appears as the caller but lacks the resource permission.
How to verify:
# Check if the caller can impersonate the target service account
gcloud iam service-accounts get-iam-policy \
target-sa@my-project.iam.gserviceaccount.com \
--format="table(bindings.role, bindings.members)"Fix: Grant roles/iam.serviceAccountTokenCreator on the target service account to the impersonating identity. Then separately ensure the target service account has the required resource permission.
Service-specific examples
The root cause is always the same (identity, permission, resource, scope), but the specific permission names and patterns vary by service. Here are the most common cases.
Cloud Storage
- Common symptom:
storage.objects.getorstorage.objects.createdenied on a bucket or object - Check first: Is the bucket using uniform bucket-level access or fine-grained ACLs? Uniform access ignores ACLs entirely. See Cloud Storage access denied errors for details.
- Likely fix: Grant
roles/storage.objectViewer(read) orroles/storage.objectAdmin(read/write) at the bucket level to the correct service account
Cloud Run
- Common symptom: The Cloud Run service returns 403 when calling another GCP API (for example, reading from a bucket or publishing to Pub/Sub)
- Check first: What service account is the Cloud Run revision using? The default Compute Engine service account has broad permissions, but a custom service account may not.
- Likely fix: Attach a dedicated service account to the Cloud Run service and grant it only the specific roles it needs
Cloud Functions
- Common symptom: Function invocation fails with 403, or the function cannot access a downstream resource
- Check first: Cloud Functions v2 uses the default Compute Engine service account unless you specify one. Check which service account is attached.
- Likely fix: Assign a dedicated service account with the required roles. For invocation, grant
roles/cloudfunctions.invokerto the calling identity.
BigQuery
- Common symptom:
bigquery.tables.getDatadenied, or access denied when querying a dataset in another project - Check first: BigQuery permissions are granted at the dataset level, not the table level. Cross-project queries require the role in the dataset’s project.
- Likely fix: Grant
roles/bigquery.dataVieweron the dataset (not the project) to the querying identity
Terraform and CI/CD service accounts
- Common symptom: Terraform plan or apply fails with 403 on resource creation, modification, or state access
- Check first: The service account running Terraform needs permissions for every resource type it manages, plus
storage.objects.*on the state bucket. See Terraform permission errors for a full walkthrough. - Likely fix: Grant the minimum set of predefined roles that cover the resources in the Terraform configuration. Avoid
roles/editorand use Terraform-specific IAM patterns instead.
When to use this page
This guide covers the general GCP 403 PERMISSION_DENIED error. Use it when:
- You get a 403 and are not sure whether the cause is IAM, an API, or an org policy
- You need a systematic way to diagnose any permission denied error
- You want to understand how GCP evaluates permissions
For more specific errors, use these dedicated guides instead:
- IAM AccessDenied errors for service account IAM bindings, conditional policies, or cross-project access
- API not enabled errors when the error includes SERVICE_DISABLED
- Cloud Storage access denied for bucket or object access, ACLs, or uniform bucket-level access
- Terraform permission errors when Terraform plan or apply fails with 403
- Authentication errors when the error is 401 UNAUTHENTICATED rather than 403
Verifying the fix with testIamPermissions
The IAM testIamPermissions API lets you verify whether an identity has specific permissions without making the actual API call. Use it to confirm a fix worked before re-triggering the operation that was failing:
# Test whether the current gcloud identity has the storage permissions
gcloud projects test-iam-permissions PROJECT_ID \
--permissions="storage.objects.get,storage.objects.list"The response lists only the permissions the caller actually has from the set you requested. Any permission absent from the response is missing.
Common mistakes
Granting the role to the wrong identity. The error message tells you the exact caller. Read it before granting. If you are unsure about the difference between user accounts and service accounts, see user identity vs service accounts.
Over-granting with Editor or Owner. It is tempting to grant
roles/editorwhen you cannot identify the specific role. This silences the error but creates a serious security problem. Use the permission name from the error to find the appropriate predefined role.Not checking API enablement first. A
SERVICE_DISABLED403 looks like a permission error but has a completely different fix. Confirm the API is enabled before touching IAM.Granting at the wrong scope. A role granted at the project level does not help if the resource is in a different project. A role on one bucket does not cover another bucket. Match the scope to the resource in the error.
Missing cross-project access. When your service account is in Project A and the resource is in Project B, the IAM binding must exist in Project B. Granting roles in Project A does not help.
Confusing authentication with authorisation. A 401 means GCP does not know who you are (authentication). A 403 means GCP knows who you are but you lack permission (authorisation). The fixes are different. See troubleshooting authentication errors for 401 issues.
Permission Denied vs related errors
GCP returns several error types that are easy to confuse. Here is how to tell them apart.
Means: The authenticated identity lacks a required IAM permission, an org policy blocked the action, or the API is disabled.
Key signal: The request included valid credentials. GCP knows who you are.
Fix: Work through the root causes section above.
Means: A specific IAM evaluation denied the request. This is the IAM-specific subset of PERMISSION_DENIED.
Key signal: The error references a specific IAM binding, condition, or service account misconfiguration. It does not involve disabled APIs or org policies.
Fix: See IAM AccessDenied errors.
Means: GCP could not verify your identity. The token is missing, expired, or malformed.
Key signal: No identity was established. The issue is authentication, not authorisation.
Fix: See troubleshooting authentication errors.
Means: The API for the requested service is not enabled in the project.
Key signal: The error may include “API not enabled” or SERVICE_DISABLED. It surfaces as a 403 but has nothing to do with IAM.
Fix: See API not enabled errors.
Summary
- Permission Denied (403) covers IAM failures, disabled APIs, org policy blocks, and propagation delays. Read the full error first.
- Extract the caller, permission, and resource from the error before granting any role
- Check that the API is enabled before debugging IAM
- Grant at the most specific resource level following least privilege
- Cross-project access requires the binding in the resource’s project, not the service account’s project
- Org policies override IAM and can block actions that IAM allows
- IAM changes take up to 60 seconds to propagate. Wait before re-testing.
- Use
testIamPermissionsto confirm a fix without re-triggering the failing operation
Frequently asked questions
Why am I getting GCP 403 PERMISSION_DENIED even after granting the role?
IAM policy changes can take up to 60 seconds to propagate globally. If you test immediately after granting a role, the binding may not have reached the regional endpoint yet. Wait a minute and retry. If it still fails, confirm the role was granted to the correct identity (the error message includes the caller email). Also verify the role was granted at the right scope (project, folder, or resource level) and that no organisation policy is overriding the IAM binding.
How do I find the exact missing permission in GCP?
Read the full error message. Most GCP APIs include the exact permission, the resource path, and the calling identity in the 403 response. If your tooling truncates the error, open Logs Explorer in Cloud Logging and filter on protoPayload.status.code=7. The authorizationInfo field in the audit log entry lists every permission that was checked and whether each was granted or denied.
What is the difference between PERMISSION_DENIED and UNAUTHENTICATED?
UNAUTHENTICATED (HTTP 401) means GCP could not verify who you are. The token is missing, expired, or malformed. PERMISSION_DENIED (HTTP 403) means GCP knows who you are but that identity lacks a required permission on the target resource. The fix for 401 is to authenticate (refresh the token, activate the correct service account). The fix for 403 is to grant the correct IAM role to the authenticated identity.
Can org policies cause a permission denied error?
Yes. Organisation policies sit above IAM and can block actions that IAM would otherwise allow. Constraints like iam.allowedPolicyMemberDomains, gcp.resourceLocations, and iam.disableServiceAccountKeyCreation all surface as 403 errors. If your IAM bindings look correct, ask your GCP organisation administrator to check which org policies apply to the project or folder.
How do I know whether the wrong service account is making the call?
The error message includes the caller identity. Compare it against the service account you intended. Run gcloud auth list to see the active CLI identity, or check the service account attached to the compute resource (Cloud Run, GKE pod, Cloud Function) that made the call. A mismatch between the intended and actual caller is the single most common cause of permission denied errors.