--- id: ACTIVITY-WP-0025 type: workplan title: "SSO access control for ops console and Temporal UI" domain: infotech repo: activity-core status: finished owner: codex topic_slug: activity-core created: "2026-07-22" updated: "2026-07-22" state_hub_workstream_id: "347cad20-d66c-5151-b577-50eb93a85023" --- # SSO access control for ops console and Temporal UI ## Origin ACTIVITY-WP-0024 shipped the operator automation console (`/ops/ui`) with ClusterIP + port-forward access and a shared operator token for mutations. Operators also use Temporal Web UI (`actcore-temporal-ui:8080`) for workflow debugging; the ops UI now deep-links to it. **Pain:** port-forward + kubeconfig switching is fragile; shared token is not identity-aware. **Goal:** proper browser access via SSO without port-forward. Closes SCOPE **G10** (API external access) for the **ops and Temporal UIs** (not necessarily for unauthenticated programmatic API from the public internet). ## Goal Expose **activity-core ops UI** and **Temporal Web UI** behind authenticated ingress with **Keycloak / key-cape OIDC SSO**, so an authorized operator can: 1. Open HTTPS URLs in a browser (no `kubectl port-forward`) 2. Authenticate with org SSO (MFA per IAM profile where required) 3. Use ops console mutations under **their identity** (not only a shared token) 4. Open Temporal UI under the same access policy ## Non-goals - Public anonymous access to `/ops` or Temporal UI - Replacing Temporal’s own RBAC model with fine-grained multi-tenant Temporal namespaces for all of Coulomb (single activity-core Temporal is fine for MVP) - Full API productization for third-party clients (focus is **human UI access**) - Moving Temporal server hosting ownership out of the activity-core namespace (unless railiance-platform already owns that path) ## Architecture sketch (proposed) ``` Browser │ HTTPS ▼ Ingress (TLS) ──► oauth2-proxy / key-cape edge ── OIDC ──► Keycloak (key-cape) │ │ │ allowlisted groups │ identity headers / session ▼ ▼ actcore-api (/ops, /ops/ui) actcore-temporal-ui mutations: require SSO subject (retire shared token as primary; keep break-glass optional) ``` Credential routing: **key-cape** owns OIDC/login (`warden route show key-cape-oidc-login`); **flex-auth** for authorization decisions if used; **do not** invent secrets in git or State Hub. ## Design decisions to lock in T01 | Decision | Options | Lean | | --- | --- | --- | | Hostnames | e.g. `activity.ops.…` / `temporal.ops.…` vs path-based single host | Prefer **two hostnames** or one host with path prefixes | | Auth edge | oauth2-proxy vs nginx auth_request vs key-cape native | Prefer **oauth2-proxy** or fleet-standard edge if one already exists | | Mutation identity | SSO email/sub as principal; drop shared token for normal use | Shared token = break-glass only | | Temporal UI auth | Same edge in front of `actcore-temporal-ui` | Yes for MVP | | Who may access | Explicit Keycloak group (e.g. `activity-core-operators`) | Least privilege | ## Tasks ## Task: Access policy brief and hostnames ```task id: ACTIVITY-WP-0025-T01 status: done priority: high state_hub_task_id: "44899e72-0481-5cd2-baa9-b96a547e4cd2" ``` 1. Document intended audience (founders / operators), MFA expectation, and allowed IdP groups. 2. Propose hostnames + TLS cert path (cluster issuer / railiance DNS). 3. Align with railiance-platform / key-cape conventions; open coordination messages if host or client registration is external. 4. Update SCOPE G10 with the chosen posture (UI SSO vs full public API). **Done when:** short design note in `docs/` or this workplan is approved; hostnames and IdP group named. ## Task: Inventory fleet SSO ingress patterns ```task id: ACTIVITY-WP-0025-T02 status: done priority: high state_hub_task_id: "b48ff4e4-bf85-54f5-a107-aa247e9fc625" ``` 1. Find an existing oauth2-proxy / OIDC ingress pattern on railiance01 or coulombcore (railiance-platform, state-hub dashboard, etc.). 2. Prefer **reuse** over inventing a new edge stack. 3. Record the pattern (manifest paths, secrets ownership, callback URLs). **Done when:** “pattern to copy” is written with repo/file pointers. ## Task: Ingress + TLS for ops API (read path) ```task id: ACTIVITY-WP-0025-T03 status: done priority: high state_hub_task_id: "58c8e105-e73a-5417-b8b9-d0dc64b20194" ``` 1. Add Ingress (or Gateway) for `actcore-api` serving at least `/ops` and `/ops/ui` (and health for probes as needed). 2. Terminate TLS with the platform cert strategy. 3. Gate with SSO edge; unauthenticated browser hits redirect to login. 4. Keep ClusterIP service; no LoadBalancer unless required. **Done when:** authorized browser reaches `/ops/ui` over HTTPS without port-forward; unauthorized gets login or 403. ## Task: Ingress + SSO for Temporal Web UI ```task id: ACTIVITY-WP-0025-T04 status: done priority: high state_hub_task_id: "99f56e9a-6a5a-5fe9-a4c4-4a497a5c256e" ``` 1. Expose `actcore-temporal-ui` behind the same auth edge (hostname or path). 2. Ensure Temporal UI env (`TEMPORAL_ADDRESS`, public URL / CORS if required) works behind the external hostname. 3. Ops console `ACTIVITY_CORE_TEMPORAL_UI_URL` points at the **public SSO URL**. **Done when:** Temporal UI opens from the ops nav link without a second port-forward, under SSO. ## Task: Map SSO identity into ops mutations ```task id: ACTIVITY-WP-0025-T05 status: done priority: high state_hub_task_id: "7d948dc6-374e-59ac-a6c9-e7fa6e8a128b" ``` 1. Accept trusted identity from the auth edge (e.g. `X-Forwarded-User` / `X-Auth-Request-Email` — exact headers depend on T02 pattern). 2. Prefer SSO principal over shared `ACTIVITY_CORE_OPERATOR_TOKEN` for mutations when present. 3. Audit log records SSO subject (never tokens). 4. Optional break-glass: keep operator token for emergency; document custody. **Done when:** Run now / pause audit shows human identity from SSO; shared token not required for normal operator use. ## Task: Keycloak client and group membership ```task id: ACTIVITY-WP-0025-T06 status: done priority: medium state_hub_task_id: "a8354b03-2e43-5fe1-8d40-4a39502d9090" ``` 1. Register OIDC client(s) via key-cape / netkingdom process (not ad-hoc). 2. Create/bind operator group; grant only named operators. 3. Document how to add/remove operators. **Done when:** login works for a test operator and fails for an unprivileged account. **Disposition (2026-07-22):** No new KeyCape/Keycloak OIDC client — fleet path is Traefik ForwardAuth → Authelia. Least-privilege group work implemented in **net-kingdom** `NK-WP-0021` (finished): LLDAP `activity-core-operators`, Authelia domain allow/deny for both public hosts, membership runbook `sso-mfa/k8s/lldap/OPERATOR-GROUPS.md`. Intakes NK-IN-0001/0002 closed `promoted_to: NK-WP-0021`. ## Task: Docs and runbook cutover ```task id: ACTIVITY-WP-0025-T07 status: done priority: medium state_hub_task_id: "4cfe1761-9bc4-58d1-bd0b-b0112230344a" ``` 1. Runbook: primary access = SSO URLs; port-forward = break-glass only. 2. k8s README: Ingress manifests and secret ownership. 3. Credential routing notes: key-cape for login; no secrets in git. **Done when:** an operator can follow runbook without kubectl for daily use. ## Task: Verify and harden ```task id: ACTIVITY-WP-0025-T08 status: done priority: medium state_hub_task_id: "a3bddd50-deec-5e72-9311-02694d0fc2dc" ``` 1. Confirm ClusterIP APIs without Ingress path remain not publicly reachable. 2. Confirm mutations without SSO (and without break-glass token) fail closed. 3. Smoke: inventory, status, Run now dry path, Temporal UI namespaces list. 4. Optional: network policies locking ingress → services. **Done when:** checklist signed off in workplan closeout notes. ## Success criteria - [x] HTTPS SSO URLs for ops console and Temporal UI (no port-forward required) - [x] Only authorized IdP group can access (`activity-core-operators` via NK-WP-0021) - [x] Ops mutations attribute to SSO identity; audits show principal (`sso:`) - [x] Ops nav Temporal link points at SSO Temporal URL - [x] Runbook documents SSO primary + break-glass port-forward - [x] SCOPE G10 updated to reflect UI SSO posture ## Dependencies / coordination - **key-cape** / Keycloak: OIDC clients, groups (`warden route show key-cape-oidc-login`) - **railiance-platform**: DNS, cert-manager, ingress controller norms - **ACTIVITY-WP-0024**: ops console exists; this WP unlocks production access ## Implementation order 1. T01–T02 design + pattern reuse 2. T06 IdP client (can parallel with T03) 3. T03 ops Ingress + SSO 4. T04 Temporal UI Ingress + env URL 5. T05 mutation identity 6. T07–T08 docs and verify ## References - `docs/runbook.md` — operator console + dual port-forward (pre-SSO) - `src/activity_core/ops_api.py` — `ACTIVITY_CORE_TEMPORAL_UI_URL` - `k8s/railiance/` — `actcore-api`, `actcore-temporal-ui` ClusterIP services - ACTIVITY-WP-0024 operator automation console ## Progress 2026-07-22 | Task | Status | Notes | | --- | --- | --- | | T01 | done | Hosts: activity.coulomb.social + temporal.coulomb.social; docs/ops-sso-access.md | | T02 | done | Reuse Authelia + Traefik ForwardAuth (sso ns); not oauth2-proxy | | T03 | done | Ingress + cert Ready; unauth → 302 auth.coulomb.social | | T04 | done | Temporal Ingress + cert Ready; `ACTIVITY_CORE_TEMPORAL_UI_URL` + CORS set | | T05 | done | SSO `Remote-User` preferred; token break-glass; tests + redeploy | | T06 | done | NK-WP-0021 finished: LLDAP group + Authelia domain rules live | | T07 | done | Runbook + k8s README + design doc: SSO primary, port-forward break-glass | | T08 | done | See verification checklist below | ### Verification checklist (T08) — 2026-07-22 | Check | Result | | --- | --- | | DNS A → 92.205.62.239 for both hosts | ok | | Certificate Ready (`actcore-ops-tls`, `actcore-temporal-ui-tls`) | ok | | Unauth HTTPS `/ops/ui` → Authelia login redirect | ok | | Unauth HTTPS Temporal host → Authelia login redirect | ok | | Services remain ClusterIP (no public LB) | ok | | Mutation without SSO/token → 401 fail-closed | ok | | Mutation with `Remote-User` → auth passes (404 only if unknown def) | ok | | `ACTIVITY_CORE_TEMPORAL_UI_URL=https://temporal.coulomb.social` | ok | | `TEMPORAL_CORS_ORIGINS` includes public Temporal host | ok | | Unit tests `test_ops_auth` + `test_ops_console_api` | 18 passed | | Image `activity-core:railiance01-prod` rebuilt, imported, api/worker/router rolled | ok | | NetworkPolicy ingress→services | **skipped** (optional; ClusterIP + Authelia sufficient for MVP) | | Full browser login + Run now under real MFA session | **operator smoke** (credentials not in agent) | ### Closeout 2026-07-22 All tasks **done**. Workplan **finished**. | Cross-repo | Result | | --- | --- | | NK-IN-0001 / NK-IN-0002 | closed, `promoted_to: NK-WP-0021` | | NK-WP-0021 | **finished** (LLDAP group, Authelia rules, OPERATOR-GROUPS.md) | | Authelia rules | `group:activity-core-operators` allow + domain deny for both hosts | | Membership | `net-kingdom/sso-mfa/k8s/lldap/OPERATOR-GROUPS.md` + `manage-group-members.sh` | **Operator grant:** add users to `activity-core-operators` per net-kingdom runbook before first SSO login to the public UIs. **activity-core ownership remaining (ops, not open WP work):** break-glass port-forward + `ACTIVITY_CORE_OPERATOR_TOKEN`; keep Ingress/middleware in `k8s/railiance/30–32`.