Least Privilege in GCP Explained for Beginners
Least privilege is one of the most important security principles in GCP, and one of the most commonly skipped. This guide explains what it means, how it works across two distinct dimensions, and how to apply it to real workloads from day one.
What is least privilege?
Least privilege means giving an identity (a user, a service account, or a group) only the permissions it actually needs to do its job, on only the resources it actually touches, for only as long as it needs them.
In GCP, this plays out through IAM. Every time you grant a role, you are deciding two things: what actions are allowed, and where those actions apply. Least privilege is about keeping both of those as narrow as possible.
This page walks through what least privilege means in concrete GCP terms, how to apply it to common workloads, what makes it easy to get wrong, and how to review your access grants over time.
Simple explanation
Before diving into the GCP mechanics, here is the core idea in plain terms.
Office building analogy
A delivery driver needs to drop packages at the loading dock. You could give them a master keycard that opens every door in the building. That technically works. But it also lets them into the server room, the finance office, and the executive floor.
The better approach is a keycard that opens only the loading dock. If it is lost or copied, the damage is contained. Everything else stays locked.
That is exactly what least privilege does in GCP. Instead of giving a
service account a project-level roles/editor role (the master
keycard), you give it roles/storage.objectCreator on one
specific bucket (the loading dock key). If that service account is ever
compromised, the attacker can only create objects in that one bucket.
Everything else is out of reach.
How least privilege works in GCP
Applying least privilege in GCP means addressing two independent dimensions. Most teams address one and leave the other wide open.
Dimension 1: Role scope (which actions are allowed)
The
IAM role you grant determines
which API actions the identity can perform. Roles range from very broad
(like roles/editor, which covers almost every service) to very
narrow (like roles/storage.objectCreator, which only allows
uploading objects to Cloud Storage).
Always start from the narrowest predefined role that covers the actual task. If no predefined role fits cleanly, consider a custom role.
Dimension 2: Resource scope (where those actions apply)
Even a narrow role grants access to all matching resources within the scope
you bind it at. roles/storage.objectViewer at the project level
gives read access to every bucket in the project. The same role bound to a
specific bucket restricts it to that one bucket only.
GCP supports IAM bindings at four levels of the resource hierarchy:
- Organisation: applies to all folders, projects, and resources underneath
- Folder: applies to all projects in the folder
- Project: applies to all resources in the project
- Individual resource: applies only to that one resource (supported by Cloud Storage, BigQuery, Pub/Sub, Secret Manager, and others)
The lowest level that works for the workload is almost always the right choice. Project-level bindings are convenient but expose far more than intended.
Not every GCP service supports IAM at the individual resource level. The ones that do include Cloud Storage (bucket or object), BigQuery (dataset or table), Pub/Sub (topic or subscription), Secret Manager (secret), and Cloud Run (service). If a service does not support resource-level IAM, the narrowest scope you can use is project-level.
Optional third dimension: conditions
IAM Conditions let you layer time and context restrictions on top of role and resource scope. You can grant a role that only applies during business hours, before a specific expiry date, or only to resources whose names match a given prefix. This is especially useful for temporary access during troubleshooting or incident response.
When to use least privilege
Least privilege is not just for large production environments. It applies any time an identity needs access to a GCP resource. Here are the most common situations:
Service accounts running workloads. Every Cloud Run service, Cloud Function, or Compute VM should run as a dedicated service account with only the permissions that specific workload needs. Never use the default Compute Engine service account without reviewing and narrowing its access first.
CI/CD pipelines deploying to GCP. A pipeline deploying to Cloud Run needs deploy permissions, not editor access across the project. Bind the deploy role to the specific Cloud Run service, not the whole project.
Cloud Storage access. If an app uploads files, it needs
roles/storage.objectCreatoron that bucket, notroles/storage.adminon all buckets.Cloud SQL connections. An application reading from a database should have
roles/cloudsql.clienton the specific instance, not editor on the project.Human admin access. Engineers should be granted scoped roles for the resources they actively work on. Avoid assigning
roles/ownerto human users. Use more specific admin roles instead.Temporary troubleshooting access. When someone needs elevated permissions to investigate an incident, use IAM Conditions to set a time-based expiry. Temporary access that is never revoked becomes permanent risk.
Practical examples
Cloud Storage: upload service
# Wrong: project-level editor grants write access to every GCP service
gcloud projects add-iam-policy-binding my-app-prod \
--member="serviceAccount:upload-svc@my-app-prod.iam.gserviceaccount.com" \
--role="roles/editor"
# Right: narrow role scoped to the one bucket this service actually uses
gcloud storage buckets add-iam-policy-binding gs://my-app-prod-uploads \
--member="serviceAccount:upload-svc@my-app-prod.iam.gserviceaccount.com" \
--role="roles/storage.objectCreator"
# objectCreator allows: create objects, list objects
# It does NOT allow: read existing objects, delete objects, or access any other serviceThe better approach limits an incident to one bucket. The wrong approach exposes every service in the project.
Cloud Run: deployment pipeline
# Wrong: editor on the project gives the pipeline write access to every resource
gcloud projects add-iam-policy-binding my-app-prod \
--member="serviceAccount:ci-deployer@my-app-prod.iam.gserviceaccount.com" \
--role="roles/editor"
# Right: deploy role scoped to the specific Cloud Run service
gcloud run services add-iam-policy-binding my-api \
--region=us-central1 \
--member="serviceAccount:ci-deployer@my-app-prod.iam.gserviceaccount.com" \
--role="roles/run.developer"roles/run.developer lets the pipeline deploy new revisions. It
cannot touch Cloud Storage, BigQuery, or any other service in the project.
Cloud SQL: application database access
An application that connects to a Cloud SQL instance should have
roles/cloudsql.client on that specific instance, not
roles/cloudsql.admin or roles/editor on the
project. The client role allows connecting and running queries. It does not
allow creating or deleting instances, modifying configuration, or accessing
other databases.
Logging and monitoring: read-only access
A user who needs to read logs for debugging should have
roles/logging.viewer, not roles/viewer.
roles/viewer grants read access to almost every service in the
project, including configuration data and stored data that the person
has no reason to see.
Least privilege vs broad access
The most common mistake is reaching for basic roles (roles/viewer,
roles/editor, roles/owner) because they are
simple to apply. They are also much more powerful than almost any workload
actually needs.
Here is how the approaches compare:
| Approach | Example | Risk level | Why |
|---|---|---|---|
| Project-level basic role | roles/editor on project | High | Grants write access to almost every service in the project |
| Project-level predefined role | roles/storage.objectViewer on project | Medium | Correct action scope, but exposes all resources of that type |
| Resource-level predefined role | roles/storage.objectViewer on one bucket | Low | Correct action scope AND correct resource scope |
| Custom role at resource level | Exact permissions on one resource | Lowest | Precisely matched to the task, hardest to maintain |
For production workloads, the goal is to reach the third or fourth row. Project-level basic roles are acceptable for development exploration but should not exist in production IAM policies.
roles/editor grants write access to almost every GCP service
in the project: Cloud Storage, BigQuery, Cloud SQL, Pub/Sub, Secret
Manager, and more. A service account or user holding Editor can delete
databases, overwrite configuration, and read secrets. If that identity is
compromised, the attacker has the keys to the whole project.
roles/viewer sounds safe because it is read-only. But it
grants read access to every service in the project: Cloud Storage
buckets, Cloud SQL data, Secret Manager secrets, BigQuery datasets, and
more. A person or process that only needs to view logs should not have
access to your database or your secrets.
See
Basic vs predefined vs custom roles
for a full breakdown of what each category includes and when to use each.
The over-privileged defaults you need to fix
GCP creates two default service accounts in every new project. Both have
roles/editor granted by default, which is one of the most
common high-severity findings in GCP security reviews:
The Compute Engine default service account (
PROJECT_NUMBER-compute@developer.gserviceaccount.com) getsroles/editorautomatically. Any VM created without specifying a service account runs as this identity.The App Engine default service account (
PROJECT_ID@appspot.gserviceaccount.com) also getsroles/editorby default.
Run gcloud projects get-iam-policy PROJECT_ID and look for
bindings to the Compute Engine default service account. If it still holds
roles/editor, that is a high-priority finding to remediate.
Remove the Editor binding and create a dedicated service account with only
the permissions your application needs.
Common mistakes
Granting Editor to unblock a deployment, then forgetting to fix it. This is the most common pattern. When a permission error blocks a deployment, the fastest fix is
roles/editor. The intent is to fix it later. Later never comes. Create a ticket immediately when you make a broad temporary grant, and treat it as a high-priority item. Seetroubleshooting IAM access denied errors
for how to diagnose the correct narrow role instead.
Narrowing the role but leaving the scope at project level.
roles/storage.objectViewerat project level still gives read access to every bucket in the project. If the job only needs one bucket, bind the role to that specific bucket. Both dimensions must be addressed. Role scope alone is not enough.Leaving Compute Engine and App Engine default service accounts with Editor. Every VM or App Engine instance using the default service account inherits this risk. This is one of the most common high-severity findings in GCP security reviews.
Not reviewing inherited access from folders or the org level. IAM bindings are inherited downward through the resource hierarchy. A role granted at the organisation or folder level flows into every project underneath. Teams focused on project-level IAM often miss org-level or folder-level bindings that are far too broad.
Never removing temporary access. Permissions granted for incident response, a short-term project, or onboarding often stay forever. Without a regular review cadence, IAM policies accumulate grants that nobody remembers granting.
Using custom roles as a shortcut without auditing them. Custom roles are powerful but need maintenance. If a workload is deleted or changes, the custom role may linger with permissions attached to identities that no longer need them.
How to apply least privilege safely
Follow this process whenever you need to grant access to a GCP resource:
Identify the task. What exactly does this identity need to do? Be specific: “read objects from a bucket” is better than “access storage.”
Identify the exact resource. Which specific resource does it need access to? A specific bucket, a specific Cloud SQL instance, a specific Cloud Run service.
Choose the narrowest role. Find the predefined role that covers the task without adding extra permissions. If nothing fits, consider a custom role. Check gcloud IAM commands or the Console to search available roles.
Bind at the lowest sensible scope. Grant at the individual resource level where possible. Only move to project or folder level if the workload genuinely needs access to all resources of that type within that scope.
Test with Policy Simulator before applying if in doubt. Policy Simulator lets you verify what a proposed IAM binding would allow or deny before it takes effect. Use it when reducing an existing role to confirm the narrower role still covers required operations.
Set an expiry if access is temporary. Use IAM Conditions to add a time-based expiry for access that should not be permanent.
Review later with IAM Recommender. After 90 days of activity, IAM Recommender will have enough data to tell you whether the role you granted is still too broad. Check it on a quarterly schedule.
If you manage IAM through code rather than the Console, see
managing IAM with Terraform
for how to express these bindings declaratively.
Org-level guardrails that go beyond IAM
IAM controls what individual identities can do. For organisation-wide
constraints that apply regardless of individual role assignments, use
Organisation Policies.
For example, iam.disableServiceAccountKeyCreation prevents key
file creation across all projects, even if an admin at the project level
tries to allow it. These structural controls complement least-privilege IAM
by removing whole categories of risk at the platform level.
For service accounts specifically, avoid creating or distributing long-lived keys where possible. See
why service account keys are dangerous
and
service account impersonation
for safer alternatives.
Keeping permissions minimal over time
Permissions accumulate. People join teams, get temporary access, and that access is never revoked. Service accounts gather bindings as requirements change. IAM policies that were correct six months ago are often wrong today.
Two tools help keep things clean:
IAM Recommender: analyses the last 90 days of API call history and recommends removing roles that have not been used. Available in the IAM section of the Console and via the API.
Policy Simulator: lets you test what a proposed IAM change would allow or deny before applying it. Verify a narrower role still covers required operations before switching.
Schedule a quarterly IAM review for each production project. Focus on privileged service accounts, any remaining basic role bindings, and accounts belonging to people who have changed roles or left the organisation. Thirty minutes on a recurring schedule prevents years of permission creep.
Summary
- Least privilege has two dimensions: role scope (which permissions) and resource scope (where they apply). Both must be addressed independently.
- A narrow role at project level still exposes more than intended. Bind at the individual resource level where possible.
- Basic roles like
roles/editorandroles/viewerare much broader than most workloads need. Avoid them in production IAM policies. - The Compute Engine and App Engine default service accounts have Editor on new projects by default. Remove this in production immediately.
- IAM Conditions add a time or context dimension on top of role and resource scope, which is useful for temporary access.
- IAM Recommender flags unused permissions after 90 days of activity. Run it on a quarterly review cadence.
- Policy Simulator lets you verify that a narrower role still covers what a workload needs before you apply the change.
Frequently asked questions
What is least privilege in GCP?
Least privilege means every identity (user, service account, or group) gets only the minimum permissions needed for its specific job, scoped to the specific resources it actually uses, for no longer than necessary. In practice this means granting narrow predefined roles at the resource level rather than broad roles like Editor at the project level.
What is the difference between role scope and resource scope?
Role scope is about which permissions you grant: for example, choosing roles/storage.objectViewer instead of roles/editor. Resource scope is about where you grant them: binding that role to a specific bucket instead of the entire project. Both must be addressed. A narrow role at project level still gives access to every resource of that type across the whole project.
Why is roles/editor dangerous?
roles/editor grants write access to almost every GCP service in a project: Cloud Storage, BigQuery, Cloud SQL, Pub/Sub, and more. If a service account or user holding Editor is compromised, an attacker can read data, overwrite configuration, and delete resources across the entire project. Most workloads only need permissions for one or two services, making Editor far broader than necessary.
When should I use custom roles instead of predefined roles?
Use a custom role when no predefined role matches the exact permissions a workload needs. For example, if your CI pipeline needs to deploy a Cloud Run service but not update traffic or delete revisions, a custom role lets you grant exactly those three permissions. Custom roles add maintenance overhead, so only create them when the predefined options are meaningfully too broad.
How do I find out if my permissions are too broad?
Use IAM Recommender in the GCP Console under IAM & Admin. It analyses 90 days of API call history and flags role bindings where the principal has never used certain permissions. It then suggests narrower replacements. Use Policy Simulator before applying any change to confirm the narrower role still covers the operations you need.