Safeguard
Cloud Security

How to implement least privilege for GCP service accounts

A step-by-step guide to auditing, scoping, and enforcing least privilege service accounts GCP-wide — including key rotation and IAM audit workflows.

Karan Patel
Cloud Security Engineer
7 min read

Over-permissioned service accounts are one of the most common — and most exploitable — misconfigurations in Google Cloud environments. A single service account holding roles/editor or roles/owner can turn a leaked key or a compromised CI job into full project takeover. Implementing least privilege service accounts GCP-wide isn't a one-time cleanup; it's a discipline that touches IAM design, key management, and ongoing auditing. In this guide, you'll learn how to inventory existing service accounts, right-size their permissions, rotate credentials safely, and put automated guardrails in place so privilege creep doesn't return six months from now. By the end, you'll have a repeatable process — not just a one-off fix — for keeping every service account in your GCP organization scoped to exactly what it needs and nothing more.

Step 1: Inventory Every Service Account and Its Current Roles

You can't fix what you can't see. Start with a full inventory across every project in your organization, not just the ones you remember creating.

gcloud asset search-all-iam-policies \
  --scope="organizations/YOUR_ORG_ID" \
  --query="policy:serviceAccount" \
  --format=json > iam-policy-inventory.json

For a project-level view:

gcloud iam service-accounts list --project=YOUR_PROJECT_ID

gcloud projects get-iam-policy YOUR_PROJECT_ID \
  --flatten="bindings[].members" \
  --filter="bindings.members:serviceAccount" \
  --format="table(bindings.role, bindings.members)"

Flag any service account bound to roles/owner, roles/editor, or roles/iam.securityAdmin — these primitive and highly sensitive roles are almost always broader than the workload actually requires, and they should be the first targets for remediation.

Step 2: Run a GCP Permissions Audit Against Actual Usage

Static IAM bindings only tell you what a service account could do — not what it actually does. Google's IAM Recommender analyzes Cloud Audit Logs (typically 90 days of activity) to identify permissions that were granted but never used.

gcloud recommender recommendations list \
  --project=YOUR_PROJECT_ID \
  --location=global \
  --recommender=google.iam.policy.Recommender \
  --format=json

This gcp permissions audit step is where most of your risk reduction comes from. It's common to find service accounts using less than 10% of the permissions granted to them. Export the recommendations, review them with the owning team (automated acceptance without context can break pipelines), and apply the ones that are safe.

gcloud recommender recommendations mark-claimed RECOMMENDATION_ID \
  --project=YOUR_PROJECT_ID \
  --location=global \
  --recommender=google.iam.policy.Recommender

Step 3: Replace Broad Roles with Predefined or Custom Roles

Once you know what a service account actually uses, swap primitive roles for tightly scoped predefined roles, or build a custom role when even the narrowest predefined role grants more than necessary.

gcloud iam roles create limitedStorageWriter \
  --project=YOUR_PROJECT_ID \
  --title="Limited Storage Writer" \
  --description="Write-only access to a single bucket prefix" \
  --permissions=storage.objects.create,storage.objects.get \
  --stage=GA

Bind it at the narrowest resource scope possible — bucket, dataset, or specific resource — rather than at the project level:

gsutil iam ch \
  serviceAccount:app-writer@YOUR_PROJECT_ID.iam.gserviceaccount.com:objectCreator \
  gs://your-target-bucket

This is a core tenet of gcp iam best practices: grant permissions as close to the resource as possible, and prefer predefined roles over custom roles unless a genuine gap exists, since custom roles carry their own maintenance overhead (they don't automatically pick up new permissions as GCP services evolve).

Step 4: Enforce Least Privilege Service Accounts GCP-Wide with IAM Conditions and Org Policies

Role scoping alone isn't enough — add conditions that constrain when and how a grant applies. IAM Conditions let you restrict access by time, resource attributes, or request context:

gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
  --member="serviceAccount:batch-job@YOUR_PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/bigquery.dataEditor" \
  --condition='expression=resource.name.startsWith("projects/_/buckets/batch-input"),title=batch-input-only'

At the organization level, use policy constraints to prevent regressions — for example, disabling automatic default service account role grants and blocking creation of new service account keys where Workload Identity is available:

gcloud resource-manager org-policies enable-enforce \
  iam.automaticIamGrantsForDefaultServiceAccounts \
  --organization=YOUR_ORG_ID

gcloud resource-manager org-policies enable-enforce \
  iam.disableServiceAccountKeyCreation \
  --project=YOUR_PROJECT_ID

This combination — scoped roles plus org-level guardrails — is what makes least privilege service accounts GCP configurations durable rather than a point-in-time cleanup.

Step 5: Rotate Service Account Keys and Prefer Keyless Auth

Long-lived JSON keys are the single biggest liability in GCP IAM. Wherever workloads run on Google infrastructure (GCE, GKE, Cloud Run, Cloud Functions), eliminate keys entirely with Workload Identity Federation instead of managing rotation.

gcloud iam workload-identity-pools create "github-pool" \
  --project=YOUR_PROJECT_ID \
  --location="global" \
  --display-name="GitHub Actions Pool"

Where keys genuinely can't be avoided (some external or on-prem integrations), enforce short lifetimes and automated service account key rotation gcp workflows rather than relying on manual rotation:

gcloud iam service-accounts keys create new-key.json \
  --iam-account=external-integration@YOUR_PROJECT_ID.iam.gserviceaccount.com

# Revoke the old key only after confirming the new one is deployed
gcloud iam service-accounts keys delete OLD_KEY_ID \
  --iam-account=external-integration@YOUR_PROJECT_ID.iam.gserviceaccount.com

Set an org policy to cap key age so stale keys can't linger indefinitely:

gcloud resource-manager org-policies enable-enforce \
  iam.serviceAccountKeyExpiryHours \
  --project=YOUR_PROJECT_ID

Audit key age regularly — any key older than 90 days without a documented reason should be treated as a finding:

gcloud iam service-accounts keys list \
  --iam-account=YOUR_SA@YOUR_PROJECT_ID.iam.gserviceaccount.com \
  --managed-by=user \
  --format="table(name, validAfterTime)"

Step 6: Automate Continuous Enforcement

Manual reviews decay. Bake least-privilege checks into CI/CD and infrastructure-as-code so new service accounts can't be provisioned with excessive scope in the first place. Use a policy-as-code tool (OPA/Conftest, Sentinel, or a custom Cloud Build step) to reject Terraform plans that bind primitive roles or create keys where Workload Identity is available. Schedule a recurring job (Cloud Scheduler + Cloud Function, or a dedicated posture tool) to re-run the IAM Recommender export from Step 2 monthly, and alert when a service account's granted permissions drift meaningfully above its observed usage.

Verification and Troubleshooting

"My workload broke after I tightened a role." Check Cloud Audit Logs for PERMISSION_DENIED errors in the relevant time window:

gcloud logging read \
  'protoPayload.status.code=7 AND protoPayload.authenticationInfo.principalEmail="YOUR_SA@YOUR_PROJECT_ID.iam.gserviceaccount.com"' \
  --project=YOUR_PROJECT_ID --limit=50 --format=json

The denied permission in the log tells you exactly what to add back — grant that single permission via a custom role or condition rather than reverting to the broad role.

"IAM Recommender shows no recommendations." It needs sufficient audit log history (generally 90 days of Data Access logs) to generate confident suggestions. Confirm Data Access audit logging is enabled for the relevant services:

gcloud projects get-iam-policy YOUR_PROJECT_ID --format=json | grep -A5 auditConfigs

"I can't tell if a key rotation broke something downstream." Roll out new keys alongside old ones, monitor error rates for 24-48 hours, then revoke — never rotate and revoke in the same step for production integrations.

"Org policy enforcement is blocking a legitimate use case." Use policy exceptions scoped to a specific project or folder rather than disabling the constraint org-wide, and document the exception with an expiry date for re-review.

How Safeguard Helps

Manually maintaining least privilege service accounts GCP configurations across dozens of projects and hundreds of identities doesn't scale with spreadsheets and quarterly audits. Safeguard continuously maps every service account against its actual observed usage across your GCP organization, flags permission drift and stale keys before they become findings, and surfaces unused grants as actionable, one-click remediations rather than another dashboard to babysit. Instead of running the gcp permissions audit steps above by hand every quarter, Safeguard runs them continuously — catching a newly over-scoped service account or an aging key the day it appears, not the day an auditor or an attacker finds it. For teams serious about supply chain security, that shift from periodic cleanup to continuous enforcement is what actually keeps least privilege durable.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.