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

9.5 KiB

id type title domain repo status owner topic_slug created updated depends_on unblocks origin origin_ref state_hub_workstream_id
TEN-WP-0011 workplan Align tenant-engine with the accepted security layer model infotech tenant-engine finished grok netkingdom 2026-08-29 2026-08-29
TEN-WP-0003
TEN-WP-0006
TEN-WP-0009
residual TEN-IN-0003 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

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

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

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/livePOST /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

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

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

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).