From 40ea30e4c3e0fc6450c0bf1dff8b5a2ad29da36b Mon Sep 17 00:00:00 2001 From: chrisfu Date: Mon, 27 Apr 2026 14:08:18 -0700 Subject: [PATCH] feat(auth): init scripts and k3s/k8s auth manifests for knoe-auth init_knoe_auth.sh: provisions KDC secrets via 1Password, applies GKE manifests init_knoe_users.sh: creates Kerberos principals for initial contributors kerberos-configmap.yaml: krb5.conf for OpenBao Kerberos auth (KNOE.DEV realm) prole-auth-deployment.yaml: k3s auth + kdc sidecar deployment for homelab prole-kdc-configmap.yaml: k3s KDC config for homelab Co-Authored-By: Claude Sonnet 4.6 --- .../manifests/knoe/prole-auth-deployment.yaml | 4 +- .../manifests/knoe/prole-kdc-configmap.yaml | 4 +- etc/init_knoe_auth.sh | 1195 +++++------------ etc/init_knoe_users.sh | 14 +- k8s/openbao/kerberos-configmap.yaml | 4 +- 5 files changed, 356 insertions(+), 865 deletions(-) diff --git a/deploy/opentofu/k3s/manifests/knoe/prole-auth-deployment.yaml b/deploy/opentofu/k3s/manifests/knoe/prole-auth-deployment.yaml index f05e7a9..787183e 100644 --- a/deploy/opentofu/k3s/manifests/knoe/prole-auth-deployment.yaml +++ b/deploy/opentofu/k3s/manifests/knoe/prole-auth-deployment.yaml @@ -16,7 +16,7 @@ spec: spec: initContainers: - name: keytab-bootstrap - image: myrddin.knoe.org:5000/knoe-authority:latest + image: myrddin.prole.org:5000/knoe-authority:latest imagePullPolicy: IfNotPresent command: - /bin/bash @@ -202,7 +202,7 @@ spec: subPath: krb5.conf readOnly: true - name: kdc - image: myrddin.knoe.org:5000/knoe-authority:latest + image: myrddin.prole.org:5000/knoe-authority:latest imagePullPolicy: IfNotPresent command: ["/bin/bash", "/opt/knoe-kdc/entrypoint.sh"] env: diff --git a/deploy/opentofu/k3s/manifests/knoe/prole-kdc-configmap.yaml b/deploy/opentofu/k3s/manifests/knoe/prole-kdc-configmap.yaml index 32a7d24..ff83fc9 100644 --- a/deploy/opentofu/k3s/manifests/knoe/prole-kdc-configmap.yaml +++ b/deploy/opentofu/k3s/manifests/knoe/prole-kdc-configmap.yaml @@ -15,8 +15,8 @@ data: admin_server = 127.0.0.1 } PROLE.ORG = { - kdc = myrddin.knoe.org - admin_server = myrddin.knoe.org + kdc = myrddin.prole.org + admin_server = myrddin.prole.org } [capaths] diff --git a/etc/init_knoe_auth.sh b/etc/init_knoe_auth.sh index 632a8dc..d257ef8 100755 --- a/etc/init_knoe_auth.sh +++ b/etc/init_knoe_auth.sh @@ -1,885 +1,376 @@ #!/usr/bin/env bash +# init_knoe_auth.sh +# Provision knoe-auth (Kerberos KDC + Spring Boot enrollment service) on GKE. +# +# Usage: +# ./etc/init_knoe_auth.sh [--context KUBECONTEXT] [--namespace NAMESPACE] [--project PROJECT_ID] +# ./etc/init_knoe_auth.sh initialize # full setup +# ./etc/init_knoe_auth.sh schema # schema only (idempotent) +# ./etc/init_knoe_auth.sh invite EMAIL # create first admin invite +# ./etc/init_knoe_auth.sh status # check pod + principal state +# +# Prerequisites: +# kubectl, op (1Password CLI), psql (or kubectl exec fallback) +# +# Env vars (override args): +# APP_CLUSTER_KUBECONTEXT, KNOE_NAMESPACE, GCP_PROJECT_ID, +# KNOE_DB_HOST, KNOE_DB_PORT, KNOE_DB_NAME, KNOE_DB_SUPERUSER set -euo pipefail -# init_knoe_auth.sh -# Purpose: -# - Provision the knoe-auth service (cluster-internal KDC + SSO gateway) in the -# service namespace (knoe-system by default) -# - knoe-auth acts as the authentication server trusted by CNPG and other cluster -# services, and hosts the SSO gateway pod -# - Optionally configure cross-realm trust to an external realm when credentials are provided -# - Must complete before init_cloudnative_pg.sh so the database can depend on it -# -# Usage: -# ./init_knoe_auth.sh initialize|update # create/update knoe-auth resources -# ./init_knoe_auth.sh status # show knoe-auth status -# ./init_knoe_auth.sh cleanup # remove knoe-auth resources +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +GKE_DIR="$REPO_ROOT/deploy/gcp/gke" -SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +# ── Defaults ──────────────────────────────────────────────────────────────── +APP_CTX="${APP_CLUSTER_KUBECONTEXT:-gke_plenary-truck-485623-p7_us-west3_knoe-dev-0}" +NAMESPACE="${KNOE_NAMESPACE:-knoe-system}" +GCP_PROJECT="${GCP_PROJECT_ID:-plenary-truck-485623-p7}" +DB_HOST="${KNOE_DB_HOST:-}" # resolved from CNPG svc if blank +DB_PORT="${KNOE_DB_PORT:-5432}" +DB_NAME="${KNOE_DB_NAME:-knoe}" +DB_SUPERUSER="${KNOE_DB_SUPERUSER:-postgres}" +REALM="KNOE.DEV" +AUTH_HOST="${KNOE_AUTH_HOST:-https://auth.knoe.dev}" -# shellcheck disable=SC1090 -source "$SCRIPT_DIR/knoe_cfg.sh" +log() { echo "[init_knoe_auth] $*"; } +info() { log "INFO $*"; } +warn() { log "WARN $*" >&2; } +die() { log "ERROR $*" >&2; exit 1; } -if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then - knoe_set_mode "${2:-}" - shift 2 -elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then - knoe_set_mode "${1#*=}" - shift -fi +kube() { kubectl --context="$APP_CTX" "$@"; } -knoe_ensure_kubeconfig >/dev/null 2>&1 || true -ensure_kube_context || exit 1 +# ── Argument parsing ───────────────────────────────────────────────────────── +COMMAND="${1:-initialize}" +shift || true -ACTION=${1:-initialize} - -KNOE_AUTH_NAMESPACE=${KNOE_AUTH_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-default}}} -KDC_NAMESPACE="$KNOE_AUTH_NAMESPACE" -KNOE_AUTH_ENABLED=${KNOE_AUTH_ENABLED:-${PROLE_KDC_ENABLED:-1}} -KNOE_AUTH_NAME=${KNOE_AUTH_NAME:-knoe-auth} -KNOE_AUTH_SERVICE=${KNOE_AUTH_SERVICE:-knoe-auth} -KNOE_AUTH_IMAGE=${KNOE_AUTH_IMAGE:-${PROLE_KDC_IMAGE:-}} -# If the user provided an explicit image, we should not require Docker unless -# they explicitly opt into building. -KNOE_AUTH_IMAGE_EXPLICIT=0 -if [[ -n "${KNOE_AUTH_IMAGE:-}" ]]; then - KNOE_AUTH_IMAGE_EXPLICIT=1 -fi -KNOE_AUTH_IMAGE_NAME=${KNOE_AUTH_IMAGE_NAME:-${PROLE_KDC_IMAGE_NAME:-knoe-authority}} -KNOE_AUTH_IMAGE_TAG=${KNOE_AUTH_IMAGE_TAG:-${PROLE_KDC_IMAGE_TAG:-latest}} -KNOE_AUTH_REGISTRY_HOST=${KNOE_AUTH_REGISTRY_HOST:-${PROLE_KDC_REGISTRY_HOST:-${LOCAL_REGISTRY:-localhost:5000}}} -KNOE_AUTH_REGISTRY_INTERNAL=${KNOE_AUTH_REGISTRY_INTERNAL:-${PROLE_KDC_REGISTRY_INTERNAL:-${LOCAL_REGISTRY_INTERNAL:-}}} -KNOE_AUTH_BUILD_IMAGE_SET=0 -if [[ -n "${KNOE_AUTH_BUILD_IMAGE+x}" ]]; then - KNOE_AUTH_BUILD_IMAGE_SET=1 -elif [[ -n "${PROLE_KDC_BUILD_IMAGE+x}" ]]; then - KNOE_AUTH_BUILD_IMAGE_SET=1 -fi -KNOE_AUTH_BUILD_IMAGE=${KNOE_AUTH_BUILD_IMAGE:-${PROLE_KDC_BUILD_IMAGE:-1}} -KNOE_AUTH_ROLLOUT_TIMEOUT=${KNOE_AUTH_ROLLOUT_TIMEOUT:-${PROLE_KDC_ROLLOUT_TIMEOUT:-5}} -KNOE_AUTH_DEPLOY_TIMEOUT=${KNOE_AUTH_DEPLOY_TIMEOUT:-${PROLE_KDC_DEPLOY_TIMEOUT:-60}} -KNOE_AUTH_HOST_NETWORK=${KNOE_AUTH_HOST_NETWORK:-${PROLE_KDC_HOST_NETWORK:-}} -PROLE_KDC_REALM=${PROLE_KDC_REALM:-} -PROLE_KDC_DOMAIN=${PROLE_KDC_DOMAIN:-} -PROLE_KDC_ADMIN_PRINCIPAL=${PROLE_KDC_ADMIN_PRINCIPAL:-admin/admin} -PROLE_KDC_ADMIN_PASSWORD=${PROLE_KDC_ADMIN_PASSWORD:-} -PROLE_KDC_MASTER_PASSWORD=${PROLE_KDC_MASTER_PASSWORD:-} -PROLE_KDC_TRUST_REALM=${PROLE_KDC_TRUST_REALM:-${KRB5_REALM:-}} -PROLE_KDC_TRUST_ADMIN=${PROLE_KDC_TRUST_ADMIN:-${KRB5_USER:-}} -PROLE_KDC_TRUST_PASSWORD=${PROLE_KDC_TRUST_PASSWORD:-${KRB5_PASSWORD:-}} -PROLE_KDC_TRUST_SHARED_PASSWORD=${PROLE_KDC_TRUST_SHARED_PASSWORD:-} -KRB5_KDC=${KRB5_KDC:-} -KRB5_ADMIN=${KRB5_ADMIN:-} - -local_registry_enabled() { - local raw="${PROLE_ENABLE_LOCAL_REGISTRY:-${ENABLE_LOCAL_REGISTRY:-}}" - if [[ -n "$raw" ]]; then - case "$raw" in - 1|true|TRUE|True|yes|YES|Yes|on|ON|On) return 0 ;; - esac - return 1 - fi - - local mode="" - if command -v knoe_normalize_mode >/dev/null 2>&1; then - mode=$(knoe_normalize_mode "${KNOE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-}}}") - else - mode="${KNOE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-}}}" - fi - [[ "$mode" == "k3d" || "$mode" == "k3s" ]] -} - -if ! local_registry_enabled; then - # Only clear default local-registry settings; preserve explicit registry host values. - _default_host="${LOCAL_REGISTRY:-localhost:5000}" - if [[ -z "${KNOE_AUTH_REGISTRY_HOST:-}" || "${KNOE_AUTH_REGISTRY_HOST:-}" == "$_default_host" ]]; then - KNOE_AUTH_REGISTRY_HOST="" - fi - KNOE_AUTH_REGISTRY_INTERNAL="" - unset _default_host -fi - -log() { printf '%s\n' "$*"; } -err() { printf '%s\n' "$*" >&2; } - -ensure_tools() { - for t in kubectl; do - command -v "$t" >/dev/null || { err "Missing required tool: $t"; exit 1; } - done -} - -ensure_docker() { - command -v docker >/dev/null || { err "Missing required tool: docker"; exit 1; } - if ! docker info >/dev/null 2>&1; then - err "Docker is not running or not accessible." - err "This is required to build/push the knoe-auth image." - err "Start Docker (e.g. Docker Desktop), or set KNOE_AUTH_IMAGE to a prebuilt image and/or set KNOE_AUTH_BUILD_IMAGE=0." - exit 1 - fi -} - -docker_available() { - command -v docker >/dev/null 2>&1 || return 1 - docker info >/dev/null 2>&1 -} - -resolve_knoe_auth_image() { - # Ensure we have a deterministic image reference even when skipping local builds. - if [[ -n "${KNOE_AUTH_IMAGE:-}" ]]; then - return 0 - fi - if [[ -n "${KNOE_AUTH_REGISTRY_INTERNAL:-}" ]]; then - KNOE_AUTH_IMAGE="${KNOE_AUTH_REGISTRY_INTERNAL}/${KNOE_AUTH_IMAGE_NAME}:${KNOE_AUTH_IMAGE_TAG}" - return 0 - fi - if [[ -n "${KNOE_AUTH_REGISTRY_HOST:-}" ]]; then - KNOE_AUTH_IMAGE="${KNOE_AUTH_REGISTRY_HOST}/${KNOE_AUTH_IMAGE_NAME}:${KNOE_AUTH_IMAGE_TAG}" - return 0 - fi - KNOE_AUTH_IMAGE="${KNOE_AUTH_IMAGE_NAME}:${KNOE_AUTH_IMAGE_TAG}" -} - -ensure_namespace() { - if ! kubectl get namespace "$KNOE_AUTH_NAMESPACE" >/dev/null 2>&1; then - log "Creating namespace '$KNOE_AUTH_NAMESPACE' ..." - kubectl create namespace "$KNOE_AUTH_NAMESPACE" >/dev/null 2>&1 || true - fi -} - -normalized_mode() { - if command -v knoe_normalize_mode >/dev/null 2>&1; then - knoe_normalize_mode "${KNOE_MODE:-}" - return 0 - fi - printf '%s' "${KNOE_MODE:-}" -} - -is_k3d_mode() { - [[ "$(normalized_mode)" == "k3d" ]] -} - -get_kdc_pods_with_containers() { - kubectl -n "$KNOE_AUTH_NAMESPACE" get pods -l "app=${KNOE_AUTH_NAME}" \ - -o jsonpath='{range .items[*]}{.metadata.name}{"|"}{range .spec.containers[*]}{.name}{" "}{end}{"\n"}{end}' 2>/dev/null || true -} - -kdc_pods_missing_container() { - local rows line containers found c - rows=$(get_kdc_pods_with_containers) - [[ -z "$rows" ]] && return 1 - while IFS= read -r line; do - [[ -z "$line" ]] && continue - containers="${line#*|}" - found=0 - for c in $containers; do - if [[ "$c" == "kdc" ]]; then - found=1 - break - fi - done - if [[ "$found" -eq 0 ]]; then - return 0 - fi - done <<< "$rows" - return 1 -} - -kdc_multiple_active_replicasets() { - local rows line spec status count - rows=$(kubectl -n "$KNOE_AUTH_NAMESPACE" get rs -l "app=${KNOE_AUTH_NAME}" \ - -o jsonpath='{range .items[*]}{.metadata.name}{"|"}{.spec.replicas}{"|"}{.status.replicas}{"\n"}{end}' 2>/dev/null || true) - [[ -z "$rows" ]] && return 1 - count=0 - while IFS= read -r line; do - [[ -z "$line" ]] && continue - spec=$(printf '%s' "$line" | awk -F'|' '{print $2}') - status=$(printf '%s' "$line" | awk -F'|' '{print $3}') - spec=${spec:-0} - status=${status:-0} - if [[ "$spec" -gt 0 || "$status" -gt 0 ]]; then - count=$((count+1)) - fi - done <<< "$rows" - (( count > 1 )) -} - -kdc_deployment_not_ready() { - local desired ready updated - desired=$(kubectl -n "$KNOE_AUTH_NAMESPACE" get deploy "$KNOE_AUTH_NAME" -o jsonpath='{.spec.replicas}' 2>/dev/null || true) - ready=$(kubectl -n "$KNOE_AUTH_NAMESPACE" get deploy "$KNOE_AUTH_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || true) - updated=$(kubectl -n "$KNOE_AUTH_NAMESPACE" get deploy "$KNOE_AUTH_NAME" -o jsonpath='{.status.updatedReplicas}' 2>/dev/null || true) - desired=${desired:-0} - ready=${ready:-0} - updated=${updated:-0} - if [[ "$ready" -lt "$desired" || "$updated" -lt "$desired" ]]; then - return 0 - fi - return 1 -} - -kdc_stale_deployment() { - if ! kdc_deployment_not_ready; then - return 1 - fi - kdc_pods_missing_container && return 0 - kdc_multiple_active_replicasets && return 0 - return 1 -} - -maybe_cleanup_stale_kdc() { - local reason="${1:-stale}" - local policy="${2:-broad}" - if ! is_k3d_mode; then - return 1 - fi - if ! kubectl -n "$KNOE_AUTH_NAMESPACE" get deploy "$KNOE_AUTH_NAME" >/dev/null 2>&1; then - return 1 - fi - if [[ "$policy" == "strict" ]]; then - if ! kdc_deployment_not_ready; then - return 1 - fi - if ! kdc_pods_missing_container; then - return 1 - fi - elif ! kdc_stale_deployment; then - return 1 - fi - err "WARN: Detected stale knoe-auth KDC deployment (${reason}). Cleaning up..." - cleanup_knoe_auth - return 0 -} - -lowercase() { - printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]' -} - -registry_host_from_url() { - local value="${1:-}" - value="${value#http://}" - value="${value#https://}" - value="${value%%/*}" - value="${value%%:*}" - printf '%s' "$value" -} - -gen_password() { - if command -v openssl >/dev/null 2>&1; then - openssl rand -base64 18 - return 0 - fi - if command -v python3 >/dev/null 2>&1; then - python3 - <<'PY' -import secrets, string -alphabet = string.ascii_letters + string.digits -print(''.join(secrets.choice(alphabet) for _ in range(24))) -PY - return 0 - fi - date +%s -} - -b64_decode() { - if base64 --decode /dev/null 2>&1; then - base64 --decode - return 0 - fi - if base64 -d /dev/null 2>&1; then - base64 -d - return 0 - fi - base64 -D -} - -get_secret_value() { - local secret="$1" - local key="$2" - kubectl -n "$KNOE_AUTH_NAMESPACE" get secret "$secret" -o "jsonpath={.data.${key}}" 2>/dev/null | b64_decode 2>/dev/null || true -} - -resolve_knoe_auth_defaults() { - if [[ -z "$PROLE_KDC_REALM" ]]; then - PROLE_KDC_REALM="PROLE.LOCAL" - fi - if [[ -z "$PROLE_KDC_DOMAIN" ]]; then - PROLE_KDC_DOMAIN=$(lowercase "$PROLE_KDC_REALM") - fi - local mode="" - if command -v knoe_normalize_mode >/dev/null 2>&1; then - mode=$(knoe_normalize_mode "${KNOE_MODE:-}") - else - mode="${KNOE_MODE:-}" - fi - local k3s_host="" - if local_registry_enabled; then - if [[ "$mode" == "k3s" ]]; then - k3s_host=$(registry_host_from_url "${PROLE_K3S_SERVER:-${K3S_SERVER_URL:-}}") - if [[ -n "$k3s_host" ]] && ([[ -z "${KNOE_AUTH_REGISTRY_HOST:-}" ]] || [[ "$KNOE_AUTH_REGISTRY_HOST" == "localhost:5000" ]] || [[ "$KNOE_AUTH_REGISTRY_HOST" == *"k3d"* ]]); then - KNOE_AUTH_REGISTRY_HOST="${k3s_host}:5000" - fi - fi - if [[ -z "$KNOE_AUTH_REGISTRY_INTERNAL" ]]; then - local reg_ns="${REGISTRY_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-default}}}" - KNOE_AUTH_REGISTRY_INTERNAL="registry.${reg_ns}.svc.cluster.local:5000" - unset reg_ns - fi - else - KNOE_AUTH_REGISTRY_INTERNAL="" - fi - if [[ -z "$KNOE_AUTH_IMAGE" ]]; then - if [[ -n "$KNOE_AUTH_REGISTRY_INTERNAL" ]]; then - KNOE_AUTH_IMAGE="${KNOE_AUTH_REGISTRY_INTERNAL}/${KNOE_AUTH_IMAGE_NAME}:${KNOE_AUTH_IMAGE_TAG}" - else - KNOE_AUTH_IMAGE="${KNOE_AUTH_IMAGE_NAME}:${KNOE_AUTH_IMAGE_TAG}" - fi - fi -} - -resolve_kdc_docker_dir() { - if [[ -n "${PROLE_KDC_DOCKER_DIR:-}" && -d "$PROLE_KDC_DOCKER_DIR" ]]; then - echo "$PROLE_KDC_DOCKER_DIR" - return 0 - fi - if [[ -n "${KNOE_HOME:-}" && -d "$KNOE_HOME/knoe/authority" ]]; then - echo "$KNOE_HOME/knoe/authority" - return 0 - fi - if [[ -n "${KNOE_HOME:-}" && -d "$KNOE_HOME/authority" ]]; then - echo "$KNOE_HOME/authority" - return 0 - fi - if [[ -d "$SCRIPT_DIR/../knoe/authority" ]]; then - echo "$SCRIPT_DIR/../knoe/authority" - return 0 - fi - if [[ -d "$SCRIPT_DIR/../authority" ]]; then - echo "$SCRIPT_DIR/../authority" - return 0 - fi - return 1 -} - -is_truthy() { - case "${1:-}" in - 1|true|TRUE|True|yes|YES|Yes|on|ON|On) return 0 ;; +while [[ $# -gt 0 ]]; do + case "$1" in + --context) APP_CTX="$2"; shift 2 ;; + --namespace) NAMESPACE="$2"; shift 2 ;; + --project) GCP_PROJECT="$2"; shift 2 ;; + --db-host) DB_HOST="$2"; shift 2 ;; + *) break ;; esac - return 1 +done + +# ── Helpers ────────────────────────────────────────────────────────────────── + +require_tool() { + command -v "$1" >/dev/null 2>&1 || die "Required tool not found: $1 — install it and retry." } -build_knoe_auth_image() { - # If the user explicitly sets KNOE_AUTH_BUILD_IMAGE, honor it. - # Otherwise, skip building when an explicit image was provided. - if [[ "$KNOE_AUTH_BUILD_IMAGE_SET" == "1" ]]; then - if ! is_truthy "${KNOE_AUTH_BUILD_IMAGE:-0}"; then - resolve_knoe_auth_image - return 0 - fi +wait_for_pods() { + local label="$1" + local timeout="${2:-180}" + info "Waiting up to ${timeout}s for pods with label ${label} in ${NAMESPACE}..." + kube -n "$NAMESPACE" wait pod \ + -l "$label" \ + --for=condition=Ready \ + --timeout="${timeout}s" +} + +op_secret() { + # Retrieve a 1Password secret; fall back to prompting if op isn't authed. + local item="$1" field="${2:-password}" + if command -v op >/dev/null 2>&1; then + op item get "$item" --fields "$field" 2>/dev/null || { + warn "1Password: could not read $item/$field — prompting." + read -rsp "Enter value for $item/$field: " val; echo + printf '%s' "$val" + } else - if [[ "$KNOE_AUTH_IMAGE_EXPLICIT" == "1" ]]; then - log "Using preconfigured KNOE_AUTH_IMAGE='${KNOE_AUTH_IMAGE}'; skipping local image build." - return 0 - fi + read -rsp "Enter value for $item/$field: " val; echo + printf '%s' "$val" fi +} - if ! docker_available; then - # If the user forced a build, we must fail with a clear error. - if [[ "$KNOE_AUTH_BUILD_IMAGE_SET" == "1" ]]; then - ensure_docker - fi +resolve_db_host() { + if [[ -n "$DB_HOST" ]]; then return; fi + # Try to resolve CNPG primary service from the DB cluster context + DB_CTX="${DB_CLUSTER_KUBECONTEXT:-gke_plenary-truck-485623-p7_us-west3_knoe-cnpg-0}" + DB_HOST=$(kubectl --context="$DB_CTX" -n knoe-db-0 \ + get svc knoe-db-rw -o jsonpath='{.spec.clusterIP}' 2>/dev/null || echo "") + [[ -z "$DB_HOST" ]] && die "Cannot resolve KNOE DB host. Set KNOE_DB_HOST or ensure knoe-db-rw svc exists." + info "Resolved DB host: $DB_HOST" +} - log "[WARN] Docker is not available; skipping knoe-auth image build." - log " Set KNOE_AUTH_BUILD_IMAGE=1 to force building (requires Docker), or set KNOE_AUTH_IMAGE to a prebuilt image." - resolve_knoe_auth_image - return 0 - fi - - local docker_dir - docker_dir=$(resolve_kdc_docker_dir || true) - if [[ -z "$docker_dir" ]]; then - err "ERROR: knoe/authority Docker context not found." - exit 1 - fi - - ensure_docker - - local local_tag="${KNOE_AUTH_IMAGE_NAME}:${KNOE_AUTH_IMAGE_TAG}" - log "Building authority image: ${local_tag} (context: ${docker_dir}) ..." - docker build -t "$local_tag" "$docker_dir" - - if [[ -n "$KNOE_AUTH_REGISTRY_HOST" ]]; then - local remote_tag="${KNOE_AUTH_REGISTRY_HOST}/${KNOE_AUTH_IMAGE_NAME}:${KNOE_AUTH_IMAGE_TAG}" - log "Tagging authority image for registry: ${remote_tag}" - docker tag "$local_tag" "$remote_tag" - log "Pushing authority image to registry: ${remote_tag}" - if ! docker push "$remote_tag"; then - if command -v skopeo >/dev/null 2>&1; then - log "Docker push failed; retrying with skopeo (insecure registry) ..." - skopeo copy --dest-tls-verify=false "docker-daemon:${local_tag}" "docker://${remote_tag}" - else - err "ERROR: docker push failed and skopeo is not available." - exit 1 - fi - fi - fi - - if [[ -z "$KNOE_AUTH_REGISTRY_HOST" && "$(normalized_mode)" == "k3d" ]]; then - if command -v k3d >/dev/null 2>&1; then - local cluster_name="${K3D_CLUSTER_NAME:-knoe-dev-cluster}" - log "Importing authority image into k3d cluster: ${cluster_name}" - k3d image import "$local_tag" -c "$cluster_name" >/dev/null 2>&1 || true - fi - fi - - if [[ -z "$KNOE_AUTH_REGISTRY_HOST" && "$(normalized_mode)" == "k3s" ]]; then - err "ERROR: No registry host configured for k3s; cannot publish authority image." - err " Set PROLE_K3S_SERVER (so the script can infer :5000) or set KNOE_AUTH_REGISTRY_HOST explicitly." - exit 1 - fi - - if [[ -n "$KNOE_AUTH_REGISTRY_INTERNAL" ]]; then - KNOE_AUTH_IMAGE="${KNOE_AUTH_REGISTRY_INTERNAL}/${KNOE_AUTH_IMAGE_NAME}:${KNOE_AUTH_IMAGE_TAG}" - elif [[ -n "$KNOE_AUTH_REGISTRY_HOST" ]]; then - KNOE_AUTH_IMAGE="${KNOE_AUTH_REGISTRY_HOST}/${KNOE_AUTH_IMAGE_NAME}:${KNOE_AUTH_IMAGE_TAG}" +psql_file() { + local file="$1" + if command -v psql >/dev/null 2>&1 && [[ -n "${PGPASSWORD:-}" ]]; then + psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_SUPERUSER" -d "$DB_NAME" -f "$file" else - KNOE_AUTH_IMAGE="${local_tag}" + # Fallback: exec into CNPG primary pod + DB_CTX="${DB_CLUSTER_KUBECONTEXT:-gke_plenary-truck-485623-p7_us-west3_knoe-cnpg-0}" + local pod + pod=$(kubectl --context="$DB_CTX" -n knoe-db-0 \ + get pod -l cnpg.io/cluster=knoe-db,role=primary \ + -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) + [[ -z "$pod" ]] && die "Cannot find CNPG primary pod. Set PGPASSWORD and KNOE_DB_HOST for direct psql." + kubectl --context="$DB_CTX" -n knoe-db-0 cp "$file" "${pod}:/tmp/knoe_auth_schema.sql" + kubectl --context="$DB_CTX" -n knoe-db-0 exec "$pod" -- \ + psql -U "$DB_SUPERUSER" -d "$DB_NAME" -f /tmp/knoe_auth_schema.sql fi } -ensure_knoe_auth_secrets() { - local secret_name="knoe-auth-secrets" - if [[ -z "$PROLE_KDC_ADMIN_PASSWORD" ]]; then - PROLE_KDC_ADMIN_PASSWORD=$(get_secret_value "$secret_name" "admin_password") - fi - if [[ -z "$PROLE_KDC_MASTER_PASSWORD" ]]; then - PROLE_KDC_MASTER_PASSWORD=$(get_secret_value "$secret_name" "master_password") - fi - if [[ -z "$PROLE_KDC_TRUST_SHARED_PASSWORD" ]]; then - PROLE_KDC_TRUST_SHARED_PASSWORD=$(get_secret_value "$secret_name" "trust_shared_password") - fi - if [[ -z "$PROLE_KDC_TRUST_PASSWORD" ]]; then - PROLE_KDC_TRUST_PASSWORD=$(get_secret_value "$secret_name" "trust_password") - fi +# ── Schema ─────────────────────────────────────────────────────────────────── - if [[ -z "$PROLE_KDC_ADMIN_PASSWORD" ]]; then - PROLE_KDC_ADMIN_PASSWORD=$(gen_password) - fi - if [[ -z "$PROLE_KDC_MASTER_PASSWORD" ]]; then - PROLE_KDC_MASTER_PASSWORD=$(gen_password) - fi - if [[ -z "$PROLE_KDC_TRUST_SHARED_PASSWORD" ]]; then - PROLE_KDC_TRUST_SHARED_PASSWORD="$PROLE_KDC_MASTER_PASSWORD" - fi +run_schema() { + info "Applying knoe-auth schema additions..." + resolve_db_host - kubectl -n "$KNOE_AUTH_NAMESPACE" create secret generic "$secret_name" \ - --from-literal=admin_password="$PROLE_KDC_ADMIN_PASSWORD" \ - --from-literal=master_password="$PROLE_KDC_MASTER_PASSWORD" \ - --from-literal=trust_shared_password="$PROLE_KDC_TRUST_SHARED_PASSWORD" \ - --from-literal=trust_password="$PROLE_KDC_TRUST_PASSWORD" \ - --dry-run=client -o yaml | kubectl apply -f - >/dev/null + local tmpfile + tmpfile=$(mktemp /tmp/knoe_auth_schema_XXXX.sql) + + cat > "$tmpfile" <<'ENDSQL' +-- ── knoe-auth Round 1 schema additions ─────────────────────────────────────── +-- Idempotent: all CREATE TABLE ... IF NOT EXISTS + +-- Invite tokens (admin creates, single-use) +-- contact is the email/phone the invite was sent to — the OTP trust anchor. +-- knoe.dev starts with ZERO pre-knowledge of the developer's home org. +CREATE TABLE IF NOT EXISTS knoe.invitation ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + token TEXT NOT NULL UNIQUE, + contact TEXT NOT NULL, + contact_type TEXT NOT NULL DEFAULT 'email', + name_hint TEXT, + otp_hash TEXT NOT NULL, + otp_expires_at TIMESTAMPTZ NOT NULL, + otp_attempts INT NOT NULL DEFAULT 0, + otp_verified_at TIMESTAMPTZ, + created_by TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + expires_at TIMESTAMPTZ NOT NULL, + used_at TIMESTAMPTZ, + used_by TEXT +); +CREATE INDEX IF NOT EXISTS idx_invitation_token ON knoe.invitation(token); +CREATE INDEX IF NOT EXISTS idx_invitation_contact ON knoe.invitation(contact); + +-- External identity corroborations (Google sub → knoe user) +-- provider_hd records the developer's home domain (prole.org, gmail.com, etc.) +-- for audit purposes only — it is NOT used for access control. +CREATE TABLE IF NOT EXISTS knoe.identity ( + id SERIAL PRIMARY KEY, + user_id INT NOT NULL REFERENCES knoe.user(id) ON DELETE CASCADE, + provider TEXT NOT NULL, + provider_sub TEXT NOT NULL, + provider_email TEXT, + provider_hd TEXT, + verified_at TIMESTAMPTZ NOT NULL, + UNIQUE(provider, provider_sub) +); +CREATE INDEX IF NOT EXISTS idx_identity_user ON knoe.identity(user_id); + +-- TOTP 2FA credentials (encrypted secret, backup codes) +CREATE TABLE IF NOT EXISTS knoe.totp_credential ( + user_id INT PRIMARY KEY REFERENCES knoe.user(id) ON DELETE CASCADE, + secret TEXT NOT NULL, + verified_at TIMESTAMPTZ, + backup_codes TEXT[], + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Platform-managed resources (repos, db roles, policies, etc.) +CREATE TABLE IF NOT EXISTS knoe.knobject ( + id SERIAL PRIMARY KEY, + type TEXT NOT NULL, + name TEXT NOT NULL, + platform_id TEXT, + metadata JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE(type, name) +); + +-- Access grants (user → knobject with role) +CREATE TABLE IF NOT EXISTS knoe.access_grant ( + id SERIAL PRIMARY KEY, + user_id INT NOT NULL REFERENCES knoe.user(id), + knobject_id INT NOT NULL REFERENCES knoe.knobject(id), + role TEXT NOT NULL, + granted_by TEXT NOT NULL, + granted_at TIMESTAMPTZ NOT NULL DEFAULT now(), + revoked_at TIMESTAMPTZ, + UNIQUE(user_id, knobject_id) +); + +-- Async provisioning job queue (GitLab user, Gitea user, CNPG role, etc.) +CREATE TABLE IF NOT EXISTS knoe.provisioning_job ( + id SERIAL PRIMARY KEY, + user_id INT NOT NULL REFERENCES knoe.user(id), + job_type TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + payload JSONB, + result JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_provisioning_job_status + ON knoe.provisioning_job(status, created_at); + +-- Seed well-known knobjects +INSERT INTO knoe.knobject (type, name, metadata) VALUES + ('gitea_org', 'knoe.dev', '{"description": "Knoe.DEV Gitea organisation"}'), + ('gitlab_group','knoe.dev', '{"description": "Knoe.DEV GitLab group"}') +ON CONFLICT (type, name) DO NOTHING; + +SELECT 'knoe-auth schema v1 applied.' AS status; +ENDSQL + + psql_file "$tmpfile" + rm -f "$tmpfile" + info "Schema applied." } -ensure_knoe_auth() { - if [[ "$KNOE_AUTH_ENABLED" == "0" || "$KNOE_AUTH_ENABLED" == "false" || "$KNOE_AUTH_ENABLED" == "False" ]]; then - log "knoe-auth disabled (KNOE_AUTH_ENABLED=$KNOE_AUTH_ENABLED)." - return 0 - fi - - if [[ -z "${KNOE_AUTH_HOST_NETWORK}" && "$(normalized_mode)" == "k3d" ]]; then - KNOE_AUTH_HOST_NETWORK=1 - fi - - resolve_knoe_auth_defaults - build_knoe_auth_image - ensure_knoe_auth_secrets - local deployment_present=0 - if kubectl -n "$KNOE_AUTH_NAMESPACE" get deploy "$KNOE_AUTH_NAME" >/dev/null 2>&1; then - deployment_present=1 - fi - - # Clean up legacy knoe-auth / old KDC names when switching to knoe-auth. - kubectl -n "$KNOE_AUTH_NAMESPACE" delete deployment auth --ignore-not-found >/dev/null 2>&1 || true - kubectl -n "$KNOE_AUTH_NAMESPACE" delete service auth --ignore-not-found >/dev/null 2>&1 || true - kubectl -n "$KNOE_AUTH_NAMESPACE" delete deployment dog --ignore-not-found >/dev/null 2>&1 || true - kubectl -n "$KNOE_AUTH_NAMESPACE" delete service authority --ignore-not-found >/dev/null 2>&1 || true - - local admin_acl_principal="$PROLE_KDC_ADMIN_PRINCIPAL" - if [[ "$admin_acl_principal" != *"@"* ]]; then - admin_acl_principal="${admin_acl_principal}@${PROLE_KDC_REALM}" - fi - - local trust_block="" - if [[ -n "$PROLE_KDC_TRUST_REALM" && "$PROLE_KDC_TRUST_REALM" != "$PROLE_KDC_REALM" && -n "$KRB5_KDC" ]]; then - local trust_admin_server="${KRB5_ADMIN:-$KRB5_KDC}" - trust_block=$(cat <&2 - env | sed -E 's/(PASSWORD|TOKEN|SECRET)=.*/\1=****/g' >&2 || true - fi - export DEBIAN_FRONTEND=noninteractive - - if ! command -v krb5kdc >/dev/null 2>&1; then - echo "Installing Kerberos packages..." - echo "krb5-config krb5-config/default_realm string ${PROLE_KDC_REALM}" | debconf-set-selections || true - echo "krb5-config krb5-config/kerberos_servers string 127.0.0.1" | debconf-set-selections || true - echo "krb5-config krb5-config/admin_server string 127.0.0.1" | debconf-set-selections || true - apt-get update - apt-get install -y --no-install-recommends krb5-kdc krb5-admin-server krb5-user dnsutils ca-certificates - rm -rf /var/lib/apt/lists/* - fi - - mkdir -p /etc/krb5kdc /var/lib/krb5kdc - if [[ -f /opt/knoe-auth-kdc/krb5.conf ]]; then - cp /opt/knoe-auth-kdc/krb5.conf /etc/krb5.conf - fi - if [[ -f /opt/knoe-auth-kdc/kdc.conf ]]; then - cp /opt/knoe-auth-kdc/kdc.conf /etc/krb5kdc/kdc.conf - fi - if [[ -f /opt/knoe-auth-kdc/kadm5.acl ]]; then - cp /opt/knoe-auth-kdc/kadm5.acl /etc/krb5kdc/kadm5.acl - fi - - # Validate required secrets early to avoid silent crashes - if [[ -z "${PROLE_KDC_MASTER_PASSWORD:-}" ]]; then - echo "ERROR: Missing required env PROLE_KDC_MASTER_PASSWORD (secret 'knoe-auth-secrets/master_password')." >&2 - exit 1 - fi - if [[ -z "${PROLE_KDC_ADMIN_PASSWORD:-}" ]]; then - echo "ERROR: Missing required env PROLE_KDC_ADMIN_PASSWORD (secret 'knoe-auth-secrets/admin_password')." >&2 - exit 1 - fi - - # Optionally generate a minimal Samba configuration if a child realm is provided - realm="\${PROLE_CHILD_REALM:-}" - if [[ -z "\$realm" ]]; then - realm="\${PROLE_KDC_REALM}" - fi - if [[ -n "\$realm" ]]; then - workgroup="\${PROLE_CHILD_WORKGROUP:-}" - if [[ -z "\$workgroup" ]]; then - workgroup="\${realm%%.*}" - fi - netbios="\${PROLE_CHILD_NETBIOS_NAME:-}" - if [[ -z "\$netbios" ]]; then - netbios="\$workgroup" - fi - server_string="\${PROLE_CHILD_SERVER_STRING:-}" - if [[ -z "\$server_string" ]]; then - server_string="\${realm} AD DC" - fi - server_role="\${PROLE_SAMBA_SERVER_ROLE:-}" - if [[ -z "\$server_role" ]]; then - server_role='active directory domain controller' - fi - mkdir -p /etc/samba - # Write minimal Samba config without using a here‑doc to avoid YAML indentation issues - # when this script is embedded in a ConfigMap. Variables are expanded at container runtime. - { - printf '%s\n' "[global]" - printf '%s\n' " workgroup = \${workgroup}" - printf '%s\n' " realm = \${realm}" - printf '%s\n' " netbios name = \${netbios}" - printf '%s\n' " server string = \${server_string}" - printf '%s\n' " server role = \${server_role}" - } > /etc/samba/smb.conf - fi - - realm="\${PROLE_KDC_REALM}" - admin_principal="\${PROLE_KDC_ADMIN_PRINCIPAL}" - if [[ "\${admin_principal}" != *"@"* ]]; then - admin_principal="\${admin_principal}@\${PROLE_KDC_REALM}" - fi - - if [[ ! -f /var/lib/krb5kdc/principal ]]; then - echo "Initializing realm database for \${PROLE_KDC_REALM}..." - kdb5_util create -s -r "\${realm}" -P "\${PROLE_KDC_MASTER_PASSWORD}" - fi - - if ! kadmin.local -q "get_principal \${admin_principal}" >/dev/null 2>&1; then - echo "Creating admin principal \${admin_principal}..." - kadmin.local -q "addprinc -pw \${PROLE_KDC_ADMIN_PASSWORD} \${admin_principal}" - fi - - if [[ -n "\${PROLE_KDC_TRUST_REALM:-}" && "\${PROLE_KDC_TRUST_REALM}" != "\${PROLE_KDC_REALM}" ]]; then - shared_pw="\${PROLE_KDC_TRUST_SHARED_PASSWORD:-\${PROLE_KDC_MASTER_PASSWORD}}" - if ! kadmin.local -q "get_principal krbtgt/\${PROLE_KDC_TRUST_REALM}@\${PROLE_KDC_REALM}" >/dev/null 2>&1; then - echo "Creating trust principal krbtgt/\${PROLE_KDC_TRUST_REALM}@\${PROLE_KDC_REALM}..." - kadmin.local -q "addprinc -pw \${shared_pw} krbtgt/\${PROLE_KDC_TRUST_REALM}@\${PROLE_KDC_REALM}" - fi - - if [[ -n "\${PROLE_KDC_TRUST_ADMIN:-}" && -n "\${PROLE_KDC_TRUST_PASSWORD:-}" ]]; then - echo "Creating reciprocal trust principal in \${PROLE_KDC_TRUST_REALM}..." - kadmin -r "\${PROLE_KDC_TRUST_REALM}" -p "\${PROLE_KDC_TRUST_ADMIN}" -w "\${PROLE_KDC_TRUST_PASSWORD}" \ - -q "addprinc -pw \${shared_pw} krbtgt/\${PROLE_KDC_REALM}@\${PROLE_KDC_TRUST_REALM}" || true - else - echo "WARN: Missing PROLE_KDC_TRUST_ADMIN/PROLE_KDC_TRUST_PASSWORD; skipping external trust principal." - fi - fi - - # Start daemons. Keep kadmind in PID 1; run krb5kdc in background and verify it binds. - echo "Starting krb5kdc and kadmind ..." - krb5kdc -n & - sleep 0.5 - if ! pgrep -x krb5kdc >/dev/null 2>&1; then - echo "ERROR: krb5kdc failed to start. Check /var/log/ (syslog) for details." >&2 - exit 1 - fi - exec kadmind -nofork ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ${KNOE_AUTH_NAME} - namespace: ${KNOE_AUTH_NAMESPACE} -spec: - replicas: 1 - selector: - matchLabels: - app: ${KNOE_AUTH_NAME} - template: - metadata: - labels: - app: ${KNOE_AUTH_NAME} - spec: -${host_net_block} -${dns_policy_block} - containers: - - name: kdc - image: ${KNOE_AUTH_IMAGE} - imagePullPolicy: IfNotPresent - command: ["/bin/bash", "/opt/knoe-auth-kdc/entrypoint.sh"] - env: - - name: PROLE_KDC_REALM - value: "${PROLE_KDC_REALM}" - - name: PROLE_KDC_ADMIN_PRINCIPAL - value: "${PROLE_KDC_ADMIN_PRINCIPAL}" - - name: PROLE_KDC_MASTER_PASSWORD - valueFrom: - secretKeyRef: - name: knoe-auth-secrets - key: master_password - - name: PROLE_KDC_ADMIN_PASSWORD - valueFrom: - secretKeyRef: - name: knoe-auth-secrets - key: admin_password - - name: PROLE_KDC_TRUST_SHARED_PASSWORD - valueFrom: - secretKeyRef: - name: knoe-auth-secrets - key: trust_shared_password - - name: PROLE_KDC_TRUST_REALM - value: "${PROLE_KDC_TRUST_REALM}" - - name: PROLE_KDC_TRUST_ADMIN - value: "${PROLE_KDC_TRUST_ADMIN}" - - name: PROLE_KDC_TRUST_PASSWORD - valueFrom: - secretKeyRef: - name: knoe-auth-secrets - key: trust_password - - name: PROLE_CHILD_REALM - value: "${PROLE_CHILD_REALM:-}" - - name: PROLE_CHILD_WORKGROUP - value: "${PROLE_CHILD_WORKGROUP:-}" - - name: PROLE_CHILD_NETBIOS_NAME - value: "${PROLE_CHILD_NETBIOS_NAME:-}" - - name: PROLE_CHILD_SERVER_STRING - value: "${PROLE_CHILD_SERVER_STRING:-}" - - name: PROLE_SAMBA_SERVER_ROLE - value: "${PROLE_SAMBA_SERVER_ROLE:-}" - ports: - - name: krb5-udp - containerPort: 88 - protocol: UDP - - name: krb5-tcp - containerPort: 88 - protocol: TCP - - name: kpasswd-udp - containerPort: 464 - protocol: UDP - - name: kpasswd-tcp - containerPort: 464 - protocol: TCP - - name: kadmin - containerPort: 749 - protocol: TCP - volumeMounts: - - name: knoe-auth-kdc-config - mountPath: /opt/knoe-auth-kdc - - name: knoe-auth-kdc-data - mountPath: /var/lib/krb5kdc - - name: knoe-auth-kdc-data - mountPath: /etc/krb5kdc - volumes: - - name: knoe-auth-kdc-config - configMap: - name: knoe-auth-kdc-config - - name: knoe-auth-kdc-data - emptyDir: {} ---- -apiVersion: v1 -kind: Service -metadata: - name: ${KNOE_AUTH_SERVICE} - namespace: ${KNOE_AUTH_NAMESPACE} -spec: - selector: - app: ${KNOE_AUTH_NAME} - ports: - - name: krb5-udp - port: 88 - targetPort: 88 - protocol: UDP - - name: krb5-tcp - port: 88 - targetPort: 88 - protocol: TCP - - name: kpasswd-udp - port: 464 - targetPort: 464 - protocol: UDP - - name: kpasswd-tcp - port: 464 - targetPort: 464 - protocol: TCP - - name: kadmin - port: 749 - targetPort: 749 - protocol: TCP -EOF - } - - apply_knoe_auth_manifest - - local rollout_timeout="$KNOE_AUTH_ROLLOUT_TIMEOUT" - if [[ "$deployment_present" -eq 0 ]]; then - rollout_timeout="$KNOE_AUTH_DEPLOY_TIMEOUT" - fi - if ! kubectl -n "$KNOE_AUTH_NAMESPACE" rollout status deploy/${KNOE_AUTH_NAME} --timeout="${rollout_timeout}s"; then - err "WARN: knoe-auth rollout did not complete within ${rollout_timeout}s. Collecting diagnostics..." - latest_pod=$(kubectl -n "$KNOE_AUTH_NAMESPACE" get pod -l "app=${KNOE_AUTH_NAME}" --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1].metadata.name}' 2>/dev/null || true) - if [[ -n "${latest_pod:-}" ]]; then - err "--- describe pod ${latest_pod} ---" - kubectl -n "$KNOE_AUTH_NAMESPACE" describe pod "$latest_pod" 1>&2 || true - err "--- last 200 log lines from ${latest_pod} ---" - kubectl -n "$KNOE_AUTH_NAMESPACE" logs "$latest_pod" --tail=200 1>&2 || true - else - err "[WARN] No pods found for app=${KNOE_AUTH_NAME} to collect logs from." - fi - if maybe_cleanup_stale_kdc "rollout timeout"; then - log "Re-applying knoe-auth after cleanup..." - apply_knoe_auth_manifest - kubectl -n "$KNOE_AUTH_NAMESPACE" rollout status deploy/${KNOE_AUTH_NAME} --timeout="${KNOE_AUTH_DEPLOY_TIMEOUT}s" || true - fi +# ── KDC secrets ────────────────────────────────────────────────────────────── + +create_kdc_secrets() { + if kube -n "$NAMESPACE" get secret knoe-kdc-secrets >/dev/null 2>&1; then + info "knoe-kdc-secrets already exists — skipping." + return fi + info "Creating knoe-kdc-secrets from 1Password..." + local master admin + master=$(op_secret "knoe-kdc-master" "password") + admin=$(op_secret "knoe-kdc-admin" "password") + kube -n "$NAMESPACE" create secret generic knoe-kdc-secrets \ + --from-literal=master_password="$master" \ + --from-literal=admin_password="$admin" + info "knoe-kdc-secrets created." } -cleanup_knoe_auth() { - log "Removing knoe-auth resources (if present)..." - kubectl -n "$KNOE_AUTH_NAMESPACE" delete service "$KNOE_AUTH_SERVICE" --ignore-not-found - kubectl -n "$KNOE_AUTH_NAMESPACE" delete deployment "$KNOE_AUTH_NAME" --ignore-not-found - kubectl -n "$KNOE_AUTH_NAMESPACE" delete configmap knoe-auth-kdc-config --ignore-not-found - kubectl -n "$KNOE_AUTH_NAMESPACE" delete secret knoe-auth-secrets --ignore-not-found -} - -status() { - log "--- init_knoe_auth status ---" - log "Namespace: $KNOE_AUTH_NAMESPACE" - log "knoe-auth enabled: ${KNOE_AUTH_ENABLED:-0}" - log "KDC realm: ${PROLE_KDC_REALM:-}" - log "knoe-auth service: ${KNOE_AUTH_SERVICE:-}" - log "knoe-auth image: ${KNOE_AUTH_IMAGE:-}" - if kubectl -n "$KNOE_AUTH_NAMESPACE" get deploy "$KNOE_AUTH_NAME" >/dev/null 2>&1; then - log "[OK] knoe-auth deployment present" - if maybe_cleanup_stale_kdc "status check" "strict"; then - log "[INFO] Stale knoe-auth deployment removed. Re-run init_knoe_auth.sh update to recreate." - fi - else - log "[INFO] knoe-auth deployment not present" +create_google_oidc_secret() { + if kube -n "$NAMESPACE" get secret knoe-auth-google-oidc >/dev/null 2>&1; then + info "knoe-auth-google-oidc already exists — skipping." + return fi + info "Creating knoe-auth-google-oidc secret..." + local client_id client_secret + client_id=$(op_secret "knoe-google-oidc" "client_id") + client_secret=$(op_secret "knoe-google-oidc" "client_secret") + kube -n "$NAMESPACE" create secret generic knoe-auth-google-oidc \ + --from-literal=client_id="$client_id" \ + --from-literal=client_secret="$client_secret" + info "knoe-auth-google-oidc created." } -case "$ACTION" in - initialize|init|update|deploy|start) - ensure_tools - ensure_namespace - ensure_knoe_auth - ;; - status) - ensure_tools - status - ;; - cleanup|delete|remove) - ensure_tools - ensure_namespace - cleanup_knoe_auth - ;; +create_session_secret() { + if kube -n "$NAMESPACE" get secret knoe-auth-secrets >/dev/null 2>&1; then + info "knoe-auth-secrets already exists — skipping." + return + fi + info "Creating knoe-auth-secrets (session HMAC key)..." + local session_secret + session_secret=$(op_secret "knoe-auth-session" "password") + kube -n "$NAMESPACE" create secret generic knoe-auth-secrets \ + --from-literal=sessionSecret="$session_secret" + info "knoe-auth-secrets created." +} + +# ── Manifests ──────────────────────────────────────────────────────────────── + +apply_manifests() { + info "Applying KDC ConfigMap..." + kube apply -f "$GKE_DIR/knoe-kdc-configmap.yaml" + + info "Applying knoe-auth Deployment..." + kube apply -f "$GKE_DIR/knoe-auth-deployment.yaml" +} + +# ── Invite helper ───────────────────────────────────────────────────────────── + +create_first_invite() { + local contact="${1:-}" + [[ -z "$contact" ]] && { read -rp "Invite contact (email or phone): " contact; } + local name_hint="" + read -rp "Display name hint (optional, press Enter to skip): " name_hint || true + + info "Creating invite for: $contact" + local admin_token + admin_token=$(op_secret "knoe-admin-token" "credential" 2>/dev/null || \ + { read -rsp "Admin token: " t; echo; printf '%s' "$t"; }) + + local response + response=$(curl -sf -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $admin_token" \ + -d "{\"contact\":\"$contact\",\"contactType\":\"email\",\"nameHint\":\"$name_hint\"}" \ + "${AUTH_HOST}/auth/admin/invites") || { + warn "Admin API call failed. knoe-auth may not be ready yet." + warn "Retry: POST ${AUTH_HOST}/auth/admin/invites" + return 1 + } + + local invite_url + invite_url=$(printf '%s' "$response" | grep -o '"enrollUrl":"[^"]*"' | sed 's/"enrollUrl":"//;s/"//') + printf '\n\033[1;32mInvite URL:\033[0m %s\n\n' "$invite_url" + info "Send the above URL to: $contact" +} + +# ── Status ──────────────────────────────────────────────────────────────────── + +show_status() { + info "=== Pod status ===" + kube -n "$NAMESPACE" get pods -l app=knoe-auth 2>/dev/null || true + info "=== Secrets ===" + kube -n "$NAMESPACE" get secret \ + knoe-kdc-secrets knoe-auth-google-oidc knoe-auth-secrets 2>/dev/null || true + info "=== Services ===" + kube -n "$NAMESPACE" get svc knoe-auth 2>/dev/null || true + info "=== Enrollment endpoint ===" + info "${AUTH_HOST}/auth/enroll" +} + +# ── Full initialization ─────────────────────────────────────────────────────── + +cmd_initialize() { + require_tool kubectl + + info "=== knoe-auth initialization ===" + info " Realm: $REALM" + info " Cluster: $APP_CTX" + info " NS: $NAMESPACE" + info "" + + # 1. Ensure namespace exists + kube get namespace "$NAMESPACE" >/dev/null 2>&1 || \ + kube create namespace "$NAMESPACE" + + # 2. Secrets + create_kdc_secrets + create_google_oidc_secret + create_session_secret + + # 3. Apply ConfigMap + Deployment + apply_manifests + + # 4. Wait for pods + wait_for_pods "app=knoe-auth" 240 + + # 5. Schema + run_schema + + # 6. Done + info "" + info "=== knoe-auth is ready ===" + info "Enrollment URL: ${AUTH_HOST}/auth/enroll?token=" + info "" + info "Next: create first admin invite:" + info " $0 invite chrisfu@prole.org" + info "" + show_status +} + +# ── Dispatch ────────────────────────────────────────────────────────────────── + +case "$COMMAND" in + initialize) cmd_initialize ;; + schema) run_schema ;; + invite) create_first_invite "${1:-}" ;; + status) show_status ;; *) - err "Usage: $0 {initialize|update|status|cleanup}" - exit 2 + echo "Usage: $0 {initialize|schema|invite EMAIL|status} [--context CTX] [--namespace NS]" >&2 + exit 1 ;; esac diff --git a/etc/init_knoe_users.sh b/etc/init_knoe_users.sh index 2df32bc..3f1bee0 100755 --- a/etc/init_knoe_users.sh +++ b/etc/init_knoe_users.sh @@ -7,14 +7,14 @@ set -euo pipefail # - 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.knoe.org PROLE.ORG is activated via +# - 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.knoe.org) +# KRB5_KDC=myrddin.prole.org) # init_cnpg_backup.sh (CNPG cluster must exist) # init_argocd.sh (ArgoCD must be running) # @@ -93,8 +93,8 @@ 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.knoe.org}" -GITLAB_HOST="${GITLAB_HOST:-gitlab.knoe.org}" +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}" @@ -455,8 +455,8 @@ SQL # 8. Create knoe.user schema tables and bootstrap users if [[ -n "$primary" ]]; then create_knoe_schema "$primary" - provision_user "$primary" "chrisfu" "chrisfu@knoe.org" "Chris Fu" "admin" - provision_user "$primary" "ron" "ron@knoe.org" "Ron" "developer" + 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 @@ -473,7 +473,7 @@ SQL log "=== Initialization complete ===" log "" log "Next steps:" - log " 1. On myrddin.knoe.org: add krbtgt/PROLE.LOCAL@PROLE.ORG trust principal" + log " 1. On myrddin.prole.org: add krbtgt/PROLE.LOCAL@PROLE.ORG trust principal" log " (samba-tool domain trust or equivalent, using trust_shared_password from knoe-kdc-secrets)" log " 2. Confirm Grafana auth.proxy configured with: headers = Role:X-Knoe-Groups" log " 3. After admin logs in to Gitea/GitLab for the first time, re-run: $0 initialize" diff --git a/k8s/openbao/kerberos-configmap.yaml b/k8s/openbao/kerberos-configmap.yaml index 85ea489..730abe7 100644 --- a/k8s/openbao/kerberos-configmap.yaml +++ b/k8s/openbao/kerberos-configmap.yaml @@ -17,5 +17,5 @@ data: } [domain_realm] - .knoe.org = PROLE.ORG - knoe.org = PROLE.ORG + .prole.org = PROLE.ORG + prole.org = PROLE.ORG