Skip to content

fix(gcp): default KMS key rotation to 90 days and reject sub-30-day periods - #370

Open
Cre-eD wants to merge 2 commits into
mainfrom
fix/kms-rotation-default
Open

fix(gcp): default KMS key rotation to 90 days and reject sub-30-day periods#370
Cre-eD wants to merge 2 commits into
mainfrom
fix/kms-rotation-default

Conversation

@Cre-eD

@Cre-eD Cre-eD commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Problem

When keyRotationPeriod is unset, the provisioned GCP secrets-provider key defaults to 100000s — 27.8 hours. That reads like a typo for 10000000s (~116 days), and because it sits just 16% above GCP's own 86400s floor it passes provider validation silently.

It is not a cosmetic default:

  • Cloud KMS bills every ACTIVE key version — ENABLED, DISABLED and DESTROY_SCHEDULED all count; only DESTROYED is free.
  • Rotation never re-encrypts existing ciphertext, so every version a key mints stays load-bearing (something still decrypts with it) and billed for the lifetime of the key.
  • A key is provisioned per stack, so daily rotation adds a billed version per stack per day, indefinitely.

The cost therefore compounds rather than plateauing. Observed on a real fleet: thousands of accrued key versions across a few dozen stacks, dominating that project's KMS spend and growing every month. Nothing surfaces it, because each individual version costs cents and the resource looks healthy.

Changes

  • Default rotation period 100000s7776000s (90 days).
  • ValidateKeyRotationPeriod() rejects explicit periods under 30 days, plus malformed values (missing s suffix, non-integer, duration shorthand like 90d). The next such typo fails at provisioning time instead of becoming an unattributed bill months later. GCP's own floor of 1 day is deliberately not the bound here — a few hours or days is valid to GCP and still wrong for a per-stack provisioned key.
  • EffectiveKeyRotationPeriod() centralises the default so the provisioner and any future consumer cannot disagree about it.

Compatibility

Existing keys are unaffected — rotation period is an in-place property, and changing the default does not alter already-provisioned keys or their existing versions. Operators who deliberately want faster rotation can still set keyRotationPeriod, down to the 30-day floor.

Tests

pkg/clouds/gcloud/kms_rotation_test.go: default fallback and explicit-value precedence; a guard that the default itself satisfies the floor and stays well clear of GCP's 1-day minimum (so 100000s cannot be reintroduced); and a table covering the floor boundary (2592000s accepted, 2591999s rejected), 100000s, 86400s, and each malformed form. Full pkg/clouds/gcloud suite passes with no regressions.

…eriods

The provisioned secrets-provider key defaulted to a 100000s rotation
period when keyRotationPeriod was unset. That is 27.8 hours, which reads
like a typo for 10000000s (~116 days) and sits just 16% above GCP's own
86400s floor, so it passes provider validation silently.

It is not a cosmetic default. Cloud KMS bills every ACTIVE key version
(ENABLED, DISABLED and DESTROY_SCHEDULED all count; only DESTROYED is
free) and rotation never re-encrypts existing ciphertext, so every
version a key mints stays load-bearing and billed for the lifetime of
the key. Since a key is provisioned per stack, a daily rotation adds a
billed version per stack per day indefinitely, and the resulting cost
compounds rather than plateauing. Observed in a real fleet: thousands of
accrued versions across a few dozen stacks, dominating the project's KMS
spend and growing every month.

Changes:
- default rotation period 100000s -> 7776000s (90 days)
- ValidateKeyRotationPeriod rejects explicit periods under 30 days, plus
  malformed values (no 's' suffix, non-integer, duration shorthand), so
  the next such typo fails at provisioning time instead of becoming an
  unattributed bill months later
- EffectiveKeyRotationPeriod centralises the default so the provisioner
  and any future consumer cannot disagree

Existing keys are unaffected: rotation period is an in-place property and
changing the default does not alter already-provisioned keys or their
versions. Operators who deliberately want faster rotation can still set
keyRotationPeriod explicitly, down to the 30-day floor.

Signed-off-by: Dmitrii Creed <creeed22@gmail.com>
@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown

Semgrep Scan Results

Repository: api | Commit: 42db120

Check Status Details
⚠️ Semgrep Warning 1 warning(s), 5 total

Scanned at 2026-07-28 16:35 UTC

@github-actions

Copy link
Copy Markdown

📊 Statement coverage

Measured on the documented included set (see docs/TESTING.md → Coverage scope). Observe-only — no regression gate is enforced yet.

Scope This PR main baseline Δ
Included set (Gold-tier denominator) 90.4% 90.4% +0.0 pp
Full set (whole repo, transparency) 28.3% 28.3% +0.0 pp

Baseline: main @ 24c1417

@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown

Security Scan Results

Repository: api | Commit: 42db120

Check Status Details
✅ Secret Scan Pass No secrets detected
⚠️ Dependencies (Trivy) High 3 high, 6 total
⚠️ Dependencies (Grype) High 3 high, 7 total
📦 SBOM Generated 523 components (CycloneDX)

Scanned at 2026-07-28 16:36 UTC

Review follow-ups on the KMS rotation default:

- Validation ran AFTER enableServicesAPI (which mutates the project) and
  AFTER kms.NewKeyRing. A GCP KeyRing can never be deleted (destroying the
  Pulumi resource only drops it from state), so a mistyped rotation period
  left a permanent, un-recreatable-by-name KeyRing behind. Validation now
  runs immediately after the config type assertion, before any side effect.

- A hard sub-30-day floor is a breaking change for consumers with a
  deliberate short rotation (7-day rotation is a common compliance
  setting), and cost is not a good reason to break someone's key-rotation
  policy. Added allowShortKeyRotation to keep the floor a typo-catcher
  rather than an imposed policy; the error message names the escape hatch,
  and the opt-out does not relax the malformed-value checks.

- One test case could not fail for the reason its name implied:
  "ninetydays" ends in 's', so it took the numeric branch whose message
  also contains "'s' suffix". It now asserts the numeric message, keeping
  the two branches distinguishable, plus cases for a bare suffix and a
  negative value.

Signed-off-by: Dmitrii Creed <creeed22@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant