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:

  1. Caller: who is making the request (user account, service account, workload identity, or anonymous)
  2. Permission: the specific action being attempted (storage.objects.get, storage.objects.list, etc.)
  3. Resource: the bucket or object the action targets
  4. 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.

How to think about it

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 allUsers access but public requests still fail
  • Terraform, your application, or gcloud cannot read from or write to a bucket
  • You see Anonymous caller in 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:

  1. 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.
  2. 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.
  3. 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.
  4. Does the tool need extra permissions? Some tools call storage.buckets.get or storage.buckets.list as part of their workflow. objectViewer does not include these.
  5. 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 allUsers bindings.
  6. Did a signed URL expire? Signed URLs have a fixed expiry. Once past, they always return 403. Generate a new one.
Before you touch IAM

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.

Analogy: one lock vs two locks

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 symptomWhat it usually meansFirst thing to check
does not have storage.objects.get accessCaller lacks read permission on the objectGrant objectViewer to the identity shown in the error, on the correct bucket or project
does not have storage.objects.list accessCaller lacks permission to list objects in the bucketGrant objectViewer on the bucket (includes storage.objects.list)
does not have storage.buckets.get accessCaller lacks permission to read bucket metadataobjectViewer 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 requestCheck that Application Default Credentials or an auth header is configured. If intentionally public, verify allUsers binding and public access prevention
Signed URL returns 403URL expired, signing SA lost permission, or clock skewCheck the URL expiry timestamp. Then verify the signing service account still has the required permission on the object
allUsers granted but public access still deniedPublic access prevention is enforced on the bucket or via org policyRun 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.

The “wrong badge” problem

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

Common 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 in objectViewer, objectAdmin, admin
  • storage.objects.list (list objects in a bucket). Included in objectViewer, objectAdmin, admin
  • storage.objects.create (upload objects). Included in objectCreator, objectAdmin, admin
  • storage.objects.delete (delete objects). Included in objectAdmin, admin
  • storage.buckets.get (read bucket metadata). Included in legacyBucketReader, admin
  • storage.buckets.list (list buckets in a project). Included in project-level viewer roles
Tip

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.

Permissions are not roles

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"
Scope matters more than you think

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

If 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)"
Tip

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_ID
The silent blocker

Public 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
Warning

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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"
Signed URL gotcha

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

IAM 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 reach for Owner or Editor

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:

ScenarioRight fixWhy
Service or user needs ongoing access to one bucketIAM role on the bucketScoped, auditable, follows least privilege
Service needs access to all buckets in a projectIAM role on the projectInherited by all buckets, simpler to manage
External user needs temporary access to specific objectsSigned URLNo Google account needed, time-limited, scoped to one object and method
Object must be accessible to the public internetallUsers IAM bindingOnly if public access prevention is not enforced and the data is intentionally public
Legacy app requires per-object permissions (fine-grained mode)Object ACLsOnly if you cannot migrate to uniform bucket-level access. Prefer IAM for all new setups

Common mistakes

  1. Granting the role at the wrong scope. Granting objectViewer on 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.

  2. Assuming read and list are the same permission. storage.objects.get (read one object) and storage.objects.list (list objects in a bucket) are separate permissions, though both are included in objectViewer. But storage.buckets.list (list buckets) and storage.buckets.get (read bucket metadata) are not included. Tools that enumerate buckets need these separately.

  3. 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.

  4. 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.

  5. Assuming “public” always works. Granting allUsers access 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.

  6. Over-granting with broad roles. Using roles/editor or roles/owner to fix a storage 403 grants access to far more than storage. Use roles/storage.objectViewer, roles/storage.objectAdmin, or roles/storage.admin depending on the exact need.

IAM vs ACLs vs Signed URLs vs Public Access

Access methodBest forRequires Google account?ScopeTime-limited?
IAM binding on bucketOngoing access to a specific bucketYes (user or service account)Bucket + all objects in itNo (until removed)
IAM binding on projectAccess to all buckets in a projectYesAll buckets in projectNo (until removed)
Object ACL (fine-grained only)Legacy per-object controlYesSingle objectNo (until removed)
Signed URLTemporary external accessNoSingle object + HTTP methodYes (fixed expiry)
allUsers public accessPublicly downloadable contentNoBucket or objectNo (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.

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.

Last verified: 27 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.