tenancy-posture draft-9: enforcement stance is not a seventh axis; reefs are canon's defect
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Answers zone-engine ZONE-WP-0001-T01.

Decision 5.6 — enforcement stance is a sibling standard, not an axis. The six
ladders are monotone and the whole current/target/guard machinery depends on it;
enforcement stance is not (ADR-0006 is the finding that the top rung is wrong for
the SSH lane). And this framework is descriptive: an accurately declared exempt
would be conformant and exempt. Membership is declared, stance belongs to the
control owner. Zone membership rides tenancy.yaml under a reserved zones: key so
the estate keeps one declaration surface; the schema permits it, unconstrained.

Decisions 8.4.1/8.4.2 — 'substrate location is not evidence' stated once instead
of three repo-local slogans, and the reef/P/V gap recorded as this document's
defect rather than zone-engine's scope. NK-WP-0027 takes it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-19 22:06:20 +02:00
parent 4a915ce6c7
commit 9e041e3662
3 changed files with 512 additions and 86 deletions

View file

@ -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
}

View file

@ -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

View file

@ -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