--- id: NK-WP-0021 type: workplan title: "Activity-core ops/Temporal UI least-privilege SSO" domain: infotech repo: net-kingdom status: finished owner: codex topic_slug: netkingdom priority: high created: "2026-07-22" updated: "2026-07-22" origin: "intake:019f88ff-c5f1-7433-9d13-4ffe37dfef00+019f88ff-cc9e-76b4-8ce3-eb354aabefe7" origin_ref: "ACTIVITY-WP-0025-T06" state_hub_workstream_id: "eba95b9e-372c-5efd-b69a-870f11582149" --- # NK-WP-0021 — Activity-core ops/Temporal UI least-privilege SSO ## Goal Close the residual least-privilege gap for public activity-core operator UIs by owning the **directory group** and **Authelia access rules** in this repo (SSO/MFA stack). Browser access to `activity.coulomb.social` and `temporal.coulomb.social` should require membership in `activity-core-operators`, not merely “any authenticated Authelia user”. ## Origin and review | Intake | Hub UUID | Title | Decision | | --- | --- | --- | --- | | NK-IN-0001 | `019f88ff-c5f1-7433-9d13-4ffe37dfef00` | LLDAP group + membership runbook | **Accept** → T01–T02 | | NK-IN-0002 | `019f88ff-cc9e-76b4-8ce3-eb354aabefe7` | Authelia domain rules | **Accept** → T03–T04 | Cross-repo handoff from activity-core `ACTIVITY-WP-0025-T06` (residual wait). Source design: `activity-core/docs/ops-sso-access.md`. File intakes: `docs/intakes/activity-core-ops-sso-operators.md`. ### Scope / intent fit | Lens | Assessment | | --- | --- | | **SCOPE in** | SSO/MFA platform (Authelia + LLDAP), identity as control plane, least-privilege access for platform operators | | **SCOPE out** | activity-core Ingress/middleware (already live), KeyCape OIDC clients, Temporal/API application code, railiance runtime | | **INTENT** | Principle 2 (identity is the control plane) and 6 (minimize exposure / least privilege from the start). Progressive capability: tighten MVP “any Authelia user” to named operator group without changing auth edge pattern | | **Repo boundary** | net-kingdom owns LLDAP groups + Authelia `access_control`; activity-core owns ForwardAuth middleware, break-glass token, and app authorization | ### Refinements vs raw intakes 1. **Single workplan** — both intakes share one deploy surface (LLDAP → Authelia) and a hard dependency (group must exist before domain rules). One plan avoids split status and double-apply risk. 2. **No KeyCape / Keycloak OIDC clients** — explicit non-goal. Path is Traefik ForwardAuth → Authelia `/api/verify` only (already shipped in activity-core). 3. **MFA posture unchanged** — keep Authelia `one_factor`; MFA remains KeyCape + privacyIDEA. Do not set Authelia `two_factor` for these hosts. 4. **App-scoped operator group pattern** — `activity-core-operators` extends the existing `net-kingdom-users` / `net-kingdom-admins` seed style for a *consumer app*, not platform-wide admin. Document as the template for future app operator groups. 5. **Membership is operator policy** — scripts create the empty group and document add/remove; named founder assignment is a live operator step (not committed identity). 6. **Verification matrix** — unauthenticated → login; authenticated non-member → deny; member → pass; break-glass remains independent of Authelia groups (activity-core runbook). 7. **Default policy stays open for other hosts** — only the two public activity-core hosts get restrictive domain rules; do not change global `default_policy: one_factor` for the rest of the fleet without a separate decision. ## Context (current state) - Authelia SSO edge for both hosts is **live** (activity-core manifests 30–32). - Authelia `access_control` today: `default_policy: one_factor`, **no** domain rules (`sso-mfa/k8s/authelia/configmap.yaml`). - LLDAP bootstrap seeds only `net-kingdom-users` and `net-kingdom-admins` (`sso-mfa/k8s/lldap/bootstrap-users.sh`). - Any authenticated Authelia user can currently open both public UIs. ## In scope - Create LLDAP group `activity-core-operators` (bootstrap script + README). - Document how operators add/remove humans (WebUI + scripted GraphQL path). - Authelia domain rules for `activity.coulomb.social` and `temporal.coulomb.social` requiring that group. - Apply/reload Authelia; verify member / non-member / unauthenticated paths. - Close intakes as promoted; notify activity-core so T06 can finish. ## Out of scope - New OIDC clients in KeyCape or Keycloak for these hosts. - Changing activity-core Ingress, middleware, or ops API authorization. - Platform-root / break-glass LLDAP accounts. - Enterprise federation / SAML (NK-WP-0011) or tutorials backlog (NK-WP-0009). - Automatically granting named users without operator confirmation. ## Acceptance (workplan-level) - [x] Group `activity-core-operators` exists in LLDAP and is re-seeded by bootstrap. - [x] Membership procedure is documented under `sso-mfa` docs (and README pointers). - [x] Authelia denies non-members and allows members on both hosts after login (`authelia access-control check-policy` on live config 2026-07-22). - [x] Unauthenticated users still hit the Authelia login portal (not open backends) (live 302 → `auth.coulomb.social` for both hosts). - [x] activity-core break-glass remains independent of Authelia group membership (unchanged; documented in OPERATOR-GROUPS.md). - [x] Intakes NK-IN-0001 / NK-IN-0002 closed as promoted to this workplan. ### Live verification evidence (2026-07-22) | Check | Result | | --- | --- | | LLDAP group `activity-core-operators` | id=7; seeded live; bootstrap script updated | | Initial member | `platform-root` (conscious bootstrap grant) | | Unauth `activity` / `temporal` | HTTP 302 → Authelia login | | `check-policy` member + group | rule #1 `one_factor` (activity); rule #3 temporal | | `check-policy` auth non-member | rule #2 / #4 `deny` | | `check-policy` other host (`kc`) | default `one_factor` (fleet unaffected) | | Authelia rollout | v4.38.19 Startup complete with rules loaded | --- ## Tasks ### T01 — LLDAP group `activity-core-operators` in bootstrap ```task id: NK-WP-0021-T01 status: done priority: high state_hub_task_id: "4b4a93fc-ad63-59ae-9658-ad59c2a2bfbb" ``` Extend LLDAP group seeding so the operator group is created the same way as platform groups: - Update `sso-mfa/k8s/lldap/bootstrap-users.sh` to create `activity-core-operators` (idempotent `create_group`). - Update `sso-mfa/k8s/lldap/README.md` and post-deploy hints in `create-secrets.sh` to list the third group and its purpose (activity-core ops console + Temporal UI only — not platform admin). - Optional small helper or flag on `create-user.sh` only if it stays simple (e.g. documented GraphQL one-liner is enough for v1). **Done when:** re-running bootstrap is safe/idempotent and the group exists (or will exist on next bootstrap) with clear docs. **Promotes:** NK-IN-0001 (group half). --- ### T02 — Operator membership runbook ```task id: NK-WP-0021-T02 status: done priority: high state_hub_task_id: "032d6d66-20bc-52e8-8490-aa3a77275ad0" ``` Document least-privilege membership for founders/operators only: - WebUI path: `https://lldap.coulomb.social` → add/remove group members. - Scripted path: GraphQL `addUserToGroup` / `removeUserFromGroup` using the same auth pattern as `bootstrap-users.sh` / `break-glass.sh` (no secrets in git or workplan). - Explicit: do **not** auto-add all `net-kingdom-admins`; membership is a conscious grant for activity-core ops. - Point activity-core operators at this runbook from a short note in `docs/intakes/activity-core-ops-sso-operators.md` (or a thin `sso-mfa/k8s/lldap/OPERATOR-GROUPS.md` if the README grows too large). **Done when:** an operator can add and remove a test user without reading activity-core internals. **Promotes:** NK-IN-0001 (runbook half). --- ### T03 — Authelia domain rules for activity + temporal hosts ```task id: NK-WP-0021-T03 status: done priority: high state_hub_task_id: "17543b91-39aa-5b03-971c-b696f8f36489" ``` Implement access control in the live Authelia ConfigMap: - File: `sso-mfa/k8s/authelia/configmap.yaml` - Keep `default_policy: one_factor` for the rest of the fleet. - Add `rules` (or equivalent for the deployed Authelia version) for: - `activity.coulomb.social` - `temporal.coulomb.social` - Require subject group `activity-core-operators` with policy `one_factor` (Authelia subject syntax: `subject: "group:activity-core-operators"` — confirm against the running Authelia major version before apply). - Comment in-file: MFA remains KeyCape/privacyIDEA; do not use Authelia `two_factor` here. - Update `sso-mfa/k8s/authelia/README.md` with the new domain-rule pattern so the next app can copy it. **Depends on:** T01 (group name must resolve from LLDAP via Authelia’s LDAP backend). Rules may be committed before the group exists, but live apply of enforcement should wait until the group is present. **Done when:** config is reviewed and ready to apply; no accidental lockdown of unrelated hosts (auth portal, KeyCape, LLDAP admin, etc.). **Promotes:** NK-IN-0002 (config half). --- ### T04 — Live apply and verification matrix ```task id: NK-WP-0021-T04 status: done priority: high state_hub_task_id: "0bbdf44c-283a-57be-8bd4-4b8b42b39803" ``` Roll out and prove least privilege on the live railiance01 SSO stack: 1. Ensure group exists (run bootstrap if needed) and assign **one** known operator (operator-confirmed username). 2. Apply Authelia ConfigMap; restart/reload Authelia in namespace `sso`. 3. Verification matrix (record non-secret results in progress notes): | Case | Expected | | --- | --- | | Unauthenticated → both hosts | Redirect to Authelia login | | Authenticated **non-member** | Denied (403 / Authelia unauthorized) | | Authenticated **member** | Pass-through to ops UI and Temporal UI | | Break-glass port-forward + operator token | Still works per activity-core runbook | 4. If verification fails, roll back ConfigMap to previous `access_control` and re-open the blocking task — do not leave partial rules that lock out all operators without a recovery path. **Done when:** matrix passes for both hosts; recovery path still documented. **Promotes:** NK-IN-0002 (verify half). --- ### T05 — Close loop with intakes and activity-core ```task id: NK-WP-0021-T05 status: done priority: medium state_hub_task_id: "327c4a7c-94a6-50cd-8f36-7a452428a70d" ``` - Keep intake source file statuses in sync (`promoted` / closed) after hub close (done at workplan registration for the intake close step; re-check if reopened). - Message `activity-core` (State Hub) that NK-WP-0021 covers residual T06 and can move T06 to done once T04 verification lands. - Brief progress event on finish. **Done when:** activity-core residual task has a clear unblock path and this workplan’s hub registration is consistent. **2026-07-22:** Intakes closed promoted; activity-core notified at registration and again on finish. Residual ACTIVITY-WP-0025-T06 unblocked. --- ## Implementation notes ### Suggested Authelia rule shape (confirm version) ```yaml access_control: default_policy: one_factor rules: - domain: activity.coulomb.social subject: "group:activity-core-operators" policy: one_factor - domain: temporal.coulomb.social subject: "group:activity-core-operators" policy: one_factor ``` If Authelia requires an explicit deny for non-matching subjects on those domains, use the version’s documented pattern (subject rules are exclusive when matched; default_policy must not re-open the host). Prefer a deny-by-default domain rule only if the running version needs it — verify in staging or with a short maintenance window. ### Non-goals reminder Do **not** invent KeyCape OIDC clients, oauth2-proxy, or Keycloak realm clients for this path. activity-core already uses ForwardAuth headers (`Remote-User`, `Remote-Groups`, …). )