403 AccessDenied

GCS Access Denied: The IAM Role Fix That Actually Works

Server & Cloud Intermediate 👁 5 views 📅 Jun 29, 2026

Google Cloud Storage returns 403 when your IAM role is missing or wrong. The fix is granting the exact role on the bucket level. Skip the big-picture stuff, just do this.

You're Getting a 403, Here's the Fix

That "AccessDenied" 403 error on Google Cloud Storage (GCS) is frustrating. You've double-checked the bucket name, the credentials are loaded, but it still fails. Drop the broad permissions—fix is strictly about the IAM role on the bucket itself.

The Fast Fix: Grant roles/storage.objectAdmin

Open Cloud Shell or your local terminal with gcloud configured. Run this single command:

gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
  --member='serviceAccount:YOUR_SERVICE_ACCT@YOUR_PROJECT.iam.gserviceaccount.com' \
  --role='roles/storage.objectAdmin'

Replace YOUR_PROJECT_ID and the service account email. If you're a user (not a service account), the member field changes to user:your.email@domain.com.

Wait 60-90 seconds for propagation. Then try your read/write operation again. It should work now.

Why the Object Admin Role?

Here's what's happening. The error usually shows up when you have a role like roles/storage.admin at the project level, but the bucket has a different IAM policy that blocks it. Or you're using a role like roles/storage.objectViewer which only lets you list and get objects—not upload or delete.

roles/storage.objectAdmin gives you full control over objects inside the bucket: create, read, update, delete. It's the right balance—lets you do everything with the data but not mess with the bucket configuration (like lifecycle rules).

If you tried roles/storage.admin and it still failed, the issue is probably the bucket's IAM policy overriding the project-level role. The trick is to set the role directly on the bucket, not just the project. Use this:

gsutil iam ch serviceAccount:YOUR_SERVICE_ACCT@YOUR_PROJECT.iam.gserviceaccount.com:objectAdmin \
  gs://YOUR_BUCKET_NAME

This sets the permission on the bucket itself. The project-level role gets inherited, but the bucket-level policy can be more restrictive. Setting it on the bucket forces the permission.

Less Common Variations

Uniform vs Fine-Grained Access Control

If your bucket uses Uniform bucket-level access (recommended), IAM is the only way. If it uses Fine-grained, you might also need to set per-object ACLs. Check with:

gsutil bucket get iam gs://YOUR_BUCKET_NAME

If you see a policy, you're on Uniform. If it says "No IAM policies", you're on Fine-grained and need to set ACLs too:

gsutil acl ch -u YOUR_SERVICE_ACCT@PROJECT.iam.gserviceaccount.com:W gs://YOUR_BUCKET_NAME

Cross-Project Access

When your bucket lives in Project A but your service account is in Project B, you must grant access from Project A's side. The service account email stays the same, but the command runs against Project A:

gcloud projects add-iam-policy-binding PROJECT_A \
  --member='serviceAccount:SERVICE_ACCT_EMAIL_FROM_B' \
  --role='roles/storage.objectAdmin'

Also check that Project B's service account has the Storage Object Admin role in its own project—without it, the call won't even reach Project A's bucket.

Workload Identity Federation

If you're using Workload Identity Federation to access GCS from outside Google Cloud (like AWS or on-prem), the error often means the principal mapping or attribute condition is wrong. The IAM role must include a condition like iam.googleapis.com/attribute.service_account:YOUR_SA_EMAIL. Without it, GCS sees an anonymous caller.

Prevention: Lock It Down Early

Stop needing to fix this again. When you create a bucket, set the IAM policy at creation time:

gsutil mb -p YOUR_PROJECT --iam-policy-file policy.json gs://YOUR_BUCKET_NAME

In policy.json, include a binding for your service account with roles/storage.objectAdmin. This avoids the default bucket policy that allows only project owners.

Also, audit your IAM roles monthly. Use gcloud projects get-iam-policy and look for roles like roles/owner or roles/editor—they're overkill for storage access. Stick to roles/storage.objectAdmin for data, roles/storage.admin only if you need to manage the bucket itself.

One more thing: never use allUsers or allAuthenticatedUsers in production. That's how buckets get pwned. If you need public access, use a signed URL or a load balancer in front.

Was this solution helpful?