# GCP Service Account Key Not Rotated Within 90 Days

> Service account keys older than 90 days trip ZopNight's RC-132, a threshold aligned with CIS GCP and NIST rotation guidance. Age derives from the oldest USER_MANAGED key's validAfterTime, read via iam.serviceAccountKeys.list; the rule abstains when key_age_days is absent or unparseable rather than assuming staleness.

Source: https://zop.dev/integrations/gcp/recommendations/gcp-service-account-key-not-rotated-within-90-days
Updated: 2026-08-19

---

## What key age actually measures

Rotation is not a ritual. Key age is a proxy for exposure window. A user-managed key is a
file, and files travel: developer laptops, CI logs, tarballs, chat threads. If the key was
copied somewhere unfortunate at any point in its life, the leak stays exploitable until the
key object is deleted. A key created 400 days ago has had 400 days of opportunities to
escape, and rotating it is the only act that retroactively cuts off every copy at once. The
90-day line the rule draws (`keyRotationMaxAgeDays = 90`) matches the window common
security benchmarks (CIS GCP among them) converge on: long enough to be operationally
sane, short enough that a silent leak does not grant a year of access.

## The oldest key sets the verdict

Accounts often accumulate keys: one per environment, one from an abandoned migration, one
nobody remembers. The discoverer computes `key_age_days` from the `validAfterTime` of the
oldest USER_MANAGED key on the account, so a fresh key added yesterday does not mask the
3-year-old one still valid beside it. That is the right bias for a security signal: the
attacker gets to pick which key to use, so the finding tracks the worst one.

## Reproduce the age math

```bash
gcloud iam service-accounts keys list \
  --iam-account=SA_NAME@PROJECT_ID.iam.gserviceaccount.com \
  --managed-by=user --format="table(name.basename(), validAfterTime)"
```

Any `validAfterTime` more than 90 days in the past reproduces the finding.

## Rotating without an outage

The zero-downtime sequence is create-migrate-delete, in that order: mint a new key, update
every consumer (secret manager entries, CI variables, deployed configs), confirm from audit
logs that authentication has moved to the new key ID, then delete the old key. Deleting
first is how rotation becomes an incident. Teams that do this more than twice usually
automate the cadence, or skip the treadmill entirely by moving the workload to Workload
Identity Federation, after which the companion rule RC-131 stops firing too, because no
key file exists to age.

## When the rule says nothing

Evaluation is fail-closed at every step: rows without the `serviceAccount:` UID prefix are
ignored (human users are never scored), and if `key_age_days` is missing or does not parse
as an integer the rule abstains instead of guessing. An account with zero user-managed keys
therefore produces no rotation finding at all. The only permission behind the signal is
the read `iam.serviceAccountKeys.list`, carried by `roles/iam.securityReviewer`; rotation
itself is always performed by you, not by ZopNight.
