This issue has been assessed as high severity. Review affected configurations immediately.
The Problem With Service Account Keys
Google Cloud service accounts authenticate workloads to GCP APIs. Every service account can have up to 10 user-managed keys — JSON credential files that contain a private key granting the service account’s permissions to whoever holds them.
These keys are designed for workloads that cannot use GCP’s native credential mechanisms: applications running outside GCP, local development environments, or legacy systems. In practice, they end up everywhere: developers download keys for local testing, CI/CD systems use them for deployments, third-party tools request them for integrations, and then the keys persist indefinitely because no one tracks or rotates them.
The security problem is fundamental: service account keys are long-lived, static credentials. Unlike OAuth tokens or short-lived workload identity credentials, a leaked service account key remains valid until explicitly revoked. The key is stored in a JSON file that can be committed to source control, left in an S3-equivalent bucket, or exfiltrated from a CI/CD system — and in all cases, the attacker gains persistent access to whatever the service account can do.
Google’s own Confidential Computing team has found that service account key exfiltration is the most common initial access vector in GCP breaches they investigate.
Auditing Key Sprawl
Start by understanding the full scope of user-managed keys across your project or organisation:
# List all service accounts in a project
gcloud iam service-accounts list --project=PROJECT_ID --format="value(email)"
# For each service account, list its keys
gcloud iam service-accounts keys list \
--iam-account=SA_EMAIL \
--managed-by=user \
--format="table(name,validAfterTime,validBeforeTime)"
# Organisation-wide audit (requires org-level permissions)
# Pipe through jq to identify keys older than 90 days
gcloud asset search-all-iam-policies \
--scope=organizations/ORG_ID \
--query="policy.bindings.role:roles/iam.serviceAccountKeyAdmin" \
--format=json | jq '.[] | select(.resource) | .resource'
For a structured audit across an organisation:
# List all service account keys org-wide with creation date
gcloud projects list --format="value(projectId)" | while read project; do
gcloud iam service-accounts list \
--project="$project" \
--format="value(email)" 2>/dev/null | while read sa; do
gcloud iam service-accounts keys list \
--iam-account="$sa" \
--project="$project" \
--managed-by=user \
--format="csv(name,validAfterTime)" 2>/dev/null | grep -v "^NAME" | \
awk -F',' -v sa="$sa" -v proj="$project" '{print proj","sa","$1","$2}'
done
done
This produces a CSV of every user-managed key, the service account it belongs to, and when it was created. Keys older than 90 days with no documented rotation schedule are immediate remediation targets.
Detection with Security Command Center
Google Security Command Center (SCC) provides built-in findings for service account key risks:
SERVICE_ACCOUNT_KEY_EXPOSED — Triggered when a service account key is found in a GCP resource (Cloud Storage object, Secret Manager secret, etc.)
SERVICE_ACCOUNT_KEY_UPLOADED — Triggered when a user-managed key is created for a service account that has elevated permissions (Project Owner, Editor, broad IAM admin roles)
KEY_NOT_ROTATED — Finding when a key has not been rotated within the policy-defined window (typically 90 days)
To query these via the SCC API:
gcloud scc findings list ORGANIZATION_ID \
--filter="category=\"SERVICE_ACCOUNT_KEY_EXPOSED\" AND state=\"ACTIVE\"" \
--format="table(resource.name,finding.category,finding.createTime)"
For custom detection, use Cloud Audit Logs to track key creation and usage:
# Query Cloud Audit Logs for service account key creation events
gcloud logging read \
'protoPayload.methodName="google.iam.admin.v1.CreateServiceAccountKey"
AND severity>=WARNING' \
--project=PROJECT_ID \
--limit=50 \
--format="json" | jq '.[] | {time: .timestamp, actor: .protoPayload.authenticationInfo.principalEmail, sa: .protoPayload.resourceName}'
Key creation by non-service-account principals (human users) is particularly notable — it suggests a developer downloading credentials rather than a workload using them.
Migrating to Workload Identity
The permanent fix for service account key sprawl is eliminating user-managed keys entirely by using Workload Identity wherever possible. Workload Identity allows GCP workloads to authenticate as service accounts without keys by presenting short-lived credentials derived from the workload’s own identity.
GKE Workload Identity
# Enable Workload Identity on a GKE cluster
gcloud container clusters update CLUSTER_NAME \
--workload-pool=PROJECT_ID.svc.id.goog \
--region=REGION
# Create a Kubernetes service account
kubectl create serviceaccount KSA_NAME -n NAMESPACE
# Allow the KSA to impersonate a GCP service account
gcloud iam service-accounts add-iam-policy-binding GSA_EMAIL \
--role="roles/iam.workloadIdentityUser" \
--member="serviceAccount:PROJECT_ID.svc.id.goog[NAMESPACE/KSA_NAME]"
# Annotate the KSA
kubectl annotate serviceaccount KSA_NAME \
--namespace NAMESPACE \
iam.gke.io/gcp-service-account=GSA_EMAIL
Workload Identity Federation (External Workloads)
For workloads outside GCP — GitHub Actions, AWS EC2, Azure VMs, on-premises systems:
# Create a Workload Identity Pool
gcloud iam workload-identity-pools create "github-pool" \
--location="global" \
--description="GitHub Actions pool"
# Create a provider for GitHub Actions OIDC
gcloud iam workload-identity-pools providers create-oidc "github-provider" \
--location="global" \
--workload-identity-pool="github-pool" \
--display-name="GitHub Actions" \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository" \
--issuer-uri="https://token.actions.githubusercontent.com"
# Bind the service account to specific repositories
gcloud iam service-accounts add-iam-policy-binding SA_EMAIL \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-pool/attribute.repository/org/repo"
In the GitHub Actions workflow:
permissions:
id-token: write
contents: read
steps:
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: 'projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-pool/providers/github-provider'
service_account: 'SA_EMAIL'
No JSON key file. No static credential. The token is valid for one hour and scoped to the specific repository.
Enforcing Policy
Prevent future key creation with an Organisation Policy constraint:
# Disable service account key creation org-wide
gcloud org-policies set-policy - <<EOF
name: organizations/ORG_ID/policies/iam.disableServiceAccountKeyCreation
spec:
rules:
- enforce: true
EOF
If blanket enforcement is too aggressive, apply it to specific folders (production environments) while allowing exceptions for development or legacy-designated projects via folder-level overrides.
Remediation Priority Matrix
| Key Age | Permissions | Priority |
|---|---|---|
| >180 days | Project Owner/Editor | Critical — revoke immediately |
| >90 days | Broad IAM roles | High — verify and rotate or revoke |
| >90 days | Limited/scoped roles | Medium — review and rotate |
| <90 days | Any high-privilege role | High — migrate to Workload Identity |
| <90 days | Scoped, documented | Low — track and schedule rotation |
Keys associated with service accounts that have roles/owner, roles/editor, or any roles/iam.* should be treated as critical regardless of age — these are the credentials attackers prize most in a GCP breach.