feat(kms): report which keys have outlived their rotation period (#5769)

rustfs/backlog#1636 rejected a built-in rotation scheduler: rotation is a policy decision with a per-backend cost and a hard upgrade-ordering constraint, and a server that rotated on its own would make that decision on an operator's behalf at a moment they did not choose. This is what that issue resolved to deliver instead — the signal, without the actuator.

RUSTFS_KMS_ROTATION_MAX_AGE_SECS names the period. Unset leaves the verdict unreported rather than assuming a policy, because how often keys must be rotated is a compliance decision and a built-in default would report keys as overdue against a rule nobody wrote; an unparsable value is refused the same way, loudly. Values below an hour are raised to it, since a threshold of seconds reports every key as overdue moments after it was rotated and teaches operators to ignore the signal.

KeyInfo gains rotation_due and rotation_due_reason, both additive on the wire and both filled in by the manager rather than by each backend, so no two backends can disagree about what overdue means. A backend that does not advertise rotation reports unsupported and is never reported as due — it must not be told to do something it cannot. A key with no recorded rotation is measured from creation, which is how long its material has actually been in use, and is distinguished from a stale rotation so an operator can tell "overdue again" from "never once". Ages are computed saturating, so a timestamp from a node running ahead cannot manufacture an overdue key.

The verdict is advisory in the strongest sense: nothing consults it before encrypting or decrypting, a key reported as due keeps serving traffic, and readiness is unaffected.

The single-key describe response deliberately does not carry the verdict. Its type records a creation date but no rotation timestamp, so a verdict computed there could not tell a key rotated last week from one never rotated, and reporting never_rotated for a key that was in fact rotated is worse than reporting nothing.

The wraps-based branch the issue also specifies is not implemented: it depends on the per-key wrap accounting that does not exist yet.
This commit is contained in:
Zhengchao An
2026-08-06 23:13:01 +08:00
committed by GitHub
parent 656a2f14bf
commit da82fd995e
14 changed files with 303 additions and 7 deletions
+5 -1
View File
@@ -503,7 +503,7 @@ mod tests {
use jiff::Zoned;
use rustfs_kms::{
KeyImpactReport, KeyInfo, KeyMetadata, KeyReference, KeyReferenceKind, KeyState, KeyStatus, KeyUsage, KmsError,
ReferenceScope,
ReferenceScope, RotationDueReason,
};
use rustfs_policy::policy::action::{Action, AdminAction, KmsAction};
use rustfs_policy::policy::{Args, Policy};
@@ -972,6 +972,10 @@ mod tests {
created_at: fixed_zoned("2026-01-01T00:00:00Z[UTC]"),
rotated_at: Some(fixed_zoned("2026-01-15T00:00:00Z[UTC]")),
created_by: Some("admin".to_string()),
// Pinned as a key with a verdict: the empty case is already the
// default, and only a populated one fixes the wire names.
rotation_due: true,
rotation_due_reason: Some(RotationDueReason::Age),
}
}
@@ -14,6 +14,8 @@ expression: "stable_json_value(ListKeysApiResponse\n{\n keys: vec![snapshot_k
"origin": "RUSTFS_KMS"
},
"rotated_at": "2026-01-15T00:00:00+00:00[UTC]",
"rotation_due": true,
"rotation_due_reason": "age",
"status": "Active",
"tags": {
"name": "key-a"
@@ -14,6 +14,8 @@ expression: "stable_json_value(ListKeysApiResponse\n{\n keys: vec![snapshot_k
"origin": "RUSTFS_KMS"
},
"rotated_at": "2026-01-15T00:00:00+00:00[UTC]",
"rotation_due": true,
"rotation_due_reason": "age",
"status": "Active",
"tags": {
"name": "key-a"
@@ -14,6 +14,8 @@ expression: "stable_json_value(ListKmsKeysResponse\n{\n success: true, messag
"origin": "RUSTFS_KMS"
},
"rotated_at": "2026-01-15T00:00:00+00:00[UTC]",
"rotation_due": true,
"rotation_due_reason": "age",
"status": "Active",
"tags": {
"name": "key-a"
@@ -14,6 +14,8 @@ expression: "stable_json_value(ListKmsKeysResponse\n{\n success: true, messag
"origin": "RUSTFS_KMS"
},
"rotated_at": "2026-01-15T00:00:00+00:00[UTC]",
"rotation_due": true,
"rotation_due_reason": "age",
"status": "Active",
"tags": {
"name": "key-a"