Summary
| Forestall ACL Alias | GCP_CREATE_SA_KEYS |
| GCP Alias | Persistence & Impersonation |
| Affected Object Types | Service Accounts |
| Exploitation Certainty | Certain |
| Granting Roles | roles/owner, roles/editor, roles/iam.serviceAccountKeyAdmin |
Description
GCP_CREATE_SA_KEYS indicates that an identity can call iam.serviceAccounts.keys.create on a GCP Service Account. This is one of the most impactful persistence mechanisms in GCP: creating a service account key produces a long-lived JSON credential file that is valid until explicitly revoked, has no expiry by default, and can be used from anywhere on the internet — completely independent of GCP’s infrastructure.
Unlike short-lived tokens (which expire in 1 hour), a SA JSON key represents untethered persistence. An attacker who creates and exfiltrates a key retains access even after being detected and removed from IAM policy bindings, until the key itself is revoked.
Key abuse scenarios:
- Create a new JSON key for a high-privilege SA and exfiltrate it for persistent access.
- Backdoor a high-privilege SA with a key under attacker control before being removed.
- Create keys on multiple SAs for redundant persistence.
Identification
gcloud CLI
# Find who has SA key creation rights at project levelPROJECT_ID="my-project"gcloud projects get-iam-policy $PROJECT_ID --format=json | \ jq '.bindings[] | select(.role | test("owner|editor|serviceAccountKeyAdmin")) | {role: .role, members: .members}'
# List existing keys for all service accounts (look for user-managed keys)for SA in $(gcloud iam service-accounts list --project=$PROJECT_ID --format="value(email)"); do KEYS=$(gcloud iam service-accounts keys list --iam-account=$SA --managed-by=user --format="table(name, validAfterTime, validBeforeTime)" 2>/dev/null) if [ ! -z "$KEYS" ]; then echo "=== $SA ===" echo "$KEYS" fidone# Check for recently created keys (last 7 days)gcloud logging read \ 'protoPayload.methodName="google.iam.admin.v1.CreateServiceAccountKey"' \ --project=$PROJECT_ID \ --freshness=7d \ --format="table(timestamp, protoPayload.authenticationInfo.principalEmail, protoPayload.resourceName)"GCP Console
- Open GCP Console → IAM & Admin → Service Accounts.
- Click on a service account.
- Select the Keys tab to see all user-managed keys and their creation dates.
Exploitation
gcloud CLI
# Create a new key for a high-privilege service accountgcloud iam service-accounts keys create /tmp/stolen_key.json \ --iam-account=$SA_EMAIL
# Verify the key worksgcloud auth activate-service-account --key-file=/tmp/stolen_key.jsongcloud auth print-access-token
# After exfiltration, use the key from any machineexport GOOGLE_APPLICATION_CREDENTIALS=/path/to/stolen_key.json# Now all gcloud/API calls authenticate as the target SAgcloud projects listThe JSON key works from any internet-connected machine and persists until explicitly revoked — even if the original attacker identity is removed from all IAM policies.
Mitigation
- Disable SA key creation with org policy:
constraints/iam.disableServiceAccountKeyCreation. - Use Workload Identity Federation instead of SA keys for non-GCP workloads.
- Audit and rotate existing user-managed keys — delete any key not tied to an active use case.
- Set key expiry policies using
constraints/iam.serviceAccountKeyExpiryHours. - Alert on key creation immediately.
Detection
| Log Type | Method | Key Fields |
|---|---|---|
| Admin Activity | CreateServiceAccountKey |
resource.type=service_account, methodName=google.iam.admin.v1.CreateServiceAccountKey |
gcloud logging read \ 'protoPayload.methodName="google.iam.admin.v1.CreateServiceAccountKey"' \ --project=$PROJECT_ID \ --format="table(timestamp, protoPayload.authenticationInfo.principalEmail, protoPayload.resourceName)"Alert on:
- Any
CreateServiceAccountKeycall — key creation should be rare and intentional. - Key creation on high-privilege SAs (Cloud Build SA, default compute SA, owner-bound SAs).
- Key creation outside of known automation pipelines.