user-engine/docs/flex-auth-caller-identity.md
tegwick eaf0af617c
Some checks failed
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Has been cancelled
Caller-identity probe: name railiance01 as target and require node check
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 58902@bnt-lap001
Assistant-Session: 7ac7f865-2dc5-4aa7-8eb7-27a342109c2f
2026-09-22 16:19:28 +02:00

6.1 KiB

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.

The target cluster is railiance01 (node 92.205.62.239). Run the commands on the node (ssh railiance01, then the commands below). If you run them from the workstation, first confirm that kubectl get nodes -o wide shows that IP. A kubeconfig pointing at the retired coulombcore cluster answers with the wrong cluster (see the-custodian/docs/agent-environment-orientation.md §1).

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