Compare commits
2 commits
101f91cbda
...
a9aec541ec
| Author | SHA1 | Date | |
|---|---|---|---|
| a9aec541ec | |||
| 3580e43e5f |
10 changed files with 664 additions and 36 deletions
|
|
@ -12,6 +12,7 @@
|
|||
| 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 |
|
||||
|
|
@ -33,5 +34,10 @@
|
|||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
|
|
|||
|
|
@ -8,16 +8,12 @@ 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`
|
||||
|
||||
Promote with (after route + owner review):
|
||||
|
||||
```bash
|
||||
statehub promote-intake <state_hub_intake_id> \
|
||||
--to workplan \
|
||||
--repo-path ~/net-kingdom \
|
||||
--repo-slug net-kingdom \
|
||||
--domain communication
|
||||
# or --to task --workplan-file workplans/<existing>.md
|
||||
```
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -28,13 +24,15 @@ id: NK-IN-0001
|
|||
kind: intake
|
||||
title: "Create LLDAP group activity-core-operators and membership runbook"
|
||||
lane: blue
|
||||
status: routed
|
||||
status: closed
|
||||
outcome: promoted
|
||||
promoted_to: NK-WP-0021
|
||||
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: "Implement in net-kingdom sso-mfa. Origin ACTIVITY-WP-0025-T06 (activity-core)."
|
||||
routed_note: "Promoted 2026-07-22 into NK-WP-0021 (T01–T02). Implement in net-kingdom sso-mfa."
|
||||
description: |
|
||||
activity-core shipped Authelia SSO for the ops console and Temporal UI
|
||||
(ACTIVITY-WP-0025). MVP accepts any authenticated Authelia user.
|
||||
|
|
@ -54,6 +52,7 @@ 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"
|
||||
```
|
||||
|
||||
|
|
@ -66,13 +65,15 @@ id: NK-IN-0002
|
|||
kind: intake
|
||||
title: "Authelia access_control for activity + temporal.coulomb.social"
|
||||
lane: blue
|
||||
status: routed
|
||||
status: closed
|
||||
outcome: promoted
|
||||
promoted_to: NK-WP-0021
|
||||
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: "Depends on NK-IN-0001 group. Implement in Authelia ConfigMap. Origin ACTIVITY-WP-0025-T06."
|
||||
routed_note: "Promoted 2026-07-22 into NK-WP-0021 (T03–T04). Depends on NK-IN-0001 group."
|
||||
description: |
|
||||
Restrict browser access to activity-core public SSO hosts to members of
|
||||
LLDAP group `activity-core-operators` (see NK-IN-0001).
|
||||
|
|
@ -104,5 +105,6 @@ 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"
|
||||
```
|
||||
|
|
|
|||
|
|
@ -44,6 +44,36 @@ 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
|
||||
|
||||
|
|
|
|||
|
|
@ -86,8 +86,26 @@ 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.
|
||||
|
|
|
|||
83
sso-mfa/k8s/lldap/OPERATOR-GROUPS.md
Normal file
83
sso-mfa/k8s/lldap/OPERATOR-GROUPS.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# 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 <uid> activity-core-operators
|
||||
|
||||
# Remove
|
||||
./manage-group-members.sh remove <uid> 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.
|
||||
|
|
@ -33,7 +33,14 @@ kubectl rollout status deployment/lldap -n sso --timeout=120s
|
|||
|
||||
## Post-deploy bootstrap
|
||||
|
||||
After the pod is Running, create the two required application groups via the web UI:
|
||||
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:
|
||||
|
||||
```
|
||||
https://lldap.coulomb.social
|
||||
|
|
@ -41,9 +48,13 @@ Username: admin
|
|||
Password: LLDAP_LDAP_USER_PASS (from KeePassXC → net-kingdom/LLDAP/admin)
|
||||
```
|
||||
|
||||
Create groups:
|
||||
- `net-kingdom-users` — standard users
|
||||
- `net-kingdom-admins` — privileged users (enforce MFA step-up in KeyCape policies)
|
||||
| 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)**.
|
||||
|
||||
## Ports
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,17 @@
|
|||
#
|
||||
# What it does:
|
||||
# 1. Authenticates to LLDAP via its GraphQL API.
|
||||
# 2. Creates the two required groups: net-kingdom-users, net-kingdom-admins.
|
||||
# 2. Creates required platform + app-operator groups (idempotent).
|
||||
# 3. Prints a user onboarding checklist (groups-only; individual users are
|
||||
# added via the WebUI or by re-running this script with USER_EMAIL set).
|
||||
# added via the WebUI, create-user.sh, or manage-group-members.sh).
|
||||
#
|
||||
# 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.
|
||||
# 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).
|
||||
#
|
||||
# Usage:
|
||||
# ./bootstrap-users.sh [lldap-url] [secrets-dir]
|
||||
|
|
@ -120,15 +123,22 @@ create_group() {
|
|||
}
|
||||
|
||||
# ── 2. Create required groups ─────────────────────────────────────────────────
|
||||
create_group "net-kingdom-users"
|
||||
create_group "net-kingdom-admins"
|
||||
REQUIRED_GROUPS=(
|
||||
"net-kingdom-users"
|
||||
"net-kingdom-admins"
|
||||
"activity-core-operators"
|
||||
)
|
||||
|
||||
for grp in "${REQUIRED_GROUPS[@]}"; do
|
||||
create_group "$grp"
|
||||
done
|
||||
|
||||
# ── 3. Verify ─────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "Verifying groups ..."
|
||||
LIST_RESP=$(gql 'query { groups { id displayName } }')
|
||||
if [[ "$LIST_RESP" != "CURL_FAILED" ]]; then
|
||||
for grp in "net-kingdom-users" "net-kingdom-admins"; do
|
||||
for grp in "${REQUIRED_GROUPS[@]}"; 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")
|
||||
|
|
@ -148,7 +158,8 @@ echo "════════════════════════
|
|||
echo " LLDAP group bootstrap: PASS=$PASS_COUNT FAIL=$FAIL_COUNT"
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo "Next: add users via the LLDAP WebUI or LDAP provisioning."
|
||||
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 ""
|
||||
echo "User onboarding checklist:"
|
||||
echo ""
|
||||
|
|
@ -157,9 +168,11 @@ 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. 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 " 3. For activity-core ops/Temporal UI: also add to activity-core-operators"
|
||||
echo " (./manage-group-members.sh add <uid> 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 ""
|
||||
echo " Break-glass account:"
|
||||
echo " Run: sso-mfa/k8s/lldap/break-glass.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, 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"
|
||||
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"
|
||||
|
|
|
|||
179
sso-mfa/k8s/lldap/manage-group-members.sh
Executable file
179
sso-mfa/k8s/lldap/manage-group-members.sh
Executable file
|
|
@ -0,0 +1,179 @@
|
|||
#!/usr/bin/env bash
|
||||
# manage-group-members.sh — add/remove/list LLDAP group membership (NK-WP-0021)
|
||||
#
|
||||
# Usage:
|
||||
# ./manage-group-members.sh add <uid> <group> [lldap-url] [secrets-dir]
|
||||
# ./manage-group-members.sh remove <uid> <group> [lldap-url] [secrets-dir]
|
||||
# ./manage-group-members.sh list <group> [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
|
||||
286
workplans/NK-WP-0021-activity-core-ops-sso-operators.md
Normal file
286
workplans/NK-WP-0021-activity-core-ops-sso-operators.md
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
---
|
||||
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`, …).
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue