flex-auth/workplans/FLEX-WP-0015-tenancy-posture-conformance.md
tegwick 3de72fe6f5
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 40s
Record the warn-first promotion sequence and operator gate on T02
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:24:16 +02:00

10 KiB
Raw Blame History

id type title domain repo status owner topic_slug planning_priority planning_order depends_on_workplans related_workplans created updated state_hub_workstream_id
FLEX-WP-0015 workplan Tenancy posture declaration and inbound caller authentication infotech flex-auth active claude netkingdom P1 150
FLEX-WP-0004
FLEX-WP-0011
2026-08-17 2026-08-18 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

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.

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. Source committed as 1e1e077.

Remaining, in this order — the sequence is a constraint, not a preference. ops-warden ruled on rollout (2026-08-17) and user-engine asked for the same:

  1. Build and pin a new immutable digest carrying the caller-auth code.
  2. Promote it in warn mode, not enforce. The reviewed desired manifests in deploy/ select enforce because that is the end state — applying them directly is the hazard. Warn authenticates and logs failures without rejecting, so it is safe for an unmigrated caller.
  3. Confirm the warn logs are clean of unauthenticated callers. user-engine is migrated and deployed as of 2026-08-18 (image sha256:c501aeb2…, token at /var/run/secrets/flex-auth-caller/token). tenant-engine's caller status is unconfirmed — asked 2026-08-18; promoting enforce while their client sends no token 401s every check and blocks their write paths. ops-warden adopts the calling side on its own schedule.
  4. Flip to enforce, then capture the live negative probe. user-engine has the three assertions written and ready in their docs/flex-auth-caller-identity.md; they deliberately have not run it, because against an unenforced digest "no token returns 401" comes back as a normal decision and would record as false evidence.
  5. Only then may policy.enabled flip anywhere — ops-warden names that the real deadline, and it is the same gate as FLEX-WP-0007.

Operator gate. Steps 14 need cluster credentials this session does not have: kubectl returns Unauthorized and the context is default, which tenant-engine documented on 2026-08-16 as indistinguishable from a wrong-cluster KUBECONFIG. Promotion follows FLEX-WP-0011 staged promotion and the CI image build, not a hand-built image.

RISK-F-0001 (risk-nexus) tracks this A0 externally. Their NetworkPolicy question was answered 2026-08-18: both Deployments carry an ingress policy restricted to one namespace and one pod label, predating this work — so the reachable set was never "any pod in the cluster", and a sentence of mine that said so was corrected to them.

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.

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.

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.