flex-auth-user-engine is enforce on sha256:138aa347. Probe from user-engine-8569d7cb87-fp7px: 200 with the projected token, 401 without, 403 when representing tenant-engine. Record fsGroup 10001 so uid 10001 can read the projected caller token.
119 lines
5.8 KiB
Markdown
119 lines
5.8 KiB
Markdown
# flex-auth caller identity contract
|
|
|
|
Status: live A2 proven 2026-08-19 on `flex-auth-user-engine` (`enforce`).
|
|
|
|
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-19 `flex-auth-user-engine` is live `enforce` on digest
|
|
`sha256:138aa347…` (Helm release rev 4). user-engine `sha256:c501aeb2…`
|
|
sends the projected token. The pod security context must set
|
|
`runAsUser` / `runAsGroup` / `fsGroup` `10001` (image uid; see
|
|
Containerfile). Without `fsGroup`, the projected token is `0440 root:root`
|
|
and the adapter never sends `Authorization`. That is persisted in
|
|
rapp-user-engine `manifests/runtime.yaml` `e599b4f`. The portal image has
|
|
no `curl`; live probes use Python.
|
|
|
|
## 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.
|
|
|
|
Closed 2026-08-19 after `flex-auth-user-engine` flipped to `enforce`.
|
|
Do not re-run these against `warn`: step 2 would return a decision.
|
|
|
|
`POD` is any ready user-engine pod. The portal image has no `curl`; the
|
|
recorded run used Python. The bash shape below is the contract, not the
|
|
exact live invocation.
|
|
|
|
```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.
|
|
|
|
Recorded 2026-08-19 from pod `user-engine-8569d7cb87-fp7px` (flex-auth
|
|
message `45852487`):
|
|
|
|
| Step | Result |
|
|
| --- | --- |
|
|
| 1. Bearer projected token | `200` `decision:350b46fb42989606` allow `tenant_admin` |
|
|
| 2. No Authorization | `401` `unauthenticated` |
|
|
| 3. `resource.system=tenant-engine` | `403` `forbidden` |
|
|
|
|
Digests: flex-auth `sha256:138aa3471c46bca6e814691fa1e6520aedda3dffd743e6b09141ab433afdb64b`,
|
|
user-engine `sha256:c501aeb204154d85017565a87fd34cff12e5c48f600d124d3f8c82f11bb9d59c`.
|
|
tenant-engine and `policy.enabled` were not moved. A pass on steps 1 and 3
|
|
without a 401 on step 2 would have meant enforcement was 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 remains owed. It was not run in the 2026-08-19 A2 session.
|