2026-07-22 15:21:22 +02:00
---
id: NK-WP-0021
type: workplan
title: "Activity-core ops/Temporal UI least-privilege SSO"
domain: infotech
repo: net-kingdom
2026-07-22 15:47:26 +02:00
status: finished
2026-07-22 15:21:22 +02:00
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"
2026-08-25 20:14:28 +02:00
state_hub_workstream_id: "eba95b9e-372c-5efd-b69a-870f11582149"
2026-07-22 15:21:22 +02:00
---
# 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)
2026-07-22 15:47:26 +02:00
- [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 |
2026-07-22 15:21:22 +02:00
---
## Tasks
### T01 — LLDAP group `activity-core-operators` in bootstrap
```task
id: NK-WP-0021-T01
2026-07-22 15:47:26 +02:00
status: done
2026-07-22 15:21:22 +02:00
priority: high
2026-08-25 20:14:28 +02:00
state_hub_task_id: "4b4a93fc-ad63-59ae-9658-ad59c2a2bfbb"
2026-07-22 15:21:22 +02:00
```
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
2026-07-22 15:47:26 +02:00
status: done
2026-07-22 15:21:22 +02:00
priority: high
2026-08-25 20:14:28 +02:00
state_hub_task_id: "032d6d66-20bc-52e8-8490-aa3a77275ad0"
2026-07-22 15:21:22 +02:00
```
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
2026-07-22 15:47:26 +02:00
status: done
2026-07-22 15:21:22 +02:00
priority: high
2026-08-25 20:14:28 +02:00
state_hub_task_id: "17543b91-39aa-5b03-971c-b696f8f36489"
2026-07-22 15:21:22 +02:00
```
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
2026-07-22 15:47:26 +02:00
status: done
2026-07-22 15:21:22 +02:00
priority: high
2026-08-25 20:14:28 +02:00
state_hub_task_id: "0bbdf44c-283a-57be-8bd4-4b8b42b39803"
2026-07-22 15:21:22 +02:00
```
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
2026-07-22 15:47:26 +02:00
status: done
2026-07-22 15:21:22 +02:00
priority: medium
2026-08-25 20:14:28 +02:00
state_hub_task_id: "327c4a7c-94a6-50cd-8f36-7a452428a70d"
2026-07-22 15:21:22 +02:00
```
- 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 15:47:26 +02:00
**2026-07-22:** Intakes closed promoted; activity-core notified at registration
and again on finish. Residual ACTIVITY-WP-0025-T06 unblocked.
2026-07-22 15:21:22 +02:00
---
## 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` , …).
)