Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a025c2-407a-7a32-b40a-f37a52f03f62
535 lines
28 KiB
Markdown
535 lines
28 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: "84c01552-4c65-45fd-bf6c-7daab0ecc9b6"
|
||
---
|
||
|
||
# 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: "65f3109a-fa98-459a-9e59-d7ac211bd2bc"
|
||
```
|
||
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: "224a12c0-d646-4c7f-9215-120e1ede4691"
|
||
```
|
||
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: "0003954e-5033-457f-b9aa-97efdefe9beb"
|
||
```
|
||
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: "e4440db3-6eb0-49f6-9aa9-930db8be9d18"
|
||
```
|
||
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: progress
|
||
priority: medium
|
||
state_hub_task_id: "30b56b30-e7eb-4873-aa7c-f4a3bf2fcb24"
|
||
```
|
||
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.
|
||
|
||
```task
|
||
id: AUDIT-WP-0008-T06
|
||
status: done
|
||
priority: medium
|
||
state_hub_task_id: "ed58ca37-755b-4284-b1ac-2a02baaceb21"
|
||
```
|
||
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: "e972a8f7-e6ee-4522-bc76-fb69d71e5ed5"
|
||
```
|
||
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.
|
||
|
||
```task
|
||
id: AUDIT-WP-0008-T08
|
||
status: done
|
||
priority: high
|
||
state_hub_task_id: "50eed5e2-06d6-4bdc-913d-5a8cd41ce613"
|
||
```
|
||
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).
|