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:
codex 2026-10-14 11:16:16 +02:00
parent 63df822f6c
commit aee6fe79d7
4 changed files with 86 additions and 5 deletions

View file

@ -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"
}
}

View file

@ -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()

View file

@ -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:

View file

@ -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).