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

223 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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