GCP Error Guide: 'storage.objects.* denied (403)' Cloud Storage Access Errors
Fix GCP storage.objects.get/create 403 errors on a bucket: diagnose missing IAM roles, uniform vs fine-grained access, wrong identity, and VPC-SC denials.
- #gcp
- #troubleshooting
- #errors
- #storage
Overview
A 403 on Cloud Storage means the caller does not have the specific storage.objects.* permission needed for the operation on that bucket or object. GCS evaluates the identity’s IAM roles (and, if enabled, the bucket’s access-control model) against the requested permission — storage.objects.get to read, storage.objects.create to write, storage.objects.list to list — and returns HTTP 403 when none grant it.
You will see this from gsutil/gcloud storage or the client libraries:
AccessDeniedException: 403 ci-deployer@my-prod-project.iam.gserviceaccount.com does not have storage.objects.list access
to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist).
Or the object-write form:
google.api_core.exceptions.Forbidden: 403 POST .../o?uploadType=...:
caller does not have storage.objects.create access to the Google Cloud Storage object.
It occurs on reads, writes, lists, deletes, and signed-URL generation against buckets and objects. Access is per-bucket (and optionally per-object), so an identity can succeed on one bucket and 403 on another.
Symptoms
gsutil/gcloud storagefail withdoes not have storage.objects.<verb> access.- Client libraries raise
Forbidden: 403ondownload/upload/list_blobs. - An app works locally (your user) but 403s in CI/Cloud Run (a service account).
- Reads work but writes fail (or vice-versa) — a permission-granularity gap.
gcloud storage ls gs://my-prod-data/
ERROR: ... AccessDeniedException: 403 ci-deployer@my-prod-project.iam.gserviceaccount.com
does not have storage.objects.list access to the Google Cloud Storage bucket.
Common Root Causes
1. The identity has no Storage role on the bucket
The caller simply lacks a role granting the needed storage.objects.* permission at the bucket (or project) level.
gcloud storage buckets get-iam-policy gs://my-prod-data \
--format="table(bindings.role, bindings.members)"
ROLE MEMBERS
roles/storage.objectViewer ['user:james@example.com']
Only your user has read access; the CI service account has no role at all, so it 403s.
2. Wrong granularity (viewer vs creator vs admin)
The role grants some object permissions but not the one needed — e.g. objectViewer can read but not write.
gcloud storage buckets get-iam-policy gs://my-prod-data \
--flatten="bindings[].members" \
--filter="bindings.members:ci-deployer@my-prod-project.iam.gserviceaccount.com" \
--format="table(bindings.role)"
ROLE
roles/storage.objectViewer
The account can get/list but uploads fail because objectViewer lacks storage.objects.create; it needs objectCreator or objectAdmin.
3. Uniform bucket-level access vs legacy ACLs
If the bucket uses uniform bucket-level access, object ACLs are ignored — only IAM applies. Code relying on per-object ACLs will 403.
gcloud storage buckets describe gs://my-prod-data \
--format="value(uniform_bucket_level_access.enabled)"
True
With uniform access True, a legacy ACL grant is ignored; the identity must have an IAM role instead.
4. Wrong active identity
The call runs as a different identity than expected (attached SA, impersonation, ADC), so you’re checking the wrong member’s access.
gcloud auth list
gcloud config get-value account
* ci-deployer@my-prod-project.iam.gserviceaccount.com
james@example.com
The active identity is the service account; grants on your user don’t help.
5. Bucket is in a different project than assumed
The bucket lives in another project, so project-level Storage roles you granted don’t apply to it.
gcloud storage buckets describe gs://my-prod-data \
--format="value(project_number)"
482910337755
If that project number isn’t where you granted the role, the binding doesn’t cover this bucket.
6. VPC Service Controls perimeter blocks the access
Even with correct IAM, a VPC-SC perimeter can deny GCS access for callers outside the perimeter, surfacing as 403.
gcloud logging read \
'protoPayload.serviceName="storage.googleapis.com" AND protoPayload.status.code=7' \
--project my-prod-project --limit 1 --format=json | grep -i violation
"violationReason": "VPC_SERVICE_CONTROLS"
A VPC_SERVICE_CONTROLS violation means the perimeter, not IAM, is denying access; add the caller to an access level or ingress rule.
Diagnostic Workflow
Step 1: Read the error for the exact permission and target
The message names the permission (storage.objects.list/create/get), the identity, and whether it’s a bucket or object. Everything keys off these.
Step 2: Confirm the active identity
gcloud auth list
gcloud config get-value account
In CI/Cloud Run this is usually a service account, not your user — grant to whatever has the *.
Step 3: Test the permission directly on the bucket
gcloud storage buckets get-iam-policy gs://my-prod-data \
--flatten="bindings[].members" \
--filter="bindings.members:ci-deployer@my-prod-project.iam.gserviceaccount.com" \
--format="table(bindings.role)"
No rows means no bucket-level role; check project-level too (gcloud projects get-iam-policy).
Step 4: Check the access model and bucket project
gcloud storage buckets describe gs://my-prod-data \
--format="value(uniform_bucket_level_access.enabled, project_number)"
If uniform access is True, ignore ACLs and rely on IAM. Confirm the bucket’s project matches where you granted roles.
Step 5: Grant the least-privilege role, then retry
# Read-only
gcloud storage buckets add-iam-policy-binding gs://my-prod-data \
--member="serviceAccount:ci-deployer@my-prod-project.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer"
# Read + write
gcloud storage buckets add-iam-policy-binding gs://my-prod-data \
--member="serviceAccount:ci-deployer@my-prod-project.iam.gserviceaccount.com" \
--role="roles/storage.objectAdmin"
Allow a short propagation delay, then re-run the operation.
Example Root Cause Analysis
A nightly export job that writes reports to gs://my-prod-data/reports/ begins failing:
google.api_core.exceptions.Forbidden: 403 ...
caller does not have storage.objects.create access to the Google Cloud Storage object.
Reads from the same bucket still work, so the identity has some access. The job runs as ci-deployer@my-prod-project.iam.gserviceaccount.com. Checking the bucket policy for that account:
gcloud storage buckets get-iam-policy gs://my-prod-data \
--flatten="bindings[].members" \
--filter="bindings.members:ci-deployer@my-prod-project.iam.gserviceaccount.com" \
--format="table(bindings.role)"
ROLE
roles/storage.objectViewer
A recent IAM tightening downgraded the export SA from objectAdmin to objectViewer. It can still read (so the failure looked partial) but can no longer create objects — hence the write-only 403.
Fix: restore write access with the least-privilege role that includes storage.objects.create:
gcloud storage buckets add-iam-policy-binding gs://my-prod-data \
--member="serviceAccount:ci-deployer@my-prod-project.iam.gserviceaccount.com" \
--role="roles/storage.objectAdmin"
The next export run writes the reports successfully.
Prevention Best Practices
- Grant the narrowest Storage role that matches the workload:
objectViewerfor read-only consumers,objectCreator/objectAdminonly where writes are needed. - Enable uniform bucket-level access and manage everything through IAM, so behavior is consistent and ACL drift can’t silently deny access.
- Bind roles at the bucket level (not just the project) for sensitive data so each bucket’s access is explicit and auditable.
- Use distinct service accounts per workload and confirm the active identity in CI/Cloud Run matches the one you granted.
- If using VPC Service Controls, maintain access levels/ingress rules so legitimate callers aren’t blocked at the perimeter while IAM looks correct.
- For triage, the free incident assistant can pull the denied permission and identity from the 403. More walkthroughs are in the GCP guides.
Quick Command Reference
# Read the permission + identity + bucket/object from the error first
# Who am I?
gcloud auth list
gcloud config get-value account
# Bucket IAM policy (filter to the identity)
gcloud storage buckets get-iam-policy gs://<BUCKET> \
--flatten="bindings[].members" \
--filter="bindings.members:<MEMBER>" --format="table(bindings.role)"
# Access model + project
gcloud storage buckets describe gs://<BUCKET> \
--format="value(uniform_bucket_level_access.enabled, project_number)"
# Grant least-privilege roles
gcloud storage buckets add-iam-policy-binding gs://<BUCKET> \
--member="serviceAccount:<SA>" --role="roles/storage.objectViewer"
gcloud storage buckets add-iam-policy-binding gs://<BUCKET> \
--member="serviceAccount:<SA>" --role="roles/storage.objectAdmin"
# VPC-SC / org-policy denials
gcloud logging read \
'protoPayload.serviceName="storage.googleapis.com" AND protoPayload.status.code=7' \
--project <PROJECT> --limit 1
Conclusion
A Cloud Storage 403 means the caller lacks the specific storage.objects.* permission for the operation on that bucket or object. The usual root causes:
- The identity has no Storage role on the bucket.
- The role grants the wrong granularity (read vs write).
- Uniform bucket-level access ignores legacy ACLs the code relied on.
- The call runs as a different active identity than expected.
- The bucket is in a different project than where roles were granted.
- A VPC Service Controls perimeter denies access independently of IAM.
Read the exact permission and identity from the error, confirm the active account and bucket’s access model, then grant the least-privilege role that includes that permission at the bucket level.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.