223 lines
10 KiB
Markdown
223 lines
10 KiB
Markdown
---
|
||
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 1–4 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`.
|