audit-core/workplans/AUDIT-WP-0008-tenancy-posture-alignment.md
codex 0bc4165cf2
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
fix(workplans): adopt ADR-007 derived identifiers for unregistered records
These workplans exist only in the retired local hub. Their random pre-ADR-007
identifiers are refused by C-06 as stale references, so they cannot be
registered. Deriving from the canonical record id takes no identity from
anything: central does not hold them and the old ids die with the cache.

Records central already holds were deliberately left untouched.

Refs CUST-WP-0068-T06

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
2026-08-25 20:07:24 +02:00

683 lines
38 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: AUDIT-WP-0008
type: workplan
title: "Tenancy posture declaration and read-path enforcement"
domain: infotech
repo: audit-core
status: active
owner: claude
topic_slug: railiance
created: "2026-08-17"
updated: "2026-08-22"
depends_on:
- AUDIT-WP-0007
state_hub_workstream_id: "a9a248b2-d26c-503e-a8fc-4f3675e6ed51"
---
# AUDIT-WP-0008 — Tenancy posture declaration and read-path enforcement
## Goal
Answer the `rapp-postgres` review request against NetKingdom Tenancy Posture
v0.1, declare an accurate posture vector, and close the one gap the assessment
exposed in running code.
Canonical framework: `net-kingdom/canon/standards/tenancy-posture_v0.1.md`,
published by `policy-nexus` at `policy.coulomb.social`.
> **Framework moved twice under this workplan.** The assessment below was made
> against **draft-5**; the framework is now at **draft-8**. The assessment still
> holds — no finding was reversed — but three things changed underneath it:
> `R` moved 1 → 2 once `rapp-postgres` declared the window, a sixth axis **V
> (availability)** appeared that we had no position on, and the
> implemented-versus-evidenced distinction we used informally for `E` became a
> schema field. `tenancy.yaml` tracks draft-8 and validates clean against
> `net-kingdom/tools/tenancy-posture/validate.py`. T07 and T08 exist because of
> draft-8, not because of the original review.
## Assessment — what the ladders say about us
`rapp-postgres` guessed `audit-core I2 A3 E2 P1 R1`, and §5 carries that guess
as a worked example. Three of the five are too high.
| Axis | Guess | Actual | Why |
|---|---|---|---|
| I | 2 | **1** | Senders hold static bearer tokens (`audit_core/senders.py`); the tenant arrives in the request body (`audit_core/ingestion.py:379`) and is checked against an allowlist bound to the credential. No IdP, no verified claim. §4.1 names this case explicitly: I1 absorbs "the tenant is taken from the request rather than from a verified token". |
| A | 3 | **2** | No `flex-auth` call exists anywhere in the repo. Authorization is a single local boundary — `permits_source`, `permits_tenant`, `may_write`, `may_read`. That is the definition of A2. |
| E | 2 | **1** | The write path is E2-shaped. The read path applies **no tenant filter at all**. See below. |
| P | 1 | 1 | `platform-pg`, database per consumer. Correct. |
| R | 1 | 1 | No `backupRetentionDays` in `rapp-postgres/consumers/audit-core.yaml`; platform default 30 days applies. Correct. |
The I-axis correction is not a defect. The credential→tenant allowlist is a real
control; it is simply an **A**-axis control sitting where a reader would expect
an **I**-axis one. Decision 3.1 doing its job on its first live use.
### The E-axis finding
`_read` (`audit_core/ingestion.py:226-272`) gates on `identity.may_read` and
nothing else. `permits_tenant` is never called on the read path. A credential
with `may_read: true` can read **any** tenant's events through
`/v1/events?correlation_id=`, `/v1/events/<id>`, `/v1/dead-letters` and
`/v1/secret-findings`.
Exposure today is bounded: the production sender `user-engine` carries
`may_read: false`, and the read surface is ClusterIP behind a default-deny
NetworkPolicy. But bounded-by-deployment is not enforced-by-code, and under §6
declaring E2 on that basis would be the overclaim the framework exists to
prohibit. E1 is the honest reading until T04 lands.
This also corrects the premise of the review request. `rapp-postgres` wrote
"you are at E2, and E3 is more urgent for you than for anyone else". E3 is
indeed the right target for a service holding audit evidence, but §4.3 requires
the E2 evidence artifact alongside any E3 claim — so the first move is reaching
E2, not adopting RLS.
## Positions to take
### On the retention ceiling (their question 1)
**The 365-day ceiling is not wrong for us. Keep it.**
The ADR-0002 context line — "audit-core holds audit evidence and may need
years" — is right about the requirement and wrong about the mechanism. Physical
backup retention is not where multi-year audit custody belongs. It is already
routed: `data/capability/audit-core-operational.json` records `data.archive`
as an unmet requirement with a separate owner and an explicitly different
bucket and lifecycle from Barman.
Consequences we accept and want stated back:
- audit-core declares nothing above the 30-day platform default. We therefore
do **not** extend any co-resident's erasure horizon, now or under this plan.
- Multi-year retention is an archive procurement question, not a WAL-window
question. Raising the ceiling would buy us nothing and cost every co-resident
volume against 20Gi.
- Service class `batch` is correct. No correction wanted. Co-residency with
latency-critical `tenant-engine` is right to be reported under §8.3.3.
Ask of `rapp-postgres`: amend the ADR-0002 opening context to point at archive
rather than at backup retention, so the two statements agree.
**This position is conditional, and the condition is dated.** Routing multi-year
custody to `data.archive` is correct mechanically and unfunded practically:
the capability case records it as unmet, with no booked cost and no procurement
owner in motion. Taking this position without a date would read as "audit-core
is fine", and audit-core is not fine — it is correctly routed and unresourced.
Escalation: **reviewed 2026-12-31.** If no `data.archive` provision is procured
by then, audit-core reopens placement. §4.5 makes retention a placement trigger,
and P2 — a dedicated cluster with an independent window — becomes the fallback
worth arguing for. P2 still does not deliver WORM, so it is a worse answer than
archive; it is named here so the fallback exists rather than being improvised
under pressure. Recorded now so the review has something to fire against.
**Also taken now: R1 → R2.** R2 needs only that retention be declared per
dataset rather than inherited silently, and that the erasure horizon be
published. That is an explicit `backupRetentionDays: 30` in the consumer
declaration plus the horizon rendered in our operator surface — a change of
hours, not a future target. Deferring it as a "target" would have been the
framework's own §8.1 failure: a value in a file nobody re-reads.
### On erasure versus audit (their question 11)
Yes to the fact/payload split — retain the fact of an event, encrypt its
personal payload per subject, shred the key. But that resolution does not
survive contact with our integrity design as built, in two ways the framework
does not currently capture.
**One: it is not retrofittable.** `_record_hash`
(`audit_core/postgres_backend.py:583`) is SHA-256 over the canonical JSON of the
*cleartext* record, and that hash is what the chain commits to. Encrypting a
payload after the fact severs the binding between `payload_hash` and the stored
record. Shreddability has to be built as encrypt-then-hash at accept time, with
the chain committing to ciphertext. Retrofitting it onto existing events is not
possible without rewriting the chain, which is the one thing AUDIT-WP-0007 was
built to make detectable.
**Two, and sharper: the retained hash is a confirmation oracle.** A SHA-256 over
a canonical record whose fields are low-entropy (event type, actor, tenant,
subject, timestamp) lets anyone holding the hash guess the payload, hash the
guess, and confirm a match. Destroying the key does not make the content
unrecoverable while that hash survives. **Crypto-shredding does not reach R4
under this design.** It needs the integrity commitment to be an HMAC under a
per-subject key that dies with the key, or a per-record salt destroyed with it.
This is a genuine gap in §4.5, not just a fact about us: the key-destroyed route
treats key destruction as sufficient without requiring that no retained
commitment reveal the erased content. Recommend §4.5 gain that condition.
Our position: audit-core targets **R2**, not R4. Verified erasure of audit
evidence is a design tension we record rather than resolve, and a "R4
everywhere" fleet target must exempt this service explicitly.
**The exemption rests on a legal basis nobody in this estate has confirmed, and
that must be said out loud rather than assumed.** Declining to build erasure
means that if an Article 17 request arrives naming a data subject in the audit
trail, audit-core has no mechanism and must rely on audit evidence being
exempt — legal obligation, or legitimate interest in fraud and security
investigation. Those grounds are ordinary and widely relied on. They are also a
legal conclusion, and this repo is not competent to reach one. Nothing in
INTENT, SCOPE or the capability case records anyone having reached it either.
This workplan therefore **routes the question rather than absorbing it**: the
position is "we do not build erasure, on a legal basis we have named and not
validated", and the validation is somebody else's to own. An unnamed dependency
here is the difference between a recorded tension and an unnoticed liability —
and an audit service discovering in the middle of a regulator's question that
its exemption was assumed is the specific failure worth spending a paragraph to
avoid.
## Framework review findings
Sent under §20.2 — the framework is validated by whether it can describe us.
> **Outcome, 2026-08-17: all five were adopted in draft-7**, alongside reviews
> from `railiance-platform` and `flex-auth`. Finding 1 became **Decision 5.2**
> (declare per path, quote the minimum), finding 2 became **13.1a** (the floor
> needs a reason, not an artifact), finding 3 became **Decision 9.2**
> (consumer-facing credentials are named in), finding 4 became **Decision 4.5.3**
> (key destruction is not sufficient alone; the confirmation-oracle defect is
> recorded as general, with audit-core exempted from R4), and the §5 worked
> example now reads `I1 A2 E1 P1 R1` self-reported. Draft-7 also added
> **Decision 5.4**, which relocates the vector to `tenancy.yaml` in the repo
> root — see T01.
>
> Two of these now bind us rather than only the framework: 9.2 makes our static
> ingest tokens a declarable gap, and 5.5 requires a provider block. Both are in
> `tenancy.yaml`. T06 is therefore reduced to confirming receipt and correcting
> the one line draft-7 still has stale about us (E, once T05 lands).
>
> Draft-7's own status section still carries "Every correction so far was found
> by research or by relocation, not by review" and "Reviewed by nobody yet",
> three paragraphs below a list of eleven review-driven changes. Stale lines,
> worth flagging in T06 — a document about not overclaiming should not
> under-report its own review history.
1. **The E ladder cannot express asymmetric enforcement.** Our write path is E2
and our read path is E1. §4.3 assumes one level per service. This will recur
estate-wide, since most services enforce harder on write than on read, and
with no rule everyone will declare their best path.
Neither obvious fix is right alone. A bare **minimum across paths** is safe
to publish but destroys information: a service at E3-write/E1-read declares
identically to one at E1/E1, though the first is materially further along.
Bare **per-path declaration** (`E: {write: 3, read: 1}`) keeps the signal and
invites exactly the selective quoting §6 exists to stop — "our write path is
E3" is a sentence someone will say.
Recommend both, with the relationship stated: **declare per path, quote the
minimum.** The posture vector's `E` is the minimum across paths; the per-path
breakdown sits beside it and is where improvement is tracked. One sentence in
§4.3, and it is the only version that keeps honesty and signal at once.
2. **§13.1 collides with the ladder floor.** "A level is claimed only with its
evidence artifact present", but §13's table has no artifact for I0, I1, A0,
A1, E0, E1, R0 or R1. §5's own worked example of a conformant absorbed repo —
`I1 A1 E1 P0 R0` — cannot satisfy 13.1 on any axis. Either declaration alone
is the evidence at the floor rungs, or 13.1 binds only above them. Small, but
it makes the framework's flagship example non-conformant against its own rule.
3. **§9.1 omits consumer-facing credentials.** Decision 9.1 bans static
long-lived credentials for database access above E0, and we comply — leases
re-read at connection time, `audit_core/credentials.py`. But our *ingest*
credentials are static long-lived bearer tokens rotated by publishing a
second token alongside the first. §9 promotes leased credentials to "a
tenancy control"; the same argument applies with more force to the credential
that actually carries the tenant claim. The omission reads as accidental.
Name consumer-facing credentials in scope, or say deliberately that they are
not.
4. **§4.5 key-destroyed route** — add the condition in "On erasure versus audit"
above: no retained integrity commitment may reveal the erased content.
5. **§5 worked example** — correct `audit-core` to `I1 A2 E1 P1 R1`, and revise
its note. "Both E3 and R2 are urgent targets" is half right: E3 is, R2 is
nearly vacuous for us while the real requirement sits in `data.archive`.
Nothing here asks the framework to change shape. Four of five axes described us
without strain, and the one that did not (E) failed in a way that found a real
defect in our code. That is the framework working.
## Non-goals
- **E3 / row-level security is not in this plan.** ADR-0003 publishes the
contract and it is the right target, but it is blocked behind reaching E2
(§4.3) and needs the `EXPLAIN` comparison first. Declared as target with a
recorded gap in T01, not built here.
**The target carries a date: 2027-03-31.** The framework says services whose
cross-tenant exposure would be a reportable breach SHOULD target E3, and no
service on `platform-pg` fits that description better than this one. Every
quarter spent at E2-with-a-target is a quarter of stated, unclosed risk, and
an undated target is what §8.1 calls documentation rather than control. If the
date slips it slips as a decision, which is the whole point of §12.
- **No per-subject encryption is built.** T03 records a position; it does not
commit to an implementation.
- **No adversarial evidence facility is built here.** §19.3 gave that a home in
`whitehat-security`. T05 coordinates, it does not volunteer.
## Sequencing
**T04 runs before T01.** Task ids are hub-registered and not renumbered; the
execution order is T02 → T04 → T01 → T03 → T05 → T06.
The reason is narrow. A posture vector published in the repo is a canon-shaped
artifact that outlives the fortnight it takes to fix the read path, and `E1` on
an audit service is a sentence that will be quoted back long after it stopped
being true. Fixing first means the declaration is `E2` on both paths from its
first commit, and the E1 interval never enters the permanent record.
This is emphatically **not** withholding the finding. T02 goes first precisely
so the correction reaches `rapp-postgres` immediately: their guess of `E2` is
live in canon *now*, and it is wrong now. A message correcting the record is the
fast instrument; a published vector is the durable one. Deferring the durable
artifact by two weeks while the fast one goes out today is sequencing. Delaying
both would be concealment, and §6 would be right to call it that.
## Tasks
```task
id: AUDIT-WP-0008-T01
status: done
priority: high
state_hub_task_id: "3b7be6c5-1861-57d2-a9ab-5770c3ea544d"
```
Publish the posture vector. **Rewritten against draft-7**, which landed after
this task was written and moved the target: Decision 5.4 fixes the location at
**`tenancy.yaml` in the repo root**, not `docs/tenancy-posture.yaml`, and fixes
the schema — `current`, `target`, `reviewed`, `gap`, `placement_exceptions`,
`service_class`, per-path detail (5.2), and a provider block (5.5). Delivered at
that path and in that shape. Cross-linked from `SCOPE.md` and
`docs/interface-card.yaml`.
**Declared `I1 A2 E1 P1 R1`, and E is quoted at 1 on purpose.** T04 put the E2
mechanism on both paths, but §13.2 says a passing CI run is not E2 evidence:
the E2 artifact is adversarial, compares separate tenant contexts, and carries a
review date rather than a green build. Our cross-tenant tests are mechanical.
Under §13.1 the level is therefore not claimable until T05 produces the artifact,
so the vector declares 1 with the mechanism recorded in `paths.E` and the reason
in `gap.E`. Claiming E2 off unit tests would be the overclaim §6 prohibits —
and refusing exactly that kind of reasoning is what found the read-path defect
in the first place.
R stays at 1: R2 needs `backupRetentionDays: 30` in
`rapp-postgres/consumers/audit-core.yaml`, requested in T02. That file is
theirs, so R2 is not ours to declare unilaterally. The erasure horizon is
already published on `/readyz` as `recoverable_days`.
Two additions draft-7 forced that the original task did not anticipate:
- **A `credentials` gap under Decision 9.2.** Our own finding was adopted as a
rule, and it binds us: ingest credentials are static long-lived bearer tokens.
Declared as a stated gap with no dated remedy rather than a silent exclusion.
Asking for the rule and then exempting ourselves from it was not available.
- **A `provides` block under Decision 5.5.** audit-core provisions
`operations.audit`, so it declares what it makes *reachable* for a sender's
trail — E2 now, E3 pending ADR-0003, **E4 and R4 unreachable**. A sender
needing a structural cross-tenant guarantee cannot get it here and should be
told so rather than sold E2 in E4's language (§11.4).
```task
id: AUDIT-WP-0008-T02
status: done
priority: high
state_hub_task_id: "3cf1d80f-4f35-5711-aa0a-aeb27912e2f6"
```
Reply to `rapp-postgres` message `56b77e20-0152-4e09-8f76-99640269063b`.
**Runs first.** Carries four things: the retention position (ceiling stays 365,
audit-core declares nothing above the 30-day default, multi-year routes to
`data.archive`) with the **2026-12-31 escalation date and the P2 fallback named**;
the request for an explicit `backupRetentionDays: 30` in `consumers/audit-core.yaml`
to take us to R2; the ADR-0002 context amendment; and `batch` confirmed.
It also **corrects their posture guess immediately**`I1 A2 E1 P1 R1`, not
`I2 A3 E2 P1 R1`, with the read-path defect stated plainly and T04 named as the
fix in flight. Their `E2` guess is live in canon and wrong today; the correction
does not wait on T01. Mark the message read on send.
```task
id: AUDIT-WP-0008-T03
status: done
priority: high
state_hub_task_id: "3c495a5c-6b0d-5fae-b237-626d298e7b2c"
```
Answer question 11 in `docs/erasure-and-audit.md`.
**Reduced by draft-8.** The framework half is already resolved upstream: the
confirmation-oracle finding landed as **Decision 4.5.3**, and §19.11 is now
marked *framework resolved*, recording that audit-core targets R2 and is
explicitly not a fleet R4 target. There is no amendment left to send. What
remains is audit-core's own position document — the fact/payload split, the
encrypt-then-hash constraint, why the retained cleartext hash defeats key
destruction, and the R2 ceiling — so the reasoning lives in this repo rather
than only in someone else's canon.
**Route the legal question to `risk-nexus`, not `the-custodian`.** §19.11 ends
"the legal basis for retaining audit facts remains a risk/legal question outside
this framework", and `policy-nexus` INTENT records that judging what an external
rule demands of the estate moved to `risk-nexus` on 2026-08-17. That is the
owner. The position is publishable with the dependency open; it is not
publishable with the dependency hidden.
```task
id: AUDIT-WP-0008-T04
status: done
priority: high
state_hub_task_id: "ab07af53-0ca8-5d8e-b699-128d462e30a2"
```
Bind the read path to permitted tenants. `permits_tenant` governs read as it
governs write, at the same choke point: tenant-scope `/v1/events`,
`/v1/events/<id>`, `/v1/dead-letters` and `/v1/secret-findings`. Decide and
record how an operator identity holding `tenants: ["*"]` is distinguished from a
scoped reader. Tests for cross-tenant read refusal. Update `docs/interface-card.yaml`
and the operator runbook. Raises E to 2 on both paths; update T01's declaration.
```task
id: AUDIT-WP-0008-T05
status: done
priority: medium
state_hub_task_id: "76d3b34e-df74-5af0-8025-e77e8644d199"
```
Produce the §13 E2 evidence artifact — adversarial, with a review date: an
identity bound to tenant A demonstrably cannot read tenant B. Coordinate with
`whitehat-security` (WHITEHAT-WP-0001) on whether this is theirs to run or ours
to write and theirs to review. Record the review cadence as the exposure window
per §19.3.
Progress 2026-08-21: Whitehat's operating rules, attacker model, calibrated
differential harness, and `audit-core` probe pack now exist at commit `beab2a0`.
Audit-core reviewed the three-route pack and sent target-owner readiness packet
`6f2ae598-06bd-4089-a5fd-9de31688a733`. It fixes the synthetic-only scope,
excludes operator routes, caps the run at one concurrent request and ten per
minute, and requires two temporary read-enabled identities each scoped to one
fixture tenant. The production `user-engine` identity remains unchanged and is
not a test credential.
Whitehat supplied and corrected a bounded adapter, including audit-core's
required `Idempotency-Key` and deterministic retry payloads. The original
production engagement was then cancelled at the session cutoff with zero
target requests and its identifier is terminal. Whitehat subsequently added
the governed test-plane contract and fail-closed admission at `95129d7`.
Audit-core now declares a dedicated ingress peer for that plane: only a pod in
namespace `whitehat` carrying both the plane and `target=audit-core` labels can
reach port 8080 through it. Authentication and tenant scope remain mandatory.
The operator runbook records the exact two-identity fixture shape and cleanup
obligation. This is readiness, not evidence or live authorization. T05 becomes
`done` only after a new engagement is approved and owner-acknowledged, the
cluster plane and value-safe custody broker are provisioned, the bounded run
completes, and its sanitized target report reaches `risk-nexus`.
The target-side ingress policy was applied to railiance01 on 2026-08-22 and its
namespace plus both pod-label selectors were read back from the API. Preflight
coordination reserves candidate identifier `WH-ENG-20260822-AUDIT-E2-01` with
Whitehat
(`c45151e1-df1a-4306-a70a-83be7875f01c`), `ops-mason`
(`ddeff394-00b6-474c-b249-3e0f7f9d6059`), and the OpenBao owner
`railiance-platform` (`10f80080-4c83-42ba-8590-f23c582d9f05`). The candidate
window is 2026-08-22 18:0018:15Z. Whitehat will issue the new engagement only
after Mason provisions the empty namespace/network/service-account foundation;
that apply was authorized without a runner or credential in
`7e0c1d68-1a68-4345-a845-44bd0dae8373`. Operator approval and target-owner
acknowledgement remain deliberately pending.
Custody review identified that expiry of a projected Secret alone does not
invalidate a token already loaded into the audit-core process. Sender
identities now accept an optional timezone-aware `expires_at`, evaluated on
every authentication, so temporary evidence credentials fail closed at the
engagement boundary even before registry cleanup completes. The field is
optional for existing production identities and is preserved across the
non-secret scope overlay.
The expiry implementation is deployed from `abd22fa` as image
`sha256:c2fe39a0185b99be3fc0cb14d2de69772b8e66e20490097c9d11d90cc39719a6`;
the receiver rolled out 1/1 Ready and returned operational `/readyz`. Target
review found and corrected one stale, undeclared 20260821 forged fixture ID;
Whitehat commit `1418dac` makes it an explicit runner argument, declares its
20260822 value, tests invocation membership, and pins the deployed digest.
Audit-core formally acknowledged the corrected production engagement at
Whitehat commit `32afceb` on 2026-08-22T10:04:56Z. It validates for the approved
18:0018:15Z window and fails closed before the window. Custody projection and
attended cleanup are now the remaining pre-run gate, routed in
`2cf6752a-c186-44a7-9d5f-2319a2ea68a2`; no credential exists yet.
Railiance-platform delivered the mount-only projection/reload/cleanup procedure
at `f04c801` and reported its value-safe preflight clean. The procedure remains
held for the attended 2026-08-22 18:0018:15Z window: no bearer has been minted,
no runner pod has been applied, and the pre-window engagement gate still fails
closed. This is readiness evidence only; T05 remains in progress until the
bounded run and sanitized report complete.
A fresh 2026-08-22T13:20Z value-safe preflight still observed no engagement KV
paths, projection resources, or temporary identities; audit-core remained on
the reviewed digest and 1/1 Ready. Whitehat's own engagement validator returned
`engagement window has not started`. The independent custody and engagement
gates therefore agree before the attended window, with no live resource to
clean up early.
The 18:0018:15Z engagement expired unused: no projection-ready notice, runner,
or packet existed, and post-window checks proved a clean non-run. Its `-01`
identifier and fixtures are terminal. The operator approved fresh engagement
`WH-ENG-20260822-AUDIT-E2-02` for 2026-08-22 19:1519:30Z. Audit-core directly
reviewed and acknowledged the new identifiers, bounded routes, target digest,
abort thresholds, and exact cleanup obligation. Whitehat commit `e1f9f81`
publishes the fail-closed engagement and runner contract; railiance-platform
commit `891d90e` publishes the new exact-path custody procedure, hard-gated to
projection at 19:1519:18Z and cleanup before 19:30Z. Whitehat's 51 tests,
custody's four focused tests, Kubernetes server-side runner dry-run, and the
live value-safe preflight pass. Before the window there are still no temporary
identities, KV paths, projection resources, runner, or packets.
The `-02` attended window reached projection but aborted before any target
packet. Custody projected exactly two bounded identities at 19:17:54Z, the
approved runner became Ready, target and engagement validation passed, and the
kill switch was clear. Whitehat's `admit-plane` then failed closed because its
live custody broker remains unconnected: there is no trusted adapter that
turns the platform's value-safe projection receipt into a plane lease. The
runner was never invoked and was deleted at 19:21:07Z. Exact custody cleanup
completed at 19:21:39Z; independent checks found no runner, temporary identity,
engagement KV path, projection resource, or mounted Secret, and audit-core was
`1/1` Ready. This is a clean, zero-packet abort, not E2 evidence. A fresh
engagement must wait for the receipt-to-broker adapter and an actual
multi-driver sequence. Evidence:
`docs/evidence/AUDIT-WP-0008-T05-whitehat-e2-02-abort-2026-08-22.md`.
Third-attempt preparation was proposed at Whitehat `96d6781` and platform
`2a5c002`. Whitehat adds terminal-unique engagement
`WH-ENG-20260822-AUDIT-E2-03`, unique
`-03` identities, tenants, fixtures, runner pod and mounted Secret, plus an
explicit authorization-id argument and cleanup-bound report finalization.
Platform adds the matching canonical WP-0025 contract and a
mode-`0600` raw broker-receipt export. The proposed window is 22:0022:15Z,
with projection closing at 22:03Z. Platform's 141 tests and Whitehat's 81 tests
pass; the runner manifest passes Kubernetes server-side dry-run; both contract
validators agree.
The operator explicitly approved the exact package at 21:29:21Z and audit-core
acknowledged it after approval at 21:29:22Z. Approved revisions are Whitehat
`5fcb3ec` and platform `d7dae01`; the final contract digest is
`fe6f1ebf…ede01`. Whitehat's direct broker receipt
`90e9dbbe-0c17-427d-8208-c64e24cf3bbb` pins adapter revision `5fcb3ec`, its
artifact digest, passing focused tests, all four schema digests, exact roles and
mount paths, and cleanup support. Live preflight reports the broker gate passed,
OpenBao unsealed, audit-core `1/1` on the approved image, and no temporary
identity, exact KV path, projection resource, runner, or secret value. The
engagement is approved but projection remains fail-closed before 22:00Z and
after 22:03Z.
The `-03` engagement completed successfully. Receipt-bound projection opened at
22:01:35Z; Whitehat admitted the exact plane lease; and the bounded runner sent
ten operations from 22:09:30Z through 22:10:25Z. All three calibrated probes
passed: a tenant-A identity could not fetch tenant B's event by id, could not
observe tenant B's correlation slice, and could not append an event attributed
to tenant B. The report carries no limitations and explicitly limits the claim
to the attacks attempted in this run.
The runner was deleted and custody cleanup completed at 22:13:48Z, before the
22:15Z expiry. Independent status found both exact KV paths, both temporary
identities, all projection resources and the runner absent, with audit-core
still `1/1` Ready; no secret value was observed or retained. The finalized
sanitized report reached `risk-nexus` as message
`40e3f825-fc70-4091-96d2-9ab01d42184a`. Durable target evidence is recorded in
`docs/evidence/AUDIT-WP-0008-T05-whitehat-e2-03-pass-2026-08-22.md` and its
companion JSON report. Audit-core notified `net-kingdom` in
`b8a7ce2a-cf8e-44ef-ab25-37b0f93337db` that the canonical worked example can
advance from E1 to E2. The facility's 24-hour baseline makes the next review or
replacement due at 2026-08-23T22:10:25Z; sustaining it without minute-precise
operator attendance now belongs in a multi-driver integration/e2e harness, not
in another manual T05 sequence.
```task
id: AUDIT-WP-0008-T06
status: done
priority: medium
state_hub_task_id: "c0b6e317-bfde-5ba7-b92a-6545257e0dff"
```
Close the review loop with `net-kingdom`.
**Mostly overtaken.** All five findings were adopted across draft-7 and draft-8,
and the two stale status lines this task was going to flag are already fixed.
Nothing is outstanding as a *finding*.
What remains is confirmation rather than argument: acknowledge the adoptions,
confirm the §5 worked example `I1 A2 E1 P1 R2 V0` is accurate as written, and
state that `tenancy.yaml` validates clean against
`tools/tenancy-posture/validate.py`. Flag one thing forward — the E line becomes
wrong the day T05 lands, and under Decision 6.1 an *upgrade* has no notification
duty, so the correction is ours to send unprompted or it will simply sit stale.
```task
id: AUDIT-WP-0008-T07
status: progress
priority: high
state_hub_task_id: "4e3ab790-daa6-5fbb-837e-9a9a26848201"
```
Reach **V1** on the availability axis, new in draft-8.
We declared `V0` because nothing exercises restart recovery for the complete
audit path. §13's V1 row asks for three things and they are all mechanical:
critical dependencies enumerated, restart/recreate recovery exercised, and the
interruption plus measured recovery time recorded. Decision 4.6.1 makes V
end-to-end, so the enumeration must include `platform-pg` and the credential
path — a receiver that restarts in 5s over a database that takes 90s is not V1
at 5s.
The 2026-08-16 reboot walk in the operator runbook is close but is not this: it
observed ~40s of unreadiness after a node reboot, without enumerating the
dependency set or recording a measured recovery time as an exercise. Decision
4.6.1 is explicit that a replica count or a status page is not evidence.
Delivered in `docs/availability-recovery.md`: the seven-dependency enumeration,
five exercise scenarios with integrity as a pass condition, and the measurement
rules. The exercise itself needs a live window on railiance01 and coordination
with `user-engine`, since two scenarios are visible to the sender.
Coordination is active as of 2026-08-22. `user-engine` was asked for the
synthetic sender lane and sender-visible measurement constraints
(`76cef7cb-bac2-43af-ae8d-e2716fedfe79`); `rapp-postgres` was asked for the
single-primary restart window, rollback, and co-resident notification needs
(`04a2aff2-5f82-4ff1-a0ab-d31d3d6f07b5`); and `railiance-platform` was asked
for the value-safe OpenBao/ESO recovery path and railiance01 operator window
(`a93fa88f-a9c5-4539-93ae-0c8f8490f53d`). Warden routes the dynamic database
credential lane to `rapp-postgres` and the OpenBao broker to
`railiance-platform`. These requests are readiness coordination only: no
restart, lease revocation, secret read, or reboot is authorized until the
owners return an approved window, rollback plan, and abort conditions.
One thing the enumeration settled: **V2 is not reachable from P1 as built.**
`platform-pg` runs `instances: 1`, and Decision 4.6.1 makes V the minimum across
synchronous providers — so no amount of replication on our Deployment raises it.
V1 is the ceiling here, not merely the next step.
Audit-core approved the final hash-bound `RAILIANCE-WP-0024` T02/T03 recovery
procedure contract at digest
`f86d418f951f829f075de04dd825c6e2e185e577019ee23d6da1fa9040302d62` on
2026-08-22. Receipt `0c0e3dff-c7fd-4fac-898b-1aec160f4753` records that all
four pinned artifacts matched and the focused tests plus both value-safe,
read-only preflights passed. Both preflights remain
`ready_for_live_execution: false`: this approves the procedure only and does
not authorize a lease revocation, restart, or reboot window.
Rapp-postgres confirmed its final procedure approval in message
`f9d4606b-56d8-4476-b3ba-166f07e8b527` and fixed the sequencing boundary: no
recovery exercise overlaps the Whitehat E2 window, and the earliest candidate
is after its attended cleanup at 18:15Z. T02 now has all three required
procedure-owner approvals. A 2026-08-22T13:20Z live, read-only preflight passed
all automated checks but remained closed on the synthetic-load contract,
approved window, named abort operator, and runtime acknowledgement fields.
Audit-core requested those remaining gates from `user-engine` and
`railiance-platform` in messages `3a99ec1e-897c-4b29-bdc8-a312c8e990e1` and
`852a7efa-4d25-4d85-a56d-6afed8bc600a`.
T03's automated node-reboot checks also remained clean and value-safe, including
fresh Barman backup and continuous archiving, while correctly refusing live
readiness without snapshot, console, Shamir quorum, window, abort operator, and
complete owner evidence. `railiance-cluster` is the sole missing procedure
owner and received the direct hash-bound review request in
`e13f90f1-0734-4160-aad7-7bba2da29275`. No live recovery action is scheduled.
Railiance-cluster subsequently approved the final contract at
2026-08-22T14:08:48Z, so both T02 and T03 now have their complete
procedure-owner sets. That closes procedure review only; T03's attended
snapshot, console, unseal-quorum, abort-operator, and window gates remain open.
For T02, railiance-platform published the direct synthetic-load contract at
`a557208`, contract digest
`94499e78c73857b406b78ece3cae2510960634145a88390b56153e8b13ac3520`.
Audit-core implemented the four-phase, value-safe executable in
`scripts/t02_synthetic_load_driver.py` at commit `8c8bcf4`, with a private
mounted-bearer boundary, immutable synthetic fixture, bounded retry behavior,
terminal-status refusal, and no server-side deletion disguised as cleanup.
Eleven focused tests and the full suite (117 passed, 25 skipped) succeeded.
Candidate receipt `7879bf65-06b7-4d0c-bbd1-873b6d20b7fc` pins driver revision
`sha256:941ba251f638869626b06e9cbf430c70188c0bdf4715e3eff9c7610366f68662`;
registration did not execute traffic or observe a secret. Live use still needs
the separately approved sender identity, attended window, and abort operator.
The unchanged registered driver subsequently passed a real local HTTP round
trip through audit-core with a dedicated scoped identity: the fixture returned
202 `accepted`, then 200 `duplicate` under the same idempotency key. The focused
driver suite is now 12 passing tests; a native full run passed 119 with 24
environment-dependent skips.
```task
id: AUDIT-WP-0008-T08
status: done
priority: high
state_hub_task_id: "f9d4ec51-6092-5c05-82fb-30e5dd201229"
```
Disclose the shared-substrate retention coupling in our **provider** block, and
notify `user-engine` under Decision 6.1.
Two defects in our own declaration, both found by reading draft-8 back against
`tenancy.yaml`:
- **`provider.R.available: 2` is misleading as written.** It tells a sender it
gets a 30-day horizon. At P1 the horizon is the *instance maximum* across
co-residents (§4.5), so a co-resident declaring longer silently extends what
we hand our senders. Decision 4.5.4 makes exactly this the reason a bare `R2`
minimum is insufficient. We are not a tier, but we are a provider quoting a
number we do not solely control, and the provider block must say so.
- **Our consumers were never told.** Decision 6.1: updating `tenancy.yaml`
without notifying dependants is declaration drift, not a completed change.
`user-engine` is the only sender and has not been told that `E4` and `R4` are
unreachable here, nor that current `E` is 1. This is not a downgrade — it is a
first accurate declaration replacing someone else's generous guess — but the
notification duty is the same and the guess was public.
## Done when
- `docs/tenancy-posture.yaml` declares an accurate vector with targets, per-path
E breakdown, and dated gaps.
- Read path enforces tenant scope, with a test that fails if it regresses.
- `rapp-postgres` has the retention answer, the corrected posture guess, and the
question 11 answer.
- NetKingdom has the five framework findings.
- The §5 worked example for `audit-core` is corrected upstream.
- Two dates are recorded and monitored per §8.1 rather than merely written:
`data.archive` review 2026-12-31, E3 target 2027-03-31.
- The erasure legal question has an owner outside audit-core (`risk-nexus`).
- `tenancy.yaml` validates clean against the normative validator, and keeps
doing so as the framework revises.
- V1 is evidenced by an exercise, not asserted from a restart policy (T07).
- The provider block discloses what it does not solely control, and every
consumer of `operations.audit` has been told what it declares (T08).