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
683 lines
38 KiB
Markdown
683 lines
38 KiB
Markdown
---
|
||
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:00–18: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:00–18: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:00–18: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:00–18: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:15–19: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:15–19: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:00–22: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).
|