prole/etc/init_knoe_users.sh
chrisfu 30c7bc88fd fix(knoe-users): keytab reliability + mode-aware next steps + Gitea auto-token
Keytab export (silent failure bug):
- Always delete+recreate the postgres SPN with fresh random key; old key gone
  after EmptyDir wipe; kadmin.local -q exits 0 even on error so -norandkey
  silently failed
- Clean /tmp/pg.keytab before ktadd; verify non-empty with [ -s ] before
  proceeding; die loudly if keytab not written
- Fix base64 pipeline: set -o pipefail inside sh -c so base64 failure is not
  masked by tr exit code

Next steps:
- Suppress myrddin.prole.org Samba trust step in k3d mode (no AD server)
- Remove Grafana auth.proxy reminder (configured by Helm values already)

Gitea admin token:
- Auto-generate via kubectl exec into running Gitea pod before falling back to
  manual warning; persist as gitea-admin-token secret for future re-runs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 16:34:03 -07:00

802 lines
31 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# init_knoe_users.sh
# Purpose:
# - Provision Kerberos principals and database accounts for knoe-system users
# - Creates: admin@PROLE.LOCAL (master password), guest@PROLE.LOCAL (read-only),
# postgres service principal (keytab for GSS auth), developer group role
# - Cross-realm trust with myrddin.prole.org PROLE.ORG is activated via
# PROLE_KDC_TRUST_REALM=PROLE.ORG in init_kdc.sh / init_knoe_auth.sh
# - Sets up service admin access: ArgoCD RBAC, Gitea, GitLab
#
# Prerequisites:
# init_kdc.sh initialize (with PROLE_KDC_REALM=PROLE.LOCAL,
# PROLE_KDC_TRUST_REALM=PROLE.ORG,
# KRB5_KDC=myrddin.prole.org)
# init_cnpg_backup.sh (CNPG cluster must exist)
# init_argocd.sh (ArgoCD must be running)
#
# Usage:
# ./init_knoe_users.sh initialize # create/update all user resources
# ./init_knoe_users.sh status # show current state
# ./init_knoe_users.sh cleanup # remove managed secrets/patches
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck disable=SC1090
source "$SCRIPT_DIR/knoe_cfg.sh"
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
knoe_set_mode "${2:-}"
shift 2
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
knoe_set_mode "${1#*=}"
shift
fi
knoe_ensure_kubeconfig >/dev/null 2>&1 || true
ensure_kube_context || exit 1
# Guard: we must never operate against the 'default' context — it means the
# desired context was not found and a placeholder was silently accepted.
_active_ctx=$(kubectl config current-context 2>/dev/null || true)
if [[ "${_active_ctx:-}" == "default" ]]; then
printf '[ERROR] Active kubectl context is '\''default'\'' — the required context was not found.\n' >&2
printf ' Add the correct kubeconfig to KUBECONFIG, e.g.:\n' >&2
printf ' export KUBECONFIG=%s:~/.kube/config\n' "${KUBECONFIG:-/path/to/knoe-k3s.kubeconfig}" >&2
printf ' Then retry.\n' >&2
exit 1
fi
unset _active_ctx
ACTION="${1:-initialize}"
resolve_knoe_mode() {
local raw_mode="${KNOE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-k3s}}}"
if command -v knoe_normalize_mode >/dev/null 2>&1; then
knoe_normalize_mode "$raw_mode"
return 0
fi
raw_mode=$(printf '%s' "$raw_mode" | tr 'A-Z' 'a-z')
case "$raw_mode" in
prod|production)
printf 'k8s'
;;
*)
printf '%s' "$raw_mode"
;;
esac
}
default_knoe_auth_deployment() {
case "$(resolve_knoe_mode)" in
k8s)
printf 'authority-gcp-auth'
;;
*)
printf 'authority-knoe-auth'
;;
esac
}
# ── Configurable variables ─────────────────────────────────────────────────
# PROLE_KDC_NAMESPACE is the canonical env set by init_kdc.sh; honour it here
# so both scripts look at the same namespace for knoe-kdc-secrets.
# Never fall through to "default" — use knoe-system as the last resort.
KNOE_USERS_NAMESPACE="${KNOE_USERS_NAMESPACE:-${SERVICE_NAMESPACE:-knoe-system}}"
KNOE_KDC_NAMESPACE="${KNOE_KDC_NAMESPACE:-${PROLE_KDC_NAMESPACE:-${KNOE_USERS_NAMESPACE}}}"
KNOE_DB_NAMESPACE="${KNOE_DB_NAMESPACE:-${DATABASE_NAMESPACE:-knoe-db}}"
KNOE_DB_CLUSTER="${KNOE_DB_CLUSTER:-knoe-db}"
KNOE_DEPLOYMENT_MODE="${KNOE_DEPLOYMENT_MODE:-$(resolve_knoe_mode)}"
KNOE_AUTH_DEPLOYMENT="${KNOE_AUTH_DEPLOYMENT:-${PROLE_KDC_NAME:-$(default_knoe_auth_deployment)}}"
KNOE_ADMIN_PRINCIPAL="${KNOE_ADMIN_PRINCIPAL:-admin}"
PROLE_KDC_REALM="${PROLE_KDC_REALM:-PROLE.LOCAL}"
GITEA_HOST="${GITEA_HOST:-git.prole.org}"
GITLAB_HOST="${GITLAB_HOST:-gitlab.prole.org}"
GITEA_NAMESPACE="${GITEA_NAMESPACE:-gitea}"
GITLAB_NAMESPACE="${GITLAB_NAMESPACE:-gitlab}"
ARGOCD_NAMESPACE="${ARGOCD_NAMESPACE:-argocd}"
# Provided externally or resolved from knoe-kdc-secrets
PROLE_KDC_MASTER_PASSWORD="${PROLE_KDC_MASTER_PASSWORD:-}"
KNOE_GUEST_PASSWORD="${KNOE_GUEST_PASSWORD:-}"
# UI-supplied credentials (set by knoe_users.py from the Database / Kerberos Auth screens)
PROLE_LOCAL_ADMIN_PASSWORD="${PROLE_LOCAL_ADMIN_PASSWORD:-}"
PROLE_ORG_ADMIN_PASSWORD="${PROLE_ORG_ADMIN_PASSWORD:-}"
# Optional: personal access tokens for service admin promotion
GITEA_ADMIN_TOKEN="${GITEA_ADMIN_TOKEN:-}"
GITLAB_ADMIN_TOKEN="${GITLAB_ADMIN_TOKEN:-}"
log() { printf '[INFO] %s\n' "$*"; }
err() { printf '[ERROR] %s\n' "$*" >&2; }
warn() { printf '[WARN] %s\n' "$*" >&2; }
die() { err "$*"; exit 1; }
ensure_tools() {
for t in kubectl curl python3; do
command -v "$t" >/dev/null || die "Missing required tool: $t"
done
}
b64_decode() {
if base64 --decode </dev/null >/dev/null 2>&1; then
base64 --decode
elif base64 -d </dev/null >/dev/null 2>&1; then
base64 -d
else
base64 -D
fi
}
gen_password() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -base64 18
return 0
fi
python3 - <<'PY'
import secrets, string
alphabet = string.ascii_letters + string.digits
print(''.join(secrets.choice(alphabet) for _ in range(24)))
PY
}
# ── Secret helpers ─────────────────────────────────────────────────────────
# Read a base64-encoded key from a k8s Secret in KNOE_KDC_NAMESPACE.
# Mirrors get_secret_value() in init_kdc.sh (which reads from KDC_NAMESPACE).
get_secret_value() {
local secret="$1" key="$2"
kubectl -n "$KNOE_KDC_NAMESPACE" get secret "$secret" \
-o "jsonpath={.data.${key}}" 2>/dev/null | b64_decode 2>/dev/null || true
}
# ── KDC helpers ────────────────────────────────────────────────────────────
get_kdc_pod() {
# The kdc container lives inside the authority deployment pod selected by mode.
# Only return Running pods — Terminating pods from a prior rollout must be excluded.
local pod
pod=$(kubectl -n "$KNOE_KDC_NAMESPACE" get pods \
-l "app=${KNOE_AUTH_DEPLOYMENT}" \
--field-selector=status.phase=Running \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -n "$pod" ]]; then
printf '%s' "$pod"
return 0
fi
return 0
}
ensure_kdc_pod() {
local kdc_pod init_kdc_script
kdc_pod=$(get_kdc_pod)
if [[ -n "$kdc_pod" ]]; then
printf '%s' "$kdc_pod"
return 0
fi
init_kdc_script="$SCRIPT_DIR/init_kdc.sh"
[[ -f "$init_kdc_script" ]] || die "Required script not found: $init_kdc_script"
log "No authority pod found in namespace ${KNOE_KDC_NAMESPACE}; bootstrapping ${KNOE_AUTH_DEPLOYMENT} via init_kdc.sh ..." >&2
PROLE_KDC_NAMESPACE="$KNOE_KDC_NAMESPACE" \
PROLE_KDC_MASTER_PASSWORD="$PROLE_KDC_MASTER_PASSWORD" \
PROLE_KDC_NAME="$KNOE_AUTH_DEPLOYMENT" \
bash "$init_kdc_script" initialize >&2
kdc_pod=$(get_kdc_pod)
[[ -n "$kdc_pod" ]] || die "No KDC pod found in namespace ${KNOE_KDC_NAMESPACE} (label app=${KNOE_AUTH_DEPLOYMENT})."
printf '%s' "$kdc_pod"
}
kdc_principal_exists() {
local pod="$1" principal="$2"
kubectl -n "$KNOE_KDC_NAMESPACE" exec "$pod" -c kdc -- \
kadmin.local -q "get_principal ${principal}" >/dev/null 2>&1
}
kdc_addprinc() {
local pod="$1" principal="$2" password="$3"
kubectl -n "$KNOE_KDC_NAMESPACE" exec "$pod" -c kdc -- \
kadmin.local -q "addprinc -pw ${password} ${principal}"
}
kdc_addprinc_randkey() {
local pod="$1" principal="$2"
kubectl -n "$KNOE_KDC_NAMESPACE" exec "$pod" -c kdc -- \
kadmin.local -q "addprinc -randkey ${principal}"
}
# ── CNPG helpers ───────────────────────────────────────────────────────────
get_cnpg_primary() {
local pod=""
# CNPG 1.20+: instanceRole label
pod=$(kubectl -n "$KNOE_DB_NAMESPACE" get pods \
-l "cnpg.io/cluster=${KNOE_DB_CLUSTER},cnpg.io/instanceRole=primary" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -z "$pod" ]]; then
# Older label set
pod=$(kubectl -n "$KNOE_DB_NAMESPACE" get pods \
-l "cnpg.io/cluster=${KNOE_DB_CLUSTER},role=primary" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
fi
printf '%s' "$pod"
}
cnpg_psql() {
local pod="$1"; shift
kubectl -n "$KNOE_DB_NAMESPACE" exec "$pod" -- \
psql -U postgres -d postgres "$@"
}
# ── Main: initialize ───────────────────────────────────────────────────────
initialize() {
ensure_tools
log "=== knoe-system user provisioning ==="
log "Namespaces: kdc/authority=${KNOE_KDC_NAMESPACE} db=${KNOE_DB_NAMESPACE}"
log "Mode: ${KNOE_DEPLOYMENT_MODE} authority deployment: ${KNOE_AUTH_DEPLOYMENT}"
# 1. Resolve passwords — read from knoe-kdc-secrets (written by init_kdc.sh)
if [[ -z "$PROLE_KDC_MASTER_PASSWORD" ]]; then
PROLE_KDC_MASTER_PASSWORD=$(get_secret_value "knoe-kdc-secrets" "master_password")
fi
# Allow the UI-supplied PROLE_LOCAL_ADMIN_PASSWORD (DB master password field) to
# override when the secret is absent or empty.
if [[ -z "$PROLE_KDC_MASTER_PASSWORD" && -n "${PROLE_LOCAL_ADMIN_PASSWORD:-}" ]]; then
PROLE_KDC_MASTER_PASSWORD="$PROLE_LOCAL_ADMIN_PASSWORD"
log "Using PROLE_LOCAL_ADMIN_PASSWORD as PROLE_KDC_MASTER_PASSWORD"
fi
if [[ -z "$PROLE_KDC_MASTER_PASSWORD" ]]; then
die "PROLE_KDC_MASTER_PASSWORD not found.
The KDC master password must match the running KDC database and cannot be
auto-generated. Resolve one of:
1. Run: ./etc/init_kdc.sh initialize
(creates/stores the secret in namespace '${KNOE_KDC_NAMESPACE}')
2. Export: export PROLE_KDC_MASTER_PASSWORD=<password>
3. Set the database master password on the Database screen and retry
4. Check: kubectl -n ${KNOE_KDC_NAMESPACE} get secret knoe-kdc-secrets -o yaml"
fi
if [[ -z "$KNOE_GUEST_PASSWORD" ]]; then
KNOE_GUEST_PASSWORD=$(get_secret_value "knoe-kdc-secrets" "guest_password")
fi
if [[ -z "$KNOE_GUEST_PASSWORD" ]]; then
KNOE_GUEST_PASSWORD=$(gen_password)
log "Auto-generated guest password — storing in knoe-kdc-secrets ..."
if kubectl -n "$KNOE_KDC_NAMESPACE" get secret knoe-kdc-secrets >/dev/null 2>&1; then
local guest_b64
guest_b64=$(printf '%s' "$KNOE_GUEST_PASSWORD" | base64 | tr -d '\n')
kubectl -n "$KNOE_KDC_NAMESPACE" patch secret knoe-kdc-secrets \
--type=merge \
--patch "{\"data\":{\"guest_password\":\"${guest_b64}\"}}" >/dev/null
else
kubectl -n "$KNOE_KDC_NAMESPACE" create secret generic knoe-kdc-secrets \
--from-literal=guest_password="$KNOE_GUEST_PASSWORD" \
--dry-run=client -o yaml | kubectl apply -f - >/dev/null
fi
fi
# 2. Locate (or bootstrap) KDC pod
local kdc_pod
kdc_pod=$(ensure_kdc_pod)
log "KDC pod: $kdc_pod"
# 2a. Pre-flight: verify the KDC database stash file is present and kadmin.local
# can access the database. A missing stash means init_kdc.sh has not been
# run (or the pod restarted with ephemeral storage — EmptyDir volumes are
# wiped on pod restart). Auto-recover by re-initialising the database.
log "Verifying KDC database accessibility ..."
if ! kubectl -n "$KNOE_KDC_NAMESPACE" exec "$kdc_pod" -c kdc -- \
sh -c 'kadmin.local -q listprincs' >/dev/null 2>&1; then
log "KDC database not accessible — pod may have restarted with ephemeral storage."
log "Re-initialising KDC database via init_kdc.sh ..."
local init_kdc_script="$SCRIPT_DIR/init_kdc.sh"
[[ -f "$init_kdc_script" ]] || die "Required script not found: $init_kdc_script"
PROLE_KDC_NAMESPACE="$KNOE_KDC_NAMESPACE" \
PROLE_KDC_MASTER_PASSWORD="$PROLE_KDC_MASTER_PASSWORD" \
PROLE_KDC_NAME="$KNOE_AUTH_DEPLOYMENT" \
bash "$init_kdc_script" initialize >&2
# Poll until the new pod's entrypoint finishes kdb5_util create.
# The rollout completes before the in-container DB init finishes (no readiness probe).
local _reinit_timeout=120 _reinit_interval=5 _reinit_elapsed=0
log "Waiting up to ${_reinit_timeout}s for KDC database to become accessible ..."
while (( _reinit_elapsed < _reinit_timeout )); do
kdc_pod=$(get_kdc_pod)
if [[ -n "$kdc_pod" ]] && kubectl -n "$KNOE_KDC_NAMESPACE" exec "$kdc_pod" -c kdc -- \
sh -c 'kadmin.local -q listprincs' >/dev/null 2>&1; then
log "KDC database re-initialised successfully (pod: $kdc_pod)"
break
fi
sleep "$_reinit_interval"
_reinit_elapsed=$(( _reinit_elapsed + _reinit_interval ))
done
if (( _reinit_elapsed >= _reinit_timeout )); then
die "KDC database still not accessible after ${_reinit_timeout}s (pod: ${kdc_pod:-<none>}).
Check init_kdc.sh logs above for errors."
fi
else
log "KDC database accessible"
fi
# 3. Create admin@PROLE.LOCAL (UI login with master password)
local admin_princ="${KNOE_ADMIN_PRINCIPAL}@${PROLE_KDC_REALM}"
if kdc_principal_exists "$kdc_pod" "$admin_princ"; then
log "${admin_princ} already exists"
else
log "Creating ${admin_princ} ..."
kdc_addprinc "$kdc_pod" "$admin_princ" "$PROLE_KDC_MASTER_PASSWORD"
fi
# 4. Create guest@PROLE.LOCAL (read-only)
local guest_princ="guest@${PROLE_KDC_REALM}"
if kdc_principal_exists "$kdc_pod" "$guest_princ"; then
log "${guest_princ} already exists"
else
log "Creating ${guest_princ} ..."
kdc_addprinc "$kdc_pod" "$guest_princ" "$KNOE_GUEST_PASSWORD"
fi
# 5. Ensure BOTH cross-realm trust principals exist in the MIT KDC
# Direction A: krbtgt/PROLE.ORG@PROLE.LOCAL — cluster users → AD services
# (created by init_kdc.sh PROLE_KDC_TRUST_REALM mechanism)
# Direction B: krbtgt/PROLE.LOCAL@PROLE.ORG — AD users → cluster services ← THIS IS WHAT WE NEED
local trust_princ_in="krbtgt/PROLE.ORG@${PROLE_KDC_REALM}"
local trust_princ_out="krbtgt/${PROLE_KDC_REALM}@PROLE.ORG"
local trust_shared_pw
trust_shared_pw=$(kubectl -n "$KNOE_KDC_NAMESPACE" get secret knoe-kdc-secrets \
-o jsonpath='{.data.trust_shared_password}' 2>/dev/null | b64_decode || true)
if [[ -z "$trust_shared_pw" ]]; then
warn "trust_shared_password not set in knoe-kdc-secrets; cross-realm trust principals will not be created"
warn "Add trust_shared_password to knoe-kdc-secrets and re-run"
else
# Direction A (init_kdc.sh may already have created this)
if kdc_principal_exists "$kdc_pod" "$trust_princ_in"; then
log "${trust_princ_in} already exists"
else
log "Creating cross-realm principal ${trust_princ_in} ..."
kdc_addprinc "$kdc_pod" "$trust_princ_in" "$trust_shared_pw"
fi
# Direction B — the principal that allows AD users to reach cluster services
if kdc_principal_exists "$kdc_pod" "$trust_princ_out"; then
log "${trust_princ_out} already exists"
else
log "Creating cross-realm principal ${trust_princ_out} ..."
kdc_addprinc "$kdc_pod" "$trust_princ_out" "$trust_shared_pw"
fi
fi
# 6. Create postgres service principal + export keytab.
# Always (re)create the SPN with a fresh random key — the old key is gone when
# the KDC pod restarts on ephemeral storage. kadmin.local -q exits 0 even on
# failure, so we verify success by checking the keytab file was written.
local pg_spn="postgres/knoe-db-rw.${KNOE_DB_NAMESPACE}.svc.cluster.local@${PROLE_KDC_REALM}"
log "Creating/refreshing service principal ${pg_spn} ..."
kubectl -n "$KNOE_KDC_NAMESPACE" exec "$kdc_pod" -c kdc -- \
sh -c "kadmin.local -q 'delprinc -force ${pg_spn}' 2>/dev/null; kadmin.local -q 'addprinc -randkey ${pg_spn}'"
log "Exporting keytab for ${pg_spn} ..."
kubectl -n "$KNOE_KDC_NAMESPACE" exec "$kdc_pod" -c kdc -- \
sh -c "rm -f /tmp/pg.keytab; kadmin.local -q 'ktadd -k /tmp/pg.keytab ${pg_spn}'"
# kadmin.local -q exits 0 even on failure — verify the file was actually written
kubectl -n "$KNOE_KDC_NAMESPACE" exec "$kdc_pod" -c kdc -- \
sh -c '[ -s /tmp/pg.keytab ]' \
|| die "Keytab export failed for ${pg_spn} — /tmp/pg.keytab is missing or empty. Check KDC logs."
log "Storing keytab in Secret knoe-db-pg-keytab ..."
local keytab_b64
# Use pipefail so a base64 failure propagates through the tr pipeline
keytab_b64=$(kubectl -n "$KNOE_KDC_NAMESPACE" exec "$kdc_pod" -c kdc -- \
sh -c 'set -o pipefail; base64 /tmp/pg.keytab | tr -d "\n"')
[[ -n "$keytab_b64" ]] || die "base64 encoding of keytab produced empty output."
# Write secret into the DB namespace where CNPG mounts it
kubectl -n "$KNOE_DB_NAMESPACE" create secret generic knoe-db-pg-keytab \
--from-literal=pg.keytab="$(printf '%s' "$keytab_b64" | b64_decode)" \
--dry-run=client -o yaml | kubectl apply -f - >/dev/null
log "knoe-db-pg-keytab stored in namespace ${KNOE_DB_NAMESPACE}"
# 6. Patch CNPG cluster with managed.roles (idempotent merge)
log "Patching CNPG cluster ${KNOE_DB_CLUSTER} managed.roles ..."
kubectl -n "$KNOE_DB_NAMESPACE" patch cluster "$KNOE_DB_CLUSTER" \
--type=merge --patch '{
"spec": {
"managed": {
"roles": [
{
"name": "admin",
"ensure": "present",
"login": true,
"superuser": true,
"comment": "Kerberos admin principal — full cluster access"
},
{
"name": "guest",
"ensure": "present",
"login": true,
"superuser": false,
"comment": "Kerberos guest principal — read-only access to demo schema"
},
{
"name": "developer",
"ensure": "present",
"login": false,
"superuser": false,
"comment": "Developer group role — granted to knoe-system user accounts"
}
]
}
}
}'
# 7. Locate CNPG primary and run SQL
local primary
primary=$(get_cnpg_primary)
if [[ -z "$primary" ]]; then
warn "CNPG primary pod not found — skipping SQL setup (run again once cluster is ready)"
else
log "CNPG primary pod: $primary"
log "Setting up demo schema and role grants ..."
cnpg_psql "$primary" <<'SQL'
-- demo schema for guest read-only access (evolves over time)
CREATE SCHEMA IF NOT EXISTS demo;
-- Ensure roles exist (CNPG managed.roles may not have reconciled yet)
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'admin') THEN
CREATE ROLE admin LOGIN SUPERUSER;
ELSE
ALTER ROLE admin SUPERUSER LOGIN;
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'guest') THEN
CREATE ROLE guest LOGIN;
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'developer') THEN
CREATE ROLE developer NOLOGIN;
END IF;
END $$;
-- guest: read-only on demo schema
GRANT USAGE ON SCHEMA demo TO guest;
GRANT SELECT ON ALL TABLES IN SCHEMA demo TO guest;
ALTER DEFAULT PRIVILEGES IN SCHEMA demo GRANT SELECT ON TABLES TO guest;
-- developer: read/write access on public and demo schemas
GRANT USAGE ON SCHEMA public TO developer;
GRANT USAGE ON SCHEMA demo TO developer;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO developer;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA demo TO developer;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO developer;
ALTER DEFAULT PRIVILEGES IN SCHEMA demo GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO developer;
SQL
log "SQL setup complete"
fi
# 8. Create knoe.user schema tables and bootstrap users
if [[ -n "$primary" ]]; then
create_knoe_schema "$primary"
provision_user "$primary" "chrisfu" "chrisfu@prole.org" "Chris Fu" "admin"
provision_user "$primary" "ron" "ron@prole.org" "Ron" "developer"
else
warn "CNPG primary not found — skipping knoe.user schema and user provisioning"
fi
# 9. ArgoCD RBAC patch
patch_argocd_rbac
# 10. Gitea admin promotion
promote_gitea_admin
# 11. GitLab admin promotion
promote_gitlab_admin
log "=== Initialization complete ==="
log ""
log "Next steps:"
if [[ "$KNOE_DEPLOYMENT_MODE" != "k3d" ]]; then
log " 1. On myrddin.prole.org: add krbtgt/PROLE.LOCAL@PROLE.ORG trust principal"
log " (samba-tool domain trust, using trust_shared_password from knoe-kdc-secrets)"
log " 2. After admin logs in to Gitea/GitLab for the first time, re-run: $0 initialize"
else
log " 1. After admin logs in to Gitea/GitLab for the first time, re-run: $0 initialize"
fi
}
# ── knoe.user schema + user provisioning ──────────────────────────────────
create_knoe_schema() {
local primary="$1"
log "Creating knoe.user schema tables (idempotent)..."
cnpg_psql "$primary" <<'SQL'
CREATE TABLE IF NOT EXISTS knoe.user (
id SERIAL PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
realm TEXT NOT NULL DEFAULT 'PROLE.LOCAL',
email TEXT,
display_name TEXT,
tenant_realm TEXT,
is_realm_admin BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE IF NOT EXISTS knoe.user_role (
user_id INT NOT NULL REFERENCES knoe.user(id) ON DELETE CASCADE,
role TEXT NOT NULL,
granted_at TIMESTAMPTZ DEFAULT now(),
PRIMARY KEY (user_id, role)
);
GRANT SELECT, INSERT, UPDATE ON knoe.user TO knoe;
GRANT SELECT, INSERT, UPDATE ON knoe.user_role TO knoe;
GRANT USAGE, SELECT ON SEQUENCE knoe.user_id_seq TO knoe;
SQL
log "knoe.user schema ready."
}
# provision_user <primary_pod> <username> <email> <display_name> <role>
# role: admin | developer | guest (default: developer)
# Idempotent — safe to re-run.
provision_user() {
local primary="$1" username="$2" email="${3:-}" display_name="${4:-$2}" role="${5:-developer}"
log "Provisioning user: ${username} <${email}> role=${role}"
# 1. Kerberos principal (randkey — user sets own password via kpasswd / kadmin)
local user_princ="${username}@${PROLE_KDC_REALM}"
local kdc_pod
kdc_pod=$(get_kdc_pod)
if [[ -n "$kdc_pod" ]]; then
if kdc_principal_exists "$kdc_pod" "$user_princ"; then
log " Kerberos: ${user_princ} already exists"
else
log " Kerberos: creating ${user_princ} ..."
kdc_addprinc_randkey "$kdc_pod" "$user_princ"
fi
else
warn " No KDC pod — skipping Kerberos principal for ${username}"
fi
# 2. PostgreSQL role (GSS-API: username@PROLE.LOCAL → pg role 'username')
cnpg_psql "$primary" -v username="$username" -v role="$role" <<SQL
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${username}') THEN
CREATE ROLE ${username} LOGIN;
END IF;
END \$\$;
GRANT ${role} TO ${username};
SQL
# 3. knoe.user record (upsert)
cnpg_psql "$primary" <<SQL
INSERT INTO knoe.user (username, realm, email, display_name)
VALUES ('${username}', '${PROLE_KDC_REALM}', '${email}', '${display_name}')
ON CONFLICT (username) DO UPDATE SET
email = EXCLUDED.email,
display_name = EXCLUDED.display_name,
updated_at = now();
INSERT INTO knoe.user_role (user_id, role)
SELECT id, '${role}' FROM knoe.user WHERE username = '${username}'
ON CONFLICT DO NOTHING;
SQL
log " User ${username} provisioned."
}
# ── ArgoCD RBAC ────────────────────────────────────────────────────────────
patch_argocd_rbac() {
if ! kubectl -n "$ARGOCD_NAMESPACE" get configmap argocd-rbac-cm >/dev/null 2>&1; then
warn "argocd-rbac-cm not found in namespace ${ARGOCD_NAMESPACE}; skipping ArgoCD RBAC patch"
return 0
fi
local admin_line="g, ${KNOE_ADMIN_PRINCIPAL}, role:admin"
local current_csv
current_csv=$(kubectl -n "$ARGOCD_NAMESPACE" get configmap argocd-rbac-cm \
-o jsonpath='{.data.policy\.csv}' 2>/dev/null || true)
if printf '%s' "$current_csv" | grep -qF "$admin_line"; then
log "ArgoCD RBAC already contains admin entry"
return 0
fi
local new_csv="${current_csv}"$'\n'"${admin_line}"
local escaped_csv
escaped_csv=$(python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))' <<< "$new_csv")
kubectl -n "$ARGOCD_NAMESPACE" patch configmap argocd-rbac-cm \
--type=merge \
--patch "{\"data\":{\"policy.csv\":${escaped_csv}}}" >/dev/null
log "ArgoCD RBAC: added '${admin_line}'"
}
# ── Gitea admin promotion ───────────────────────────────────────────────────
promote_gitea_admin() {
local token="$GITEA_ADMIN_TOKEN"
if [[ -z "$token" ]]; then
token=$(kubectl -n "$GITEA_NAMESPACE" get secret gitea-admin-token \
-o jsonpath='{.data.token}' 2>/dev/null | b64_decode || true)
fi
# Auto-generate an admin token by exec-ing into the Gitea pod (Gitea ≥ 1.17)
if [[ -z "$token" ]]; then
local _gitea_pod
_gitea_pod=$(kubectl -n "$GITEA_NAMESPACE" get pods \
-l "app.kubernetes.io/name=gitea" \
--field-selector=status.phase=Running \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -n "$_gitea_pod" ]]; then
log "Gitea: generating admin token via kubectl exec into ${_gitea_pod} ..."
token=$(kubectl -n "$GITEA_NAMESPACE" exec "$_gitea_pod" -- \
gitea admin user generate-access-token \
--username admin --token-name knoe-installer \
--scopes "write:admin,read:user" --raw 2>/dev/null | tail -1 || true)
if [[ -n "$token" ]]; then
# Persist so subsequent calls (and future re-runs) reuse the same token
local _tok_b64; _tok_b64=$(printf '%s' "$token" | base64 | tr -d '\n')
kubectl -n "$GITEA_NAMESPACE" create secret generic gitea-admin-token \
--from-literal=token="$token" \
--dry-run=client -o yaml | kubectl apply -f - >/dev/null 2>&1 || true
log "Gitea: admin token generated and stored in gitea-admin-token secret"
fi
fi
fi
if [[ -z "$token" ]]; then
warn "No GITEA_ADMIN_TOKEN available — skipping Gitea admin promotion"
warn " Manually: curl -X PATCH https://${GITEA_HOST}/api/v1/admin/users/${KNOE_ADMIN_PRINCIPAL} \\"
warn " -H 'Authorization: token <token>' -H 'Content-Type: application/json' \\"
warn " -d '{\"admin\":true,\"login_name\":\"${KNOE_ADMIN_PRINCIPAL}\",\"source_id\":0}'"
return 0
fi
local http_code
http_code=$(curl -s -o /dev/null -w '%{http_code}' \
-H "Authorization: token ${token}" \
"https://${GITEA_HOST}/api/v1/users/${KNOE_ADMIN_PRINCIPAL}" 2>/dev/null || true)
if [[ "$http_code" == "404" ]]; then
warn "Gitea: user '${KNOE_ADMIN_PRINCIPAL}' not found (must log in first)"
warn " Re-run '$0 initialize' after first login"
return 0
fi
http_code=$(curl -s -o /dev/null -w '%{http_code}' \
-X PATCH \
-H "Authorization: token ${token}" \
-H "Content-Type: application/json" \
-d "{\"admin\":true,\"login_name\":\"${KNOE_ADMIN_PRINCIPAL}\",\"source_id\":0}" \
"https://${GITEA_HOST}/api/v1/admin/users/${KNOE_ADMIN_PRINCIPAL}" 2>/dev/null || true)
if [[ "$http_code" =~ ^2 ]]; then
log "Gitea: '${KNOE_ADMIN_PRINCIPAL}' promoted to admin"
else
warn "Gitea admin promotion returned HTTP ${http_code}"
fi
}
# ── GitLab admin promotion ─────────────────────────────────────────────────
promote_gitlab_admin() {
local token="$GITLAB_ADMIN_TOKEN"
if [[ -z "$token" ]]; then
warn "No GITLAB_ADMIN_TOKEN (personal access token) available — skipping GitLab admin promotion"
warn " Set GITLAB_ADMIN_TOKEN and re-run, or use:"
warn " kubectl exec -n ${GITLAB_NAMESPACE} <rails-pod> -- gitlab-rails runner \\"
warn " \"User.find_by_username('${KNOE_ADMIN_PRINCIPAL}')&.update(admin: true)\""
return 0
fi
# Look up user ID by username
local user_json user_id
user_json=$(curl -s \
-H "PRIVATE-TOKEN: ${token}" \
"https://${GITLAB_HOST}/api/v4/users?username=${KNOE_ADMIN_PRINCIPAL}" 2>/dev/null || true)
user_id=$(python3 -c \
'import sys,json; u=json.load(sys.stdin); print(u[0]["id"] if u else "")' \
<<< "$user_json" 2>/dev/null || true)
if [[ -z "$user_id" ]]; then
warn "GitLab: user '${KNOE_ADMIN_PRINCIPAL}' not found (must log in first)"
warn " Re-run '$0 initialize' after first login"
return 0
fi
local http_code
http_code=$(curl -s -o /dev/null -w '%{http_code}' \
-X PUT \
-H "PRIVATE-TOKEN: ${token}" \
-H "Content-Type: application/json" \
-d '{"admin":true}' \
"https://${GITLAB_HOST}/api/v4/users/${user_id}" 2>/dev/null || true)
if [[ "$http_code" =~ ^2 ]]; then
log "GitLab: '${KNOE_ADMIN_PRINCIPAL}' promoted to admin"
else
warn "GitLab admin promotion returned HTTP ${http_code}"
fi
}
# ── Status ─────────────────────────────────────────────────────────────────
show_status() {
log "=== KDC Principals ==="
local kdc_pod
kdc_pod=$(get_kdc_pod)
if [[ -n "$kdc_pod" ]]; then
kubectl -n "$KNOE_KDC_NAMESPACE" exec "$kdc_pod" -c kdc -- \
kadmin.local -q "list_principals" 2>/dev/null \
| grep -E "^(admin|guest|developer|postgres/|krbtgt/)" || true
else
warn "No KDC pod found"
fi
log ""
log "=== CNPG Managed Roles ==="
kubectl -n "$KNOE_DB_NAMESPACE" get cluster "$KNOE_DB_CLUSTER" \
-o jsonpath='{.spec.managed.roles}' 2>/dev/null \
| python3 -m json.tool 2>/dev/null || warn "Could not read managed.roles"
log ""
log "=== PostgreSQL Roles ==="
local primary
primary=$(get_cnpg_primary)
if [[ -n "$primary" ]]; then
cnpg_psql "$primary" -c "\\du admin guest developer" 2>/dev/null || true
else
warn "No CNPG primary pod found"
fi
log ""
log "=== Keytab Secret ==="
if kubectl -n "$KNOE_DB_NAMESPACE" get secret knoe-db-pg-keytab >/dev/null 2>&1; then
log "knoe-db-pg-keytab exists in namespace ${KNOE_DB_NAMESPACE}"
else
warn "knoe-db-pg-keytab NOT FOUND in namespace ${KNOE_DB_NAMESPACE}"
fi
log ""
log "=== ArgoCD RBAC ==="
kubectl -n "$ARGOCD_NAMESPACE" get configmap argocd-rbac-cm \
-o jsonpath='{.data.policy\.csv}' 2>/dev/null || warn "argocd-rbac-cm not found"
}
# ── Cleanup ────────────────────────────────────────────────────────────────
cleanup() {
warn "cleanup removes the pg keytab secret and clears managed.roles from CNPG."
warn "KDC principals are NOT deleted (use kadmin.local to remove them manually)."
kubectl -n "$KNOE_DB_NAMESPACE" delete secret knoe-db-pg-keytab \
--ignore-not-found >/dev/null && log "Deleted knoe-db-pg-keytab"
kubectl -n "$KNOE_DB_NAMESPACE" patch cluster "$KNOE_DB_CLUSTER" \
--type=merge --patch '{"spec":{"managed":null}}' >/dev/null 2>&1 \
&& log "Cleared managed.roles from cluster ${KNOE_DB_CLUSTER}" || true
}
# ── Dispatch ───────────────────────────────────────────────────────────────
case "$ACTION" in
initialize|init) initialize ;;
status) show_status ;;
cleanup) cleanup ;;
*)
err "Unknown action: ${ACTION}"
err "Usage: $(basename "$0") [initialize|status|cleanup]"
exit 1
;;
esac