From a2d84bdd9bbdce1d8d417d9e2933d3c5554ba154 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 17 Aug 2026 22:55:31 +0200 Subject: [PATCH] Declare the tenancy posture vector (AUDIT-WP-0008-T01). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Written against draft-7, which landed after the task was drafted and moved the target. Decision 5.4 fixes the location at tenancy.yaml in the repo root rather than docs/, and fixes the schema: current, target, reviewed, gap, placement_exceptions, service_class, per-path detail, provider block. Declares I1 A2 E1 P1 R1. E is quoted at 1 although T04 put the E2 mechanism on both paths, because §13.2 states a passing CI run is not E2 evidence -- the artifact is adversarial, compares separate tenant contexts and carries a review date. Our cross-tenant tests are mechanical, so under §13.1 the level is not claimable until T05. The mechanism is recorded in paths.E and the reason in gap.E. Claiming E2 off unit tests would be the overclaim §6 prohibits, and refusing that reasoning is what found the read-path defect. R stays at 1: R2 needs backupRetentionDays in rapp-postgres's consumer file, requested in T02 and not ours to declare. Two additions draft-7 forced. A credentials gap under Decision 9.2 -- our own finding, adopted as a rule, and it binds us: ingest credentials are static long-lived bearer tokens, declared as a stated gap rather than a silent exclusion. And a provides block under Decision 5.5, declaring what a sender can reach through this service: E2 now, E3 pending ADR-0003, E4 and R4 unreachable. Co-Authored-By: Claude Opus 5 --- SCOPE.md | 4 + WORK-RECORDS.md | 4 +- docs/interface-card.yaml | 10 ++ tenancy.yaml | 133 ++++++++++++++++++ ...AUDIT-WP-0008-tenancy-posture-alignment.md | 69 +++++++-- 5 files changed, 205 insertions(+), 15 deletions(-) create mode 100644 tenancy.yaml diff --git a/SCOPE.md b/SCOPE.md index 94fe26c..1054634 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -38,6 +38,10 @@ audit-core exists to provide the capability described in INTENT.md. (`docs/evidence/chain-head-20260816.json`). - ITC-CAP case: `data/capability/audit-core-operational.json` at D4. `data.archive` is an unmet requirement. +- Tenancy posture: `tenancy.yaml` (NetKingdom Tenancy Posture v0.1 draft-7, + Decision 5.4). Declared `I1 A2 E1 P1 R1`, target `E3` and `R2`. E is quoted + at 1 although the E2 mechanism is in place on both paths — the §13 E2 + artifact is adversarial and does not exist yet (AUDIT-WP-0008-T05). ## Getting Oriented diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 4cba5f5..d4fa265 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -48,8 +48,8 @@ | task | AUDIT-WP-0007-T04 | done | — | workplans/AUDIT-WP-0007-integrity-verification.md | | task | AUDIT-WP-0007-T05 | done | — | workplans/AUDIT-WP-0007-integrity-verification.md | | task | AUDIT-WP-0008-T01 | todo | — | workplans/AUDIT-WP-0008-tenancy-posture-alignment.md | -| task | AUDIT-WP-0008-T02 | todo | — | workplans/AUDIT-WP-0008-tenancy-posture-alignment.md | +| task | AUDIT-WP-0008-T02 | done | — | workplans/AUDIT-WP-0008-tenancy-posture-alignment.md | | task | AUDIT-WP-0008-T03 | todo | — | workplans/AUDIT-WP-0008-tenancy-posture-alignment.md | -| task | AUDIT-WP-0008-T04 | todo | — | workplans/AUDIT-WP-0008-tenancy-posture-alignment.md | +| task | AUDIT-WP-0008-T04 | done | — | workplans/AUDIT-WP-0008-tenancy-posture-alignment.md | | task | AUDIT-WP-0008-T05 | todo | — | workplans/AUDIT-WP-0008-tenancy-posture-alignment.md | | task | AUDIT-WP-0008-T06 | todo | — | workplans/AUDIT-WP-0008-tenancy-posture-alignment.md | diff --git a/docs/interface-card.yaml b/docs/interface-card.yaml index ebe032f..d6b0348 100644 --- a/docs/interface-card.yaml +++ b/docs/interface-card.yaml @@ -90,6 +90,16 @@ consumer_needs: requested_extensions: - data.archive sink if events must survive past the 30-day backup window feedback: [] +tenancy_posture: + declared_at: tenancy.yaml + framework: net-kingdom/canon/standards/tenancy-posture_v0.1.md (draft-7) + current: I1 A2 E1 P1 R1 + target: I1 A2 E3 P1 R2 + provides_reachable: + E2: now + E3: not yet — needs rapp-postgres ADR-0003 GUC contract; target 2027-03-31 + E4: unreachable — one database, one runtime credential, no per-tenant substrate + R4: unreachable by design — see Decision 4.5.3 and docs/erasure-and-audit.md known_deviations: - no data.archive sink - tamper_evidence is a hash chain plus external head, not WORM diff --git a/tenancy.yaml b/tenancy.yaml new file mode 100644 index 0000000..5a90a14 --- /dev/null +++ b/tenancy.yaml @@ -0,0 +1,133 @@ +# audit-core tenancy posture +# +# Declared per NetKingdom Tenancy Posture v0.1 (draft-7), +# net-kingdom/canon/standards/tenancy-posture_v0.1.md. +# Location and schema per Decision 5.4; per-path detail per Decision 5.2; +# provider block per Decision 5.5. +# +# Conformance is accuracy, not altitude (§6). Nothing here is claimed above +# what this repo can evidence today, and two rungs are deliberately declared +# lower than the mechanism in place — see `gap.E` and `gap.R`. + +tenancy: + reviewed: "2026-08-17" + service_class: batch # §8.3.2. Co-resident with latency-critical + # tenant-engine on platform-pg; the mixture is + # reported by the platform, not hidden. + + current: { I: 1, A: 2, E: 1, P: 1, R: 1 } + target: { I: 1, A: 2, E: 3, P: 1, R: 2 } + + # §5.2 — declare per path, quote the minimum. The quoted E above is the + # minimum across paths. As of AUDIT-WP-0008-T04 both paths carry the same + # mechanism; the quoted level stays at 1 for the evidence reason in gap.E, + # not because a path is weaker. + paths: + E: + write: 2 # Sender credential bound to the sources and tenants it may + # claim; checked at one choke point (audit_core/ingestion.py). + read: 2 # Event reads filtered to permitted tenants; surfaces with no + # tenant key require full scope. Cross-tenant fetch returns + # 404, not 403, so the surface is not an existence oracle. + + placement_exceptions: [] # No tenant is on dedicated substrate. All + # tenants share database audit_core on + # platform-pg. + + gap: + I: >- + I1 is accurate and is not currently a target. Senders authenticate with + bearer tokens and the tenant arrives in the request body, checked against + an allowlist bound to the credential (§4.1 places request-supplied tenant + identifiers at I1 however canonical the string). The allowlist is a real + control, but it is an A-axis control; it does not make the identity + verified. Moving to I2 means senders carrying a verified token with a + tenant claim, which is a change to every sender and not audit-core's to + make alone. Recorded as accurate, not as ambition. + + A: >- + A2 is accurate. Authorization is a single local boundary — permitted + sources, permitted tenants, may_write, may_read — bound once and centrally. + No flex-auth delegation. A3 is not a near-term target: flex-auth is itself + at A0 on its own self-report (/v1/check authenticates no caller), so + delegating to it today would lower this service's assurance, not raise it. + + E: >- + The E2 mechanism is in place on both paths as of AUDIT-WP-0008-T04, and + the quoted level is still 1. This is deliberate. §13.2 states that a + passing CI run is not E2 evidence: the E2 artifact is adversarial, needs + separate tenant contexts compared against each other, and carries a review + date rather than a green build. The repo's cross-tenant tests are + mechanical. Under §13.1 the level is not claimable until that artifact + exists, so E stays at 1 until AUDIT-WP-0008-T05 produces it with + whitehat-security. Declaring E2 on the strength of unit tests would be the + overclaim §6 prohibits, and the read-path defect this repo just fixed was + found precisely by refusing that kind of reasoning. + E_target: >- + E3 (row-level security per rapp-postgres ADR-0003) targeted 2027-03-31. + Blocked behind the E2 artifact — §4.3 requires E2 evidence alongside any + E3 claim — and needs the EXPLAIN comparison first, since RLS disables + functional indexes built on non-leakproof functions. E4 is unreachable at + P1 by the §3.2 coupling and is not a target. + + R: >- + R1 today: the platform default 30-day window applies and audit-core has + declared nothing above it. R2 is requested and in flight — audit-core has + asked rapp-postgres to add `backupRetentionDays: 30` to + consumers/audit-core.yaml, making the window declared rather than + inherited. That file is rapp-postgres's, so R2 is not audit-core's to + declare unilaterally. The erasure horizon is published on /readyz as + recoverable_days. + R_ceiling: >- + R4 is unreachable under the current design and is not a target. Per + Decision 4.5.3 — which this repo found — the hash chain commits to a + SHA-256 of the cleartext record, which survives key destruction as a + confirmation oracle over low-entropy fields. A fleet R4 target must exempt + this service explicitly. See docs/erasure-and-audit.md (AUDIT-WP-0008-T03). + + credentials: >- + Stated gap against Decision 9.2 rather than a silent exclusion. Database + credentials comply with 9.1: dynamic leases re-read from a mounted Secret + at connection time, no restart on rotation. Ingest credentials do not: + they are static long-lived bearer tokens, rotated overlap-first by + publishing a replacement alongside the incumbent and then dropping the + predecessor. audit-core raised this omission during review and is not + exempting itself from the rule it asked for. No dated remedy yet; leasing + consumer-facing credentials needs a broker path that does not exist. + + retention_placement: >- + audit-core's INTENT wants unbounded WORM archive. That is data.archive in + ITC-CAP terms, recorded as an unmet requirement in + data/capability/audit-core-operational.json, and it is deliberately not a + backup-window question: a 30-day WAL window is not an archive, is not + searchable, and restores instance-wide. Reviewed 2026-12-31. If no + data.archive provision is procured by then, audit-core reopens placement + under the §4.5 retention trigger, with P2 as the fallback — a worse answer + than archive, named now so it is not improvised later. + + # §5.5 — audit-core provisions operations.audit to senders, so it declares + # what it makes reachable for a consumer's audit trail, not only where it + # sits. A sender's tenant separation inside the trail is audit-core's to + # enforce; the sender cannot reach a level this service does not offer. + provides: + capability: operations.audit + profile: administrative + reachable: + E2: >- + Reachable now. A sender credential is bound to the tenants it may write + for and, if it may read, to the tenants it may read back. + E3: >- + Not reachable yet. Requires rapp-postgres's ADR-0003 GUC contract + applied to the audit_core schema. Targeted 2027-03-31. + E4: >- + Unreachable. One database, one runtime credential, no per-tenant + credential and no per-tenant substrate. A sender needing a structural + guarantee that another tenant cannot reach its audit records cannot get + it here, and should be told so rather than sold E2 in E4's language + (§11.4). + R2: >- + Reachable once the declared window lands; the horizon is already + published on /readyz. + R4: >- + Unreachable by design, per R_ceiling above. A consumer with a verified + erasure obligation over its audit trail cannot discharge it here. diff --git a/workplans/AUDIT-WP-0008-tenancy-posture-alignment.md b/workplans/AUDIT-WP-0008-tenancy-posture-alignment.md index 2d0945a..adb7878 100644 --- a/workplans/AUDIT-WP-0008-tenancy-posture-alignment.md +++ b/workplans/AUDIT-WP-0008-tenancy-posture-alignment.md @@ -161,6 +161,28 @@ avoid. 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 @@ -246,23 +268,44 @@ both would be concealment, and §6 would be right to call it that. ```task id: AUDIT-WP-0008-T01 -status: todo +status: done priority: high state_hub_task_id: "65f3109a-fa98-459a-9e59-d7ac211bd2bc" ``` -Publish the posture vector in the repo per Decision 5.1 — `docs/tenancy-posture.yaml`. -**Runs after T04.** Declares `current: I1 A2 E2 P1 R2`, `target: I1 A2 E3 P1 R2`, -reviewed date, and a gap note per axis. Include the per-path E breakdown -(`write: 2, read: 2`) alongside the vector, per finding 1. The E gap names RLS -with its 2027-03-31 date; the R gap names `data.archive` with its 2026-12-31 -review. If T04 has not landed when this is published, declare `E1` and say why — -the vector describes what is true on the day it is written, never what is -expected. Cross-link from `SCOPE.md` and `docs/interface-card.yaml`. +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`. -Fold in the R1 → R2 move: explicit `backupRetentionDays: 30` requested of -`rapp-postgres` for `consumers/audit-core.yaml` (that file is theirs, so this is -a request, not an edit), and the erasure horizon rendered in the operator -surface alongside `recoverable_days`. +**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