GCP service account key security is one of the most overlooked gaps in cloud infrastructure. Long-lived JSON key files get downloaded to laptops, committed to repos, embedded in CI pipelines, and forgotten — yet each one is a permanent, offline credential that never expires unless someone remembers to rotate or revoke it. Attackers know this: leaked service account keys are a recurring vector in real-world GCP breaches, because a stolen key grants the same privileges as the account itself, with no MFA, no session limits, and often no alerting until it's too late.
This guide walks through a practical, step-by-step approach to locking this down: auditing your existing key sprawl, disabling key creation at the org level, moving workloads to short-lived credentials via service account impersonation, rotating any keys you can't yet eliminate, and setting up monitoring so misuse doesn't go unnoticed. By the end, you'll have a concrete plan to eliminate most static keys and secure whatever remains.
Step 1: Establish a Baseline for GCP Service Account Key Security
Before you can fix the problem, you need to know how big it is. Most organizations are surprised by how many user-managed keys exist across projects, many tied to service accounts nobody remembers creating.
Pull a full inventory across your organization:
gcloud asset search-all-resources \
--scope=organizations/ORG_ID \
--asset-types=iam.googleapis.com/ServiceAccountKey \
--format="table(name,createTime)"
For a single project, list keys per service account:
for sa in $(gcloud iam service-accounts list --format="value(email)"); do
echo "== $sa =="
gcloud iam service-accounts keys list \
--iam-account="$sa" \
--managed-by=user \
--format="table(name,validAfterTime,validBeforeTime)"
done
Flag any key older than 90 days, any key attached to a service account with broad roles (roles/editor, roles/owner), and any key that hasn't been used recently. Cloud Logging can confirm actual usage:
gcloud logging read \
'protoPayload.authenticationInfo.serviceAccountKeyName!=""' \
--limit=50 --format=json
This baseline becomes your prioritization list — dead keys get deleted immediately, active keys get scheduled for migration.
Step 2: Disable Service Account Key Creation at the Org Level
Once you know what exists, stop the bleeding. The single highest-leverage control here is an organization policy that blocks new key creation outright, so teams can't reintroduce the problem while you clean up the rest.
Apply the constraint at the organization or folder level:
gcloud resource-manager org-policies enable-enforce \
iam.disableServiceAccountKeyCreation \
--organization=ORG_ID
Or, if you need more granularity, write it as a YAML policy and set it per project during rollout:
constraint: constraints/iam.disableServiceAccountKeyCreation
booleanPolicy:
enforced: true
gcloud resource-manager org-policies set-policy policy.yaml \
--project=PROJECT_ID
Roll this out in waves: start with new or low-risk projects, confirm nothing breaks, then expand to production. Pair it with iam.disableServiceAccountKeyUpload so nobody can work around the restriction by generating a key locally and uploading the public portion. Teams that genuinely need an exception (rare — usually only for on-prem or third-party systems that can't use federation) should go through an approval process, not a policy bypass.
Step 3: Migrate Workloads to Service Account Impersonation
Disabling key creation only works long-term if there's a viable alternative, and there is: service account impersonation. Instead of distributing a static key, you grant a caller (a human, a CI job, or another service account) the roles/iam.serviceAccountTokenCreator role on the target service account, and it mints short-lived tokens on demand.
Grant the impersonation role:
gcloud iam service-accounts add-iam-policy-binding \
TARGET_SA@PROJECT_ID.iam.gserviceaccount.com \
--member="user:developer@example.com" \
--role="roles/iam.serviceAccountTokenCreator"
Impersonate from the CLI:
gcloud auth print-access-token \
--impersonate-service-account=TARGET_SA@PROJECT_ID.iam.gserviceaccount.com
In application code, most Google client libraries support impersonation natively:
from google.auth import impersonated_credentials, default
source_credentials, _ = default()
target_credentials = impersonated_credentials.Credentials(
source_credentials=source_credentials,
target_principal="TARGET_SA@PROJECT_ID.iam.gserviceaccount.com",
target_scopes=["https://www.googleapis.com/auth/cloud-platform"],
lifetime=3600,
)
For workloads running on GCP compute (GCE, GKE, Cloud Run, Cloud Functions), skip impersonation and attached keys entirely by using the resource's built-in identity — the metadata server issues credentials automatically with no key material anywhere. For workloads outside GCP (AWS, on-prem, GitHub Actions), use Workload Identity Federation so external identities exchange a short-lived token for GCP credentials without ever holding a service account key.
Step 4: Rotate Service Account Keys You Can't Yet Eliminate
Some integrations — legacy third-party tools, certain SaaS connectors — genuinely can't use impersonation or federation yet. For those, rotate service account keys on a fixed schedule rather than letting them live indefinitely.
Create the replacement key first:
gcloud iam service-accounts keys create new-key.json \
--iam-account=TARGET_SA@PROJECT_ID.iam.gserviceaccount.com
Deploy new-key.json to the consuming system, confirm it authenticates successfully, then revoke the old one:
gcloud iam service-accounts keys delete OLD_KEY_ID \
--iam-account=TARGET_SA@PROJECT_ID.iam.gserviceaccount.com
A 90-day rotation cadence is a reasonable default; tighten it for high-privilege accounts. Automate this with a Cloud Scheduler job triggering a Cloud Function, or with a secrets manager integration (HashiCorp Vault's GCP secrets engine and Google Secret Manager both support key lifecycle automation) so rotation doesn't depend on a human remembering. Never email, Slack, or hardcode a key file as part of the rotation handoff — pass it through a secrets manager with short-lived access.
Step 5: Enforce Least Privilege and IAM Conditions
Key hygiene doesn't matter much if the underlying service account is over-provisioned. Audit each account's role bindings and remove anything broader than what the workload actually calls:
gcloud projects get-iam-policy PROJECT_ID \
--flatten="bindings[].members" \
--filter="bindings.members:serviceAccount:TARGET_SA@PROJECT_ID.iam.gserviceaccount.com" \
--format="table(bindings.role)"
Replace primitive roles (Owner, Editor) with predefined or custom roles scoped to the exact resources and APIs in use. Where possible, add IAM Conditions to restrict access further — for example, limiting a bucket-access role to a specific prefix or time window:
gcloud storage buckets add-iam-policy-binding gs://BUCKET_NAME \
--member="serviceAccount:TARGET_SA@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer" \
--condition='expression=resource.name.startsWith("projects/_/buckets/BUCKET_NAME/objects/reports/"),title=reports-only'
This way, even if a credential is compromised, the blast radius is contained.
Step 6: Monitor and Alert on Key and Impersonation Activity
Prevention controls will eventually have gaps — someone will find an exception, a legacy pipeline will linger — so detection is the backstop. Route Cloud Audit Logs to a log sink and alert on the events that matter most:
gcloud logging sinks create sa-key-monitoring \
bigquery.googleapis.com/projects/PROJECT_ID/datasets/security_logs \
--log-filter='protoPayload.methodName="google.iam.admin.v1.CreateServiceAccountKey" OR
protoPayload.methodName="google.iam.admin.v1.DeleteServiceAccountKey" OR
protoPayload.methodName="GenerateAccessToken"'
Set alerts for: key creation events (should be near-zero after Step 2), authentication from unfamiliar IP ranges or geographies, a service account authenticating via key when it's expected to use impersonation, and unusual spikes in GenerateAccessToken calls that could indicate impersonation abuse by a compromised principal with serviceAccountTokenCreator. Security Command Center's built-in findings for exposed or unused keys are a good complement to custom log-based alerts.
Verification and Troubleshooting
Policy shows enforced but keys still get created. Org policies inherit down the resource hierarchy but can be overridden at a lower level unless you also set --enforce at the folder/project and check for conflicting legacy policies with gcloud resource-manager org-policies describe iam.disableServiceAccountKeyCreation --project=PROJECT_ID.
Impersonation calls fail with PERMISSION_DENIED. The caller needs roles/iam.serviceAccountTokenCreator on the target service account specifically — a project-level IAM role is not sufficient, since impersonation permissions are evaluated on the target resource itself. Double-check with gcloud iam service-accounts get-iam-policy TARGET_SA@PROJECT_ID.iam.gserviceaccount.com.
Workload Identity Federation tokens are rejected. Confirm the attribute mapping and condition on the workload identity pool provider match the claims in the external token exactly (gcloud iam workload-identity-pools providers describe), and that the target service account's IAM policy grants roles/iam.workloadIdentityUser to the correct principal set.
Rotation breaks a downstream integration. Stagger rotation so the old and new key overlap briefly — delete the old key only after confirming the new one authenticates in production, not just in a test call.
Alerts are too noisy or too quiet. Baseline normal GenerateAccessToken volume for each high-privilege service account for a week before setting thresholds; static thresholds without a baseline tend to either flood you with false positives or miss the anomaly entirely.
How Safeguard Helps
Manually inventorying keys, chasing down owners, and verifying every service account across dozens of GCP projects doesn't scale once an organization grows past a handful of teams. Safeguard continuously discovers every service account and key across your GCP organization, flags stale, unused, or over-privileged keys automatically, and tracks org policy drift so a well-intentioned exception doesn't silently reopen the door to key creation.
Beyond visibility, Safeguard maps service account permissions against actual usage to recommend least-privilege role reductions, verifies that CI/CD pipelines and third-party integrations are authenticating through impersonation or workload identity federation rather than static credentials, and correlates anomalous GenerateAccessToken activity with your broader supply chain risk signals — so a compromised key or abused impersonation grant is caught in context, not as an isolated log line. For teams treating GCP service account key security as an ongoing program rather than a one-time cleanup, Safeguard turns the manual steps above into continuous, automated enforcement.