user-engine/docs/flex-auth-caller-identity.md
tegwick e54f67c537 Document the live caller-identity and lifecycle probes
The caller side of USER-WP-0023 is deployed, but FLEX-WP-0015-T02 is still
wait: flex-auth's TokenReview choke point exists in source while the running
digest is unchanged, so production accepts unauthenticated callers and our
Authorization header is ignored. Running the T03 probe now would record a
false pass on the negative case.

Captures both owed live exercises as concrete commands so they can be run by
whoever holds cluster credentials the moment flex-auth promotes A2, and notes
that they share one rollout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:52:40 +02:00

4.9 KiB

flex-auth caller identity contract

Status: source implemented; production promotion pending.

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.

The current deployed image predates this file-based caller token. Promote only with the matching flex-auth A2 image and bindings; otherwise enforcing flex-auth will correctly return 401 to the old caller.

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.

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

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.