2026-07-22 00:30:17 +02:00
|
|
|
|
---
|
|
|
|
|
|
id: ACTIVITY-WP-0025
|
|
|
|
|
|
type: workplan
|
|
|
|
|
|
title: "SSO access control for ops console and Temporal UI"
|
|
|
|
|
|
domain: infotech
|
|
|
|
|
|
repo: activity-core
|
2026-07-22 17:47:57 +02:00
|
|
|
|
status: finished
|
2026-07-22 00:30:17 +02:00
|
|
|
|
owner: codex
|
|
|
|
|
|
topic_slug: activity-core
|
|
|
|
|
|
created: "2026-07-22"
|
|
|
|
|
|
updated: "2026-07-22"
|
2026-08-25 20:05:21 +02:00
|
|
|
|
state_hub_workstream_id: "347cad20-d66c-5151-b577-50eb93a85023"
|
2026-07-22 00:30:17 +02:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# 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
|
2026-07-22 00:47:29 +02:00
|
|
|
|
status: done
|
2026-07-22 00:30:17 +02:00
|
|
|
|
priority: high
|
2026-08-25 20:05:21 +02:00
|
|
|
|
state_hub_task_id: "44899e72-0481-5cd2-baa9-b96a547e4cd2"
|
2026-07-22 00:30:17 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-07-22 00:47:29 +02:00
|
|
|
|
status: done
|
2026-07-22 00:30:17 +02:00
|
|
|
|
priority: high
|
2026-08-25 20:05:21 +02:00
|
|
|
|
state_hub_task_id: "b48ff4e4-bf85-54f5-a107-aa247e9fc625"
|
2026-07-22 00:30:17 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-07-22 10:23:36 +02:00
|
|
|
|
status: done
|
2026-07-22 00:30:17 +02:00
|
|
|
|
priority: high
|
2026-08-25 20:05:21 +02:00
|
|
|
|
state_hub_task_id: "58c8e105-e73a-5417-b8b9-d0dc64b20194"
|
2026-07-22 00:30:17 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-07-22 10:23:36 +02:00
|
|
|
|
status: done
|
2026-07-22 00:30:17 +02:00
|
|
|
|
priority: high
|
2026-08-25 20:05:21 +02:00
|
|
|
|
state_hub_task_id: "99f56e9a-6a5a-5fe9-a4c4-4a497a5c256e"
|
2026-07-22 00:30:17 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-07-22 10:23:36 +02:00
|
|
|
|
status: done
|
2026-07-22 00:30:17 +02:00
|
|
|
|
priority: high
|
2026-08-25 20:05:21 +02:00
|
|
|
|
state_hub_task_id: "7d948dc6-374e-59ac-a6c9-e7fa6e8a128b"
|
2026-07-22 00:30:17 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-07-22 17:47:57 +02:00
|
|
|
|
status: done
|
2026-07-22 00:30:17 +02:00
|
|
|
|
priority: medium
|
2026-08-25 20:05:21 +02:00
|
|
|
|
state_hub_task_id: "a8354b03-2e43-5fe1-8d40-4a39502d9090"
|
2026-07-22 00:30:17 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
2026-07-22 17:47:57 +02:00
|
|
|
|
**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`.
|
|
|
|
|
|
|
2026-07-22 00:30:17 +02:00
|
|
|
|
## Task: Docs and runbook cutover
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: ACTIVITY-WP-0025-T07
|
2026-07-22 10:23:36 +02:00
|
|
|
|
status: done
|
2026-07-22 00:30:17 +02:00
|
|
|
|
priority: medium
|
2026-08-25 20:05:21 +02:00
|
|
|
|
state_hub_task_id: "4cfe1761-9bc4-58d1-bd0b-b0112230344a"
|
2026-07-22 00:30:17 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-07-22 10:23:36 +02:00
|
|
|
|
status: done
|
2026-07-22 00:30:17 +02:00
|
|
|
|
priority: medium
|
2026-08-25 20:05:21 +02:00
|
|
|
|
state_hub_task_id: "a3bddd50-deec-5e72-9311-02694d0fc2dc"
|
2026-07-22 00:30:17 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
2026-07-22 10:23:36 +02:00
|
|
|
|
- [x] HTTPS SSO URLs for ops console and Temporal UI (no port-forward required)
|
2026-07-22 17:47:57 +02:00
|
|
|
|
- [x] Only authorized IdP group can access (`activity-core-operators` via NK-WP-0021)
|
2026-07-22 10:23:36 +02:00
|
|
|
|
- [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
|
2026-07-22 00:30:17 +02:00
|
|
|
|
|
|
|
|
|
|
## 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
|
2026-07-22 00:47:29 +02:00
|
|
|
|
|
|
|
|
|
|
## Progress 2026-07-22
|
|
|
|
|
|
|
|
|
|
|
|
| Task | Status | Notes |
|
|
|
|
|
|
| --- | --- | --- |
|
2026-07-22 01:17:52 +02:00
|
|
|
|
| T01 | done | Hosts: activity.coulomb.social + temporal.coulomb.social; docs/ops-sso-access.md |
|
2026-07-22 00:47:29 +02:00
|
|
|
|
| T02 | done | Reuse Authelia + Traefik ForwardAuth (sso ns); not oauth2-proxy |
|
2026-07-22 10:23:36 +02:00
|
|
|
|
| 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 |
|
2026-07-22 17:47:57 +02:00
|
|
|
|
| T06 | done | NK-WP-0021 finished: LLDAP group + Authelia domain rules live |
|
2026-07-22 10:23:36 +02:00
|
|
|
|
| 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) |
|
|
|
|
|
|
|
2026-07-22 17:47:57 +02:00
|
|
|
|
### Closeout 2026-07-22
|
2026-07-22 10:23:36 +02:00
|
|
|
|
|
2026-07-22 17:47:57 +02:00
|
|
|
|
All tasks **done**. Workplan **finished**.
|
2026-07-22 10:47:25 +02:00
|
|
|
|
|
2026-07-22 17:47:57 +02:00
|
|
|
|
| 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` |
|
2026-07-22 10:47:25 +02:00
|
|
|
|
|
2026-07-22 17:47:57 +02:00
|
|
|
|
**Operator grant:** add users to `activity-core-operators` per net-kingdom runbook
|
|
|
|
|
|
before first SSO login to the public UIs.
|
2026-07-22 10:47:25 +02:00
|
|
|
|
|
2026-07-22 17:47:57 +02:00
|
|
|
|
**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`.
|