diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index d702755..1c83cd9 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -12,7 +12,6 @@ | workplan | NET-WP-0020 | finished | — | workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md | | workplan | NK-WP-0009 | backlog | — | workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md | | workplan | NK-WP-0011 | backlog | — | workplans/NK-WP-0011-enterprise-federation-saml.md | -| workplan | NK-WP-0021 | ready | — | workplans/NK-WP-0021-activity-core-ops-sso-operators.md | | task | ADHOC-2026-07-02-T01 | done | — | workplans/ADHOC-2026-07-02.md | | task | ADHOC-2026-07-02-T02 | done | — | workplans/ADHOC-2026-07-02.md | | task | NET-WP-0020-T01 | done | — | workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md | @@ -34,10 +33,5 @@ | task | NK-WP-0011-T6 | todo | — | workplans/NK-WP-0011-enterprise-federation-saml.md | | task | NK-WP-0011-T7 | todo | — | workplans/NK-WP-0011-enterprise-federation-saml.md | | task | NK-WP-0011-T8 | todo | — | workplans/NK-WP-0011-enterprise-federation-saml.md | -| task | NK-WP-0021-T01 | todo | — | workplans/NK-WP-0021-activity-core-ops-sso-operators.md | -| task | NK-WP-0021-T02 | todo | — | workplans/NK-WP-0021-activity-core-ops-sso-operators.md | -| task | NK-WP-0021-T03 | todo | — | workplans/NK-WP-0021-activity-core-ops-sso-operators.md | -| task | NK-WP-0021-T04 | todo | — | workplans/NK-WP-0021-activity-core-ops-sso-operators.md | -| task | NK-WP-0021-T05 | todo | — | workplans/NK-WP-0021-activity-core-ops-sso-operators.md | -| intake | NK-IN-0001 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md | -| intake | NK-IN-0002 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md | +| intake | NK-IN-0001 | open | blue | docs/intakes/activity-core-ops-sso-operators.md | +| intake | NK-IN-0002 | open | blue | docs/intakes/activity-core-ops-sso-operators.md | diff --git a/docs/intakes/activity-core-ops-sso-operators.md b/docs/intakes/activity-core-ops-sso-operators.md index b3db1a5..84d31b5 100644 --- a/docs/intakes/activity-core-ops-sso-operators.md +++ b/docs/intakes/activity-core-ops-sso-operators.md @@ -8,12 +8,16 @@ request least-privilege group gating on the public hosts. Source design: `activity-core/docs/ops-sso-access.md` Hosts: `activity.coulomb.social`, `temporal.coulomb.social` -**Promotion (2026-07-22):** both intakes accepted after scope/intent review and -promoted into **`NK-WP-0021`** -(`workplans/NK-WP-0021-activity-core-ops-sso-operators.md`). Single workplan -because LLDAP group and Authelia domain rules share one deploy surface and a -hard dependency. See that workplan for refined tasks, non-goals, and the -verification matrix. +Promote with (after route + owner review): + +```bash +statehub promote-intake \ + --to workplan \ + --repo-path ~/net-kingdom \ + --repo-slug net-kingdom \ + --domain communication +# or --to task --workplan-file workplans/.md +``` --- @@ -24,15 +28,13 @@ id: NK-IN-0001 kind: intake title: "Create LLDAP group activity-core-operators and membership runbook" lane: blue -status: closed -outcome: promoted -promoted_to: NK-WP-0021 +status: routed priority: high owner: net-kingdom repo: net-kingdom origin: "ACTIVITY-WP-0025-T06" origin_ref: "activity-core/workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md" -routed_note: "Promoted 2026-07-22 into NK-WP-0021 (T01–T02). Implement in net-kingdom sso-mfa." +routed_note: "Implement in net-kingdom sso-mfa. Origin ACTIVITY-WP-0025-T06 (activity-core)." description: | activity-core shipped Authelia SSO for the ops console and Temporal UI (ACTIVITY-WP-0025). MVP accepts any authenticated Authelia user. @@ -52,7 +54,6 @@ description: | notes: | Related Authelia domain rules are NK-IN-0002 (depends on this group existing). Requester residual task: ACTIVITY-WP-0025-T06 (status wait). - Promoted to NK-WP-0021 (combined workplan with NK-IN-0002). state_hub_intake_id: "019f88ff-c5f1-7433-9d13-4ffe37dfef00" ``` @@ -65,15 +66,13 @@ id: NK-IN-0002 kind: intake title: "Authelia access_control for activity + temporal.coulomb.social" lane: blue -status: closed -outcome: promoted -promoted_to: NK-WP-0021 +status: routed priority: high owner: net-kingdom repo: net-kingdom origin: "ACTIVITY-WP-0025-T06" origin_ref: "activity-core/workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md" -routed_note: "Promoted 2026-07-22 into NK-WP-0021 (T03–T04). Depends on NK-IN-0001 group." +routed_note: "Depends on NK-IN-0001 group. Implement in Authelia ConfigMap. Origin ACTIVITY-WP-0025-T06." description: | Restrict browser access to activity-core public SSO hosts to members of LLDAP group `activity-core-operators` (see NK-IN-0001). @@ -105,6 +104,5 @@ notes: | Depends on NK-IN-0001 (group must exist). activity-core Ingress + middleware already applied (k8s/railiance/30–32). No activity-core code change required for group enforcement — headers already carry Remote-Groups. - Promoted to NK-WP-0021 (combined workplan with NK-IN-0001). state_hub_intake_id: "019f88ff-cc9e-76b4-8ce3-eb354aabefe7" ``` diff --git a/sso-mfa/k8s/authelia/README.md b/sso-mfa/k8s/authelia/README.md index 83f154a..317bad1 100644 --- a/sso-mfa/k8s/authelia/README.md +++ b/sso-mfa/k8s/authelia/README.md @@ -44,36 +44,6 @@ Key config points: - `identity_providers.oidc.clients[0].redirect_uris` — must match CP-NK-004 (`kc.coulomb.social`) - `session.domain` — set to parent domain `coulomb.social` so cookies are valid across both `auth.coulomb.social` and `kc.coulomb.social` -- `access_control.default_policy: one_factor` — fleet default; MFA is not Authelia’s job -- `access_control.rules` — domain allowlists by LLDAP group (see below) - -### Domain rules (app operator groups) - -Authelia evaluates `rules` top-down; **first match wins**. A rule that matches -`domain` but not `subject` is skipped, so non-members would fall through to -`default_policy` and stay allowed unless a later domain-only `deny` rule exists. - -Pattern used for activity-core public hosts (NK-WP-0021): - -```yaml -access_control: - default_policy: one_factor - rules: - - domain: app.example.com - subject: "group:app-operators" - policy: one_factor - - domain: app.example.com - policy: deny -``` - -| Host | Required LLDAP group | -|------|----------------------| -| `activity.coulomb.social` | `activity-core-operators` | -| `temporal.coulomb.social` | `activity-core-operators` | - -Membership: `sso-mfa/k8s/lldap/OPERATOR-GROUPS.md`. Do **not** set `two_factor` -on these rules. Do **not** invent KeyCape OIDC clients for Traefik ForwardAuth -apps — the edge already calls Authelia `/api/verify`. ## Secrets managed diff --git a/sso-mfa/k8s/authelia/configmap.yaml b/sso-mfa/k8s/authelia/configmap.yaml index f0fe4a4..25949d2 100644 --- a/sso-mfa/k8s/authelia/configmap.yaml +++ b/sso-mfa/k8s/authelia/configmap.yaml @@ -86,26 +86,8 @@ data: # ── Access control ──────────────────────────────────────────────────────── # one_factor = password only. MFA is handled by KeyCape + privacyIDEA. # Do NOT change to two_factor here. - # - # Domain rules (NK-WP-0021): first matching rule wins. For restricted hosts - # put the group allow rule first, then a domain-wide deny so non-members do - # not fall through to default_policy (which would re-open the host). - # MFA stays KeyCape/privacyIDEA — policy remains one_factor, never two_factor. access_control: default_policy: one_factor - rules: - # activity-core ops console (ForwardAuth; no KeyCape OIDC client) - - domain: activity.coulomb.social - subject: "group:activity-core-operators" - policy: one_factor - - domain: activity.coulomb.social - policy: deny - # Temporal Web UI (same operator group) - - domain: temporal.coulomb.social - subject: "group:activity-core-operators" - policy: one_factor - - domain: temporal.coulomb.social - policy: deny # ── OIDC identity provider ──────────────────────────────────────────────── # Authelia acts as an upstream OIDC provider for KeyCape. diff --git a/sso-mfa/k8s/lldap/OPERATOR-GROUPS.md b/sso-mfa/k8s/lldap/OPERATOR-GROUPS.md deleted file mode 100644 index 68f6c5f..0000000 --- a/sso-mfa/k8s/lldap/OPERATOR-GROUPS.md +++ /dev/null @@ -1,83 +0,0 @@ -# Operator groups — membership runbook - -Least-privilege **app-scoped** LLDAP groups that gate browser SSO via Authelia -domain rules. Distinct from platform groups (`net-kingdom-users` / -`net-kingdom-admins`). - -## Groups - -| Group | Gates (Authelia) | Who belongs | -|-------|------------------|-------------| -| `activity-core-operators` | `activity.coulomb.social`, `temporal.coulomb.social` | Named founders / platform operators who need the activity-core ops console or Temporal UI | - -**Do not** auto-add everyone in `net-kingdom-admins`. Membership is a conscious -grant. Platform admin ≠ activity-core operator. - -Seed empty groups with `./bootstrap-users.sh` (idempotent). Authelia config: -`sso-mfa/k8s/authelia/configmap.yaml` (`access_control.rules`). - -## WebUI path - -1. Open https://lldap.coulomb.social (IP-restricted admin UI). -2. Sign in as `admin` (password from KeePassXC / custody — never commit it). -3. **Groups** → `activity-core-operators` → add or remove members. -4. User must already exist (create via WebUI or `./create-user.sh`). -5. User re-authenticates at Authelia if an old session lacks the group claim - (sign out / new browser session). - -## Scripted path - -```bash -cd sso-mfa/k8s/lldap - -# Add a user to the operator group -./manage-group-members.sh add activity-core-operators - -# Remove -./manage-group-members.sh remove activity-core-operators - -# List members -./manage-group-members.sh list activity-core-operators -``` - -Defaults: LLDAP URL `https://lldap.coulomb.social`, secrets from -`../../bootstrap/secrets/lldap/secrets.env`. Override: - -```bash -./manage-group-members.sh add bernd activity-core-operators \ - https://lldap.coulomb.social /path/to/secrets -``` - -Live ops without a local secrets file: export `LLDAP_ADMIN_PASS` from the -cluster secret (operator workstation only; do not log or commit): - -```bash -export LLDAP_ADMIN_PASS="$(kubectl get secret -n sso lldap-secrets \ - -o jsonpath='{.data.LLDAP_LDAP_USER_PASS}' | base64 -d)" -# manage-group-members.sh also accepts LLDAP_ADMIN_PASS when secrets.env is absent -``` - -## Onboarding checklist (activity-core ops) - -1. User has LLDAP account and is in `net-kingdom-users` (normal onboarding). -2. Operator adds them to `activity-core-operators` (WebUI or script). -3. User signs in at https://auth.coulomb.social (password; MFA via KeyCape path - when using OIDC apps — ForwardAuth to ops hosts uses Authelia session). -4. Open https://activity.coulomb.social and https://temporal.coulomb.social — - both should pass only for group members. -5. Offboard: remove from `activity-core-operators` first; full account offboard - remains the platform lifecycle path. - -## Break-glass (independent of this group) - -activity-core ClusterIP port-forward + `ACTIVITY_CORE_OPERATOR_TOKEN` does **not** -use Authelia groups. Documented in activity-core runbooks. Keep that path for -SSO outages; do not gate break-glass on LLDAP group membership. - -## Adding a future app-operator group - -1. Add the group name to `REQUIRED_GROUPS` in `bootstrap-users.sh`. -2. Document it in this table. -3. Add Authelia domain rules (member `one_factor` + domain `deny` fallback) in - `sso-mfa/k8s/authelia/configmap.yaml` — see Authelia README. -4. Never invent a KeyCape OIDC client solely for Traefik ForwardAuth apps. diff --git a/sso-mfa/k8s/lldap/README.md b/sso-mfa/k8s/lldap/README.md index d734b02..10b18f6 100644 --- a/sso-mfa/k8s/lldap/README.md +++ b/sso-mfa/k8s/lldap/README.md @@ -33,14 +33,7 @@ kubectl rollout status deployment/lldap -n sso --timeout=120s ## Post-deploy bootstrap -After the pod is Running, seed required groups (preferred — idempotent): - -```bash -./bootstrap-users.sh -# Groups: net-kingdom-users, net-kingdom-admins, activity-core-operators -``` - -Or create them via the web UI: +After the pod is Running, create the two required application groups via the web UI: ``` https://lldap.coulomb.social @@ -48,13 +41,9 @@ Username: admin Password: LLDAP_LDAP_USER_PASS (from KeePassXC → net-kingdom/LLDAP/admin) ``` -| Group | Purpose | -|-------|---------| -| `net-kingdom-users` | Standard users (all human accounts) | -| `net-kingdom-admins` | Privileged users (KeyCape MFA step-up / extra scopes) | -| `activity-core-operators` | App-scoped: browser SSO to activity-core ops + Temporal UI (Authelia domain rules). **Not** platform admin — grant consciously. | - -Operator membership (add/remove humans): see **[OPERATOR-GROUPS.md](./OPERATOR-GROUPS.md)**. +Create groups: +- `net-kingdom-users` — standard users +- `net-kingdom-admins` — privileged users (enforce MFA step-up in KeyCape policies) ## Ports diff --git a/sso-mfa/k8s/lldap/bootstrap-users.sh b/sso-mfa/k8s/lldap/bootstrap-users.sh index d09e9ae..97a2dcb 100755 --- a/sso-mfa/k8s/lldap/bootstrap-users.sh +++ b/sso-mfa/k8s/lldap/bootstrap-users.sh @@ -5,17 +5,14 @@ # # What it does: # 1. Authenticates to LLDAP via its GraphQL API. -# 2. Creates required platform + app-operator groups (idempotent). +# 2. Creates the two required groups: net-kingdom-users, net-kingdom-admins. # 3. Prints a user onboarding checklist (groups-only; individual users are -# added via the WebUI, create-user.sh, or manage-group-members.sh). +# added via the WebUI or by re-running this script with USER_EMAIL set). # # Groups created: -# net-kingdom-users — standard users; all human accounts go here. -# net-kingdom-admins — privileged users; KeyCape policies can enforce -# MFA step-up or grant extra scopes to this group. -# activity-core-operators — app-scoped: browser access to activity-core -# ops console + Temporal UI (Authelia domain rules). -# Not platform admin — grant consciously (NK-WP-0021). +# net-kingdom-users — standard users; all human accounts go here. +# net-kingdom-admins — privileged users; KeyCape policies can enforce +# MFA step-up or grant extra scopes to this group. # # Usage: # ./bootstrap-users.sh [lldap-url] [secrets-dir] @@ -123,22 +120,15 @@ create_group() { } # ── 2. Create required groups ───────────────────────────────────────────────── -REQUIRED_GROUPS=( - "net-kingdom-users" - "net-kingdom-admins" - "activity-core-operators" -) - -for grp in "${REQUIRED_GROUPS[@]}"; do - create_group "$grp" -done +create_group "net-kingdom-users" +create_group "net-kingdom-admins" # ── 3. Verify ───────────────────────────────────────────────────────────────── echo "" echo "Verifying groups ..." LIST_RESP=$(gql 'query { groups { id displayName } }') if [[ "$LIST_RESP" != "CURL_FAILED" ]]; then - for grp in "${REQUIRED_GROUPS[@]}"; do + for grp in "net-kingdom-users" "net-kingdom-admins"; do EXISTS=$(echo "$LIST_RESP" | python3 -c \ "import sys,json; d=json.load(sys.stdin); print('yes' if any(g['displayName']=='$grp' for g in d.get('data',{}).get('groups',[])) else 'no')" \ 2>/dev/null || echo "no") @@ -158,8 +148,7 @@ echo "════════════════════════ echo " LLDAP group bootstrap: PASS=$PASS_COUNT FAIL=$FAIL_COUNT" echo "════════════════════════════════════════════════════════════" echo "" -echo "Next: add users via the LLDAP WebUI, create-user.sh, or manage-group-members.sh." -echo "App-operator membership: see OPERATOR-GROUPS.md (do not auto-grant all admins)." +echo "Next: add users via the LLDAP WebUI or LDAP provisioning." echo "" echo "User onboarding checklist:" echo "" @@ -168,11 +157,9 @@ echo " 1. Create account in LLDAP WebUI ($LLDAP_URL)" echo " Fields: username (uid), display name, email" echo " 2. Assign to net-kingdom-users group (mandatory)" echo " Assign to net-kingdom-admins too if privileged access is needed" -echo " 3. For activity-core ops/Temporal UI: also add to activity-core-operators" -echo " (./manage-group-members.sh add activity-core-operators)" -echo " 4. User logs in to Authelia (auth.coulomb.social) to verify their password" -echo " 5. User self-enrolls TOTP at pink-account.coulomb.social" -echo " 6. User tests end-to-end login via an OIDC-protected application" +echo " 3. User logs in to Authelia (auth.coulomb.social) to verify their password" +echo " 4. User self-enrolls TOTP at pink-account.coulomb.social" +echo " 5. User tests end-to-end login via an OIDC-protected application" echo "" echo " Break-glass account:" echo " Run: sso-mfa/k8s/lldap/break-glass.sh" diff --git a/sso-mfa/k8s/lldap/create-secrets.sh b/sso-mfa/k8s/lldap/create-secrets.sh index 132a5e0..46d0b71 100644 --- a/sso-mfa/k8s/lldap/create-secrets.sh +++ b/sso-mfa/k8s/lldap/create-secrets.sh @@ -51,7 +51,7 @@ echo "Done. Secret lldap-secrets created in namespace: sso" echo "" echo "Next:" echo " Apply manifests (see README.md apply order)." -echo " After LLDAP is Running, seed groups:" -echo " ./bootstrap-users.sh" -echo " Groups: net-kingdom-users, net-kingdom-admins, activity-core-operators" -echo " Membership runbook: OPERATOR-GROUPS.md" +echo " After LLDAP is Running, create application groups:" +echo " - Log in to https://lldap.coulomb.social with the admin account." +echo " - Create group: net-kingdom-users" +echo " - Create group: net-kingdom-admins" diff --git a/sso-mfa/k8s/lldap/manage-group-members.sh b/sso-mfa/k8s/lldap/manage-group-members.sh deleted file mode 100755 index 7abf5bd..0000000 --- a/sso-mfa/k8s/lldap/manage-group-members.sh +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env bash -# manage-group-members.sh — add/remove/list LLDAP group membership (NK-WP-0021) -# -# Usage: -# ./manage-group-members.sh add [lldap-url] [secrets-dir] -# ./manage-group-members.sh remove [lldap-url] [secrets-dir] -# ./manage-group-members.sh list [lldap-url] [secrets-dir] -# -# Auth: LLDAP_LDAP_USER_PASS from secrets-dir/lldap/secrets.env, or env -# LLDAP_ADMIN_PASS (e.g. from kubectl secret for live ops). -# -# Examples: -# ./manage-group-members.sh add bernd activity-core-operators -# ./manage-group-members.sh list activity-core-operators - -set -euo pipefail - -ACTION="${1:-}" -shift || true - -LLDAP_URL="https://lldap.coulomb.social" -SECRETS_DIR="../../bootstrap/secrets" - -usage() { - sed -n '2,15p' "$0" | sed 's/^# \?//' - exit 1 -} - -case "$ACTION" in - add|remove) - USERNAME="${1:-}" - GROUP_NAME="${2:-}" - [[ -z "$USERNAME" || -z "$GROUP_NAME" ]] && usage - shift 2 || true - ;; - list) - GROUP_NAME="${1:-}" - [[ -z "$GROUP_NAME" ]] && usage - shift 1 || true - USERNAME="" - ;; - *) - usage - ;; -esac - -[[ -n "${1:-}" ]] && LLDAP_URL="$1" -[[ -n "${2:-}" ]] && SECRETS_DIR="$2" - -LLDAP_ENV="$SECRETS_DIR/lldap/secrets.env" -LLDAP_ADMIN_PASS="${LLDAP_ADMIN_PASS:-}" - -if [[ -z "$LLDAP_ADMIN_PASS" ]]; then - if [[ ! -f "$LLDAP_ENV" ]]; then - echo "ERROR: $LLDAP_ENV not found and LLDAP_ADMIN_PASS unset." >&2 - exit 1 - fi - LLDAP_ADMIN_PASS=$(bash -c "source '$LLDAP_ENV' 2>/dev/null; echo \"\${LLDAP_LDAP_USER_PASS:-}\"") -fi - -if [[ -z "$LLDAP_ADMIN_PASS" ]]; then - echo "ERROR: empty LLDAP admin password" >&2 - exit 1 -fi - -echo "Authenticating to LLDAP at $LLDAP_URL ..." -AUTH_RESP=$(curl -sS -X POST "$LLDAP_URL/auth/simple/login" \ - -H "Content-Type: application/json" \ - -d "{\"username\":\"admin\",\"password\":\"$LLDAP_ADMIN_PASS\"}") - -LLDAP_TOKEN=$(echo "$AUTH_RESP" | python3 -c \ - "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null || echo "") -if [[ -z "$LLDAP_TOKEN" ]]; then - echo "ERROR: Authentication failed" >&2 - exit 1 -fi - -# Build GraphQL POST body without shell-expanding $variables inside the query. -gql_post() { - local query_file="$1" - local vars_json="$2" - python3 - "$LLDAP_URL" "$LLDAP_TOKEN" "$query_file" "$vars_json" <<'PY' -import json, sys, urllib.request -url, token, qpath, vars_s = sys.argv[1:5] -query = open(qpath, encoding="utf-8").read() -body = json.dumps({"query": query, "variables": json.loads(vars_s)}).encode() -req = urllib.request.Request( - url.rstrip("/") + "/api/graphql", - data=body, - headers={ - "Authorization": f"Bearer {token}", - "Content-Type": "application/json", - }, - method="POST", -) -with urllib.request.urlopen(req, timeout=30) as resp: - print(resp.read().decode()) -PY -} - -TMPDIR_GQL=$(mktemp -d) -trap 'rm -rf "$TMPDIR_GQL"' EXIT - -cat >"$TMPDIR_GQL/list.gql" <<'EOF' -query { groups { id displayName users { id displayName email } } } -EOF - -GROUPS_JSON=$(gql_post "$TMPDIR_GQL/list.gql" '{}') -GROUP_ID=$(echo "$GROUPS_JSON" | python3 -c " -import sys, json -name = sys.argv[1] -d = json.load(sys.stdin) -for g in d.get('data', {}).get('groups', []) or []: - if g.get('displayName') == name: - print(g['id']) - break -" "$GROUP_NAME") - -if [[ -z "$GROUP_ID" ]]; then - echo "ERROR: group '$GROUP_NAME' not found — run bootstrap-users.sh first" >&2 - exit 1 -fi - -case "$ACTION" in - list) - echo "$GROUPS_JSON" | python3 -c " -import sys, json -name = sys.argv[1] -d = json.load(sys.stdin) -for g in d.get('data', {}).get('groups', []) or []: - if g.get('displayName') == name: - users = g.get('users') or [] - print(f\"Group {name} (id={g.get('id')}): {len(users)} member(s)\") - for u in users: - print(f\" - {u.get('id')} {u.get('displayName') or ''} {u.get('email') or ''}\") - break -" "$GROUP_NAME" - ;; - add) - cat >"$TMPDIR_GQL/add.gql" <<'EOF' -mutation AddToGroup($userId: String!, $groupId: Int!) { - addUserToGroup(userId: $userId, groupId: $groupId) { ok } -} -EOF - VARS=$(python3 -c "import json,sys; print(json.dumps({'userId':sys.argv[1],'groupId':int(sys.argv[2])}))" "$USERNAME" "$GROUP_ID") - RESP=$(gql_post "$TMPDIR_GQL/add.gql" "$VARS") - echo "$RESP" | python3 -c " -import sys, json -d = json.load(sys.stdin) -if d.get('errors'): - msg = d['errors'][0].get('message', str(d['errors'])) - if 'UNIQUE constraint' in msg or 'already' in msg.lower(): - print('OK: already a member (no-op)') - sys.exit(0) - print('ERROR:', msg, file=sys.stderr) - sys.exit(1) -print('OK: added user to group') -" - echo " user=$USERNAME group=$GROUP_NAME (id=$GROUP_ID)" - ;; - remove) - cat >"$TMPDIR_GQL/remove.gql" <<'EOF' -mutation RemoveFromGroup($userId: String!, $groupId: Int!) { - removeUserFromGroup(userId: $userId, groupId: $groupId) { ok } -} -EOF - VARS=$(python3 -c "import json,sys; print(json.dumps({'userId':sys.argv[1],'groupId':int(sys.argv[2])}))" "$USERNAME" "$GROUP_ID") - RESP=$(gql_post "$TMPDIR_GQL/remove.gql" "$VARS") - echo "$RESP" | python3 -c " -import sys, json -d = json.load(sys.stdin) -if d.get('errors'): - print('ERROR:', d['errors'][0].get('message', d['errors']), file=sys.stderr) - sys.exit(1) -print('OK: removed user from group') -" - echo " user=$USERNAME group=$GROUP_NAME (id=$GROUP_ID)" - ;; -esac diff --git a/workplans/NK-WP-0021-activity-core-ops-sso-operators.md b/workplans/NK-WP-0021-activity-core-ops-sso-operators.md deleted file mode 100644 index e066d10..0000000 --- a/workplans/NK-WP-0021-activity-core-ops-sso-operators.md +++ /dev/null @@ -1,286 +0,0 @@ ---- -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: "3e40e29a-a13a-47b6-9575-4fdc7024af6a" ---- - -# 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: "50d59099-d3df-4087-93ef-9a4e537807b8" -``` - -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: "e8d6dae5-257f-4862-98de-ab4cef6cc722" -``` - -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: "71bfd7ea-1637-4c1a-ac3b-dc8dca38b82f" -``` - -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: "f74a2a59-f126-45e3-832a-5574bd0feb6f" -``` - -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: "a1acd507-9532-45ea-bb08-387b11a96868" -``` - -- 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`, …). -)