Cloud Storage 403 — permission denied on objects or bucket
gsutil cp/ls fails with AccessDeniedException: 403
Focused incident mode — strips away everything but this runbook.
Symptoms
- gsutil cp/ls fails with AccessDeniedException: 403
- gcloud storage cp errors with 'does not have storage.objects.get access'
- App using the service account gets 403 on object reads
- Requests work from Console but not from the service's SA
Read the 403 error string — it names the exact missing permission. Under uniform bucket-level access, IAM (not object ACLs) is authoritative.
Evidence to grab
Collect these artifacts before you change anything — they prove (or rule out) each cause.
Active credentials and project
gcloud auth list && gcloud config get-value projectOffending command error
gsutil ls gs://<BUCKET>/<PATH>
Diagnostic flow
Run the steps in order, marking each claim confirmed or refuted. The evidence you gather feeds the ranked causes below.
Guided decision tree is ready.
Steps with commands, expected output, and pass/fail branches.
Ranked root causes
Evidence checklist
- 403 names a specific storage permission missing
- The same command works with an admin account
- Bucket policy has no binding for the calling SA
Confirm before fixing
gsutil iam get gs://<BUCKET> | grep <CALLER>Confirm the caller is not in the bucket policy
Expected: No match for the caller
Fix
gsutil iam ch serviceAccount:<CALLER_SA>:objectViewer gs://<BUCKET>Grant objectViewer (read) to the caller on the bucket
Expected: Updated IAM configuration for gs://<BUCKET>.
Resolution runbook
Apply from top to bottom after the cause is confirmed. Review every command before running it — risk tags are shown on each.
gsutil ls gs://<BUCKET>/Reproduce and read the 403 permission in the message
gsutil iam get gs://<BUCKET> | grep <CALLER>Confirm the caller is absent from the policy
gsutil iam ch serviceAccount:<CALLER_SA>:objectViewer gs://<BUCKET>Grant the caller read access
gsutil ls gs://<BUCKET>/Confirm the command now succeeds
Prevention
- Grant least-privilege storage roles per bucket rather than broad project roles.
- Enable uniform bucket-level access and stop creating fine-grained ACLs.
- Keep bucket policies in IaC so drift does not silently remove a grant.
- Test with a dedicated service account in CI the way production will call it.
Related runbooks
Same stack, same failure family — worth a skim.
Role was just granted but calls still fail with Permission Denied
Downloads via signed URL fail with 'The given URL is not valid' or 403
gsutil cp fails with 403 'denied by VPC Service Controls' or PERMISSION_DENIED
Commands marked READ CHANGE DESTRUCTIVE — never run DESTRUCTIVE commands against production data you cannot restore. Copy → verify → execute.