diff --git a/canon/schemas/tenancy-posture_v0.1.schema.json b/canon/schemas/tenancy-posture_v0.1.schema.json index 39714fd..3159319 100644 --- a/canon/schemas/tenancy-posture_v0.1.schema.json +++ b/canon/schemas/tenancy-posture_v0.1.schema.json @@ -3,36 +3,93 @@ "$id": "https://policy.coulomb.social/schemas/tenancy-posture/v0.1", "title": "NetKingdom tenancy posture declaration v0.1", "type": "object", - "required": ["schema_version", "framework"], + "required": [ + "schema_version", + "framework" + ], "properties": { - "schema_version": {"const": "0.1"}, - "framework": {"const": "netkingdom-tenancy-posture"}, - "service": {"$ref": "#/$defs/serviceName"}, - "role": {"type": "string", "minLength": 1}, - "tenancy": {"$ref": "#/$defs/tenancy"}, - "provider": {"$ref": "#/$defs/provider"}, - "evidence": {"$ref": "#/$defs/evidence"}, - "notes": {"$ref": "#/$defs/stringList"}, + "schema_version": { + "const": "0.1" + }, + "framework": { + "const": "netkingdom-tenancy-posture" + }, + "service": { + "$ref": "#/$defs/serviceName" + }, + "role": { + "type": "string", + "minLength": 1 + }, + "tenancy": { + "$ref": "#/$defs/tenancy" + }, + "provider": { + "$ref": "#/$defs/provider" + }, + "zones": { + "description": "Reserved for security-zone membership (tenancy-posture_v0.1 Decision 5.6). Its internal shape is defined by the NetKingdom security-zone standard drafted by zone-engine, not by this schema, and is deliberately unconstrained here until that standard lands. Present so a conformant combined declaration is not rejected by this validator." + }, + "evidence": { + "$ref": "#/$defs/evidence" + }, + "notes": { + "$ref": "#/$defs/stringList" + }, "services": { "type": "array", "minItems": 1, - "items": {"$ref": "#/$defs/service"} + "items": { + "$ref": "#/$defs/service" + } } }, "oneOf": [ { - "required": ["service", "role", "tenancy"], - "not": {"required": ["services"]} + "required": [ + "service", + "role", + "tenancy" + ], + "not": { + "required": [ + "services" + ] + } }, { - "required": ["services"], - "not": {"anyOf": [ - {"required": ["service"]}, - {"required": ["role"]}, - {"required": ["tenancy"]}, - {"required": ["provider"]}, - {"required": ["evidence"]} - ]} + "required": [ + "services" + ], + "not": { + "anyOf": [ + { + "required": [ + "service" + ] + }, + { + "required": [ + "role" + ] + }, + { + "required": [ + "tenancy" + ] + }, + { + "required": [ + "provider" + ] + }, + { + "required": [ + "evidence" + ] + } + ] + } } ], "additionalProperties": false, @@ -42,24 +99,65 @@ "pattern": "^[a-z0-9][a-z0-9._-]*$" }, "axisName": { - "enum": ["I", "A", "E", "P", "R", "V"] + "enum": [ + "I", + "A", + "E", + "P", + "R", + "V" + ] }, "level": { "oneOf": [ - {"type": "integer", "minimum": 0, "maximum": 4}, - {"const": "n/a"} + { + "type": "integer", + "minimum": 0, + "maximum": 4 + }, + { + "const": "n/a" + } ] }, "axisVector": { "type": "object", - "required": ["I", "A", "E", "P", "R", "V"], + "required": [ + "I", + "A", + "E", + "P", + "R", + "V" + ], "properties": { - "I": {"oneOf": [{"type": "integer", "minimum": 0, "maximum": 3}, {"const": "n/a"}]}, - "A": {"$ref": "#/$defs/level"}, - "E": {"$ref": "#/$defs/level"}, - "P": {"$ref": "#/$defs/level"}, - "R": {"$ref": "#/$defs/level"}, - "V": {"$ref": "#/$defs/level"} + "I": { + "oneOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 3 + }, + { + "const": "n/a" + } + ] + }, + "A": { + "$ref": "#/$defs/level" + }, + "E": { + "$ref": "#/$defs/level" + }, + "P": { + "$ref": "#/$defs/level" + }, + "R": { + "$ref": "#/$defs/level" + }, + "V": { + "$ref": "#/$defs/level" + } }, "additionalProperties": false }, @@ -67,97 +165,214 @@ "type": "object", "minProperties": 1, "properties": { - "I": {"oneOf": [{"type": "integer", "minimum": 0, "maximum": 3}, {"const": "n/a"}]}, - "A": {"$ref": "#/$defs/level"}, - "E": {"$ref": "#/$defs/level"}, - "P": {"$ref": "#/$defs/level"}, - "R": {"$ref": "#/$defs/level"}, - "V": {"$ref": "#/$defs/level"} + "I": { + "oneOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 3 + }, + { + "const": "n/a" + } + ] + }, + "A": { + "$ref": "#/$defs/level" + }, + "E": { + "$ref": "#/$defs/level" + }, + "P": { + "$ref": "#/$defs/level" + }, + "R": { + "$ref": "#/$defs/level" + }, + "V": { + "$ref": "#/$defs/level" + } }, "additionalProperties": false }, "stringMap": { "type": "object", - "additionalProperties": {"type": "string", "minLength": 1} + "additionalProperties": { + "type": "string", + "minLength": 1 + } }, "stringList": { "type": "array", - "items": {"type": "string", "minLength": 1} + "items": { + "type": "string", + "minLength": 1 + } }, "paths": { "type": "object", - "propertyNames": {"$ref": "#/$defs/axisName"}, + "propertyNames": { + "$ref": "#/$defs/axisName" + }, "additionalProperties": { "type": "object", "minProperties": 1, - "additionalProperties": {"$ref": "#/$defs/level"} + "additionalProperties": { + "$ref": "#/$defs/level" + } } }, "placementException": { "type": "object", - "required": ["tenants", "P", "reason"], + "required": [ + "tenants", + "P", + "reason" + ], "properties": { - "tenants": {"$ref": "#/$defs/stringList"}, - "P": {"type": "integer", "minimum": 0, "maximum": 4}, - "reason": {"type": "string", "minLength": 1}, - "tier_ref": {"type": "string", "minLength": 1} + "tenants": { + "$ref": "#/$defs/stringList" + }, + "P": { + "type": "integer", + "minimum": 0, + "maximum": 4 + }, + "reason": { + "type": "string", + "minLength": 1 + }, + "tier_ref": { + "type": "string", + "minLength": 1 + } }, "additionalProperties": false }, "tenancy": { "type": "object", - "required": ["current", "target", "reviewed", "review_due", "service_class"], + "required": [ + "current", + "target", + "reviewed", + "review_due", + "service_class" + ], "properties": { - "current": {"$ref": "#/$defs/axisVector"}, - "implemented": {"$ref": "#/$defs/partialAxisVector"}, - "target": {"$ref": "#/$defs/axisVector"}, - "reviewed": {"type": "string", "format": "date"}, - "review_due": {"type": "string", "format": "date"}, - "service_class": {"enum": ["latency-critical", "interactive", "batch"]}, + "current": { + "$ref": "#/$defs/axisVector" + }, + "implemented": { + "$ref": "#/$defs/partialAxisVector" + }, + "target": { + "$ref": "#/$defs/axisVector" + }, + "reviewed": { + "type": "string", + "format": "date" + }, + "review_due": { + "type": "string", + "format": "date" + }, + "service_class": { + "enum": [ + "latency-critical", + "interactive", + "batch" + ] + }, "permanent": { "type": "array", "uniqueItems": true, - "items": {"$ref": "#/$defs/axisName"} + "items": { + "$ref": "#/$defs/axisName" + } + }, + "reason": { + "$ref": "#/$defs/stringMap" + }, + "gap": { + "$ref": "#/$defs/stringMap" + }, + "paths": { + "$ref": "#/$defs/paths" }, - "reason": {"$ref": "#/$defs/stringMap"}, - "gap": {"$ref": "#/$defs/stringMap"}, - "paths": {"$ref": "#/$defs/paths"}, "placement_exceptions": { "type": "array", - "items": {"$ref": "#/$defs/placementException"} + "items": { + "$ref": "#/$defs/placementException" + } }, - "secondary_stores": {"$ref": "#/$defs/stringList"} + "secondary_stores": { + "$ref": "#/$defs/stringList" + } }, "additionalProperties": false }, "providerAxis": { "type": "object", - "required": ["available", "maximum"], + "required": [ + "available", + "maximum" + ], "properties": { - "available": {"$ref": "#/$defs/level"}, - "maximum": {"$ref": "#/$defs/level"}, - "conditions": {"$ref": "#/$defs/stringList"}, - "evidence": {"$ref": "#/$defs/stringList"}, - "reason": {"type": "string", "minLength": 1} + "available": { + "$ref": "#/$defs/level" + }, + "maximum": { + "$ref": "#/$defs/level" + }, + "conditions": { + "$ref": "#/$defs/stringList" + }, + "evidence": { + "$ref": "#/$defs/stringList" + }, + "reason": { + "type": "string", + "minLength": 1 + } }, "additionalProperties": false }, "provider": { "type": "object", - "required": ["axes"], + "required": [ + "axes" + ], "properties": { - "capability": {"type": "string", "minLength": 1}, - "profile": {"type": "string", "minLength": 1}, + "capability": { + "type": "string", + "minLength": 1 + }, + "profile": { + "type": "string", + "minLength": 1 + }, "axes": { "type": "object", "minProperties": 1, "properties": { - "I": {"$ref": "#/$defs/providerAxis"}, - "A": {"$ref": "#/$defs/providerAxis"}, - "E": {"$ref": "#/$defs/providerAxis"}, - "P": {"$ref": "#/$defs/providerAxis"}, - "R": {"$ref": "#/$defs/providerAxis"}, - "V": {"$ref": "#/$defs/providerAxis"} + "I": { + "$ref": "#/$defs/providerAxis" + }, + "A": { + "$ref": "#/$defs/providerAxis" + }, + "E": { + "$ref": "#/$defs/providerAxis" + }, + "P": { + "$ref": "#/$defs/providerAxis" + }, + "R": { + "$ref": "#/$defs/providerAxis" + }, + "V": { + "$ref": "#/$defs/providerAxis" + } }, "additionalProperties": false } @@ -168,21 +383,43 @@ "type": "object", "additionalProperties": { "oneOf": [ - {"type": "string", "minLength": 1}, - {"$ref": "#/$defs/stringList"} + { + "type": "string", + "minLength": 1 + }, + { + "$ref": "#/$defs/stringList" + } ] } }, "service": { "type": "object", - "required": ["service", "role", "tenancy"], + "required": [ + "service", + "role", + "tenancy" + ], "properties": { - "service": {"$ref": "#/$defs/serviceName"}, - "role": {"type": "string", "minLength": 1}, - "tenancy": {"$ref": "#/$defs/tenancy"}, - "provider": {"$ref": "#/$defs/provider"}, - "evidence": {"$ref": "#/$defs/evidence"}, - "notes": {"$ref": "#/$defs/stringList"} + "service": { + "$ref": "#/$defs/serviceName" + }, + "role": { + "type": "string", + "minLength": 1 + }, + "tenancy": { + "$ref": "#/$defs/tenancy" + }, + "provider": { + "$ref": "#/$defs/provider" + }, + "evidence": { + "$ref": "#/$defs/evidence" + }, + "notes": { + "$ref": "#/$defs/stringList" + } }, "additionalProperties": false } diff --git a/canon/standards/tenancy-posture_v0.1.md b/canon/standards/tenancy-posture_v0.1.md index 30d2e81..4d38ad8 100644 --- a/canon/standards/tenancy-posture_v0.1.md +++ b/canon/standards/tenancy-posture_v0.1.md @@ -6,11 +6,11 @@ domain: netkingdom status: proposed version: "0.1" created: "2026-08-17" -updated: "2026-08-17" +updated: "2026-08-19" scope: multi-tenancy-security-framework -revision: "draft-8" +revision: "draft-9" owner: net-kingdom -last_reviewed: "2026-08-17" +last_reviewed: "2026-08-19" review_interval: 6m declaration_schema: canon/schemas/tenancy-posture_v0.1.schema.json adr: @@ -28,7 +28,7 @@ related: ## Status -**Proposed, draft-8; ratification-ready.** Relocated from +**Proposed, draft-9; ratification-ready.** Relocated from `the-custodian/canon/architecture` on 2026-08-17: multi-tenancy is part of the IT-security framework NetKingdom provides, so this framework belongs in NetKingdom canon beside the IAM Profile @@ -59,6 +59,12 @@ and the tenant-engine boundary contract, not in the work-factory canon. customer language. It also corrects the distinction between an implemented control and an evidenced current level. +- **draft-9** answers `zone-engine`'s `ZONE-WP-0001-T01`. It rules that + enforcement stance is **not** a seventh axis (Decision 5.6) while reserving + `zones:` in `tenancy.yaml` so the estate keeps one declaration surface, and it + records the reef/`P`/`V` reconciliation as an open defect of this document + rather than of the repo that noticed it (Decision 8.4). + **Reviewed by all six. The score:** six repos found three live defects in their own code by reading the ladders — `tenant-engine`'s unfiltered event accessor, `audit-core`'s unfiltered read path, `flex-auth`'s @@ -583,6 +589,67 @@ it. A provider's own `P` is `n/a`, not a number. `apps-pg` *provides* `P1`; it is not *at* `P1`, and writing `P: 1` there would later read as an isolation claim. +**Decision 5.6 — enforcement stance is not a seventh axis, and `zones:` is +reserved in this file.** `zone-engine` asked whether *enforcement stance* — +whether a given control is enforced, advisory or exempt in a given band of the +estate — should fold in here rather than become a second standard. It should +not, for a reason that is structural rather than territorial. + +**Every one of the six ladders is monotone: higher is stronger, and higher is +what a service wants.** That assumption is load-bearing throughout. §12's +*improve* step moves a service up. §12's *guard* checks that none is below what +it declared. §6 has to make a special allowance for a level that is +*permanently* low by design, and §12's guard is told not to nag it — the +allowance exists because low-is-normal is the exception here. + +Enforcement stance is not monotone. The correct stance for a bootstrap lane is +deliberately and permanently *below* the top rung, and the top rung is +sometimes the wrong answer outright: `ops-warden`'s `ADR-0006` is exactly the +finding that a fail-closed authorization gate on the SSH lane the tunnels +depend on is not a stronger position, it is an outage. A ladder whose top is +sometimes wrong is an enumeration, not a ladder, and putting one inside this +vector would break `current`/`target`/`gap`, the guard, and §6 for the six that +are. + +§8.3 already refused an axis for a weaker reason than this one — that a QoS +level would be an unenforced claim. Enforcement stance *is* enforced. It fails +the other half of the same test. + +There is a second, sharper reason. §6's conformance rule is *accuracy, not +altitude*, and it works because this framework is **descriptive**: it never +blocks anything by itself. Prescription enters only through §11 and Decision +8.2, where a **requirer** — never the declaring repo — sets a minimum level and +the two are machine-reconciled. Enforcement stance is prescriptive by nature. +Fold it in as an axis and an accurately declared `exempt` becomes conformant +*and* exempt: a conformance rule that hands out the exemption it exists to +audit. The declarer must not be the party that sets the stance. + +So the split is the one `flex-auth` already argued to `zone-engine`: +**membership is data and is declared; stance is a rule and belongs to the +control's owner.** Membership is posture-shaped and behaves like a level. +Stance behaves like a tier minimum under Decision 8.2 — asserted elsewhere, +joined by machine. + +**What canon rules, and it is binding on the sibling standard:** + +- A security-zone standard is a **separate document** in this family, drafted + by `zone-engine` and published in NetKingdom canon beside this one and the + `*-engine` boundary contracts. It carries over §6 verbatim and §13's evidence + discipline. +- **Zone membership is declared in `tenancy.yaml`**, under a reserved top-level + `zones:` key, sibling to `tenancy:` and `provider:` — *not* inside + `tenancy.current`. Decision 5.4 makes this file the repo's single posture + declaration surface, and a second root file would recreate the divergence + §5.4 was written to end. One file, one review cadence, one validator; two + standards, because the two have different owners and different conformance + semantics. +- `organization_posture` (`ops-warden` WP-0029) does **not** belong in this + file at all, under either key. It is a fleet-wide, time-varying scalar + describing the estate, not a property of the declaring service, and a + per-repo copy of a global would go stale in as many places as there are + repos. It is an *input* to stance selection and should be read by the zone + model, not absorbed into a declaration. + Worked examples after applying the evidence rule and minimum-across-paths rule consistently: @@ -752,6 +819,46 @@ things, none of which is priority: classes are co-resident. An unenforceable risk that nobody can see is strictly worse than one that is stated. +### 8.4 Substrate location is not evidence — and reefs are not reconciled with `P` or `V` + +**Decision 8.4.1 — location is not evidence of any property of the workload on +it.** Three repos have now written this rule locally in three vocabularies: +`railiance-master`'s *topology is not readiness*, `zone-engine`'s *placement is +not posture*, and §3.1 here, which requires every use of "isolation" to name +its axis. They are one rule. Stated once: **the substrate a workload sits on is +never, by itself, evidence for a level on any ladder in this framework.** +Binding to a reef, being on a dedicated instance, or naming a rail proves +placement and nothing else. Other repos should cite this rather than restate +it. + +**Decision 8.4.2 — the reef taxonomy and this framework are not reconciled, and +that is this document's defect.** `zone-engine` asked whether canon should +reconcile reefs with the posture axes, on the assumption that this was a scope +question for the zone model. It is not: the unreconciled pair is not +zone ↔ reef, it is **reef ↔ `P` and `V`**, and it belongs to canon. + +`repo-manager` owns substrate placement (`reef-railiance`, `reef-storage`) with +an explicit residual-risk acceptance attached to a binding. §7 and §8 of this +document presuppose that placement is fully described by the `P` ladder. It is +not. `P` grades **tenant data isolation within a datastore**; a reef is a named +**compute substrate carrying an accepted residual risk**. The `P` ladder has no +rung meaning "single node, shared control plane, risk accepted", and inventing +one would be the fabrication §6 prohibits. + +The live consequence is on `V`, not `P`. §4.6 already warns that "a dedicated +cluster can still be a single instance on a single node", and Decision 4.6.1 +makes `V` the minimum across the synchronous path. `reef-railiance` is +single-node with a shared control plane, which **caps `V` for every workload +bound to it** regardless of that workload's own replica count — which Decision +4.6.1 already says is not evidence. Nothing today joins the reef's facts to a +consumer's `V` declaration, so a rapp can declare `V2` accurately by its own +reading and be wrong by this document's own composition rule. + +This is `railiance-platform`'s provider-declaration finding (Decision 5.5) +generalised one layer down. A reef is a **provider** and has nowhere to state +what it makes reachable. Tracked as `NK-WP-0027`; the fix is canon's, and the +zone model is not blocked on it. + The known escalation short of P2 is gateway-level prioritisation — ordering submissions in a connection proxy by the requesting tenant's current consumption. It is real, it is where the industry puts this when it must, and diff --git a/workplans/NK-WP-0027-reef-placement-reconciliation.md b/workplans/NK-WP-0027-reef-placement-reconciliation.md new file mode 100644 index 0000000..846a44c --- /dev/null +++ b/workplans/NK-WP-0027-reef-placement-reconciliation.md @@ -0,0 +1,82 @@ +--- +id: NK-WP-0027 +type: workplan +title: "Reconcile the reef taxonomy with the P and V ladders" +domain: infotech +repo: net-kingdom +status: proposed +owner: net-kingdom +topic_slug: netkingdom +planning_priority: P2 +created: "2026-08-19" +updated: "2026-08-19" +--- + +# NK-WP-0027 — Reefs, placement, and what a substrate makes reachable + +Raised by answering `zone-engine`'s `ZONE-WP-0001-T01`. `zone-engine` asked +whether canon should reconcile security zones with reefs, expecting the answer +to enlarge *its* scope. It does not: a zone is genuinely not a reef, and +`zone-engine` should keep *placement is not posture* and move on. The +unreconciled pair is **reef ↔ `P` and `V` in `tenancy-posture_v0.1`**, and it +is this repo's defect, recorded as Decision 8.4.2 in draft-9. + +`repo-manager` owns substrate placement (`reef-railiance`, `reef-storage`) with +residual-risk acceptance attached to a binding. §7 and §8 of the standard +presuppose placement is fully described by the `P` ladder. `P` grades tenant +data isolation *within a datastore*; a reef is a named compute substrate +carrying an accepted residual risk. There is no rung for "single node, shared +control plane, risk accepted", and there should not be one — inventing it is +the fabrication §6 prohibits. + +The live consequence is on `V`. Decision 4.6.1 makes `V` the minimum across the +synchronous path and says a replica count is not evidence. `reef-railiance` is +single-node with a shared control plane and therefore caps `V` for everything +bound to it. Nothing joins those facts today, so a rapp can declare `V2` +accurately by its own reading and be wrong by the standard's own composition +rule. This is Decision 5.5's provider-declaration finding one layer down: a +reef is a provider with nowhere to say what it makes reachable. + +```task +id: NK-WP-0027-T01 +status: todo +priority: medium +``` + +**Establish the boundary between a reef and the `P` ladder in canon text.** Say +what each answers, why a reef is not a `P` level, and which of `P`'s couplings +(§3.2) a reef binding does and does not satisfy. Do not extend the `P` ladder. + +```task +id: NK-WP-0027-T02 +status: todo +priority: medium +``` + +**Extend Decision 5.5's provider declaration to substrate providers, and agree +it with `repo-manager`.** A reef should state, per axis it bounds, the maximum +level it makes reachable and what a consumer must do to reach it — the same +sentence `apps-pg` now owes its consumers. `reef-railiance`'s first line is +almost certainly a `V` ceiling. This is a proposal to `repo-manager`, not a +canon fiat: it owns the reef vocabulary and the acceptance record. + +```task +id: NK-WP-0027-T03 +status: wait +priority: low +``` + +**Join reef ceilings to consumer `V` declarations mechanically.** Waits on T02. +Decision 8.2's pattern applies — the requirer asserts, the declarer declares, +a machine reconciles. Until then a consumer bound to a reef declares `V` with +the reef named as a synchronous dependency, which is already required by +Decision 4.6.1 and is not being done. + +## Related + +- `canon/standards/tenancy-posture_v0.1.md` Decisions 4.6.1, 5.5, 8.4.1, 8.4.2 +- `repo-manager/docs/RailianceAppDeploymentGuide.md` — reefs, `bound_reefs` +- `railiance-master/docs/adr/ADR-0006-reef-production-admission.md` — topology + is not readiness +- `zone-engine/workplans/ZONE-WP-0001-security-zone-model.md` — where the + question came from, and why it is not answered there