RPF-WP-0015-T02: lease harness ignores attest Job pods and avoids attest-chain schedule
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 680842@bnt-lap001 Assistant-Session: 54eb9e8e-7d6b-4f0c-b7a6-a6db62075883
This commit is contained in:
parent
63df822f6c
commit
aee6fe79d7
4 changed files with 86 additions and 5 deletions
|
|
@ -140,7 +140,7 @@
|
|||
"docs/audit-core-database-lease-recovery.md": "5aa8f2723d7216d775990e0b2cd770290284b62d185526a9dffe7d6a4f925d95",
|
||||
"docs/openbao-reboot-snapshot-receipt.example.json": "b95903a3e1aacb607eecc3f0440f08e62480d09da47bd7ab96c3eb8814f2eaee",
|
||||
"docs/railiance01-coordinated-reboot.md": "9969c2f498587710154e8d9a650fb8a13159e9e7b99918291dbdf577b131b7e7",
|
||||
"scripts/audit-core-database-lease-recovery.py": "c87fb026c527fc17116067c5742d324340707ce4aecf440d0d4e618db9b872a1",
|
||||
"scripts/audit-core-database-lease-recovery.py": "0a214a492af863a0e396625b50fbd795623f7e4b928240cb43f9becfac4fd387",
|
||||
"scripts/audit-core-recovery-preflight.py": "248c663bad31860dd4f62870db00f6d624143d18c18f86634a39ceda12c7bc66"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -220,11 +220,52 @@ def secret_state(remote: Any) -> dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
ATTEST_CRONJOB = "audit-core-attest-chain"
|
||||
ATTEST_GUARD = timedelta(minutes=20)
|
||||
|
||||
|
||||
def next_daily_run(schedule: str, time_zone: str | None, now: datetime) -> datetime:
|
||||
"""Next run of a fixed daily "M H * * *" UTC schedule; anything else fails closed."""
|
||||
fields = schedule.split()
|
||||
if (
|
||||
time_zone not in ("Etc/UTC", "UTC")
|
||||
or len(fields) != 5
|
||||
or fields[2:] != ["*", "*", "*"]
|
||||
or not fields[0].isdigit()
|
||||
or not fields[1].isdigit()
|
||||
):
|
||||
raise ProcedureError("attest-chain schedule is not a fixed daily UTC time; choose the window manually")
|
||||
run = now.replace(hour=int(fields[1]), minute=int(fields[0]), second=0, microsecond=0)
|
||||
return run if run >= now - ATTEST_GUARD else run + timedelta(days=1)
|
||||
|
||||
|
||||
def assert_attest_chain_quiet(cronjob: dict[str, Any], now: datetime) -> None:
|
||||
"""The attest CronJob mounts the same database Secret; never revoke under it."""
|
||||
spec = cronjob.get("spec", {})
|
||||
if cronjob.get("status", {}).get("active"):
|
||||
raise ProcedureError("an attest-chain Job is active; wait for it to finish")
|
||||
if spec.get("suspend"):
|
||||
return
|
||||
run = next_daily_run(str(spec.get("schedule", "")), spec.get("timeZone"), now)
|
||||
if abs(run - now) < ATTEST_GUARD:
|
||||
raise ProcedureError("window is within 20 minutes of the attest-chain schedule")
|
||||
|
||||
|
||||
def attest_chain_state(remote: Any) -> None:
|
||||
cronjob = remote.kubectl_json(
|
||||
["-n", "audit-core", "get", "cronjob", ATTEST_CRONJOB],
|
||||
label="read attest-chain schedule",
|
||||
)
|
||||
assert_attest_chain_quiet(cronjob, datetime.now(UTC))
|
||||
|
||||
|
||||
def pod_state(remote: Any) -> dict[str, Any]:
|
||||
pods = remote.kubectl_json(
|
||||
["-n", "audit-core", "get", "pods", "-l", "app.kubernetes.io/name=audit-core"],
|
||||
label="read audit-core pod metadata",
|
||||
).get("items", [])
|
||||
pods = PREFLIGHT.serving_pods(
|
||||
remote.kubectl_json(
|
||||
["-n", "audit-core", "get", "pods", "-l", "app.kubernetes.io/name=audit-core"],
|
||||
label="read audit-core pod metadata",
|
||||
)
|
||||
)
|
||||
if len(pods) != 1:
|
||||
raise ProcedureError(f"expected one audit-core pod, observed {len(pods)}")
|
||||
statuses = pods[0].get("status", {}).get("containerStatuses", [])
|
||||
|
|
@ -304,6 +345,7 @@ def exercise(args: argparse.Namespace) -> dict[str, Any]:
|
|||
if not preflight["ready_for_live_execution"]:
|
||||
raise ProcedureError("database lease recovery preflight is not ready")
|
||||
|
||||
attest_chain_state(remote)
|
||||
before_pod = pod_state(remote)
|
||||
before_secret = secret_state(remote)
|
||||
lease = authority.single_runtime_lease()
|
||||
|
|
|
|||
|
|
@ -39,6 +39,31 @@ def approved_receipt(now: datetime) -> dict:
|
|||
|
||||
|
||||
class DatabaseLeaseRecoveryTests(unittest.TestCase):
|
||||
def test_attest_chain_guard_refuses_active_job_and_schedule_overlap(self) -> None:
|
||||
cronjob = {"spec": {"schedule": "17 3 * * *", "timeZone": "Etc/UTC"}, "status": {}}
|
||||
quiet = datetime(2026, 10, 15, 10, 0, tzinfo=UTC)
|
||||
module.assert_attest_chain_quiet(cronjob, quiet)
|
||||
for near in (datetime(2026, 10, 15, 3, 0, tzinfo=UTC), datetime(2026, 10, 15, 3, 30, tzinfo=UTC)):
|
||||
with self.assertRaises(module.ProcedureError):
|
||||
module.assert_attest_chain_quiet(cronjob, near)
|
||||
with self.assertRaises(module.ProcedureError):
|
||||
module.assert_attest_chain_quiet({**cronjob, "status": {"active": [{"name": "j"}]}}, quiet)
|
||||
with self.assertRaises(module.ProcedureError):
|
||||
module.assert_attest_chain_quiet(
|
||||
{"spec": {"schedule": "*/5 * * * *", "timeZone": "Etc/UTC"}, "status": {}}, quiet
|
||||
)
|
||||
|
||||
def test_pod_state_ignores_attest_chain_job_pods(self) -> None:
|
||||
class Remote:
|
||||
def kubectl_json(self, args, label):
|
||||
owned = lambda kind, uid: {
|
||||
"metadata": {"uid": uid, "ownerReferences": [{"kind": kind}]},
|
||||
"status": {"containerStatuses": [{"restartCount": 0}]},
|
||||
}
|
||||
return {"items": [owned("ReplicaSet", "serving"), owned("Job", "attest")]}
|
||||
|
||||
self.assertEqual("serving", module.pod_state(Remote())["uid"])
|
||||
|
||||
def test_approval_requires_exact_owner_and_window_contract(self) -> None:
|
||||
now = datetime(2026, 8, 22, 12, 0, tzinfo=UTC)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
|
|
|
|||
|
|
@ -378,3 +378,17 @@ Both preflights then pass every automated check with no secret value observed; o
|
|||
operator gates remain. The review contract's preflight hash is refreshed, so owner
|
||||
receipts must be renewed against it. Unrelated pre-existing test failures (credential
|
||||
change, factory audit delivery, informed_decision import) are unchanged.
|
||||
|
||||
Owner review round 2026-10-14 (contract b31836ca): railiance-platform approved;
|
||||
audit-core, rapp-postgres, railiance-cluster and railiance-infra requested changes
|
||||
(hub messages 570ca050, a190118e, 4e9d4e58, e3b7bb24). T02 fixes applied: the live
|
||||
harness now uses the preflight's serving-pod filter and refuses while an attest-chain
|
||||
Job is active or within 20 minutes of its 03:17 UTC schedule (the Job mounts the same
|
||||
database Secret). rapp-postgres confirms revocation terminates the role's sessions, so
|
||||
readiness does drop. audit-core registered the T02 load driver (8c8bcf4, receipt
|
||||
2de1c6e7). Still open for T02: a T02 synthetic-sender projection lane (no generic one
|
||||
exists; the -02 Whitehat procedure must not be copied). Open for T03: the reboot
|
||||
checklist revisions requested by cluster/infra/postgres/platform (full-cluster recovery
|
||||
checks, pre-reboot failing-pod baseline, all CNPG clusters, firewall/boot-id/time sync,
|
||||
named console path, infra abort ownership, ops-warden login revision re-acceptance and
|
||||
stop-on-credential-output rule, PVC/cluster UID capture).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue