Identity error catalogue — access denied, actAs, quota refusals, and the one tool that answers "why"
Cause, check and fix for the refusals that come from IAM rather than from the service being called. Includes the Policy Troubleshooter command verbatim, the five-way policy evaluation that decides a denial, the eventual-consistency false alarm, and the fixed limits that look like bugs.
markdownreferencev1HBJ7-0Z5Tstage: operatetrust: officialfamily: identityservice: iaminterface: anyaccess deniedpermission denied403forbiddeninsufficientPermissionsauthErroractAsPolicy Troubleshooterdeny policyprincipal access boundaryeventual consistency
Identity error catalogue
Every row is cause → check → fix. The check is what confirms the cause; run it before changing a policy.
1. The refusals
| Signature | Cause | Check | Fix |
|---|---|---|---|
HTTP 403, reason forbidden — "According to access control policy, the current user does not have access" | the allow policy does not grant a role carrying the permission | run Policy Troubleshooter (§2) with the exact permission from the error | grant the predefined role that carries it (reference/identity/predefined-roles-and-permissions.md), at the narrowest resource that works |
HTTP 403, reason insufficientPermissions | as above, with the service reporting it per-action rather than per-resource | same | same |
HTTP 401, reason authError | the credential itself: the token is expired or its format is not recognised | re-mint the credential; if the caller federates, confirm the exchange still returns a token | fix the credential path, not the policy — a 401 is never answered by a role grant |
| A permission error carrying an error ID | the service is telling you which principal, which resource and which permission | the ID "provides context for the error, including the principal, resource, permission" — read all three off it | the three facts are the whole input to Policy Troubleshooter |
| "User does not have permission to access namespace" on a Cloud Run deploy | the deployer cannot act as the runtime service account, or the default Compute Engine service account has been deleted | check whether the deployer holds roles/iam.serviceAccountUser on the runtime account, and whether that account still exists | grant roles/iam.serviceAccountUser (not TokenCreator) and pass --service-account explicitly |
HTTP 429, reason rateLimitExceeded | a usage limit, not a permission | read the live quota (reference/overview/quotas-and-limits-model.md) | back off with truncated exponential backoff, then request an increase if it is adjustable |
| A grant that "did not work", and works a minute later | not an error. The IAM API is eventually consistent; a change "might take time to affect access checks" | retry the original call once, unchanged | wait. Do not write the policy again — a second read-modify-write on a stale etag is how a binding gets lost |
2. The one tool that answers "why", verbatim
gcloud beta policy-intelligence troubleshoot-policy iam RESOURCE \
--principal-email=EMAIL --permission=PERMISSION
It evaluates every policy type that bears on the call. The page's own rule is the one to hold on to:
"For a user to be able to use the permission to access the resource, all policy types must permit access."
3. The five things it checks, in the order a denial is decided
- a Principal Access Boundary policy makes the resource ineligible;
- a deny policy blocks the permission — and deny overrides allow, so this outranks any role the principal
holds;
- the allow policies simply do not carry the permission;
- a conditional role binding exists and its condition evaluated false;
- group membership could not be verified.
A sixth outcome is the troubleshooter's own: "Unknown", when the caller running it lacks permission to read
one of the policies involved. An Unknown is not a pass. It means you need a higher-privileged principal to
run the diagnosis, and treating it as "no deny policy found" is the single most common misread.
4. Limits that present as errors
Each of these is fixed; the IAM quotas page states, verbatim, "Limits cannot be changed." A call failing against one of them is not a quota you can raise.
| Limit | Value | How it shows up |
|---|---|---|
| principals in one allow policy | 1,500 | adding a binding starts failing on a mature project; the fix is groups, not retries |
| deny policies per resource | 500 | a policy-as-code rollout stops applying |
| custom roles per project / per organization | 300 each | a generated-role pipeline stalls |
| service account keys per account | 10 | a key rotation that creates before deleting fails on the 11th |
| maximum access token lifetime | 3,600 s, extendable to 12 h by organization policy | a long job dies mid-run; lengthen the lifetime by policy or re-mint |
5. Where to go next
- the order to check things in:
guides/identity/fix-permission-denied.md - the keyless setup that removes a whole class of 401s:
guides/identity/github-actions-without-keys.md
- the same refusals' AWS and Azure counterparts, and why a migrated workload hits different ones:
guides/parity/identity.md