Service account keys older than 90 days
What does ZopNight detect here?
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.
Signal and threshold
| Field | Value |
|---|---|
| Rule IDs | RC-132 |
| Category | compliance |
| Severity | medium |
| Metric | none — pure configuration read |
| Source | iam_key_rotation.go |
Where it applies
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
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.