Service accounts carrying downloadable user-managed keys
What does ZopNight detect here?
User-managed service account keys are static credentials that keep working until explicitly deleted. Leaked once, they authenticate indefinitely. ZopNight raises RC-131 at medium severity when the discoverer records user_managed_keys_present=true on a service-account principal. Workload Identity Federation removes the key file entirely, which beats any rotation cadence, including 90-day.
Signal and threshold
| Field | Value |
|---|---|
| Rule IDs | RC-131 |
| Category | compliance |
| Severity | medium |
| Metric | none — pure configuration read |
| Source | iam_user_managed_key.go |
Where it applies
A credential file with no expiry
Creating a user-managed key downloads a JSON file containing a private key that
authenticates as the service account from anywhere on the internet, with no expiration
date. From that moment the security of the account equals the security of every place the
file ever lands: a laptop, a CI secret store, a container image layer, a git commit someone
force-pushed away but never purged. Public-repo scanners find committed GCP key files
within minutes, and unlike a session token, a harvested key does not age out. Access
persists until the key object itself is deleted. Google-managed keys have none of these
properties (they never leave Google’s infrastructure), which is why the rule counts only
USER_MANAGED keys.
The federation alternative
Most keys exist to let something outside GCP call GCP, and every mainstream case now has a
keyless answer. GKE workloads get identity through Workload Identity; Cloud Run and Compute
Engine attach service accounts natively; GitHub Actions, other clouds, and on-prem systems
authenticate via Workload Identity Federation, exchanging their own short-lived tokens for
GCP credentials. Migration order that avoids surprises: find the key’s consumer, wire up
the keyless path, run both in parallel, then delete the key and watch for authentication
failures. Where an org is ready to forbid the pattern outright, the
constraints/iam.disableServiceAccountKeyCreation org policy stops new keys from being
minted at all.
Inventory the keys that exist today
gcloud iam service-accounts keys list \ --iam-account=SA_NAME@PROJECT_ID.iam.gserviceaccount.com \ --managed-by=userAn empty result is the goal state. Anything listed is a standing credential to track down.
Presence, not age, and a fail-closed gate
Detection here is existence: the discoverer’s IAM listing (using
iam.serviceAccountKeys.list, granted through roles/iam.securityReviewer) writes
user_managed_keys_present=true on service-account principals that have at least 1
user-managed key. Age is a different question, handled by the companion rotation rule RC-132 for keys
older than 90 days, so an account can legitimately trip this rule the day a fresh key
is created. The rule narrows to service accounts via the serviceAccount: UID prefix,
fires only on the explicit true, and abstains whenever the metadata key is absent. Key
deletion is deliberately manual: revoking a credential that something may still depend on
deserves a human decision.