--- id: FLEX-WP-0015 type: workplan title: "Tenancy posture declaration and inbound caller authentication" domain: infotech repo: flex-auth status: active owner: claude topic_slug: netkingdom planning_priority: P1 planning_order: 150 depends_on_workplans: [] related_workplans: - FLEX-WP-0004 - FLEX-WP-0011 created: "2026-08-17" updated: "2026-08-17" --- # FLEX-WP-0015 - Tenancy posture declaration and inbound caller authentication Answer `rapp-postgres`'s review request on **NetKingdom Tenancy Posture v0.1** (message `82ec43ce`), publish flex-auth's posture vector, and close the one real defect the review surfaced. **Review output:** `docs/tenancy-posture-review.md` **Declaration:** `tenancy.yaml` (repo root, per framework §5.1) ## Why this workplan exists Three of the framework's five axes describe flex-auth cleanly. Assessing against them turned up two things we did not know we were carrying: 1. **`/v1/check` authenticates no caller.** Any workload with network reach to the ClusterIP Service can assert any subject and any tenant and receive an authoritative allow. flex-auth is the estate's authorization oracle and trusts its callers completely. Framework level `A0`. 2. **The tenant-engine live-roles adapter is built and unwired.** No non-test caller exists. The IAM Profile live re-query reads as a capability we have and is not one. The framework itself needs six amendments, two of them corrections of statements it makes about flex-auth. Those are in the review document and go back to `rapp-postgres` as review output, not as work here. **Scope discipline:** this workplan closes flex-auth's own gaps and answers the review. It does not build fleet-level conformance tooling, does not adopt AuthZEN, and does not touch production enforcement (`policy.enabled` stays off per FLEX-WP-0007). ## Tasks ```task id: FLEX-WP-0015-T01 status: done priority: high ``` **Publish posture and answer the review.** Write `tenancy.yaml` and `docs/tenancy-posture-review.md`; reply to `rapp-postgres` with the posture vector, the A4 ruling, the internal-hop ruling, and the two factual corrections. Done 2026-08-17. ```task id: FLEX-WP-0015-T02 status: todo priority: high ``` **Close the A0 — authenticate callers of `/v1/check`.** Decide first, build second: this is a production-facing security boundary on a service two consumers already depend on, and the wrong instrument is worse than the delay. Record an ADR covering at minimum: - What identifies a caller — Kubernetes ServiceAccount token review, mTLS via the cluster's own CA, or a shared secret header. Note that the Deployment currently sets `automountServiceAccountToken: false`, so option one changes the pod spec. - Whether caller identity only authenticates, or also constrains: may `user-engine` ask questions about resources owned by `tenant-engine`? A caller allowlist per `system` is the natural shape and flex-auth's registry already keys on `system`. - Fail-closed vs fail-open during rollout. Given `policy.enabled` is off in production and both consumers are ours, a warn-only phase that logs unauthenticated callers before rejecting them is available and probably right. - Whether NetworkPolicy is a sufficient interim control to declare `A1` honestly before the real fix lands. Target level `A2` — a single inbound choke point binding caller identity — with the §13 evidence artifact: a test that an unbound request is refused. Ship through the FLEX-WP-0011 staged-promotion path, not by direct apply. ```task id: FLEX-WP-0015-T03 status: todo priority: medium ``` **Wire or delete `internal/adapters/tenantengine`.** The adapter is complete and has no non-test caller; `decision.Engine` has no field for it. Both outcomes are conformant, silence is not. Deciding factor is whether any live policy needs `tenant_roles` for a privileged, destructive, credential-vending or `aal2`-class decision — the IAM Profile's own trigger list. If yes, wire it into the decision path with a timeout and an explicit unavailable-path decision (`AttachToContext` already carries `tenant_roles_available: false` for exactly this), and note the new synchronous dependency on tenant-engine's availability. If no, delete it and say so in the review record rather than leaving it as latent capability. Update `tenancy.yaml` `I` either way. ```task id: FLEX-WP-0015-T04 status: wait priority: low ``` **AuthZEN evaluation endpoint (framework `A4`).** Deliberately deferred, not backlogged. `POST /access/v1/evaluation` translating onto the existing engine beside `/v1/check`, non-breaking. **Trigger to start:** either a PEP arrives that flex-auth's operator does not control, or a delegated backend under FLEX-WP-0004 needs uniform addressing. Until one of those, A4 buys swappability nobody can use. Note when starting: AuthZEN standardises the envelope, not the action vocabulary — it does not end action-string copying between repos. The cheaper fix for that is serving `registry.Actions` read-only, which is independent of this task and may be worth doing first. ```task id: FLEX-WP-0015-T05 status: todo priority: medium ``` **Guard the declaration.** Framework §12 requires verifying the declared posture still holds — against our own declaration, not a universal maximum. Small mechanical check, `make verify-posture`, in the shape tenant-engine used for `verify-pin` (bash, no dependency tree). Assert what code can actually prove: that the `/v1/check` handler has caller authentication iff `tenancy.yaml` claims `A >= 2`; that `tenantengine` has a non-test caller iff `I >= 3`; that the production Deployment carries no `--log` and no volume iff `R` is `n/a`. Exit 1 on drift. Designed for invisible failure per §12: posture regression returns correct-looking responses, exactly like the two silent pin rollbacks the estate has already been bitten by.