generated from coulomb/repo-seed
feat(sso-mfa): T07/T08 user mgmt, backups, DR & break-glass (NK-WP-0001-T07/T08)
T07 — User management & self-service: - k8s/lldap/bootstrap-users.sh: creates net-kingdom-users and net-kingdom-admins groups in LLDAP via GraphQL API; idempotent. - k8s/lldap/break-glass.sh: creates break-glass bypass account in LLDAP, sets BREAKGLASS_PASSWORD, assigns to net-kingdom-admins. - k8s/verify-t07.sh: 6 checks — groups, break-glass, self-service portal, KeyCape OIDC client registrations. T08 — Backups, DR, break-glass: - k8s/backup/cronjob-sqlite-backups.yaml: daily CronJobs for LLDAP SQLite, Authelia SQLite (with scale-down/up RBAC), and privacyIDEA enckey backup. 7-day retention, 03:00/03:15/03:30 UTC staggered schedule. - k8s/backup/DR-RUNBOOK.md: full restore runbook — scenarios, restore order, LLDAP/Authelia/PI SQLite restore procedure, full node rebuild sequence, offsite age-encrypted export. - k8s/verify-t08.sh: 9 checks — CronJobs, RBAC, run history, backup files on PVCs, DR runbook presence, offsite backup (manual confirmation). - WORKPLAN.md: T07/T08 sections with done-criteria added. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
172
sso-mfa/k8s/lldap/bootstrap-users.sh
Executable file
172
sso-mfa/k8s/lldap/bootstrap-users.sh
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env bash
|
||||
# bootstrap-users.sh — seed required groups in LLDAP
|
||||
#
|
||||
# Run AFTER LLDAP is deployed and Running (T05a).
|
||||
#
|
||||
# What it does:
|
||||
# 1. Authenticates to LLDAP via its GraphQL API.
|
||||
# 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 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.
|
||||
#
|
||||
# Usage:
|
||||
# ./bootstrap-users.sh [lldap-url] [secrets-dir]
|
||||
#
|
||||
# <lldap-url> default: https://lldap.coulomb.social
|
||||
# <secrets-dir> default: ../../bootstrap/secrets
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
LLDAP_URL="${1:-https://lldap.coulomb.social}"
|
||||
SECRETS_DIR="${2:-../../bootstrap/secrets}"
|
||||
LLDAP_ENV="$SECRETS_DIR/lldap/secrets.env"
|
||||
|
||||
PASS_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
|
||||
ok() { echo " [OK] $1"; ((PASS_COUNT++)); }
|
||||
fail() { echo " [FAIL] $1"; ((FAIL_COUNT++)); }
|
||||
info() { echo " [INFO] $1"; }
|
||||
|
||||
if [[ ! -f "$LLDAP_ENV" ]]; then
|
||||
echo "ERROR: $LLDAP_ENV not found — run sso-mfa/bootstrap/gen-secrets.sh first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
read_env() { bash -c "source '$1' 2>/dev/null; echo \${$2}"; }
|
||||
LLDAP_ADMIN_PASS=$(read_env "$LLDAP_ENV" LLDAP_LDAP_USER_PASS)
|
||||
|
||||
if [[ -z "$LLDAP_ADMIN_PASS" ]]; then
|
||||
echo "ERROR: LLDAP_LDAP_USER_PASS not found in $LLDAP_ENV" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── 1. Authenticate ───────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "Authenticating to LLDAP at $LLDAP_URL ..."
|
||||
|
||||
AUTH_RESP=$(curl -sf -X POST "$LLDAP_URL/auth/simple/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"username\":\"admin\",\"password\":\"$LLDAP_ADMIN_PASS\"}" \
|
||||
2>/dev/null || echo "CURL_FAILED")
|
||||
|
||||
if [[ "$AUTH_RESP" == "CURL_FAILED" ]]; then
|
||||
echo "ERROR: Could not reach $LLDAP_URL — is LLDAP deployed and ingress up?" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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. Response: $AUTH_RESP" >&2
|
||||
exit 1
|
||||
fi
|
||||
info "Authenticated as admin"
|
||||
|
||||
gql() {
|
||||
# gql <query> <variables-json>
|
||||
local query="$1"; local vars="${2:-{}}"
|
||||
local body
|
||||
body=$(python3 -c "
|
||||
import json, sys
|
||||
print(json.dumps({'query': sys.argv[1], 'variables': json.loads(sys.argv[2])}))
|
||||
" "$query" "$vars")
|
||||
curl -sf -X POST "$LLDAP_URL/api/graphql" \
|
||||
-H "Authorization: Bearer $LLDAP_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$body" 2>/dev/null || echo "CURL_FAILED"
|
||||
}
|
||||
|
||||
create_group() {
|
||||
local name="$1"
|
||||
echo ""
|
||||
echo "Creating group: $name ..."
|
||||
|
||||
# Check if group already exists
|
||||
LIST_RESP=$(gql 'query { groups { id displayName } }')
|
||||
if [[ "$LIST_RESP" != "CURL_FAILED" ]]; then
|
||||
EXISTS=$(echo "$LIST_RESP" | python3 -c \
|
||||
"import sys,json; d=json.load(sys.stdin); print('yes' if any(g['displayName']=='$name' for g in d.get('data',{}).get('groups',[])) else 'no')" \
|
||||
2>/dev/null || echo "no")
|
||||
if [[ "$EXISTS" == "yes" ]]; then
|
||||
ok "Group '$name' already exists — skipping"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
RESP=$(gql 'mutation CreateGroup($name: String!) { createGroup(name: $name) { id displayName } }' \
|
||||
"{\"name\":\"$name\"}")
|
||||
if [[ "$RESP" == "CURL_FAILED" ]]; then
|
||||
fail "Group '$name' — curl request failed"
|
||||
return 1
|
||||
fi
|
||||
ERR=$(echo "$RESP" | python3 -c \
|
||||
"import sys,json; d=json.load(sys.stdin); errs=d.get('errors',[]); print(errs[0]['message'] if errs else '')" \
|
||||
2>/dev/null || echo "")
|
||||
if [[ -n "$ERR" ]]; then
|
||||
fail "Group '$name' — $ERR"
|
||||
return 1
|
||||
fi
|
||||
GID=$(echo "$RESP" | python3 -c \
|
||||
"import sys,json; print(json.load(sys.stdin).get('data',{}).get('createGroup',{}).get('id','?'))" \
|
||||
2>/dev/null || echo "?")
|
||||
ok "Group '$name' created (id=$GID)"
|
||||
}
|
||||
|
||||
# ── 2. Create required groups ─────────────────────────────────────────────────
|
||||
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 "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")
|
||||
if [[ "$EXISTS" == "yes" ]]; then
|
||||
ok "Group '$grp' confirmed"
|
||||
else
|
||||
fail "Group '$grp' not found after creation"
|
||||
fi
|
||||
done
|
||||
else
|
||||
fail "Could not retrieve group list from LLDAP"
|
||||
fi
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
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 ""
|
||||
echo "User onboarding checklist:"
|
||||
echo ""
|
||||
echo " Per new user:"
|
||||
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 ""
|
||||
echo " Break-glass account:"
|
||||
echo " Run: sso-mfa/k8s/lldap/break-glass.sh"
|
||||
echo " (Creates a pre-seeded local bypass user outside the normal MFA flow.)"
|
||||
echo ""
|
||||
|
||||
if [[ "$FAIL_COUNT" -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
Reference in New Issue
Block a user