user-engine/docs/flex-auth-caller-identity.md

113 lines
5.6 KiB
Markdown
Raw Normal View History

# flex-auth caller identity contract
Status: caller side implemented and deployed; live proof pending flex-auth A2 promotion (FLEX-WP-0015-T02).
Governing published policy (2026-08-19):
- IAM Profile v0.3 Service Account Flow — audience-scoped, rotating
workload identity:
`https://policy.coulomb.social/standards/iam-profile/v0.3/`
- Tenancy Posture v0.1 A2 — inbound caller choke point:
`https://policy.coulomb.social/standards/tenancy-posture/v0.1/`
user-engine calls `flex-auth-user-engine` with a projected Kubernetes
ServiceAccount token whose audience is exactly `flex-auth`. The adapter reads
the token file for every decision so hourly projection rotation requires no
restart. A missing, empty or unreadable file fails closed as an authorization
denial; the token value is never logged.
flex-auth binds protected system `user-engine` to principal
`system:serviceaccount:user-engine:user-engine`. The token authenticates the
calling workload only. It does not replace the IAM actor/tenant/assurance facts
inside the authorization request and grants no Kubernetes API permission to
user-engine.
Runtime configuration requires `USER_ENGINE_FLEX_AUTH_TOKEN_FILE`. Local
construction keeps the adapter argument optional so unit tests and explicit
non-production adapters remain usable.
The tenant authority seam is distinct: user-engine identifies itself as actor
`user-engine` on tenant lifecycle reads and writes; tenant-engine performs its
own flex-auth decision before store access. Tenant ids remain opaque and are
URL-encoded. No client may infer existence from an unauthorized read.
As of 2026-08-18 the deployed user-engine image carries this file-based caller
token, so the caller side is no longer the laggard. The remaining asymmetry runs
the other way: flex-auth's A2 enforcement is implemented but unpromoted, so the
header is sent and ignored. That ordering is safe — a caller that authenticates
against a service that does not yet check is harmless, whereas the reverse
would have returned 401 to every decision.
## Live promotion probe (USER-WP-0023-T03)
The caller side is deployed: image `sha256:c501aeb2…` (user-engine `7604d31`)
runs in namespace `user-engine` as ServiceAccount `user-engine`, with the
projected token mounted at `/var/run/secrets/flex-auth-caller/token` under
audience `flex-auth` and `USER_ENGINE_FLEX_AUTH_TOKEN_FILE` pointing at it.
**This probe cannot pass yet, and the reason is upstream.** FLEX-WP-0015-T02
is `wait`: ADR 0004's TokenReview choke point is implemented in flex-auth
source, but the running digest is unchanged, so production still accepts
unauthenticated callers. Our Authorization header is currently sent and
ignored. Run the probe only after flex-auth promotes A2 through
FLEX-WP-0011; before then step 2 returns a normal decision instead of 401 and
would record a false pass.
Run from an operator shell with cluster credentials (this repo's sessions do
not hold them). `POD` is any ready user-engine pod.
```bash
POD=$(kubectl -n user-engine get pod -l app.kubernetes.io/name=user-engine \
-o jsonpath='{.items[0].metadata.name}')
CHECK=http://flex-auth-user-engine.flex-auth.svc.cluster.local:8080/v1/check
BODY='{"actor":{"issuer":"https://kc.coulomb.social","subject":"probe",
"tenant":"tenant:friendly:binky","principal_type":"human",
"audience":["user-engine"],"roles":["tenant-admin"]},
"resource":{"system":"user-engine","type":"user-engine:user","id":"probe"},
"action":"user.update","tenant":"tenant:friendly:binky",
"correlation_id":"probe-t03","context":{}}'
```
1. **A valid caller succeeds.** Expect `200` and a decision id.
```bash
kubectl -n user-engine exec "$POD" -c portal -- sh -c \
"curl -sS -o /dev/null -w '%{http_code}\n' -X POST $CHECK \
-H 'Content-Type: application/json' \
-H \"Authorization: Bearer \$(cat /var/run/secrets/flex-auth-caller/token)\" \
-d '$BODY'"
```
2. **No token is refused.** Expect `401`, not a decision.
```bash
kubectl -n user-engine exec "$POD" -c portal -- sh -c \
"curl -sS -o /dev/null -w '%{http_code}\n' -X POST $CHECK \
-H 'Content-Type: application/json' -d '$BODY'"
```
3. **user-engine cannot represent another protected system.** Re-run step 1
with `resource.system` set to `tenant-engine`. Expect a refusal from the
binding, not an allow: the token binds `user-engine` to
`system:serviceaccount:user-engine:user-engine` only. That binding is the
A2 choke point named by Tenancy Posture v0.1; publication does not change
the remaining operator sequence.
Record all three results, the flex-auth digest they ran against, and the
user-engine digest, in USER-WP-0023-T03 before marking it done. A pass on
steps 1 and 3 without a 401 on step 2 means enforcement is still off.
## Live cross-service check (USER-WP-0021)
Owed separately to tenant-engine after production served 404 on the lifecycle
routes from 2026-08-13 to 2026-08-16 while our contract-level suite passed
throughout. Contract evidence structurally cannot see a pin rollback, so this
must be exercised live, from a pod, against the real authority.
Use a disposable tenant — never a real one, since retire is a lifecycle
mutation. Through the portal's own platform routes as a platform operator:
`GET` the record, `PATCH` its display name, `retire`, then `reactivate`,
echoing each returned version as the next `If-Match`. Expect `200` throughout
and no `404`, which is what the rollback window produced. Capture the
tenant-engine digest alongside the results; both silent rollbacks this month
were digest-level and neither raised an alert.
This shares the operator rollout with the T03 probe above and is most
efficiently run in the same session.