Implement inbound caller authentication (ADR 0004); close T03 and T05
TokenReview-based caller identity with audience-scoped tokens and exact resource.system to ServiceAccount bindings, per ops-warden's recommendation. Deletes the unwired tenant-engine live-roles adapter (T03) and adds make verify-posture (T05). Source implements A2; running digest is still A0 until promotion, so tenancy.current.A stays 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
6d82ef7f14
commit
1e1e077b27
18 changed files with 768 additions and 357 deletions
65
docs/adr/0004-inbound-caller-authentication.md
Normal file
65
docs/adr/0004-inbound-caller-authentication.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# ADR 0004 — authenticate and bind authorization callers
|
||||
|
||||
Status: accepted (source implemented; production promotion pending)
|
||||
|
||||
Date: 2026-08-18
|
||||
|
||||
## Context
|
||||
|
||||
`POST /v1/check` and `/v1/batch_check` accepted an asserted subject, tenant and
|
||||
protected-system name from any workload with network reach. NetworkPolicy
|
||||
limited reachability but did not establish caller identity. A caller could
|
||||
therefore represent another protected system and obtain an authoritative
|
||||
decision under the wrong policy package.
|
||||
|
||||
The boundary must authenticate workloads without turning flex-auth into an
|
||||
identity issuer, sharing a long-lived secret between services, or coupling
|
||||
authorization availability to an unrelated identity-provider round trip.
|
||||
|
||||
## Decision
|
||||
|
||||
Use Kubernetes ServiceAccount tokens with audience `flex-auth`. flex-auth calls
|
||||
the Kubernetes TokenReview API through a separately projected reviewer token
|
||||
and binds the authenticated ServiceAccount principal to every
|
||||
`resource.system` in the request. Both the single and batch endpoints use the
|
||||
same choke point; health remains unauthenticated.
|
||||
|
||||
Each deployed policy instance has an explicit, exact binding. For example:
|
||||
|
||||
```text
|
||||
tenant-engine=system:serviceaccount:tenant-engine:tenant-engine
|
||||
user-engine=system:serviceaccount:user-engine:user-engine
|
||||
```
|
||||
|
||||
Unknown systems, missing or invalid tokens, audience mismatch and principal
|
||||
mismatch fail closed. TokenReview unavailability returns 503 rather than an
|
||||
authorization answer. Tokens and reviewer credentials are re-read rather than
|
||||
cached across rotation.
|
||||
|
||||
Three modes support promotion: `disabled`, `warn`, and `enforce`. Warn mode
|
||||
records the same authentication failures without logging credentials. It is a
|
||||
bounded migration aid, not a conformant steady state. The reviewed desired
|
||||
manifests select `enforce`; promotion still follows FLEX-WP-0011 and requires a
|
||||
new immutable image digest plus caller rollout evidence.
|
||||
|
||||
## Rejected alternatives
|
||||
|
||||
- NetworkPolicy alone proves network position, not workload identity.
|
||||
- A shared header secret has broad replay and rotation blast radius and cannot
|
||||
bind a Kubernetes workload principal.
|
||||
- Application mTLS would add a separate certificate lifecycle where the
|
||||
cluster already has short-lived projected workload identity.
|
||||
- Using the caller token itself to invoke TokenReview would grant callers an
|
||||
unnecessary API permission. A narrow reviewer ServiceAccount holds only
|
||||
`create` on `tokenreviews.authentication.k8s.io`.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Caller identity is bound once at the flex-auth ingress and cannot be swapped
|
||||
by changing request JSON.
|
||||
- flex-auth depends on the Kubernetes authentication API for uncached checks;
|
||||
an outage fails closed with 503.
|
||||
- Each new protected system needs an explicit binding and projected caller
|
||||
token. There is no wildcard binding.
|
||||
- Source and desired state reach A2, while the live declaration remains A0
|
||||
until the immutable digest is promoted and probed.
|
||||
|
|
@ -1,4 +1,9 @@
|
|||
# flex-auth review — NetKingdom Tenancy Posture v0.1 (draft-5)
|
||||
# flex-auth review — NetKingdom Tenancy Posture v0.1 (draft-5 review, draft-8 reconciliation)
|
||||
|
||||
> **Draft-8 outcome, 2026-08-17:** all amendments below were incorporated.
|
||||
> Root `tenancy.yaml` now uses the canonical provider block instead of the
|
||||
> provisional `enables_for_consumers` field, adds V, and reports implemented
|
||||
> E2 separately from evidenced current E1.
|
||||
|
||||
**Reviewer:** flex-auth
|
||||
**Date:** 2026-08-17
|
||||
|
|
@ -19,8 +24,10 @@ propose the smallest changes that fix them.
|
|||
Declared in `tenancy.yaml` at repo root, per §5.1. Summary:
|
||||
|
||||
```
|
||||
current: I1 A0 E2 P n/a R n/a enables A3 for consumers
|
||||
target: I1 A2 E2 P n/a R n/a
|
||||
current: I1 A0 E1 P n/a R n/a V0
|
||||
implemented: A2 E2
|
||||
target: I1 A2 E2 P n/a R n/a V1
|
||||
provider: enables A3 for consumers
|
||||
```
|
||||
|
||||
Three of these need defending.
|
||||
|
|
@ -35,21 +42,25 @@ explicitly refuses to be. flex-auth is at I1 permanently and by design, and the
|
|||
framework should be able to say that a permanent low rung is a decision rather
|
||||
than a stalled trajectory.
|
||||
|
||||
**A0 is the finding this review actually produced.** `POST /v1/check` and
|
||||
`POST /v1/batch_check` authenticate no caller (`cmd/flex-auth/main.go:349`).
|
||||
**A0 is the finding this review actually produced.** The running digest's
|
||||
`POST /v1/check` and `POST /v1/batch_check` authenticate 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 it currently trusts its own callers
|
||||
completely. That is A0 — "no authorization" — on its own inbound surface, and
|
||||
it is not something we knew we were carrying before this exercise. The
|
||||
framework earned its keep here. Target A2 under `FLEX-WP-0015-T02`.
|
||||
framework earned its keep here. Source and desired manifests now implement A2
|
||||
under `FLEX-WP-0015-T02`: Kubernetes TokenReview validates an audience-scoped
|
||||
caller token and exact bindings prevent one ServiceAccount from representing
|
||||
another protected system. Current remains A0 until staged immutable-image
|
||||
promotion and a live negative probe.
|
||||
|
||||
**E2 is asserted, not evidenced.** Tenant scoping runs through one choke
|
||||
**E2 was asserted in draft-5 but is not evidenced.** Tenant scoping runs through one choke
|
||||
point (`internal/decision/engine.go:188` normalisation, `:248` relationship
|
||||
tenant match). But §13.2 rules E2 evidence adversarial and explicitly says a
|
||||
passing CI run is not E2 evidence. We have unit tests, not a cross-tenant
|
||||
adversarial probe. E2 is therefore claimed with its gap stated rather than
|
||||
claimed clean, which we read as what §6 requires.
|
||||
adversarial probe. Draft-8 introduced `implemented` for exactly this state, so
|
||||
the canonical declaration now reports current E1 and implemented E2.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -121,22 +132,20 @@ and the scope needs one amendment.**
|
|||
§4.2 states as fact: "`flex-auth` calls `tenant-engine` synchronously on the
|
||||
authorization path." It does not, as of this commit.
|
||||
|
||||
`internal/adapters/tenantengine/` exists and is complete — an HTTP client for
|
||||
`GET /tenants/{id}/roles/live`, a `LiveRolesResult`, and `AttachToContext` to
|
||||
fold live roles into decision context. It has **no non-test caller**. The
|
||||
decision engine has no field for it and no hook that would invoke it
|
||||
(`internal/decision/engine.go:21`). The IAM Profile's live re-query capability
|
||||
is built and unwired.
|
||||
`internal/adapters/tenantengine/` existed as an HTTP client for
|
||||
`GET /tenants/{id}/roles/live` and had **no non-test caller**. No current policy
|
||||
uses `tenant_roles` for a privileged, destructive, credential-vending or
|
||||
`aal2` decision. It was therefore deleted on 2026-08-18 rather than turning
|
||||
tenant-engine into an unused synchronous availability dependency. A future
|
||||
policy that needs live roles must introduce that dependency explicitly.
|
||||
|
||||
Two consequences for the framework:
|
||||
|
||||
- The internal-hop anti-pattern it worries about is not live in flex-auth,
|
||||
because the internal hop is not live at all.
|
||||
- flex-auth's `I` cannot reach I3 today for the same reason. We had assumed
|
||||
otherwise before this review; that assumption is now corrected in
|
||||
`tenancy.yaml` and tracked as `FLEX-WP-0015-T03`, whose honest outcome is
|
||||
either wiring the adapter or deleting it. A built-and-unwired adapter is the
|
||||
worst of the three states because it reads as capability.
|
||||
- flex-auth remains I1 by design. The earlier I3 assumption is corrected in
|
||||
`tenancy.yaml`, and deletion closes `FLEX-WP-0015-T03` without advertising a
|
||||
latent capability.
|
||||
|
||||
### 3.2 The assertion itself — correct, with a scoping amendment
|
||||
|
||||
|
|
@ -191,7 +200,8 @@ Recommend the A ladder state that it describes **enforcement points**, and
|
|||
that a decision point declares two numbers: its own inbound level, and the
|
||||
maximum level it enables for consumers. flex-auth then reads `A0, enables A3`
|
||||
— which is both accurate and considerably more alarming than `A3`, correctly.
|
||||
`tenancy.yaml` uses `enables_for_consumers` pending a canonical field name.
|
||||
Draft-5's declaration used `enables_for_consumers` pending a canonical field
|
||||
name. Draft-8 standardises this under `provider.axes.A`.
|
||||
|
||||
### 4.2 P and R have no rung for a service with no datastore
|
||||
|
||||
|
|
@ -261,10 +271,10 @@ volunteer.
|
|||
| Task | |
|
||||
|---|---|
|
||||
| T01 | Publish the posture vector and this review; reply to `rapp-postgres` |
|
||||
| T02 | **Close the A0**: decide and record how `/v1/check` authenticates its callers |
|
||||
| T03 | Wire or delete the tenant-engine live-roles adapter |
|
||||
| T02 | **Close the A0**: TokenReview source/desired state done; immutable promotion pending |
|
||||
| T03 | Deleted the unused tenant-engine live-roles adapter |
|
||||
| T04 | AuthZEN endpoint — `wait`, with a written trigger |
|
||||
| T05 | Guard: mechanical check that `tenancy.yaml` still matches the code |
|
||||
| T05 | `make verify-posture` guards declaration/source/deployment drift |
|
||||
|
||||
Nothing in this review changes a running system, and T02 will not be applied
|
||||
to production without the usual staged-promotion path.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue