This check flags GCP service accounts that have user-managed key pairs instead of relying on Google-managed credentials. User-managed keys are long-lived secrets that get leaked, committed to repos, and rarely rotated. Delete them and switch to Workload Identity Federation or short-lived tokens.
Service account keys are one of the most common ways GCP environments get breached. Not because the keys themselves are weak, but because of how they get handled. A developer downloads a JSON key to test something locally, drops it into a config file, and six months later that same key is sitting in a public GitHub repo or a Slack message. The key never expired, never rotated, and now anyone holding it can authenticate as that service account.
The iam_useruploadedsakeys check looks for service accounts in your project that have user-managed keys attached. These are the keys you create and manage yourself, as opposed to the keys Google creates and rotates automatically behind the scenes.
What this check detects
Every GCP service account can have two types of keys:
- Google-managed keys — created, used, and rotated automatically by Google. You never see the private key material. These power things like default service account authentication inside GCP services.
- User-managed keys — created by you, either through the console, the
gcloudCLI, or the API. You get the private key as a downloadable JSON file, and you are responsible for storing, rotating, and eventually deleting it.
This check fails when it finds one or more user-managed keys on a service account. You can spot them by their key origin: user-managed keys have a keyType of USER_MANAGED when you list them.
Note: The presence of a user-managed key is not automatically a breach. It is a risk indicator. The check tells you that a long-lived secret exists that needs to be tracked, rotated, and ideally eliminated.
You can list the user-managed keys on any service account like this:
gcloud iam service-accounts keys list \
[email protected] \
--managed-by=user
Why it matters
The core problem with user-managed keys is that they are static, long-lived credentials. By default they do not expire. A key you created in 2021 still works today unless you explicitly deleted it. That makes them an attractive target and a liability.
How these keys actually get leaked
The attack scenarios here are not theoretical. They happen constantly:
- Committed to source control. A JSON key gets added to a repo "temporarily" and ends up in the git history forever. Scanners like GitGuardian and GitHub secret scanning find thousands of these every week.
- Baked into container images. A key copied into a Docker image during build stays in a layer even if you delete it later. Anyone who pulls the image can extract it.
- Left in CI/CD logs or environment dumps. Debug output that prints environment variables can expose the entire key.
- Shared over chat or email. Keys get passed around teams in plain text and live in message history indefinitely.
Once an attacker has the key, they authenticate as the service account from anywhere on the internet. If that account has broad permissions, and many do because of over-provisioning, the blast radius is enormous. There is no MFA on a service account key. The key is the only thing standing between an attacker and your project.
Danger: A leaked service account key with the roles/editor or roles/owner role gives an attacker near-total control of your project. They can spin up crypto-mining instances, exfiltrate data from Cloud Storage and BigQuery, and create new keys to maintain persistence. Treat any exposed key as a full compromise.
The compliance angle
This is also a recognized control failure under most major frameworks. The CIS Google Cloud Platform Foundations Benchmark explicitly recommends against user-managed keys and requires rotation within 90 days if you must use them. SOC 2, ISO 27001, and PCI DSS all expect strong credential management, and unmanaged long-lived keys are a frequent audit finding.
How to fix it
The right fix depends on what the key is being used for. In most cases you can eliminate the key entirely. Work through these in order of preference.
Step 1: Find out what is using the key
Before you delete anything, figure out where the key is in use. Deleting an active key will break whatever depends on it. Check key usage in Cloud Logging:
gcloud logging read \
'protoPayload.authenticationInfo.principalEmail="[email protected]"' \
--limit=50 \
--format='table(timestamp, protoPayload.methodName, protoPayload.requestMetadata.callerIp)'
You can also enable and review the service account key usage view in the IAM console to see when each key was last authenticated.
Warning: Deleting a key that is still in active use will cause authentication failures and downtime for whatever workload relies on it. Always confirm the key is unused or migrate the workload first.
Step 2: Replace the key with a keyless alternative
This is the real fix. Most workloads do not need a downloaded key at all.
Running inside GCP? Use the attached service account. Compute Engine, Cloud Run, GKE, Cloud Functions, and App Engine can all authenticate using the metadata server with no key file. Just attach the service account to the resource and use Application Default Credentials in your code.
# Attach a service account to a Cloud Run service, no key needed
gcloud run services update my-service \
[email protected]
Running outside GCP (AWS, Azure, GitHub Actions, on-prem)? Use Workload Identity Federation. It lets external workloads exchange their native identity for short-lived GCP tokens without any stored key.
# Create a workload identity pool
gcloud iam workload-identity-pools create my-pool \
--location=global \
--display-name="External CI pool"
# Create a provider for GitHub Actions OIDC
gcloud iam workload-identity-pools providers create-oidc github-provider \
--location=global \
--workload-identity-pool=my-pool \
--issuer-uri="https://token.actions.githubusercontent.com" \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository"
# Let the GitHub repo impersonate the service account
gcloud iam service-accounts add-iam-policy-binding \
[email protected] \
--role=roles/iam.workloadIdentityUser \
--member="principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/my-pool/attribute.repository/my-org/my-repo"
Tip: For GKE workloads, use GKE Workload Identity. It maps a Kubernetes service account to a GCP service account so pods authenticate without ever touching a key file. This removes an entire class of leaked-key incidents.
Need a human to authenticate locally? Use short-lived credentials with impersonation instead of a key:
# Authenticate as yourself, then impersonate the service account
gcloud auth application-default login
gcloud config set auth/impersonate_service_account \
[email protected]
Step 3: Delete the user-managed key
Once nothing depends on the key, delete it. List the keys first to get the key ID:
gcloud iam service-accounts keys list \
[email protected] \
--managed-by=user
Danger: Key deletion is irreversible and immediate. There is no recovery and no grace period. Any client still holding this key will start failing authentication the moment you delete it.
gcloud iam service-accounts keys delete KEY_ID \
[email protected]
If you genuinely cannot avoid a key
Some legacy systems still require a static key. If that is your situation, do not download the key to disk in plaintext. Store it in Secret Manager, restrict access tightly, and enforce rotation. Create a fresh key, deploy it, then delete the old one:
# Create a new key and store it directly in Secret Manager
gcloud iam service-accounts keys create /tmp/key.json \
[email protected]
gcloud secrets create my-sa-key --data-file=/tmp/key.json
shred -u /tmp/key.json
How to prevent it from happening again
Fixing existing keys is only half the job. Without guardrails, someone will create a new one next week. Lock the door.
Block key creation with an org policy
GCP has a built-in organization policy constraint that disables service account key creation entirely. This is the single most effective control:
gcloud resource-manager org-policies enable-enforce \
iam.disableServiceAccountKeyCreation \
--organization=ORGANIZATION_ID
You can apply it at the org, folder, or project level. Apply it broadly and grant exceptions only where a legitimate need exists.
Note: There is a companion constraint, iam.disableServiceAccountKeyUpload, that blocks uploading externally generated public keys. Enable both for full coverage.
Set a maximum key age
For projects that need an exception, enforce an expiry window so stale keys cannot linger:
gcloud resource-manager org-policies set-policy policy.yaml
# policy.yaml
constraint: constraints/iam.serviceAccountKeyExpiryHours
listPolicy:
allowedValues:
- "2160h" # 90 days max
Gate it in Terraform
If you provision service accounts with Terraform, catch any google_service_account_key resource in code review or CI before it ever reaches your project. A simple OPA or Conftest policy works well:
# deny.rego
package main
deny[msg] {
resource := input.resource_changes[_]
resource.type == "google_service_account_key"
msg := sprintf("User-managed service account key not allowed: %s", [resource.address])
}
Wire this into your pipeline so the plan fails before apply:
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
conftest test plan.json --policy deny.rego
Tip: Pair the org policy with continuous monitoring in Lensix. The org policy stops new keys, and the iam_useruploadedsakeys check catches anything created before the policy was in place or through a path the policy does not cover.
Best practices
- Default to keyless. Treat a downloaded service account key as a last resort, not a starting point. Workload Identity Federation and attached service accounts cover the overwhelming majority of use cases.
- Scope service accounts tightly. If a key does leak, least privilege limits the damage. Avoid
roles/editorandroles/owneron automation accounts. Grant only the specific permissions the workload needs. - One service account per workload. Do not share a single account and its key across multiple services. Shared keys make rotation painful and incident scope unclear.
- Never store keys in source control. Add JSON key patterns to your
.gitignoreand run a secret scanner like Gitleaks or GitGuardian in CI. - Monitor key usage. Enable service account key usage tracking and alert on keys that have not been used in 90 days. Unused keys are pure risk with no benefit.
- Rotate what you cannot remove. If a key must exist, rotate it on a schedule and store it in Secret Manager, never on disk or in plaintext config.
The goal is simple: get to zero user-managed keys. Every key you eliminate is one fewer secret that can leak, one fewer thing to rotate, and one fewer audit finding. Start with the highest-privilege accounts, migrate their workloads to keyless authentication, and turn on the org policy so the problem stays solved.

