highFoundationSymptom-first#cloud storage#gcs#403#iam#permissions

Cloud Storage 403 — permission denied on objects or bucket

gsutil cp/ls fails with AccessDeniedException: 403

Likely source:securityconfiguration

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 project
  • Offending 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>
READ

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>
CHANGE

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.

1
gsutil ls gs://<BUCKET>/
READ

Reproduce and read the 403 permission in the message

2
gsutil iam get gs://<BUCKET> | grep <CALLER>
READ

Confirm the caller is absent from the policy

3
gsutil iam ch serviceAccount:<CALLER_SA>:objectViewer gs://<BUCKET>
CHANGE

Grant the caller read access

4
gsutil ls gs://<BUCKET>/
READ

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.

Commands marked READ CHANGE DESTRUCTIVE — never run DESTRUCTIVE commands against production data you cannot restore. Copy → verify → execute.