Google Cloud · lesson 2 of 10
Also part of: Cloud Security
GCP IAM: Roles and Service Accounts
Create a service account, watch it fail with PERMISSION_DENIED, then grant exactly the role it needs — the real workflow behind every GCP access error.
By SkillByExample · Published
Nearly every GCP access problem is the same shape: something authenticates fine, then gets refused. This lesson creates a service account, deliberately hits that refusal, and fixes it — so the pattern is recognizable the next time it shows up on a service you haven’t used before.
Creating a service account
gcloud iam service-accounts create deploy-bot \
--display-name="Deploy Bot" \
--description="Used by the CI pipeline to push builds"
Created service account [deploy-bot].
gcloud iam service-accounts list
DISPLAY NAME EMAIL DISABLED
Deploy Bot deploy-bot@skillbyexample-437219.iam.gserviceaccount.com False
A service account’s identity is that email-shaped string, not a human login. It’s what you’ll grant roles to and what other resources will impersonate.
Hitting PERMISSION_DENIED on purpose
Create a bucket, then try to list its objects while impersonating the brand-new service account, which has no roles yet.
gcloud storage buckets create gs://skillbyexample-assets-437219 --location=us-central1
gcloud storage ls gs://skillbyexample-assets-437219 \
--impersonate-service-account=deploy-bot@skillbyexample-437219.iam.gserviceaccount.com
Creating gs://skillbyexample-assets-437219/...
ERROR: (gcloud.storage.ls) HTTPError 403: deploy-bot@skillbyexample-437219.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).
That 403 is PERMISSION_DENIED — the service account authenticated correctly (GCP knows exactly
who it is), it simply has zero IAM bindings, so it holds zero permissions. This is the default
state of every new service account: an identity with nothing granted.
Inspecting the policy before touching it
gcloud storage buckets get-iam-policy gs://skillbyexample-assets-437219
etag: ACAB
version: 1
An empty-looking policy — no bindings key at all — confirms nobody but the project’s default
owners has any explicit grant on this bucket yet.
Granting the role
gcloud storage buckets add-iam-policy-binding gs://skillbyexample-assets-437219 \
--member="serviceAccount:deploy-bot@skillbyexample-437219.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer"
bindings:
- members:
- serviceAccount:deploy-bot@skillbyexample-437219.iam.gserviceaccount.com
role: roles/storage.objectViewer
etag: ACACyx8=
version: 1
roles/storage.objectViewer is a predefined role bundling storage.objects.get and
storage.objects.list — enough to read, nothing that lets it delete or overwrite. Rerun the
same ls command:
gcloud storage ls gs://skillbyexample-assets-437219 \
--impersonate-service-account=deploy-bot@skillbyexample-437219.iam.gserviceaccount.com
(no output — the bucket is empty, but the call succeeded)
No error this time. The call succeeded and printed nothing because the bucket has no objects yet — an empty result looks identical to a satisfied, boring success, which is the correct outcome for a viewer role hitting an empty bucket.
Reading a policy binding back
gcloud storage buckets get-iam-policy gs://skillbyexample-assets-437219 \
--format="table(bindings.role, bindings.members)"
ROLE MEMBERS
roles/storage.objectViewer ['serviceAccount:deploy-bot@skillbyexample-437219.iam.gserviceaccount.com']
Practice
1. Try the same impersonated call after granting only roles/storage.objectCreator instead, then attempt to list objects.
gcloud storage buckets remove-iam-policy-binding gs://skillbyexample-assets-437219 \
--member="serviceAccount:deploy-bot@skillbyexample-437219.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer"
gcloud storage buckets add-iam-policy-binding gs://skillbyexample-assets-437219 \
--member="serviceAccount:deploy-bot@skillbyexample-437219.iam.gserviceaccount.com" \
--role="roles/storage.objectCreator"
gcloud storage ls gs://skillbyexample-assets-437219 \
--impersonate-service-account=deploy-bot@skillbyexample-437219.iam.gserviceaccount.com
ERROR: (gcloud.storage.ls) HTTPError 403: deploy-bot@skillbyexample-437219.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).
objectCreator grants storage.objects.create only — it can upload but not list or read back,
proof that roles are additive and narrow, not a single generic “storage access” toggle.
2. List every predefined role whose name mentions "storage".
gcloud iam roles list --filter="name:roles/storage" --format="value(name)"
roles/storage.admin
roles/storage.objectAdmin
roles/storage.objectCreator
roles/storage.objectViewer
roles/storage.legacyBucketReader
Predefined roles are scoped per service and per action shape (admin / creator / viewer) rather than one all-or-nothing role — this is the pattern to expect from every GCP service, not just storage.
3. Revoke the role entirely and confirm the original error comes back.
gcloud storage buckets remove-iam-policy-binding gs://skillbyexample-assets-437219 \
--member="serviceAccount:deploy-bot@skillbyexample-437219.iam.gserviceaccount.com" \
--role="roles/storage.objectCreator"
gcloud storage ls gs://skillbyexample-assets-437219 \
--impersonate-service-account=deploy-bot@skillbyexample-437219.iam.gserviceaccount.com
ERROR: (gcloud.storage.ls) HTTPError 403: deploy-bot@skillbyexample-437219.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).
IAM bindings are the only thing standing between “authenticated” and “authorized” — remove the binding and the identity is exactly as powerless as the moment it was created.
4. Check which permissions your own user account has on the project, without granting anything.
gcloud projects get-iam-policy skillbyexample-437219 \
--flatten="bindings[].members" \
--filter="bindings.members:you@gmail.com" \
--format="table(bindings.role)"
ROLE
roles/owner
The account that created a project is bound to roles/owner automatically — full control,
including the ability to grant or revoke every other role, which is why production setups avoid
doing day-to-day work as the Owner.
5. Delete the service account when it's no longer needed.
gcloud iam service-accounts delete \
deploy-bot@skillbyexample-437219.iam.gserviceaccount.com --quiet
deleted service account [deploy-bot@skillbyexample-437219.iam.gserviceaccount.com]
Deleting the account also invalidates every key it held immediately — any pipeline still using one starts failing authentication, not authorization, on its very next call.
Next: Compute Engine — creating a VM, opening a firewall port, and connecting over SSH.