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