--- 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-18" state_hub_workstream_id: "31846b19-c2a3-428e-950b-5985bc9146eb" --- # 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 The original five axes described flex-auth unevenly; draft-8's sixth availability axis and canonical provider declaration now cover the remaining shape. 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 state_hub_task_id: "0a32500d-511a-4bd7-972e-de4cd3e62dd9" ``` **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: wait priority: high state_hub_task_id: "b2e87a81-b63d-4be4-ad44-01426b7e6f74" ``` **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. **Waiting on `ops-warden`** (asked 2026-08-17): it owns how workloads prove identity in this estate, and it is a flex-auth PEP, so it implements the calling side of whatever we choose. `warden route find` was run first per the credential-routing rules — the catalog holds credential lanes and has no entry for service-to-service caller authentication as a pattern, which is part of what was asked. Our stated instinct is ServiceAccount TokenReview; the question is open pending their answer. Nothing is built until it lands. 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. 2026-08-18 source outcome: ADR 0004 selects audience-scoped Kubernetes ServiceAccount TokenReview with a separately projected reviewer identity and exact `resource.system` → ServiceAccount bindings. Both check endpoints share the choke point; missing/mismatched identity fails 401/403 and reviewer outage fails 503. Unit and handler tests include the required unbound request. Desired manifests select enforce mode and carry narrow TokenReview RBAC. The running digest is unchanged, so `tenancy.current.A` honestly remains 0 while `tenancy.implemented.A` is 2. Remaining: build/pin/promote through FLEX-WP-0011, migrate both callers and capture the live negative probe. ```task id: FLEX-WP-0015-T03 status: done priority: medium state_hub_task_id: "64eb7652-3b67-4bfb-879b-8588deeec8b5" ``` **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. Completed 2026-08-18 by deletion. Repository-wide search found no non-test caller and no policy consuming `tenant_roles`; wiring it would add a synchronous dependency without a decision that uses the result. The adapter and its isolated tests were removed, and the review records that any future live-role policy must introduce the dependency explicitly. ```task id: FLEX-WP-0015-T04 status: wait priority: low state_hub_task_id: "06432560-c28c-4de2-a672-87e74be54a6a" ``` **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: done priority: medium state_hub_task_id: "9195ba20-ab0f-4d75-b293-86978073beb1" ``` **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. Completed 2026-08-18. `make verify-posture` checks the current/implemented distinction, both authenticated handlers, enforce-mode desired manifests, TokenReview RBAC, absence of a latent tenant-engine caller, and the stateless deployment assumptions behind `R: n/a`.