Propose AUDIT-WP-0008 for tenancy posture alignment.
Assesses audit-core against NetKingdom Tenancy Posture v0.1 following the rapp-postgres review request. The guessed vector I2 A3 E2 P1 R1 is too high on three axes; the honest reading is I1 A2 E1 P1 R1. The E correction is a real defect rather than a bookkeeping one: the read path in ingestion._read gates on may_read alone and never calls permits_tenant, so a reader credential can read any tenant. Bounded today by may_read: false on the production sender and by default-deny networking, but not enforced in code. Also records the retention position (ceiling stays 365, multi-year belongs to data.archive not the WAL window) and the question-11 answer: crypto-shredding does not reach R4 here, because the chain commits to a SHA-256 of the cleartext record, which survives key destruction as a confirmation oracle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b463df8031
commit
94051bd1b6
1 changed files with 244 additions and 0 deletions
244
workplans/AUDIT-WP-0008-tenancy-posture-alignment.md
Normal file
244
workplans/AUDIT-WP-0008-tenancy-posture-alignment.md
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
---
|
||||
id: AUDIT-WP-0008
|
||||
type: workplan
|
||||
title: "Tenancy posture declaration and read-path enforcement"
|
||||
domain: infotech
|
||||
repo: audit-core
|
||||
status: proposed
|
||||
owner: claude
|
||||
topic_slug: railiance
|
||||
created: "2026-08-17"
|
||||
updated: "2026-08-17"
|
||||
depends_on:
|
||||
- AUDIT-WP-0007
|
||||
---
|
||||
|
||||
# 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`
|
||||
(rendered copy in `policy-nexus/build/tenancy-posture.html`).
|
||||
|
||||
## 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.
|
||||
|
||||
### 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.
|
||||
|
||||
## Framework review findings
|
||||
|
||||
Sent under §20.2 — the framework is validated by whether it can describe us.
|
||||
|
||||
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. Recommend the vector take
|
||||
the **minimum across paths**, stated explicitly — the reading consistent with
|
||||
§6. Per-path declaration (`E: {write: 2, read: 1}`) is the richer option but
|
||||
invites the same selective quoting downstream.
|
||||
|
||||
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.
|
||||
- **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.
|
||||
|
||||
## Tasks
|
||||
|
||||
```task
|
||||
id: AUDIT-WP-0008-T01
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
Publish the posture vector in the repo per Decision 5.1 — `docs/tenancy-posture.yaml`,
|
||||
declaring `current: I1 A2 E1 P1 R1`, `target: I1 A2 E3 P1 R2`, reviewed date, and a
|
||||
gap note per axis. The E gap names the read path and cites T04; the R gap names
|
||||
`data.archive` as the real requirement. Cross-link from `SCOPE.md` and
|
||||
`docs/interface-card.yaml`.
|
||||
|
||||
```task
|
||||
id: AUDIT-WP-0008-T02
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
Reply to `rapp-postgres` message `56b77e20-0152-4e09-8f76-99640269063b` with the
|
||||
retention position: ceiling stays at 365, audit-core declares nothing above the
|
||||
30-day default, multi-year routes to `data.archive`, service class `batch`
|
||||
confirmed. Ask for the ADR-0002 context amendment. Mark the message read on send.
|
||||
|
||||
```task
|
||||
id: AUDIT-WP-0008-T03
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
Answer question 11 in `docs/erasure-and-audit.md`: the fact/payload split, the
|
||||
encrypt-then-hash constraint, the confirmation-oracle finding, and the R2-not-R4
|
||||
target. Record as an audit-core position; send the §4.5 condition to NetKingdom
|
||||
as a framework amendment.
|
||||
|
||||
```task
|
||||
id: AUDIT-WP-0008-T04
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
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: todo
|
||||
priority: medium
|
||||
```
|
||||
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.
|
||||
|
||||
```task
|
||||
id: AUDIT-WP-0008-T06
|
||||
status: todo
|
||||
priority: medium
|
||||
```
|
||||
Send the five framework review findings to `net-kingdom` (owner of the canon
|
||||
path) and `policy-nexus` (owner of the rendered copy), as a §20.2 review
|
||||
response rather than as objections.
|
||||
|
||||
## Done when
|
||||
|
||||
- `docs/tenancy-posture.yaml` declares an accurate vector with targets and gaps.
|
||||
- Read path enforces tenant scope, with a test that fails if it regresses.
|
||||
- `rapp-postgres` has the retention answer and the question 11 answer.
|
||||
- NetKingdom has the five framework findings.
|
||||
- The §5 worked example for `audit-core` is corrected upstream.
|
||||
Loading…
Add table
Add a link
Reference in a new issue