#!/usr/bin/env bash set -euo pipefail # init_kdc.sh # Purpose: # - Provision the in-cluster Knoe KDC (MIT Kerberos) in the common services namespace # - Optionally configure cross-realm trust to an external realm when credentials are provided # # Usage: # ./init_kdc.sh initialize|update # create/update KDC resources # ./init_kdc.sh status # show KDC status # ./init_kdc.sh cleanup # remove KDC resources SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) # shellcheck disable=SC1090 source "$SCRIPT_DIR/knoe_cfg.sh" if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then knoe_set_mode "${2:-}" shift 2 elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then knoe_set_mode "${1#*=}" shift fi knoe_ensure_kubeconfig >/dev/null 2>&1 || true ensure_kube_context || exit 1 ACTION=${1:-initialize} resolve_kdc_mode_hint() { local mode_hint="${KNOE_MODE:-${DEPLOYMENT_MODE:-${CLUSTER_ENV:-k3s}}}" if command -v knoe_normalize_mode >/dev/null 2>&1; then knoe_normalize_mode "$mode_hint" return 0 fi mode_hint=$(printf '%s' "$mode_hint" | tr 'A-Z' 'a-z') case "$mode_hint" in prod|production) printf 'k8s' ;; *) printf '%s' "$mode_hint" ;; esac } default_knoe_kdc_name() { case "$(resolve_kdc_mode_hint)" in k8s) printf 'authority-gcp-auth' ;; *) printf 'authority-knoe-auth' ;; esac } KDC_NAMESPACE=${PROLE_KDC_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-default}}} # Namespace where knoe-auth runs and expects the `knoe-kdc-config` ConfigMap. # Defaults to PROLE_NAMESPACE (from knoe.cfg) when present. PROLE_AUTH_NAMESPACE=${PROLE_AUTH_NAMESPACE:-${PROLE_NAMESPACE:-}} PROLE_KDC_ENABLED=${PROLE_KDC_ENABLED:-1} PROLE_KDC_NAME=${PROLE_KDC_NAME:-$(default_knoe_kdc_name)} PROLE_KDC_SERVICE=${PROLE_KDC_SERVICE:-auth} PROLE_KDC_IMAGE=${PROLE_KDC_IMAGE:-} # If the user provided an explicit image, we should not require Docker unless # they explicitly opt into building. PROLE_KDC_IMAGE_EXPLICIT=0 if [[ -n "${PROLE_KDC_IMAGE:-}" ]]; then PROLE_KDC_IMAGE_EXPLICIT=1 fi PROLE_KDC_IMAGE_NAME=${PROLE_KDC_IMAGE_NAME:-knoe-authority} PROLE_KDC_IMAGE_TAG=${PROLE_KDC_IMAGE_TAG:-latest} PROLE_KDC_REGISTRY_HOST=${PROLE_KDC_REGISTRY_HOST:-${LOCAL_REGISTRY:-localhost:5000}} PROLE_KDC_REGISTRY_INTERNAL=${PROLE_KDC_REGISTRY_INTERNAL:-${LOCAL_REGISTRY_INTERNAL:-}} PROLE_KDC_BUILD_IMAGE_SET=0 if [[ -n "${PROLE_KDC_BUILD_IMAGE+x}" ]]; then PROLE_KDC_BUILD_IMAGE_SET=1 fi PROLE_KDC_BUILD_IMAGE=${PROLE_KDC_BUILD_IMAGE:-1} PROLE_KDC_ROLLOUT_TIMEOUT=${PROLE_KDC_ROLLOUT_TIMEOUT:-5} PROLE_KDC_DEPLOY_TIMEOUT=${PROLE_KDC_DEPLOY_TIMEOUT:-60} PROLE_KDC_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 "${PROLE_KDC_REGISTRY_HOST:-}" || "${PROLE_KDC_REGISTRY_HOST:-}" == "$_default_host" ]]; then PROLE_KDC_REGISTRY_HOST="" fi PROLE_KDC_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 KDC image." err "Start Docker (e.g. Docker Desktop), or set PROLE_KDC_IMAGE to a prebuilt image and/or set PROLE_KDC_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_kdc_image() { # Ensure we have a deterministic image reference even when skipping local builds. if [[ -n "${PROLE_KDC_IMAGE:-}" ]]; then return 0 fi if [[ -n "${PROLE_KDC_REGISTRY_INTERNAL:-}" ]]; then PROLE_KDC_IMAGE="${PROLE_KDC_REGISTRY_INTERNAL}/${PROLE_KDC_IMAGE_NAME}:${PROLE_KDC_IMAGE_TAG}" return 0 fi if [[ -n "${PROLE_KDC_REGISTRY_HOST:-}" ]]; then PROLE_KDC_IMAGE="${PROLE_KDC_REGISTRY_HOST}/${PROLE_KDC_IMAGE_NAME}:${PROLE_KDC_IMAGE_TAG}" return 0 fi PROLE_KDC_IMAGE="${PROLE_KDC_IMAGE_NAME}:${PROLE_KDC_IMAGE_TAG}" } ensure_namespace() { if ! kubectl get namespace "$KDC_NAMESPACE" >/dev/null 2>&1; then log "Creating namespace '$KDC_NAMESPACE' ..." kubectl create namespace "$KDC_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 "$KDC_NAMESPACE" get pods -l "app=${PROLE_KDC_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 "$KDC_NAMESPACE" get rs -l "app=${PROLE_KDC_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 "$KDC_NAMESPACE" get deploy "$PROLE_KDC_NAME" -o jsonpath='{.spec.replicas}' 2>/dev/null || true) ready=$(kubectl -n "$KDC_NAMESPACE" get deploy "$PROLE_KDC_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || true) updated=$(kubectl -n "$KDC_NAMESPACE" get deploy "$PROLE_KDC_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 "$KDC_NAMESPACE" get deploy "$PROLE_KDC_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 KDC deployment (${reason}). Cleaning up..." cleanup_knoe_kdc 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 "$KDC_NAMESPACE" get secret "$secret" -o "jsonpath={.data.${key}}" 2>/dev/null | b64_decode 2>/dev/null || true } resolve_knoe_kdc_defaults() { if [[ -z "$PROLE_KDC_REALM" ]]; then # Post-rebrand default — must match knoe-db/etc/init_kdc.sh. The old # "PROLE.LOCAL" string seeded a stale KDC config on the prole k3s # cluster that took an afternoon of cross-realm trust debugging to # find — see ~/.claude/plans/chrisfu-myrddin-dev-prole-git-pull-*.md. PROLE_KDC_REALM="KNOE.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 "${PROLE_KDC_REGISTRY_HOST:-}" ]] || [[ "$PROLE_KDC_REGISTRY_HOST" == "localhost:5000" ]] || [[ "$PROLE_KDC_REGISTRY_HOST" == *"k3d"* ]]); then PROLE_KDC_REGISTRY_HOST="${k3s_host}:5000" fi fi if [[ -z "$PROLE_KDC_REGISTRY_INTERNAL" ]]; then local reg_ns="${REGISTRY_NAMESPACE:-${SERVICE_NAMESPACE:-${NAMESPACE:-default}}}" PROLE_KDC_REGISTRY_INTERNAL="registry.${reg_ns}.svc.cluster.local:5000" unset reg_ns fi else PROLE_KDC_REGISTRY_INTERNAL="" fi if [[ -z "$PROLE_KDC_IMAGE" ]]; then if [[ -n "$PROLE_KDC_REGISTRY_INTERNAL" ]]; then PROLE_KDC_IMAGE="${PROLE_KDC_REGISTRY_INTERNAL}/${PROLE_KDC_IMAGE_NAME}:${PROLE_KDC_IMAGE_TAG}" else PROLE_KDC_IMAGE="${PROLE_KDC_IMAGE_NAME}:${PROLE_KDC_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 ;; esac return 1 } build_knoe_kdc_image() { # If the user explicitly sets PROLE_KDC_BUILD_IMAGE, honor it. # Otherwise, skip building when an explicit image was provided. if [[ "$PROLE_KDC_BUILD_IMAGE_SET" == "1" ]]; then if ! is_truthy "${PROLE_KDC_BUILD_IMAGE:-0}"; then resolve_knoe_kdc_image return 0 fi else if [[ "$PROLE_KDC_IMAGE_EXPLICIT" == "1" ]]; then log "Using preconfigured PROLE_KDC_IMAGE='${PROLE_KDC_IMAGE}'; skipping local image build." return 0 fi fi if ! docker_available; then # If the user forced a build, we must fail with a clear error. if [[ "$PROLE_KDC_BUILD_IMAGE_SET" == "1" ]]; then ensure_docker fi log "[WARN] Docker is not available; skipping Knoe KDC image build." log " Set PROLE_KDC_BUILD_IMAGE=1 to force building (requires Docker), or set PROLE_KDC_IMAGE to a prebuilt image." resolve_knoe_kdc_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="${PROLE_KDC_IMAGE_NAME}:${PROLE_KDC_IMAGE_TAG}" log "Building authority image: ${local_tag} (context: ${docker_dir}) ..." docker build -t "$local_tag" "$docker_dir" if [[ -n "$PROLE_KDC_REGISTRY_HOST" ]]; then local remote_tag="${PROLE_KDC_REGISTRY_HOST}/${PROLE_KDC_IMAGE_NAME}:${PROLE_KDC_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 "$PROLE_KDC_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 "$PROLE_KDC_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 PROLE_KDC_REGISTRY_HOST explicitly." exit 1 fi if [[ -n "$PROLE_KDC_REGISTRY_INTERNAL" ]]; then PROLE_KDC_IMAGE="${PROLE_KDC_REGISTRY_INTERNAL}/${PROLE_KDC_IMAGE_NAME}:${PROLE_KDC_IMAGE_TAG}" elif [[ -n "$PROLE_KDC_REGISTRY_HOST" ]]; then PROLE_KDC_IMAGE="${PROLE_KDC_REGISTRY_HOST}/${PROLE_KDC_IMAGE_NAME}:${PROLE_KDC_IMAGE_TAG}" else PROLE_KDC_IMAGE="${local_tag}" fi } ensure_knoe_kdc_secrets() { local secret_name="knoe-kdc-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 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 kubectl -n "$KDC_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 } ensure_knoe_kdc() { if [[ "$PROLE_KDC_ENABLED" == "0" || "$PROLE_KDC_ENABLED" == "false" || "$PROLE_KDC_ENABLED" == "False" ]]; then log "Knoe KDC disabled (PROLE_KDC_ENABLED=$PROLE_KDC_ENABLED)." return 0 fi if [[ -z "${PROLE_KDC_HOST_NETWORK}" && "$(normalized_mode)" == "k3d" ]]; then PROLE_KDC_HOST_NETWORK=1 fi resolve_knoe_kdc_defaults build_knoe_kdc_image ensure_knoe_kdc_secrets local deployment_present=0 if kubectl -n "$KDC_NAMESPACE" get deploy "$PROLE_KDC_NAME" >/dev/null 2>&1; then deployment_present=1 fi # Clean up legacy names if we're switching to the new auth naming. if [[ "$PROLE_KDC_NAME" != "dog" ]]; then kubectl -n "$KDC_NAMESPACE" delete deployment dog --ignore-not-found >/dev/null 2>&1 || true fi if [[ "$PROLE_KDC_SERVICE" != "authority" ]]; then kubectl -n "$KDC_NAMESPACE" delete service authority --ignore-not-found >/dev/null 2>&1 || true fi 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-kdc/krb5.conf ]]; then cp /opt/knoe-kdc/krb5.conf /etc/krb5.conf fi if [[ -f /opt/knoe-kdc/kdc.conf ]]; then cp /opt/knoe-kdc/kdc.conf /etc/krb5kdc/kdc.conf fi if [[ -f /opt/knoe-kdc/kadm5.acl ]]; then cp /opt/knoe-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-kdc-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-kdc-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}}" # ------------------------------------------------------------------ # Cross-realm krbtgt principals — RC4 only. # # Both directions of the trust live as their own krbtgt principal, # each keyed to the same shared password. We pin RC4 (arcfour-hmac) # because AES key derivation requires a salt, and Samba's salt # convention ( + UPN) does not match MIT's # ( + ). RC4 derives keys from # the password alone, so both sides converge with no salt fight. # ------------------------------------------------------------------ # Outbound: KNOE.LOCAL → PROLE.ORG (issued here, decrypted by Samba) if ! kadmin.local -q "get_principal krbtgt/\${PROLE_KDC_TRUST_REALM}@\${PROLE_KDC_REALM}" >/dev/null 2>&1; then echo "Creating outbound trust principal krbtgt/\${PROLE_KDC_TRUST_REALM}@\${PROLE_KDC_REALM}..." kadmin.local -q "addprinc -pw \${shared_pw} -e arcfour-hmac:normal krbtgt/\${PROLE_KDC_TRUST_REALM}@\${PROLE_KDC_REALM}" fi # Inbound: PROLE.ORG → KNOE.LOCAL (issued by Samba, decrypted here) if ! kadmin.local -q "get_principal krbtgt/\${PROLE_KDC_REALM}@\${PROLE_KDC_TRUST_REALM}" >/dev/null 2>&1; then echo "Creating inbound trust principal krbtgt/\${PROLE_KDC_REALM}@\${PROLE_KDC_TRUST_REALM}..." kadmin.local -q "addprinc -pw \${shared_pw} -e arcfour-hmac:normal krbtgt/\${PROLE_KDC_REALM}@\${PROLE_KDC_TRUST_REALM}" fi # NOTE: The Samba-side trust account (user "krbtgt_\${PROLE_KDC_REALM}" # in PROLE.ORG with UPN/SPN krbtgt/\${PROLE_KDC_REALM}) is provisioned # OUT-OF-BAND by this repo's Ansible playbook: # infrastructure/playbooks/kerberos_trust_setup.yml # Earlier versions of this script tried to use a remote "kadmin" # client to write that principal into Samba, but Samba AD does not # accept additions over MIT's kadmin protocol — it always failed # with "Missing parameters in krb5.conf required for kadmin client". # Run the playbook once after this KDC comes up: # ANSIBLE_VAULT_PASSWORD_FILE=\$PWD/.vault_pass \\ # ansible-playbook infrastructure/playbooks/kerberos_trust_setup.yml echo "Note: Samba-side trust account is provisioned out-of-band by" echo " infrastructure/playbooks/kerberos_trust_setup.yml" 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: ${PROLE_KDC_NAME} namespace: ${KDC_NAMESPACE} spec: replicas: 1 selector: matchLabels: app: ${PROLE_KDC_NAME} template: metadata: labels: app: ${PROLE_KDC_NAME} spec: ${host_net_block} ${dns_policy_block} containers: - name: kdc image: ${PROLE_KDC_IMAGE} imagePullPolicy: IfNotPresent command: ["/bin/bash", "/opt/knoe-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-kdc-secrets key: master_password - name: PROLE_KDC_ADMIN_PASSWORD valueFrom: secretKeyRef: name: knoe-kdc-secrets key: admin_password - name: PROLE_KDC_TRUST_SHARED_PASSWORD valueFrom: secretKeyRef: name: knoe-kdc-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-kdc-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-kdc-config mountPath: /opt/knoe-kdc - name: knoe-kdc-data mountPath: /var/lib/krb5kdc - name: knoe-kdc-data mountPath: /etc/krb5kdc volumes: - name: knoe-kdc-config configMap: name: knoe-kdc-config - name: knoe-kdc-data persistentVolumeClaim: claimName: knoe-kdc-data --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: knoe-kdc-data namespace: ${KDC_NAMESPACE} spec: accessModes: - ReadWriteOnce resources: requests: storage: ${PROLE_KDC_STORAGE_SIZE:-1Gi} ${storage_class_block} --- apiVersion: v1 kind: Service metadata: name: ${PROLE_KDC_SERVICE} namespace: ${KDC_NAMESPACE} spec: selector: app: ${PROLE_KDC_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_kdc_manifest # knoe-auth runs in PROLE_AUTH_NAMESPACE (default: PROLE_NAMESPACE) and mounts # ConfigMap `knoe-kdc-config` for its embedded KDC sidecar. When the KDC itself # is deployed into a different namespace (default: SERVICE_NAMESPACE), ensure # the configmap also exists in the knoe-auth namespace to prevent FailedMount. if [[ -n "${PROLE_AUTH_NAMESPACE:-}" && "${PROLE_AUTH_NAMESPACE}" != "${KDC_NAMESPACE}" ]]; then if ! kubectl get namespace "$PROLE_AUTH_NAMESPACE" >/dev/null 2>&1; then log "Creating namespace '$PROLE_AUTH_NAMESPACE' ..." kubectl create namespace "$PROLE_AUTH_NAMESPACE" >/dev/null 2>&1 || true fi log "Ensuring ConfigMap 'knoe-kdc-config' exists in namespace '${PROLE_AUTH_NAMESPACE}' for knoe-auth ..." cat </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-kdc/krb5.conf ]]; then cp /opt/knoe-kdc/krb5.conf /etc/krb5.conf fi if [[ -f /opt/knoe-kdc/kdc.conf ]]; then cp /opt/knoe-kdc/kdc.conf /etc/krb5kdc/kdc.conf fi if [[ -f /opt/knoe-kdc/kadm5.acl ]]; then cp /opt/knoe-kdc/kadm5.acl /etc/krb5kdc/kadm5.acl fi if [[ -z "\${PROLE_KDC_MASTER_PASSWORD:-}" ]]; then echo "ERROR: Missing required env PROLE_KDC_MASTER_PASSWORD (secret 'knoe-kdc-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-kdc-secrets/admin_password')." >&2 exit 1 fi if [[ ! -f /var/lib/krb5kdc/principal ]]; then echo "Initializing realm database for \${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 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 EOF fi local rollout_timeout="$PROLE_KDC_ROLLOUT_TIMEOUT" if [[ "$deployment_present" -eq 0 ]]; then rollout_timeout="$PROLE_KDC_DEPLOY_TIMEOUT" fi if ! kubectl -n "$KDC_NAMESPACE" rollout status deploy/${PROLE_KDC_NAME} --timeout="${rollout_timeout}s"; then err "WARN: Knoe KDC rollout did not complete within ${rollout_timeout}s. Collecting diagnostics..." # Dump a quick describe and last logs from the newest pod (if any) to aid troubleshooting latest_pod=$(kubectl -n "$KDC_NAMESPACE" get pod -l "app=${PROLE_KDC_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 "$KDC_NAMESPACE" describe pod "$latest_pod" 1>&2 || true err "--- last 200 log lines from ${latest_pod} ---" kubectl -n "$KDC_NAMESPACE" logs "$latest_pod" --tail=200 1>&2 || true else err "[WARN] No pods found for app=${PROLE_KDC_NAME} to collect logs from." fi if maybe_cleanup_stale_kdc "rollout timeout"; then log "Re-applying Knoe KDC after cleanup..." apply_kdc_manifest kubectl -n "$KDC_NAMESPACE" rollout status deploy/${PROLE_KDC_NAME} --timeout="${PROLE_KDC_DEPLOY_TIMEOUT}s" || true fi fi } cleanup_knoe_kdc() { log "Removing Knoe KDC resources (if present)..." kubectl -n "$KDC_NAMESPACE" delete service "$PROLE_KDC_SERVICE" --ignore-not-found kubectl -n "$KDC_NAMESPACE" delete deployment "$PROLE_KDC_NAME" --ignore-not-found kubectl -n "$KDC_NAMESPACE" delete configmap knoe-kdc-config --ignore-not-found if [[ -n "${PROLE_AUTH_NAMESPACE:-}" && "${PROLE_AUTH_NAMESPACE}" != "${KDC_NAMESPACE}" ]]; then kubectl -n "$PROLE_AUTH_NAMESPACE" delete configmap knoe-kdc-config --ignore-not-found fi } status() { log "--- init_kdc status ---" log "Namespace: $KDC_NAMESPACE" log "Knoe KDC enabled: ${PROLE_KDC_ENABLED:-0}" log "Knoe KDC realm: ${PROLE_KDC_REALM:-}" log "Knoe KDC service: ${PROLE_KDC_SERVICE:-}" log "Knoe KDC image: ${PROLE_KDC_IMAGE:-}" if kubectl -n "$KDC_NAMESPACE" get deploy "$PROLE_KDC_NAME" >/dev/null 2>&1; then log "[OK] Knoe KDC deployment present" if maybe_cleanup_stale_kdc "status check" "strict"; then log "[INFO] Stale Knoe KDC deployment removed. Re-run init_kdc.sh update to recreate." fi else log "[INFO] Knoe KDC deployment not present" fi } case "$ACTION" in initialize|init|update|deploy|start) ensure_tools ensure_namespace ensure_knoe_kdc ;; status) ensure_tools status ;; cleanup|delete|remove) ensure_tools ensure_namespace cleanup_knoe_kdc ;; *) err "Usage: $0 {initialize|update|status|cleanup}" exit 2 ;; esac