Cloud Storage Access Denied (403): Fix IAM, ACL and Signed URL Errors
A Cloud Storage 403 means the identity making the request does not have the permission it needs on the bucket or object. The error message tells you the exact permission, the exact resource, and the exact caller. Most engineers skip those details and start guessing. This page shows you how to read the error, find the root cause, and apply the right fix without over-granting.
The most common symptoms you will see: does not have storage.objects.get access,
does not have storage.objects.list access,
does not have storage.buckets.get access,
Anonymous caller does not have storage.objects.get access,
or a signed URL that suddenly
returns 403. All of these trace back to the same evaluation:
caller + permission + resource + policy layer.
Why Cloud Storage returns access denied
Think of Cloud Storage like a building with a security desk. Every request has to pass four checks before it gets through:
- Caller: who is making the request (user account, service account, workload identity, or anonymous)
- Permission: the specific action being attempted (
storage.objects.get,storage.objects.list, etc.) - Resource: the bucket or object the action targets
- Policy layer: the IAM policy on the resource, plus any higher-level restrictions like organisation policy constraints or VPC Service Controls
If the caller’s badge (identity) does not match the access list (IAM policy) for the floor (resource) and the action (permission) they are requesting, the security desk turns them away with a 403.
A 403 is not “something is broken.” It is the system working correctly. It checked four things and at least one did not match. Your job is to figure out which one.
Use this page when…
- Reading or downloading a Cloud Storage object fails with 403
- Listing objects in a bucket returns access denied
- A signed URL that worked before now returns 403
- You granted
allUsersaccess but public requests still fail - Terraform, your application, or
gcloudcannot read from or write to a bucket - You see
Anonymous callerin the error and do not know why - You are unsure whether to fix this with IAM, ACLs, or signed URLs
For broader GCP 403 errors beyond Cloud Storage, see GCP Permission Denied Errors. For authentication problems where credentials are not found at all, see Troubleshooting Authentication Errors.
Fast triage checklist
Run through these checks before changing any IAM policy. Most Cloud Storage 403 errors are solved by one of these:
- Who is actually calling? Read the identity in the error message. It may not be who you expect. A Compute Engine VM uses its attached service account, not your user account.
- What exact permission is missing? The error message names it. Match the permission (
storage.objects.get,storage.objects.list,storage.buckets.get, etc.) to the right IAM role. - Is the role granted at the right scope? A role on the project applies to all buckets. A role on a specific bucket applies only to that bucket. Make sure the grant covers the resource in the error.
- Does the tool need extra permissions? Some tools call
storage.buckets.getorstorage.buckets.listas part of their workflow.objectViewerdoes not include these. - Is public access prevention enforced? If you are trying to make an object public and it still fails, check whether the bucket or org policy blocks
allUsersbindings. - Did a signed URL expire? Signed URLs have a fixed expiry. Once past, they always return 403. Generate a new one.
Resist the urge to immediately grant a broader role. More than half of Cloud Storage 403
errors are caused by the wrong identity making the call, not a missing role. Confirm the
caller first. If you grant objectViewer to the wrong service account, the
error stays the same and you have an unnecessary binding to clean up later.
How Cloud Storage access is evaluated
Cloud Storage access control has several layers. Understanding which layers apply to your bucket is the key to fast diagnosis. For a deep comparison, see Cloud Storage IAM vs ACLs.
IAM (the primary layer)
IAM is the recommended way to control Cloud Storage access. You bind a role (containing one or more permissions) to an identity on either the project or the bucket. Permissions inherit down the resource hierarchy: a project-level grant applies to every bucket and object in that project.
Bucket-level vs project-level scope
Granting a role at the project level gives access to all buckets. Granting at the bucket level scopes access to one bucket. Follow the principle of least privilege and prefer bucket-level grants unless the identity genuinely needs access to every bucket in the project.
Legacy ACLs (fine-grained mode)
If a bucket uses fine-grained access control, both IAM policies and per-object ACLs are evaluated. Access is granted if either IAM or an ACL allows the request. ACLs are allow-only. They cannot deny access that IAM grants. The real problem with fine-grained mode is the complexity: you end up managing two permission systems side by side, which is why Google recommends uniform bucket-level access instead.
Uniform bucket-level access (recommended)
With uniform bucket-level access enabled, per-object ACLs are disabled. Only IAM controls access. This makes permission evaluation predictable and eliminates the legacy ACL layer entirely. Once enabled for 90 days, it cannot be reversed.
Uniform bucket-level access is like having one deadbolt on the door (IAM). You know exactly who has a key. Fine-grained mode adds a second lock (ACLs) to every individual object. Either key opens the door, but now you have to check two key rings whenever someone is locked out. If you are starting fresh, use one lock.
Public access prevention
Public access prevention is a bucket-level setting (also enforceable via
organisation policy) that blocks
allUsers and allAuthenticatedUsers bindings. When enforced, IAM
commands to grant public access succeed silently but the access is blocked.
Higher-level policy restrictions
VPC Service Controls perimeters, IAM deny policies, and organisation policy constraints can all block access that IAM would otherwise allow. If your IAM policy looks correct, check for restrictions at the org or folder level.
Error message and likely cause
| Error or symptom | What it usually means | First thing to check |
|---|---|---|
does not have storage.objects.get access | Caller lacks read permission on the object | Grant objectViewer to the identity shown in the error, on the correct bucket or project |
does not have storage.objects.list access | Caller lacks permission to list objects in the bucket | Grant objectViewer on the bucket (includes storage.objects.list) |
does not have storage.buckets.get access | Caller lacks permission to read bucket metadata | objectViewer does not include this. Grant roles/storage.legacyBucketReader on the bucket or use a broader role like storage.admin |
Anonymous caller does not have… | No credentials were sent with the request | Check that Application Default Credentials or an auth header is configured. If intentionally public, verify allUsers binding and public access prevention |
| Signed URL returns 403 | URL expired, signing SA lost permission, or clock skew | Check the URL expiry timestamp. Then verify the signing service account still has the required permission on the object |
allUsers granted but public access still denied | Public access prevention is enforced on the bucket or via org policy | Run gcloud storage buckets describe gs://BUCKET —format=“value(iamConfiguration.publicAccessPrevention)“ |
Step 1: Identify the real caller
The identity in the error message is the one that matters, not the identity you think should be calling. The most common mistake is assuming your user account is making the call when a service account is actually being used.
Imagine walking into a building with your colleague’s ID badge. The security desk does not
care that you have clearance. They see the badge, not your face. Cloud Storage
works the same way: it checks the credential attached to the request, not who you think
you are. If a VM sends a request, Cloud Storage sees the VM’s service account, not your
gcloud auth session.
# Check which identity gcloud is using
gcloud auth list
# Check which service account a Compute Engine VM uses
gcloud compute instances describe INSTANCE_NAME \
--zone=ZONE \
--format="value(serviceAccounts.email)"
# Check Application Default Credentials
gcloud auth application-default print-access-token 2>&1 | head -1Common caller mismatches:
- Compute Engine / GKE: uses the VM’s attached service account, not your user account
- Cloud Functions / Cloud Run: uses the service’s runtime service account
- Terraform: uses whatever credentials are configured in the provider block or environment
- Anonymous: no credentials were sent. Check your authentication setup or see Troubleshooting Authentication Errors
Step 2: Read the exact missing permission
The error message names the specific permission that was checked and denied. Map it to the role that contains it:
storage.objects.get(read object data). Included inobjectViewer,objectAdmin,adminstorage.objects.list(list objects in a bucket). Included inobjectViewer,objectAdmin,adminstorage.objects.create(upload objects). Included inobjectCreator,objectAdmin,adminstorage.objects.delete(delete objects). Included inobjectAdmin,adminstorage.buckets.get(read bucket metadata). Included inlegacyBucketReader,adminstorage.buckets.list(list buckets in a project). Included in project-level viewer roles
If the error is truncated in your terminal or SDK output, find the full version in
Cloud Audit Logs. Filter on
protoPayload.status.code=7 in Logs Explorer. The
protoPayload.authorizationInfo field shows every permission checked and
whether each was granted or denied.
Be aware that some tools need extra permissions beyond the obvious one. For example,
gcloud storage ls (without a bucket path) calls storage.buckets.list,
not storage.objects.list. The objectViewer role does not include
bucket-level metadata permissions.
A common source of confusion: storage.objects.get is a permission.
roles/storage.objectViewer is a role that bundles multiple
permissions together. You grant roles, not individual permissions. The error tells you the
permission. You need to find the role that includes it. See
IAM Roles in GCP for the full mapping.
Step 3: Check scope (project vs bucket vs object)
A role granted at the wrong scope is the second most common cause after wrong identity. In GCP’s resource hierarchy, permissions inherit downward: project, then bucket, then object.
# Check IAM policy on a specific bucket
gcloud storage buckets get-iam-policy gs://my-bucket
# Check IAM bindings at project level
gcloud projects get-iam-policy PROJECT_ID \
--flatten="bindings[].members" \
--filter="bindings.members:my-sa@my-project.iam.gserviceaccount.com" \
--format="table(bindings.role)"
# Grant objectViewer at bucket level (preferred for least privilege)
gcloud storage buckets add-iam-policy-binding gs://my-bucket \
--member="serviceAccount:my-sa@my-project.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer"
# Grant objectViewer at project level (all buckets)
gcloud projects add-iam-policy-binding PROJECT_ID \
--member="serviceAccount:my-sa@my-project.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer"Think of scope like a key card system. A project-level role is a master key that opens every bucket in the project. A bucket-level role is a room key that opens one bucket only. Handing out the master key because someone needs one room creates unnecessary risk. Grant at the narrowest scope that meets the requirement. This follows the principle of least privilege.
Step 4: Check uniform bucket-level access and legacy ACL behaviour
The access control model on the bucket determines whether ACLs are in play. For a detailed comparison of both models, see Cloud Storage IAM vs ACLs.
# Check whether uniform access is enabled
gcloud storage buckets describe gs://my-bucket \
--format="value(iamConfiguration.uniformBucketLevelAccess.enabled)"
# Enable uniform bucket-level access (recommended)
# Warning: cannot be reversed after 90 days
gcloud storage buckets update gs://my-bucket \
--uniform-bucket-level-accessIf uniform access is enabled (returns True): only IAM matters.
Object-level ACLs are disabled. If the IAM policy grants the permission, access is allowed.
If uniform access is not enabled (fine-grained mode): both IAM and per-object ACLs are evaluated. Access is granted if either IAM or an ACL allows the request. The complexity comes from having two systems to check. If you cannot find the issue in IAM, check the object’s ACL.
# View ACLs on a specific object (only relevant in fine-grained mode)
gcloud storage objects describe gs://my-bucket/my-object.txt \
--format="yaml(acl)"If you are setting up a new bucket or migrating an existing one, enable uniform bucket-level access. It eliminates ACL complexity and makes permission evaluation predictable. See Cloud Storage Security for broader security best practices.
Step 5: Check public access prevention and organisation-level restrictions
If you are trying to make a bucket or object publicly accessible and it still returns 403, the
most likely cause is public access prevention. This setting blocks allUsers and
allAuthenticatedUsers bindings, even when the IAM command to add them succeeds
without error.
# Check whether public access prevention is enforced on the bucket
gcloud storage buckets describe gs://my-bucket \
--format="value(iamConfiguration.publicAccessPrevention)"
# Returns "enforced" or "inherited"
# Check org policy constraints on the project
gcloud org-policies describe storage.publicAccessPrevention \
--project=PROJECT_IDPublic access prevention is the sneakiest Cloud Storage 403. You run the gcloud
command to add an allUsers binding, it succeeds with no error, the binding
even shows up in the policy, but requests are still denied. The prevention setting acts as
a separate gate that blocks public access regardless of what the IAM policy says. Always
check this setting before spending time debugging the IAM binding itself.
Other restrictions that can block access even when IAM allows it:
- Organisation policy constraints: can enforce public access prevention, restrict allowed domains, or limit which locations buckets can use
- VPC Service Controls: perimeters block API access from outside the perimeter, even for identities with correct IAM permissions
- IAM deny policies: explicitly deny specific permissions regardless of allow policies
Never make buckets containing sensitive data publicly accessible. Once a bucket is public, anyone with an object URL can read its contents. Use signed URLs for temporary access or IAM bindings for authenticated access.
Step 6: Check signed URL problems
Signed URLs grant temporary access without requiring Google credentials. Think of them as pre-approved visitor passes with an expiry date stamped on them. When they return 403, check these in order:
Expiry. Signed URLs have a fixed expiry time set at creation. Once expired, all requests return 403. The fix is to generate a new URL.
Signing service account permissions. The service account that signed the URL must have the required permission (e.g.,
storage.objects.get) on the object at the time the URL is used, not just when it was generated. If the IAM binding was removed after signing, the URL stops working.Wrong HTTP method. A signed URL is scoped to a specific HTTP method (GET, PUT, etc.). Using a different method returns 403. Make sure the method in the URL matches the request.
Clock skew. Significant time difference between the signing system and Google servers causes signature validation to fail.
# Generate a signed URL valid for 1 hour
gcloud storage sign-url gs://my-bucket/my-object.txt \
--duration=1h \
--service-account=my-sa@my-project.iam.gserviceaccount.com
# Verify the signing service account still has the required permission
gcloud storage buckets get-iam-policy gs://my-bucket \
--flatten="bindings[].members" \
--filter="bindings.members:my-sa@my-project.iam.gserviceaccount.com"A signed URL is not a snapshot of the permission. It is a promise that the signing service account will vouch for the request at the time it arrives. If the service account loses the permission between signing and usage, the promise is broken and the URL fails with 403.
Step 7: Confirm the fix safely
After making an IAM change, verify it works without over-granting:
# Test whether a specific identity has a permission on a bucket
# (requires iam.serviceAccounts.getAccessToken on the SA)
gcloud storage ls gs://my-bucket \
--impersonate-service-account=my-sa@my-project.iam.gserviceaccount.com
# View the effective IAM policy on the bucket to confirm the binding
gcloud storage buckets get-iam-policy gs://my-bucketIAM changes can take up to 60 seconds to propagate. If the change looks correct but access still fails, wait a minute and retry.
Do not solve 403 errors by granting roles/editor or roles/owner.
These roles grant far more access than needed and violate
least privilege. Use the narrowest
predefined role that includes the required permission. Usually that is
roles/storage.objectViewer for read access or
roles/storage.objectAdmin for read/write.
When to use which fix
Choosing the wrong access model is a common reason for follow-up 403 errors. Use this to pick the right approach:
| Scenario | Right fix | Why |
|---|---|---|
| Service or user needs ongoing access to one bucket | IAM role on the bucket | Scoped, auditable, follows least privilege |
| Service needs access to all buckets in a project | IAM role on the project | Inherited by all buckets, simpler to manage |
| External user needs temporary access to specific objects | Signed URL | No Google account needed, time-limited, scoped to one object and method |
| Object must be accessible to the public internet | allUsers IAM binding | Only if public access prevention is not enforced and the data is intentionally public |
| Legacy app requires per-object permissions (fine-grained mode) | Object ACLs | Only if you cannot migrate to uniform bucket-level access. Prefer IAM for all new setups |
Common mistakes
Granting the role at the wrong scope. Granting
objectVieweron the project when the identity only needs access to one bucket, or granting on the wrong bucket entirely. Always check the resource path in the error message and match your grant to that resource.Assuming read and list are the same permission.
storage.objects.get(read one object) andstorage.objects.list(list objects in a bucket) are separate permissions, though both are included inobjectViewer. Butstorage.buckets.list(list buckets) andstorage.buckets.get(read bucket metadata) are not included. Tools that enumerate buckets need these separately.Debugging the wrong identity. You granted the role to your user account, but the code runs as a Compute Engine service account. Always confirm the identity in the error message before changing any binding.
Using signed URLs for permanent access. Signed URLs expire. For ongoing access, use IAM bindings on the service or user account. Signed URLs are for temporary, shareable access to specific objects.
Assuming “public” always works. Granting
allUsersaccess succeeds silently even when public access prevention is enforced. The binding appears in the policy but requests are still blocked. Always check the prevention setting on the bucket and any inherited org policy.Over-granting with broad roles. Using
roles/editororroles/ownerto fix a storage 403 grants access to far more than storage. Useroles/storage.objectViewer,roles/storage.objectAdmin, orroles/storage.admindepending on the exact need.
IAM vs ACLs vs Signed URLs vs Public Access
| Access method | Best for | Requires Google account? | Scope | Time-limited? |
|---|---|---|---|---|
| IAM binding on bucket | Ongoing access to a specific bucket | Yes (user or service account) | Bucket + all objects in it | No (until removed) |
| IAM binding on project | Access to all buckets in a project | Yes | All buckets in project | No (until removed) |
| Object ACL (fine-grained only) | Legacy per-object control | Yes | Single object | No (until removed) |
| Signed URL | Temporary external access | No | Single object + HTTP method | Yes (fixed expiry) |
allUsers public access | Publicly downloadable content | No | Bucket or object | No (until removed) |
For a detailed comparison of IAM and ACL evaluation, when each applies, and which to use on new buckets, see Cloud Storage IAM vs ACLs.
Summary
- Read the full error message. It names the caller, the missing permission, and the resource.
- Confirm the calling identity is who you think it is before changing any IAM policy
- Grant the narrowest role at the narrowest scope: prefer bucket-level
objectViewerover project-leveleditor - Enable uniform bucket-level access to eliminate ACL complexity
- Signed URLs expire. Generate a new one when a previously-valid URL returns 403
- Public access prevention silently blocks
allUserseven when the IAM command succeeds - Check org policies and VPC Service Controls if IAM looks correct but access is still denied
Frequently asked questions
What does "does not have storage.objects.get access" mean?
The identity shown in the error message lacks the storage.objects.get permission on the specific object. This permission is included in roles/storage.objectViewer, roles/storage.objectAdmin, and roles/storage.admin. Grant the appropriate role to the calling identity on the bucket or project. Before granting anything, confirm the identity in the error matches the one you expect. The most common cause is the wrong service account making the call.
Why does objectViewer still not let me list the bucket?
roles/storage.objectViewer includes storage.objects.list, which lets you list objects inside a bucket. But it does not include storage.buckets.list or storage.buckets.get. If your tool runs gcloud storage ls without a bucket path, it needs storage.buckets.list to enumerate buckets in the project. That is a different permission not included in objectViewer. Specify the bucket path directly or grant roles/storage.legacyBucketReader at the bucket level for bucket metadata access.
What does "Anonymous caller does not have storage.objects.get access" mean?
Anonymous caller means the request carried no authentication credentials. This happens when a client sends a request without an Authorization header, when Application Default Credentials are not configured, or when a signed URL has expired and the client falls back to an unauthenticated request. Check that your application is authenticating correctly, or if you intend public access, grant roles/storage.objectViewer to allUsers and confirm public access prevention is not enforced on the bucket.
Why does my signed URL return 403?
The three most common causes: (1) the URL expired. Signed URLs have a fixed expiry and return 403 once it passes. (2) The service account that signed the URL no longer has the required permission on the object. If the IAM binding was removed after signing, the URL stops working. (3) Clock skew between the signing system and Google servers causes validation to fail. Check expiry first, then the signing identity permissions.
Why is public access still blocked after I granted allUsers?
Most likely public access prevention is enforced on the bucket or inherited from an organisation policy constraint (storage.publicAccessPrevention). When enforced, allUsers and allAuthenticatedUsers bindings are silently blocked even though the IAM command succeeds without error. Check with gcloud storage buckets describe gs://BUCKET --format="value(iamConfiguration.publicAccessPrevention)". If it returns "enforced", you must either disable the constraint (if allowed) or use signed URLs or authenticated access instead.