mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 12:03:59 +00:00
The Gitea Helm chart (gitea-12.5.3) stores GITEA_ADMIN_USERNAME and GITEA_ADMIN_PASSWORD as plain values in the configure-gitea init container spec — not in a k8s Secret with key 'admin-password'. The previous code looked for a non-existent secretKeyRef and returned empty, causing the REST API bootstrap path to fail. Now reads credentials via: kubectl get deployment gitea ... env[?(@.name=="GITEA_ADMIN_PASSWORD")].value Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1186 lines
49 KiB
Bash
Executable File
1186 lines
49 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@KNOE.LOCAL (master password), guest@KNOE.LOCAL (read-only),
|
|
# postgres service principal (keytab for GSS auth), developer group role
|
|
# - Cross-realm trust with myrddin.prole.org PROLE.ORG is referenced via
|
|
# PROLE_KDC_TRUST_REALM=PROLE.ORG in init_kdc.sh / init_knoe_auth.sh
|
|
# - Sets up service admin access: ArgoCD RBAC, Gitea, GitLab
|
|
# - Ensures Gitea SPNEGO keytab (HTTP/git.prole.org@PROLE.ORG) is provisioned
|
|
#
|
|
# 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)
|
|
|
|
# Pre-scan argv for --mode before sourcing knoe_cfg.sh.
|
|
# knoe_cfg.sh selects the .cfg file based on KNOE_MODE at source time;
|
|
# without this, KNOE_MODE is empty and k3d.cfg wins the fallback loop even
|
|
# when --mode k3s is passed — causing k3s.cfg values (e.g. KNOE_ADMIN_PRINCIPAL)
|
|
# to never be loaded. knoe_normalize_mode is not yet available here so we
|
|
# accept k3d/k3s/k8s verbatim; the canonical normalisation runs in knoe_set_mode.
|
|
_knoe_prescan_mode() {
|
|
local _prev=""
|
|
for _arg in "$@"; do
|
|
case "$_prev" in --mode|-m) export KNOE_MODE="$_arg"; return ;; esac
|
|
case "$_arg" in --mode=*|-m=*) export KNOE_MODE="${_arg#*=}"; return ;; esac
|
|
_prev="$_arg"
|
|
done
|
|
}
|
|
_knoe_prescan_mode "$@"
|
|
unset -f _knoe_prescan_mode
|
|
|
|
# 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: reject the 'default' context in k3d mode — it means the desired
|
|
# k3d context was not found and a placeholder was silently accepted.
|
|
# In k3s/k8s mode the k3s-generated kubeconfig legitimately uses 'default'
|
|
# as the context name (e.g. on the cluster-server node itself), so allow it.
|
|
_active_ctx=$(kubectl config current-context 2>/dev/null || true)
|
|
_active_mode="${KNOE_MODE:-}"
|
|
if [[ "${_active_ctx:-}" == "default" && "$_active_mode" != "k3s" && "$_active_mode" != "k8s" ]]; then
|
|
printf '[ERROR] Active kubectl context is '\''default'\'' — the required context was not found.\n' >&2
|
|
printf ' In k3d mode this indicates a missed context switch.\n' >&2
|
|
printf ' Pass --mode k3s if you intend to target a k3s cluster, e.g.:\n' >&2
|
|
printf ' %s --mode k3s initialize\n' "$(basename "$0")" >&2
|
|
printf ' Or add the correct kubeconfig: export KUBECONFIG=%s:~/.kube/config\n' "${KUBECONFIG:-/path/to/knoe-k3s.kubeconfig}" >&2
|
|
exit 1
|
|
fi
|
|
unset _active_ctx _active_mode
|
|
|
|
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:-KNOE.LOCAL}"
|
|
GITEA_HOST="${GITEA_HOST:-git.prole.org}"
|
|
GITEA_SPNEGO_HOST="${GITEA_SPNEGO_HOST:-${GITEA_HOST}}"
|
|
GITEA_KRB5_AD_REALM="${GITEA_KRB5_AD_REALM:-PROLE.ORG}"
|
|
GITEA_KRB5_AD_USER="${GITEA_KRB5_AD_USER:-gitea-http}"
|
|
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 k8s secrets / 1Password
|
|
PROLE_KDC_MASTER_PASSWORD="${PROLE_KDC_MASTER_PASSWORD:-}"
|
|
KNOE_GUEST_PASSWORD="${KNOE_GUEST_PASSWORD:-}"
|
|
# 1Password secret references (op:// URIs). Set in conf/k3s.cfg or via env.
|
|
# When set and op CLI is available, these are used as fallback if the k8s
|
|
# secret is absent or empty.
|
|
OP_KDC_MASTER_PASSWORD_REF="${OP_KDC_MASTER_PASSWORD_REF:-}"
|
|
OP_KDC_GUEST_PASSWORD_REF="${OP_KDC_GUEST_PASSWORD_REF:-}"
|
|
# 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:-}"
|
|
# 1Password vault for Gitea/service credentials (default: Personal)
|
|
GITEA_OP_VAULT="${GITEA_OP_VAULT:-Personal}"
|
|
|
|
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 value from 1Password using the op CLI.
|
|
# Returns 1 (empty output) if op is not installed or the ref is empty.
|
|
# Usage: try_op_read "op://vault/item/field"
|
|
try_op_read() {
|
|
local ref="${1:-}"
|
|
[[ -n "$ref" ]] || return 1
|
|
command -v op >/dev/null 2>&1 || return 1
|
|
local val
|
|
val=$(op read "$ref" 2>/dev/null) || return 1
|
|
[[ -n "$val" ]] || return 1
|
|
printf '%s' "$val"
|
|
}
|
|
|
|
# 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
|
|
}
|
|
|
|
# _resolve_kdc_registry — sets _kdc_reg_internal and _kdc_reg_host in the
|
|
# caller's scope for use as env-var prefixes on init_kdc.sh invocations.
|
|
# In k3s mode derives correct values from KNOE_IMAGE_REGISTRY / PROLE_K3S_SERVER
|
|
# so stale k3d LOCAL_REGISTRY_INTERNAL values cannot leak into subprocesses.
|
|
_resolve_kdc_registry() {
|
|
_kdc_reg_internal=""
|
|
_kdc_reg_host=""
|
|
if [[ "$(resolve_knoe_mode)" == "k3s" ]]; then
|
|
if [[ -n "${KNOE_IMAGE_REGISTRY:-}" ]]; then
|
|
_kdc_reg_internal="${KNOE_IMAGE_REGISTRY}"
|
|
else
|
|
local _rns="${REGISTRY_NAMESPACE:-${SERVICE_NAMESPACE:-${KNOE_KDC_NAMESPACE}}}"
|
|
_kdc_reg_internal="registry.${_rns}.svc.cluster.local:5000"
|
|
fi
|
|
local _k3s_url="${PROLE_K3S_SERVER:-${K3S_SERVER:-${K3S_SERVER_URL:-}}}"
|
|
if [[ -n "${_k3s_url:-}" ]]; then
|
|
_kdc_reg_host="${_k3s_url#https://}"
|
|
_kdc_reg_host="${_kdc_reg_host#http://}"
|
|
_kdc_reg_host="${_kdc_reg_host%%:*}"
|
|
_kdc_reg_host="${_kdc_reg_host}:5000"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
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
|
|
|
|
_resolve_kdc_registry
|
|
LOCAL_REGISTRY_INTERNAL="${_kdc_reg_internal}" \
|
|
PROLE_KDC_REGISTRY_INTERNAL="${_kdc_reg_internal}" \
|
|
PROLE_KDC_REGISTRY_HOST="${_kdc_reg_host}" \
|
|
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 KDC master password — try multiple sources in order:
|
|
# a) env var (already set) b) knoe-kdc-secrets k8s Secret
|
|
# c) prole-kdc-secrets k8s Secret d) 1Password (OP_KDC_MASTER_PASSWORD_REF)
|
|
# e) PROLE_LOCAL_ADMIN_PASSWORD f) die
|
|
if [[ -z "$PROLE_KDC_MASTER_PASSWORD" ]]; then
|
|
PROLE_KDC_MASTER_PASSWORD=$(get_secret_value "knoe-kdc-secrets" "master_password")
|
|
fi
|
|
if [[ -z "$PROLE_KDC_MASTER_PASSWORD" ]]; then
|
|
# The live prole deployment may use a differently-named secret
|
|
PROLE_KDC_MASTER_PASSWORD=$(get_secret_value "prole-kdc-secrets" "master_password")
|
|
[[ -n "$PROLE_KDC_MASTER_PASSWORD" ]] && log "Resolved KDC master password from prole-kdc-secrets"
|
|
fi
|
|
if [[ -z "$PROLE_KDC_MASTER_PASSWORD" && -n "${OP_KDC_MASTER_PASSWORD_REF:-}" ]]; then
|
|
log "Resolving KDC master password from 1Password (${OP_KDC_MASTER_PASSWORD_REF}) ..."
|
|
PROLE_KDC_MASTER_PASSWORD=$(try_op_read "$OP_KDC_MASTER_PASSWORD_REF" || true)
|
|
[[ -n "$PROLE_KDC_MASTER_PASSWORD" ]] && log "KDC master password resolved from 1Password"
|
|
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
|
|
err "PROLE_KDC_MASTER_PASSWORD not found."
|
|
err "The KDC master password must match the running KDC database and cannot be auto-generated."
|
|
err "Resolve one of:"
|
|
err " 1. Store in k8s secret:"
|
|
err " kubectl -n ${KNOE_KDC_NAMESPACE} patch secret knoe-kdc-secrets \\"
|
|
err " --type=merge -p '{\"data\":{\"master_password\":\"BASE64PW\"}}'"
|
|
err " 2. Store in 1Password and set in conf/k3s.cfg:"
|
|
err " OP_KDC_MASTER_PASSWORD_REF = op://personal/<item>/password"
|
|
err " then: op run -- bash etc/init_knoe_users.sh --mode k3s initialize"
|
|
err " 3. Export directly: export PROLE_KDC_MASTER_PASSWORD=<password>"
|
|
err " 4. Check existing: kubectl -n ${KNOE_KDC_NAMESPACE} get secrets"
|
|
exit 1
|
|
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=$(get_secret_value "prole-kdc-secrets" "guest_password")
|
|
fi
|
|
if [[ -z "$KNOE_GUEST_PASSWORD" && -n "${OP_KDC_GUEST_PASSWORD_REF:-}" ]]; then
|
|
KNOE_GUEST_PASSWORD=$(try_op_read "$OP_KDC_GUEST_PASSWORD_REF" || true)
|
|
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"
|
|
_resolve_kdc_registry
|
|
LOCAL_REGISTRY_INTERNAL="${_kdc_reg_internal}" \
|
|
PROLE_KDC_REGISTRY_INTERNAL="${_kdc_reg_internal}" \
|
|
PROLE_KDC_REGISTRY_HOST="${_kdc_reg_host}" \
|
|
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@KNOE.LOCAL — cluster users → AD services
|
|
# (created by init_kdc.sh PROLE_KDC_TRUST_REALM mechanism)
|
|
# Direction B: krbtgt/KNOE.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
|
|
keytab_b64=$(kubectl -n "$KNOE_KDC_NAMESPACE" exec "$kdc_pod" -c kdc -- \
|
|
sh -c '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"
|
|
# ron has no prole.org Google Workspace account; identity is ron@KNOE.LOCAL (Kerberos only)
|
|
provision_user "$primary" "ron" "" "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 SPNEGO keytab (HTTP/git.prole.org@PROLE.ORG)
|
|
ensure_gitea_spnego_keytab
|
|
|
|
# 11. Gitea admin promotion
|
|
promote_gitea_admin
|
|
|
|
# 12. GitLab admin promotion
|
|
promote_gitlab_admin
|
|
|
|
log "=== Initialization complete ==="
|
|
log ""
|
|
log "Next steps:"
|
|
if [[ "$KNOE_DEPLOYMENT_MODE" != "k3d" ]]; then
|
|
log " 1. Kerberos SSO: ensure krbtgt/PROLE.ORG@KNOE.LOCAL and krbtgt/KNOE.LOCAL@PROLE.ORG"
|
|
log " principals exist in the MIT KDC (created by init_kdc.sh or manually via kadmin.local)"
|
|
log " 2. After admin logs in to Gitea/GitLab for the first time, re-run: $0 initialize"
|
|
log " 3. Browser SSO (Chrome): defaults write com.google.Chrome AuthServerAllowlist git.prole.org"
|
|
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 'KNOE.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}"
|
|
|
|
# 1. Kerberos principal (randkey — user sets own password via kpasswd / kadmin)
|
|
local user_princ="${username}@${PROLE_KDC_REALM}"
|
|
if [[ -n "$email" ]]; then
|
|
log "Provisioning user: ${username} kerberos=${user_princ} email=${email} role=${role}"
|
|
else
|
|
log "Provisioning user: ${username} kerberos=${user_princ} (no contact email) role=${role}"
|
|
fi
|
|
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}', NULLIF('${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 SPNEGO keytab ─────────────────────────────────────────────────────
|
|
|
|
# Ensures the Secret gitea-krb5-keytab exists in GITEA_NAMESPACE.
|
|
# If samba-tool is available (i.e. running on myrddin), creates the service
|
|
# account, SPN, and keytab automatically. Otherwise prints the manual steps.
|
|
ensure_gitea_spnego_keytab() {
|
|
local spnego_principal="HTTP/${GITEA_SPNEGO_HOST}"
|
|
local spn="${spnego_principal}@${GITEA_KRB5_AD_REALM}"
|
|
local keytab_tmp="/tmp/gitea-http-$$.keytab"
|
|
|
|
log "=== Gitea SPNEGO keytab (${GITEA_NAMESPACE}/gitea-krb5-keytab) ==="
|
|
|
|
if kubectl -n "$GITEA_NAMESPACE" get secret gitea-krb5-keytab >/dev/null 2>&1; then
|
|
log "gitea-krb5-keytab already exists in namespace ${GITEA_NAMESPACE} — OK"
|
|
return 0
|
|
fi
|
|
|
|
log "gitea-krb5-keytab not found — attempting to provision ..."
|
|
|
|
if ! command -v samba-tool >/dev/null 2>&1; then
|
|
warn "samba-tool not available — run the following on myrddin.prole.org as root:"
|
|
warn " samba-tool user create ${GITEA_KRB5_AD_USER} --random-password 2>/dev/null || true"
|
|
warn " samba-tool spn add ${spnego_principal} ${GITEA_KRB5_AD_USER}"
|
|
warn " samba-tool domain exportkeytab /tmp/http.keytab --principal ${spnego_principal}"
|
|
warn " klist -k /tmp/http.keytab # verify"
|
|
warn " kubectl -n ${GITEA_NAMESPACE} create secret generic gitea-krb5-keytab \\"
|
|
warn " --from-file=http.keytab=/tmp/http.keytab"
|
|
warn " rm -f /tmp/http.keytab"
|
|
warn " kubectl -n ${GITEA_NAMESPACE} rollout restart deployment gitea-spnego-proxy"
|
|
return 0
|
|
fi
|
|
|
|
log "Creating AD service account '${GITEA_KRB5_AD_USER}' (idempotent) ..."
|
|
samba-tool user create "$GITEA_KRB5_AD_USER" --random-password 2>/dev/null \
|
|
&& log "Created ${GITEA_KRB5_AD_USER}" \
|
|
|| log "${GITEA_KRB5_AD_USER} already exists"
|
|
|
|
log "Adding SPN ${spnego_principal} → ${GITEA_KRB5_AD_USER} ..."
|
|
samba-tool spn add "$spnego_principal" "$GITEA_KRB5_AD_USER" \
|
|
|| warn "SPN add returned non-zero (may already exist)"
|
|
|
|
log "Exporting keytab for ${spn} ..."
|
|
rm -f "$keytab_tmp"
|
|
samba-tool domain exportkeytab "$keytab_tmp" --principal "$spnego_principal"
|
|
|
|
if [[ ! -s "$keytab_tmp" ]]; then
|
|
rm -f "$keytab_tmp"
|
|
die "Keytab export failed — ${keytab_tmp} is missing or empty. Check samba-tool output."
|
|
fi
|
|
|
|
log "Storing keytab in Secret gitea-krb5-keytab ..."
|
|
kubectl -n "$GITEA_NAMESPACE" create secret generic gitea-krb5-keytab \
|
|
--from-file=http.keytab="$keytab_tmp" \
|
|
--dry-run=client -o yaml | kubectl apply -f - >/dev/null
|
|
rm -f "$keytab_tmp"
|
|
log "gitea-krb5-keytab provisioned — ${spn}"
|
|
|
|
log "Restarting gitea-spnego-proxy to load new keytab ..."
|
|
kubectl -n "$GITEA_NAMESPACE" rollout restart deployment gitea-spnego-proxy 2>/dev/null || true
|
|
}
|
|
|
|
# ── Gitea admin promotion ───────────────────────────────────────────────────
|
|
|
|
# ── Gitea credential management (1Password-backed) ────────────────────────
|
|
#
|
|
# Design: Gitea passwords are vault credentials — strong, random, generated
|
|
# by 1Password, never logged, never stored in k8s secrets. Primary auth is
|
|
# always Kerberos SPNEGO; the password is an emergency recovery path only.
|
|
# API tokens (automation) are stored in k8s secret and are revocable.
|
|
|
|
# gitea_op_item_title <username> — canonical 1Password item title
|
|
gitea_op_item_title() {
|
|
printf 'Gitea — %s@%s' "$1" "${GITEA_HOST}"
|
|
}
|
|
|
|
# gitea_ensure_password <username>
|
|
# Prints a strong password to stdout (never to stderr/log).
|
|
# If a 1Password item exists: retrieves it (idempotent re-runs safe).
|
|
# If not: creates a new item with a generated password.
|
|
# Returns 1 if op CLI is unavailable or unauthenticated.
|
|
gitea_ensure_password() {
|
|
local username="$1"
|
|
local title
|
|
title=$(gitea_op_item_title "$username")
|
|
|
|
if ! command -v op >/dev/null 2>&1; then
|
|
err "Gitea credential management requires the 1Password CLI (op)."
|
|
err "Install: brew install 1password-cli then: op signin"
|
|
return 1
|
|
fi
|
|
|
|
# Retrieve existing password (silent on error = item not found yet)
|
|
local pw
|
|
pw=$(op item get "$title" --vault="${GITEA_OP_VAULT}" --fields=password 2>/dev/null) || true
|
|
if [[ -n "$pw" ]]; then
|
|
log " Gitea: retrieved existing credential for '${username}' from 1Password (vault: ${GITEA_OP_VAULT})"
|
|
printf '%s' "$pw"
|
|
return 0
|
|
fi
|
|
|
|
# Generate and store a new credential in 1Password.
|
|
# --generate-password uses 1Password's generator — no plaintext secret
|
|
# ever appears in the process environment or shell history.
|
|
log " Gitea: generating credential for '${username}' → 1Password vault '${GITEA_OP_VAULT}'"
|
|
op item create \
|
|
--category=Login \
|
|
--title="$title" \
|
|
--vault="${GITEA_OP_VAULT}" \
|
|
--url="https://${GITEA_HOST}" \
|
|
--generate-password='letters,digits,32' \
|
|
"username=${username}" \
|
|
"notesPlain=Generated by init_knoe_users.sh. Primary auth is Kerberos SPNEGO — this password is for emergency recovery only." \
|
|
>/dev/null 2>&1 || { err " Gitea: failed to create 1Password item '${title}'"; return 1; }
|
|
|
|
pw=$(op item get "$title" --vault="${GITEA_OP_VAULT}" --fields=password 2>/dev/null) || true
|
|
if [[ -z "$pw" ]]; then
|
|
err " Gitea: 1Password item created but password could not be retrieved"
|
|
return 1
|
|
fi
|
|
log " Gitea: credential stored as '${title}' in vault '${GITEA_OP_VAULT}'"
|
|
printf '%s' "$pw"
|
|
}
|
|
|
|
# gitea_set_password <username> <password>
|
|
# Sets the Gitea user's password via kubectl exec (no prior Gitea auth needed).
|
|
gitea_set_password() {
|
|
local username="$1" pw="$2"
|
|
local pod
|
|
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)
|
|
[[ -n "$pod" ]] || { err " Gitea: no running pod in namespace ${GITEA_NAMESPACE}"; return 1; }
|
|
# gitea refuses to run as root; su to the git user (uid 1000).
|
|
# Capture stderr so we can surface it on failure without ever logging the password.
|
|
local _out
|
|
if ! _out=$(kubectl -n "$GITEA_NAMESPACE" exec "$pod" -c gitea -- \
|
|
su git -s /bin/bash -c \
|
|
"gitea admin user change-password --username '${username}' --password '${pw}'" \
|
|
2>&1); then
|
|
warn " Gitea: change-password failed for '${username}': ${_out}"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# gitea_api_token <username> <password> [token_name]
|
|
# Obtains a fresh Gitea API token via REST basic-auth.
|
|
# Deletes any existing token with the same name first (idempotency).
|
|
# Prints the token sha1 to stdout; never logs it.
|
|
gitea_api_token() {
|
|
local username="$1" pw="$2" token_name="${3:-knoe-installer}"
|
|
# Remove stale token if present (ignore errors — may not exist)
|
|
curl -s -o /dev/null \
|
|
-u "${username}:${pw}" \
|
|
-X DELETE \
|
|
"https://${GITEA_HOST}/api/v1/users/${username}/tokens/${token_name}" 2>/dev/null || true
|
|
local resp
|
|
resp=$(curl -sf \
|
|
-u "${username}:${pw}" \
|
|
-X POST \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"name\":\"${token_name}\"}" \
|
|
"https://${GITEA_HOST}/api/v1/users/${username}/tokens" 2>/dev/null) || true
|
|
# Extract sha1 from {"id":...,"name":...,"sha1":"<token>",...}
|
|
printf '%s' "$resp" | grep -o '"sha1":"[^"]*"' | cut -d'"' -f4
|
|
}
|
|
|
|
# gitea_helm_admin_token [token_name]
|
|
# Reads the Helm bootstrap admin credentials from the Gitea Deployment's
|
|
# configure-gitea init container environment variables (GITEA_ADMIN_USERNAME /
|
|
# GITEA_ADMIN_PASSWORD). The Helm chart stores these as plain values in the
|
|
# pod spec — there is no k8s Secret holding the admin password.
|
|
#
|
|
# This works even when the gitea admin CLI is broken (e.g. wrong DB hostname
|
|
# in app.ini) because the request goes through the running web server which
|
|
# has the correct GITEA__database__HOST env var.
|
|
# Prints the token sha1 to stdout; returns 1 on failure.
|
|
gitea_helm_admin_token() {
|
|
local token_name="${1:-knoe-installer-helm}"
|
|
local release="${GITEA_HELM_RELEASE:-gitea}"
|
|
|
|
# Extract credentials from the configure-gitea init container env block.
|
|
# The Helm chart renders them as plain values (not secretKeyRefs).
|
|
local admin_user admin_pw
|
|
admin_user=$(kubectl -n "$GITEA_NAMESPACE" get deployment "$release" \
|
|
-o jsonpath='{.spec.template.spec.initContainers[?(@.name=="configure-gitea")].env[?(@.name=="GITEA_ADMIN_USERNAME")].value}' \
|
|
2>/dev/null || true)
|
|
admin_pw=$(kubectl -n "$GITEA_NAMESPACE" get deployment "$release" \
|
|
-o jsonpath='{.spec.template.spec.initContainers[?(@.name=="configure-gitea")].env[?(@.name=="GITEA_ADMIN_PASSWORD")].value}' \
|
|
2>/dev/null || true)
|
|
|
|
# Fall back to env-var override (useful for testing / manual bootstrap)
|
|
admin_user="${GITEA_HELM_ADMIN_USER:-${admin_user}}"
|
|
|
|
if [[ -z "$admin_user" || -z "$admin_pw" ]]; then
|
|
warn " Gitea: could not read Helm admin credentials from Deployment '${release}' (namespace ${GITEA_NAMESPACE})"
|
|
return 1
|
|
fi
|
|
|
|
local tok
|
|
tok=$(gitea_api_token "$admin_user" "$admin_pw" "$token_name")
|
|
if [[ -z "$tok" ]]; then
|
|
warn " Gitea: could not obtain API token for Helm admin '${admin_user}'"
|
|
return 1
|
|
fi
|
|
log " Gitea: obtained bootstrap token from Helm admin '${admin_user}'"
|
|
printf '%s' "$tok"
|
|
}
|
|
|
|
# gitea_api_set_password <admin_token> <username> <new_password>
|
|
# Changes a user's password via the Gitea admin REST API.
|
|
# Uses the running web server (not the CLI), so it works even when app.ini
|
|
# has a wrong database hostname (the server overrides via GITEA__ env vars).
|
|
gitea_api_set_password() {
|
|
local token="$1" username="$2" pw="$3"
|
|
local http_code
|
|
http_code=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
-X PATCH \
|
|
-H "Authorization: token ${token}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"login_name\":\"${username}\",\"source_id\":0,\"password\":\"${pw}\"}" \
|
|
"https://${GITEA_HOST}/api/v1/admin/users/${username}" 2>/dev/null) || true
|
|
if [[ "$http_code" =~ ^2 ]]; then
|
|
return 0
|
|
else
|
|
warn " Gitea: admin API set-password returned HTTP ${http_code} for '${username}'"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
promote_gitea_admin() {
|
|
local token="$GITEA_ADMIN_TOKEN"
|
|
|
|
# 1. Try GITEA_ADMIN_TOKEN from env or persisted k8s secret
|
|
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
|
|
|
|
# 2. Bootstrap via the Helm admin account (k8s secret → REST API token).
|
|
# This path works even when the gitea admin CLI is broken (app.ini has wrong
|
|
# DB hostname) because REST calls go through the running server which has the
|
|
# correct GITEA__database__HOST env var. Once we have a bootstrap token, use
|
|
# it to also set KNOE_ADMIN_PRINCIPAL's password via API (from 1Password).
|
|
local _helm_token=""
|
|
if [[ -z "$token" ]]; then
|
|
if _helm_token=$(gitea_helm_admin_token "knoe-installer-helm"); then
|
|
token="$_helm_token"
|
|
# Opportunistically set KNOE_ADMIN_PRINCIPAL's 1Password-backed password
|
|
# via REST so chrisfu can log in as fallback if SPNEGO is unavailable.
|
|
if command -v op >/dev/null 2>&1; then
|
|
local _pw=""
|
|
if _pw=$(gitea_ensure_password "${KNOE_ADMIN_PRINCIPAL}"); then
|
|
log "Gitea: setting password for '${KNOE_ADMIN_PRINCIPAL}' via admin REST API ..."
|
|
gitea_api_set_password "$_helm_token" "${KNOE_ADMIN_PRINCIPAL}" "$_pw" \
|
|
&& log "Gitea: password set for '${KNOE_ADMIN_PRINCIPAL}'" \
|
|
|| true
|
|
_pw=""
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# 3. Try 1Password → set password via CLI → exchange for token.
|
|
# Kept as fallback for when the Helm admin secret is absent (e.g. non-Helm deploy).
|
|
if [[ -z "$token" ]]; then
|
|
local _pw=""
|
|
if _pw=$(gitea_ensure_password "${KNOE_ADMIN_PRINCIPAL}"); then
|
|
log "Gitea: setting password for '${KNOE_ADMIN_PRINCIPAL}' via kubectl exec ..."
|
|
if gitea_set_password "${KNOE_ADMIN_PRINCIPAL}" "$_pw"; then
|
|
token=$(gitea_api_token "${KNOE_ADMIN_PRINCIPAL}" "$_pw" "knoe-installer")
|
|
[[ -n "$token" ]] && log "Gitea: API token obtained for '${KNOE_ADMIN_PRINCIPAL}'"
|
|
else
|
|
warn "Gitea: CLI password change failed (DB host mismatch in app.ini?)"
|
|
fi
|
|
_pw=""
|
|
fi
|
|
fi
|
|
|
|
# 4. Last resort: kubectl exec generate-access-token (CLI path, may fail if DB broken).
|
|
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: attempting token generation via kubectl exec (last resort) ..."
|
|
local _u
|
|
for _u in "gitea_admin" "${KNOE_ADMIN_PRINCIPAL}"; do
|
|
token=$(kubectl -n "$GITEA_NAMESPACE" exec "$_gitea_pod" -c gitea -- \
|
|
su git -s /bin/bash -c \
|
|
"gitea admin user generate-access-token --username ${_u} --token-name knoe-installer --scopes write:admin,read:user --raw 2>/dev/null" \
|
|
2>/dev/null | tail -1 || true)
|
|
[[ -n "$token" ]] && { log "Gitea: token generated for user '${_u}'"; break; }
|
|
done
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "$token" ]]; then
|
|
warn "No Gitea admin token available — skipping admin promotion"
|
|
warn " Ensure 'op signin' is active and the Helm admin secret exists, then re-run."
|
|
warn " Or set GITEA_ADMIN_TOKEN and re-run."
|
|
return 0
|
|
fi
|
|
|
|
# Persist token in k8s secret for automation reuse (tokens are revocable)
|
|
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
|
|
|
|
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 — SPNEGO auto-registration not yet triggered"
|
|
warn " Visit https://${GITEA_HOST} with a valid Kerberos ticket, then re-run: $0 initialize"
|
|
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 Secrets ==="
|
|
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
|
|
if kubectl -n "$GITEA_NAMESPACE" get secret gitea-krb5-keytab >/dev/null 2>&1; then
|
|
log "gitea-krb5-keytab exists in namespace ${GITEA_NAMESPACE} (HTTP/${GITEA_SPNEGO_HOST}@${GITEA_KRB5_AD_REALM})"
|
|
else
|
|
warn "gitea-krb5-keytab NOT FOUND in namespace ${GITEA_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
|