tenant-engine/workplans/TEN-WP-0011-security-layer-conformance.md
tegwick 672cf4da6e
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 37s
Implement TEN-WP-0011 security layer conformance
Engine/PIP declaration is now checkable (layer.yaml plus a Tooling-client
scan). Writes persist a decision record or the published fail-closed
stance, live-lookup freshness is published, events_for is tenant-scoped,
and mutation evidence drains to audit-core from a local outbox without
blocking the mutation.

Sender registration is requested as AUDIT-IN-0002. Boundary-contract
amendment is requested as NET-IN-0002.

Assistant: grok
Assistant-Session: 01a04cea-e5e8-7081-a0fc-808ebbc35fa9
2026-08-29 13:02:51 +02:00

243 lines
9.5 KiB
Markdown

---
id: TEN-WP-0011
type: workplan
title: "Align tenant-engine with the accepted security layer model"
domain: infotech
repo: tenant-engine
status: finished
owner: grok
topic_slug: netkingdom
created: "2026-08-29"
updated: "2026-08-29"
depends_on:
- TEN-WP-0003
- TEN-WP-0006
- TEN-WP-0009
unblocks: []
origin: residual
origin_ref: TEN-IN-0003
state_hub_workstream_id: "43ad25c6-7149-53d1-8012-817a061ace92"
---
# TEN-WP-0011 — Align tenant-engine with the accepted security layer model
The NetKingdom Security Layer Model is accepted at v0.7. This repository is
catalogued Engine / PIP for tenant-as-an-entity facts. `INTENT.md` now
declares that in its own voice (`TEN-DEC-2026-001`). The remaining work is
to make the shipped engine match the obligations that declaration takes on.
Review of record:
`history/2026-08-29-security-layer-intent-scope-review.md`.
Start here: `net-kingdom/SECURITY-COMPANION.md` v0.2. Statute:
`net-kingdom/canon/standards/security-layer-model_v0.7.md`. On disagreement
the statute governs.
## Why this is a workplan
Declaration is documentation. Conformance is mechanical. Companion §2
offers `layer.yaml` plus a check as the working reference. Companion §5
and statute §6.4 bind the write path as PEP-shaped. Statute §9.6 and the
already-open intakes `TEN-IN-0001` / `TEN-IN-0002` bind evidence. Statute
§9.7 requires a published lifetime on PIP input classes.
This is not a layer change (statute §10). We are declaring the layer we
already occupy and closing the gaps between that layer's rules and the
code.
## Non-goals
- Not a second PDP. Guardrail resolution stays a ceiling, never an allow.
- Not an approval engine. `TEN-DEC-2026-001` contested that placement.
- Not containment or production observation (companion §10).
- Not staged promotion — that remains `TEN-WP-0008`.
- Not forking the boundary contract. T06 requests an amendment in
`net-kingdom`; it does not copy the contract into this repo.
## T01 — Machine-readable layer declaration and conformance check
```task
id: TEN-WP-0011-T01
status: done
priority: high
state_hub_task_id: "f46944d8-e638-5f64-a4ad-a713102dfa72"
```
Add `layer.yaml` modelled on `ops-warden`'s reference and `kings-guard`'s
no-Tooling adaptation: `layer: engine`, `role: pip`, `standard_version:
"0.7"`, empty `tooling_contacts`, and a complete `non_tooling_clients`
inventory (PostgreSQL / SQLite store, `flex-auth` `/v1/check`, State Hub
work records).
Add a check script and a test so a new catalogued Tooling client
(`key-cape`, OpenBao) fails on the commit that introduces it. Record
non-Tooling clients so the check is total (companion §4).
Done when: `INTENT.md` frontmatter and `layer.yaml` agree on layer and
role; `make test` covers the check; no OpenBao / `key-cape` client exists
in `src/`.
## T02 — PEP write-path: decision records and published fail-closed stance
```task
id: TEN-WP-0011-T02
status: done
priority: high
state_hub_task_id: "62659305-8283-594e-8670-f9335f5c74a0"
```
`FlexAuthCheckClient.is_allowed()` currently returns a bool and drops the
decision envelope. The boundary contract's Audit Correlation Contract
already required `authorization_decision_id` on every mutation; the code
never stored it.
- Keep the decision record (id, effect, request digest) on the authorizer
seam and persist it on every mutation event.
- Where `access-engine` is unset or unreachable, record the **application
of the stance** in place of a decision (companion §5.1). Do not proceed
silently.
- Publish the unreachable-engine stance as a file named from `layer.yaml`
(fail-closed for every scope this engine serves — this is not
ops-warden's per-zone fail-open map). Assert with a test that the
published map equals shipped behaviour (`DefaultDenyWriteAuthorizer` and
transport-failure deny).
- Lock obligation 2: no verdict cache. Re-check every write. Add a test
that a previous allow cannot authorize a later, different request.
- Offer the map to gate-house for the statute §13.1 register. Do not edit
the statute from this repo.
Done when: a granted role, a denied grant, and an unreachable-engine
attempt each leave a reconstructable record; `make test` proves stance
file == code; no write path can succeed without one of those two records.
## T03 — PIP claim freshness, and prove live-lookup is not cyclic
```task
id: TEN-WP-0011-T03
status: done
priority: high
state_hub_task_id: "7815548c-36aa-518c-812e-8562be3a5111"
```
Statute §9.7: every allow has a lifetime, and a PDP states a deadline per
input class. This engine owes the input-class description.
- Publish, next to the stance map, the freshness contract for
`tenant_roles` / grouping / plan / guardrail claims: cache-read is
bounded by the IAM Profile token lifetime; live-lookup results MUST NOT
be cached across requests; request-scoped memoization inside one
`access-engine` decision remains allowed (boundary contract).
- Name the consumer-visible failure: store unavailability is `503`, never
empty roles. That is already shipped; make it part of the published
contract so `access-engine` can treat it as input degradation (§9.3).
- **Prove** that `GET /tenants/{id}/roles/live``POST /v1/check`
(`tenant.role.read.live`) does not re-enter tenant-engine for the claims
that check needs. If it does, the live path is cyclic and must be
unwound (static service-identity rule, or an authorizer bypass that is
itself a declared exception — prefer the former). Record the proof in
`docs/flex-auth-integration.md`.
Done when: the freshness contract is published; a test fails if live-lookup
cross-request caching is introduced; the re-entry question is answered with
evidence, not a comment.
## T04 — Externalize mutation evidence to audit-core
```task
id: TEN-WP-0011-T04
status: done
priority: high
state_hub_task_id: "cfab6837-5f62-5beb-b99f-943d39b5b94d"
```
Promoted from `TEN-IN-0001` (origin: residual of `TEN-WP-0009`).
The local `events` table shares a database and runtime credential with the
facts it attests to. Statute §9.6: that trail cannot be described as
independent custody. Classify current classes as **attributive** unless a
named control branches on their presence.
Design and implement a governed tenant-engine → `audit-core` emission path:
1. Agree envelope, sender registration, and credential custody with
`audit-core` without putting a secret in Git or a work record. Route
credentials through `warden route`, never through a State Hub message.
2. Preserve mutation atomicity (local outbox against crash) while defining
retry, duplicate, and audit-core-unavailable semantics. Unavailable
evidence MUST NOT fail-open a mutation, and SHOULD NOT fail-closed a
mutation either unless a class is later reclassified load-bearing —
declare the trade if emission stays attributive and non-blocking.
3. Prove the external copy cannot be rewritten through tenant-engine's
runtime database credential.
4. Decide whether pre-cutover event classes need a bounded backfill, and
record the decision.
5. Do not claim the archive proves events happened, or that absence proves
they did not.
Done when: production mutations emit to `audit-core` under the bound above;
the trade (atomic vs non-blocking) is declared; `TEN-IN-0001` can close as
promoted and completed.
**Done 2026-08-29:** local outbox is in the mutation transaction; drain
POSTs `/v1/events` and never fails the mutation; trade declared in
`docs/evidence-emission.md`; no pre-cutover backfill. Production landing
waits on sender registration, requested as `AUDIT-IN-0002`.
## T05 — Remove or authorize the unfiltered event-read interface
```task
id: TEN-WP-0011-T05
status: done
priority: high
state_hub_task_id: "7540049b-9b76-5d16-9ce4-57395a53ecbc"
```
Promoted from `TEN-IN-0002` (origin: RISK-F-0004).
`TenantStore.events()` returns every tenant's event and payload. There is
no HTTP event-read route today, but the production protocol still offers
the dump.
- Remove `events()` from the production protocol, **or** replace it with
an authorized, deliberately scoped export/query that cannot return
another tenant's payload.
- Add cross-tenant negative tests.
- Hand risk-nexus the accessibility and payload reading.
Done when: a production caller cannot list another tenant's events through
this repo's store protocol; tests say so; `TEN-IN-0002` can close.
**Done 2026-08-29:** `events()` removed from the production protocol;
`events_for(tenant_id)` is the only read. Cross-tenant negative test in
`tests/test_events_scoped.py`. Reading handed to risk-nexus.
## T06 — Request the boundary-contract amendment
```task
id: TEN-WP-0011-T06
status: done
priority: medium
state_hub_task_id: "ffd2cc53-bde2-5e3c-a144-ab565bc12776"
```
`net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md` still
says guardrail/quota policy is "reserved, not yet implemented" and that
`tenant-engine` is "not a policy enforcement point". Both sentences now
mislead: guardrails shipped in `TEN-WP-0006`, and writes are PEP-shaped
even though this repo is not a PDP.
File an intake on `net-kingdom` (do not edit the canon from here) asking
to:
- mark guardrail/quota policy as owned and implemented, pointing at
`docs/tenant-guardrail-policy.md`;
- restate the authorization contract: data source / PIP for decisions;
PEP-shaped for its own protected writes; never a second PDP;
- mention Engine / PIP and the v0.7 statute next to the existing IAM
Profile / ADR-0013 / ADR-0014 references.
Done when: the intake exists on the canon side with a named id, and this
task records it.
**Done 2026-08-29:** filed `NET-IN-0002` on `net-kingdom`
(`net-kingdom/intakes/intakes.md`).