RPF-WP-0064-T07: in-memory /openbao/audit for the restored file audit device; native reseal regression
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 4384@bnt-lap001 Assistant-Session: 80d1f3da-7a91-4519-9737-9ba128b4da64
This commit is contained in:
parent
624843e122
commit
7084b57d57
4 changed files with 39 additions and 1 deletions
|
|
@ -155,6 +155,11 @@ TMPFS = {
|
|||
# The image declares VOLUMEs here; tmpfs overlays stop anonymous volumes.
|
||||
"/openbao/file": "rw,nosuid,nodev,noexec,size=1048576,uid=100,gid=1000,mode=0700",
|
||||
"/openbao/logs": "rw,nosuid,nodev,noexec,size=1048576,uid=100,gid=1000,mode=0700",
|
||||
# Production declares a file audit device at /openbao/audit/openbao-audit.log
|
||||
# (rapp-openbao values). The restored audit table re-enables it on unseal; if
|
||||
# the path is unwritable OpenBao seals itself again (T07 attempts -01/-02).
|
||||
# The log holds HMAC'd entries only and dies with the container.
|
||||
"/openbao/audit": "rw,nosuid,nodev,noexec,size=16777216,uid=100,gid=1000,mode=0700",
|
||||
}
|
||||
DIGEST_RE = re.compile(r"^sha256:[a-f0-9]{64}$")
|
||||
IMAGE_RE = re.compile(r"^" + re.escape(IMAGE_REPOSITORY) + r"@(sha256:[a-f0-9]{64})$")
|
||||
|
|
|
|||
|
|
@ -72,8 +72,13 @@ def make_source_root(base: Path, mutate: dict[str, bytes] | None = None) -> tupl
|
|||
# node id differing from this voter still restored and elected a leader, so the
|
||||
# node id alone did not explain T07 attempt -01; it stays aligned with production.
|
||||
PRODUCTION_NODE_ID = "openbao-0"
|
||||
# It also declares production's file audit device (rapp-openbao values), so the
|
||||
# snapshot's audit table re-enables it on the target after unseal.
|
||||
PRODUCTION_AUDIT_PATH = "/openbao/audit/openbao-audit.log"
|
||||
SOURCE_CONFIG = json.dumps({**json.loads(adapter.SERVER_CONFIG),
|
||||
"storage": {"raft": {"path": "/openbao/data", "node_id": PRODUCTION_NODE_ID}}},
|
||||
"storage": {"raft": {"path": "/openbao/data", "node_id": PRODUCTION_NODE_ID}},
|
||||
"audit": {"file": {"file": {"description": "rehearsal mirror of production",
|
||||
"options": {"file_path": PRODUCTION_AUDIT_PATH}}}}},
|
||||
separators=(",", ":"))
|
||||
|
||||
|
||||
|
|
@ -115,6 +120,9 @@ class SyntheticBao:
|
|||
if leader.returncode == 0 and json.loads(leader.stdout)["data"].get("is_self"):
|
||||
break
|
||||
time.sleep(0.5)
|
||||
audits = json.loads(self.bao(["audit", "list", "-format=json"], auth=True, check=True).stdout)
|
||||
assert any(a.get("type") == "file" and a.get("options", {}).get("file_path") == PRODUCTION_AUDIT_PATH
|
||||
for a in audits.values()), "source must carry production's file audit device"
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
|
|
|
|||
|
|
@ -85,6 +85,19 @@ class NativeRehearsal(unittest.TestCase):
|
|||
self.assertEqual(out["reason"], "custody_identity_refused")
|
||||
self.assertEqual(out["network"]["docker_networks"], [])
|
||||
|
||||
def test_unwritable_audit_path_reseals_and_aborts(self):
|
||||
# Regression for T07 attempts -01/-02: without /openbao/audit the restored
|
||||
# file audit device fails after unseal and OpenBao seals itself again.
|
||||
removed = R.adapter.TMPFS.pop("/openbao/audit")
|
||||
try:
|
||||
session, receipt = self.run_session()
|
||||
out = self.finish(session, receipt)
|
||||
finally:
|
||||
R.adapter.TMPFS["/openbao/audit"] = removed
|
||||
self.assertEqual(out["reason"], "leader_not_elected")
|
||||
self.assertTrue(out["restored"]["unsealed_with_original_shares"])
|
||||
self.assertIs(out["restored"]["leader_wait"]["last_seal"]["sealed"], True)
|
||||
|
||||
def test_wrong_ciphertext_hash_aborts(self):
|
||||
session, receipt = self.run_session(adm_override={"ciphertext_sha256": R.adapter.sha256_bytes(b"other")})
|
||||
self.assertEqual(self.finish(session, receipt)["reason"], "ciphertext_mismatch")
|
||||
|
|
|
|||
|
|
@ -390,6 +390,18 @@ The adapter now aligns the node id with production, waits up to 180 s, aborts as
|
|||
records bounded typed `sys/leader`/`sys/seal-status` fields and a redacted first error line in
|
||||
`restored.leader_wait`. Offline 19 and native 12 pass. A new `-02` admission is needed for the new adapter commit.
|
||||
|
||||
2026-10-24 attempt `-02` (admission `33c8aab2…75ab`, adapter `624843e`): two runs refused at the first prompt
|
||||
(`custody_identity_refused`, clipboard content; the key file and a shape-only paste check were correct), then a
|
||||
third run reached `aborted`, `leader_not_elected` after 236 s. Its `leader_wait` diagnostics showed the cause:
|
||||
after the original shares unsealed the node, seal-status read `sealed: true` and `sys/leader` returned API errors
|
||||
for 180 s. Production declares a file audit device at `/openbao/audit/openbao-audit.log` (rapp-openbao values);
|
||||
the restored audit table re-enables it on unseal, the target had no writable `/openbao/audit`, and OpenBao sealed
|
||||
itself again. Both `-02` aborted receipts (`cfb2c2af…`, `4c5cd362…`, `904a9d95…` incl. the refusals) verified
|
||||
independently from a clean clone at `624843e`. Fix: an in-memory `/openbao/audit` on the target. The rehearsal
|
||||
source now carries production's file audit device, and a native regression case without that mount reproduces the
|
||||
reseal (`leader_not_elected`, `sealed: true`), while the full restore passes with it. Offline 19 and native 13 pass.
|
||||
A `-03` admission is needed for the new adapter commit.
|
||||
|
||||
Run the admitted adapter once. The outcome is a T03 receipt verified
|
||||
independently. The receipt is kept outside Git and referenced by its hash. A
|
||||
missing object or failed check is a valid `refused` outcome; it is not repaired
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue