activity-core/workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md
codex 1fa9dc18ab
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
fix(workplans): adopt ADR-007 derived identifiers for unregistered records
These workplans exist only in the retired local hub. Their random pre-ADR-007
identifiers are refused by C-06 as stale references, so they cannot be
registered. Deriving from the canonical record id takes no identity from
anything: central does not hold them and the old ids die with the cache.

Records central already holds were deliberately left untouched.

Refs CUST-WP-0068-T06

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
2026-08-25 20:05:21 +02:00

301 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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 Temporals 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:<user>`)
- [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. T01T02 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. T07T08 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/3032`.