#!/usr/bin/env bash set -euo pipefail # init_kerberos.sh # Purpose: # - Configure Kerberos realm settings for CloudNative-PG pods # - KDC provisioning is handled by init_kdc.sh # - Optionally run a Kerberos authentication test inside a Kubernetes pod # - Optionally provision an in-cluster port forwarder to a Samba AD DC # # Usage: # ./init_kerberos.sh initialize # apply krb5.conf to CNPG pods (best-effort) # ./init_kerberos.sh test # run test pod + kinit (with optional AD port forward) # ./init_kerberos.sh status # show detected config + resources # ./init_kerberos.sh cleanup # remove AD forwarder resources SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) # Load environment and config via knoe_cfg.sh # 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 ACTION=${1:-initialize} CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-knoe-db} KRB5_REALM=${KRB5_REALM:-${REALM:-}} KRB5_KDC=${KRB5_KDC:-${KDC:-}} KRB5_ADMIN=${KRB5_ADMIN:-${SERVER:-}} KRB5_USER=${KRB5_USER:-${USER:-${KRB5_USERNAME:-}}} KRB5_PASSWORD=${KRB5_PASSWORD:-${PASSWORD:-}} PROLE_KDC_NAME=${PROLE_KDC_NAME:-auth} # AD DC forwarding (socat proxy) options KRB5_AD_PORT_FORWARD=${KRB5_AD_PORT_FORWARD:-${AD_PORT_FORWARD:-}} KRB5_AD_PROXY_NAME=${KRB5_AD_PROXY_NAME:-knoe-kerberos-ad-forwarder} KRB5_AD_SERVICE_NAME=${KRB5_AD_SERVICE_NAME:-knoe-kerberos-ad-dc} KRB5_AD_PROXY_IMAGE=${KRB5_AD_PROXY_IMAGE:-alpine/socat} KRB5_AD_PROXY_HOST_NETWORK=${KRB5_AD_PROXY_HOST_NETWORK:-1} KRB5_AD_TCP_PORTS=${KRB5_AD_TCP_PORTS:-"88 389 445 464 636"} KRB5_AD_UDP_PORTS=${KRB5_AD_UDP_PORTS:-"88 464"} CNPG_WAIT_TIMEOUT=${CNPG_WAIT_TIMEOUT:-900} SERVICE_NAMESPACE=${SERVICE_NAMESPACE:-${NAMESPACE:-default}} OPENBAO_NAMESPACE=${OPENBAO_NAMESPACE:-${SERVICE_NAMESPACE:-default}} KRB5_AD_NAMESPACE=${KRB5_AD_NAMESPACE:-${SERVICE_NAMESPACE}} OPENBAO_PORT_FORWARD_LOCAL=${OPENBAO_PORT_FORWARD_LOCAL:-8200} log() { printf '%s\n' "$*"; } err() { printf '%s\n' "$*" >&2; } wait_for_ad_forwarder_ready() { local ns="$1" local deploy_name="$2" local service_name="$3" local timeout_s="${4:-120}" log "Waiting for AD forwarder deployment '${deploy_name}' to become available in namespace '${ns}' ..." if ! kubectl -n "$ns" rollout status deploy/"$deploy_name" --timeout="${timeout_s}s"; then err "ERROR: AD forwarder deployment '${deploy_name}' did not become ready in namespace '${ns}'." kubectl -n "$ns" get pods -l "app=${deploy_name}" -o wide 1>&2 || true kubectl -n "$ns" describe deploy "${deploy_name}" 1>&2 || true kubectl -n "$ns" logs -l "app=${deploy_name}" --tail=200 1>&2 || true exit 1 fi log "Waiting for AD forwarder service '${service_name}' endpoints in namespace '${ns}' ..." local deadline=$((SECONDS + timeout_s)) while [[ $SECONDS -lt $deadline ]]; do local ip ip=$(kubectl -n "$ns" get endpoints "$service_name" -o jsonpath='{.subsets[*].addresses[*].ip}' 2>/dev/null | tr ' ' '\n' | head -n 1 || true) if [[ -n "$ip" ]]; then log "[OK] AD forwarder service '${service_name}' has endpoints (e.g. ${ip})." return 0 fi sleep 2 done err "ERROR: AD forwarder service '${service_name}' has no ready endpoints in namespace '${ns}' after ${timeout_s}s." kubectl -n "$ns" get svc "$service_name" -o wide 1>&2 || true kubectl -n "$ns" get endpoints "$service_name" -o yaml 1>&2 || true kubectl -n "$ns" get pods -l "app=${deploy_name}" -o wide 1>&2 || true exit 1 } ensure_tools() { if [[ "${KNOE_MODE:-}" == "min" ]]; then for t in curl jq; do command -v "$t" >/dev/null || { err "Missing required tool: $t"; exit 1; } done return fi for t in kubectl curl jq; do command -v "$t" >/dev/null || { err "Missing required tool: $t"; exit 1; } done } cnpg_supports_projected_volume_template() { kubectl get crd clusters.postgresql.cnpg.io -o json 2>/dev/null \ | jq -e '.spec.versions[] | select(.name=="v1") | .schema.openAPIV3Schema.properties.spec.properties.projectedVolumeTemplate' >/dev/null } sha256_stdin() { if command -v sha256sum >/dev/null 2>&1; then sha256sum | awk '{print $1}' return 0 fi if command -v shasum >/dev/null 2>&1; then shasum -a 256 | awk '{print $1}' return 0 fi cat >/dev/null echo "" return 0 } ensure_namespace() { if ! kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then log "Creating namespace '$NAMESPACE' ..." kubectl create namespace "$NAMESPACE" >/dev/null 2>&1 || true fi } ensure_namespace_named() { local ns="$1" if [[ -z "$ns" ]]; then return 0 fi if ! kubectl get namespace "$ns" >/dev/null 2>&1; then log "Creating namespace '$ns' ..." kubectl create namespace "$ns" >/dev/null 2>&1 || true fi } preferred_service_namespace() { local ns="${SERVICE_NAMESPACE:-}" local primary="${NAMESPACE:-}" if [[ -n "$primary" && "$primary" != "default" ]]; then if [[ -z "$ns" || "$ns" == "default" ]]; then ns="$primary" fi fi if [[ -z "$ns" ]]; then ns="${primary:-default}" fi printf '%s' "$ns" } is_ip_address() { [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] } detect_knoe_cfg() { local cfg="" if [[ -n "${KNOE_CONF:-}" ]]; then cfg="$(_knoe_cfg_select_cfg_file "$KNOE_CONF")" if [[ -n "$cfg" ]]; then printf '%s' "$cfg" return 0 fi fi if [[ -n "${KNOE_HOME:-}" ]]; then cfg="$(_knoe_cfg_select_cfg_file "$KNOE_HOME/conf")" if [[ -n "$cfg" ]]; then printf '%s' "$cfg" return 0 fi fi cfg="$(_knoe_cfg_select_cfg_file "$SCRIPT_DIR/../conf")" if [[ -n "$cfg" ]]; then printf '%s' "$cfg" fi } cfg_extract_key() { local cfg="$1" key="$2" awk -F= -v k="$key" ' /^[[:space:]]*;/ {next} /^[[:space:]]*#/ {next} /^[[:space:]]*\[/ {next} $1 ~ "^[[:space:]]*" k "[[:space:]]*$" { v=$2 sub(/^[[:space:]]+/, "", v) sub(/[[:space:]]+$/, "", v) val=v } END { print val } ' "$cfg" 2>/dev/null || true } normalize_child_realm_preference() { local cfg val parent child cfg_realm cfg_use_child_set=0 cfg=$(detect_knoe_cfg) if [[ -n "$cfg" ]]; then val=$(cfg_extract_key "$cfg" "PROLE_USE_CHILD_REALM") if [[ -n "$val" ]]; then cfg_use_child_set=1 if _knoe_bool_true "$val"; then PROLE_USE_CHILD_REALM=1 else PROLE_USE_CHILD_REALM=0 fi fi cfg_realm=$(cfg_extract_key "$cfg" "REALM") if [[ -z "$cfg_realm" ]]; then cfg_realm=$(cfg_extract_key "$cfg" "kerberos_config.realm") fi fi child="${PROLE_CHILD_REALM:-}" if [[ -n "$child" ]]; then child=$(printf '%s' "$child" | tr '[:lower:]' '[:upper:]') fi if [[ -n "$cfg_realm" ]]; then cfg_realm=$(printf '%s' "$cfg_realm" | tr '[:lower:]' '[:upper:]') fi if [[ $cfg_use_child_set -eq 0 && -n "$cfg_realm" ]]; then # If config doesn't explicitly request child realm, prefer the configured realm. KRB5_REALM="$cfg_realm" REALM="$cfg_realm" return 0 fi if [[ -n "${PROLE_USE_CHILD_REALM:-}" ]] && _knoe_bool_true "${PROLE_USE_CHILD_REALM}"; then if [[ -n "$child" ]]; then KRB5_REALM="$child" REALM="$child" fi return 0 fi if [[ -n "$cfg_realm" ]]; then KRB5_REALM="$cfg_realm" REALM="$cfg_realm" return 0 fi if [[ -n "${PROLE_USE_CHILD_REALM:-}" ]] && ! _knoe_bool_true "${PROLE_USE_CHILD_REALM}"; then parent="${PROLE_PARENT_REALM:-${REALM:-}}" if [[ -z "$parent" && -n "$child" && "$child" == *.* ]]; then parent="${child#*.}" fi if [[ -n "$parent" ]]; then parent=$(printf '%s' "$parent" | tr '[:lower:]' '[:upper:]') KRB5_REALM="$parent" REALM="$parent" fi fi } prefer_cfg_realm_for_test() { local cfg realm cfg=$(detect_knoe_cfg) if [[ -z "$cfg" ]]; then return 0 fi realm=$(cfg_extract_key "$cfg" "REALM") if [[ -z "$realm" ]]; then realm=$(cfg_extract_key "$cfg" "kerberos_config.realm") fi if [[ -n "$realm" ]]; then realm=$(printf '%s' "$realm" | tr '[:lower:]' '[:upper:]') KRB5_REALM="$realm" REALM="$realm" fi } openbao_url() { if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then echo "$PROLE_OPENBAO_URL" return 0 fi if knoe_is_in_cluster; then echo "http://openbao.${OPENBAO_NAMESPACE}.svc.cluster.local:8200" return 0 fi # In k3s mode, scripts run outside the cluster must reach OpenBao via the k3s host # (never via localhost or kubectl port-forward). if [[ "${KNOE_MODE:-${DEPLOYMENT_MODE:-}}" == "k3s" ]]; then if command -v _knoe_host_from_url >/dev/null 2>&1; then local host host=$(_knoe_host_from_url "${PROLE_K3S_SERVER:-${K3S_SERVER_URL:-}}") if [[ -n "${host:-}" ]]; then echo "http://${host}:8200" return 0 fi fi echo "" return 0 fi if curl -sS "http://127.0.0.1:8200/v1/sys/health" >/dev/null 2>&1; then echo "http://127.0.0.1:8200" return 0 fi echo "" return 0 } _OPENBAO_PF_PID="" _OPENBAO_PF_STARTED=0 start_openbao_port_forward() { if [[ "${KNOE_MODE:-${DEPLOYMENT_MODE:-}}" == "k3s" ]]; then return 1 fi local port="${OPENBAO_PORT_FORWARD_LOCAL}" local url="http://127.0.0.1:${port}" if curl -sS "$url/v1/sys/health" >/dev/null 2>&1; then return 0 fi command -v kubectl >/dev/null 2>&1 || return 1 kubectl -n "${OPENBAO_NAMESPACE:-default}" port-forward "svc/openbao" "${port}:8200" >/dev/null 2>&1 & _OPENBAO_PF_PID=$! _OPENBAO_PF_STARTED=1 for _ in {1..20}; do if curl -sS "$url/v1/sys/health" >/dev/null 2>&1; then return 0 fi if ! kill -0 "$_OPENBAO_PF_PID" >/dev/null 2>&1; then break fi sleep 1 done return 1 } stop_openbao_port_forward() { if [[ "${_OPENBAO_PF_STARTED}" == "1" && -n "${_OPENBAO_PF_PID:-}" ]]; then kill "$_OPENBAO_PF_PID" >/dev/null 2>&1 || true wait "$_OPENBAO_PF_PID" >/dev/null 2>&1 || true fi _OPENBAO_PF_STARTED=0 _OPENBAO_PF_PID="" } ensure_openbao_url() { local url url="$(openbao_url)" if [[ -n "$url" ]]; then printf '%s' "$url" return 0 fi if [[ "${KNOE_MODE:-${DEPLOYMENT_MODE:-}}" == "k3s" ]]; then # In k3s, do not attempt to synthesize connectivity via local port-forwards. printf '' return 0 fi if start_openbao_port_forward; then trap stop_openbao_port_forward EXIT printf 'http://127.0.0.1:%s' "${OPENBAO_PORT_FORWARD_LOCAL}" return 0 fi printf '' return 0 } openbao_token() { local token="" if [[ -f "$KNOE_SERVICE/secrets/openbao-root-token" ]]; then token=$(cat "$KNOE_SERVICE/secrets/openbao-root-token" 2>/dev/null || true) fi if [[ -z "$token" || "$token" == "root" ]]; then token="${OPENBAO_ROOT_TOKEN:-}" fi if [[ -z "$token" || "$token" == "root" ]]; then if command -v kubectl >/dev/null 2>&1; then token=$(kubectl -n "${OPENBAO_NAMESPACE:-default}" get secret openbao-root -o jsonpath='{.data.token}' 2>/dev/null | base64 -d 2>/dev/null || true) fi fi printf '%s' "$token" } fetch_openbao_secret() { local path="$1" local key="$2" local token url token=$(openbao_token) url=$(ensure_openbao_url) if [[ -n "$token" && -n "$url" ]]; then local out="" out=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/$path" 2>/dev/null || true) if [[ -n "$out" ]]; then echo "$out" | jq -r ".data.data.\"$key\"" 2>/dev/null || echo "" return 0 fi fi # Fallback: use in-cluster OpenBao CLI if available if [[ -n "$token" ]] && command -v kubectl >/dev/null 2>&1; then local bao_out="" bao_out=$(kubectl -n "${OPENBAO_NAMESPACE:-default}" exec deploy/openbao -- sh -c \ "BAO_ADDR=http://127.0.0.1:8200 BAO_TOKEN=${token} bao kv get -field=${key} kv/${path}" 2>/dev/null || true) if [[ -n "$bao_out" ]]; then printf '%s' "$bao_out" return 0 fi fi echo "" } resolve_krb5_password() { if [[ -z "${KRB5_PASSWORD}" || "${KRB5_PASSWORD}" == '${OPENBAO:'* || "${KRB5_PASSWORD}" == '${KNOE_SECRET:'* ]]; then local path="knoe/${NAMESPACE:-default}/kerberos" local fetched fetched=$(fetch_openbao_secret "$path" "password") if [[ -n "$fetched" && "$fetched" != "null" ]]; then KRB5_PASSWORD="$fetched" else # Fallback to database password if kerberos secret is missing path="knoe/${NAMESPACE:-default}/db" fetched=$(fetch_openbao_secret "$path" "password") if [[ -n "$fetched" && "$fetched" != "null" ]]; then KRB5_PASSWORD="$fetched" fi fi fi } resolve_krb5_defaults() { local svc_ns kdc_app kdc_svc svc_ns="${SERVICE_NAMESPACE:-${NAMESPACE:-default}}" kdc_app="${PROLE_KDC_NAME:-auth}" kdc_svc="${PROLE_KDC_SERVICE:-auth}" # If the KDC pod is available in the service namespace, default to its service endpoint # only when no explicit KRB5_KDC was provided. if [[ -z "${KRB5_KDC}" ]]; then if kubectl get pod -n "$svc_ns" -l "app=${kdc_app}" >/dev/null 2>&1; then KRB5_KDC="${kdc_svc}.${svc_ns}.svc.cluster.local" KRB5_ADMIN="${kdc_svc}.${svc_ns}.svc.cluster.local" if [[ -z "${KRB5_REALM}" ]]; then KRB5_REALM="PROLE.ORG" fi fi fi if [[ -z "${KRB5_REALM}" && -n "${REALM:-}" ]]; then KRB5_REALM="$REALM" fi if [[ -z "${KRB5_KDC}" && -n "${DOMAIN:-}" ]]; then KRB5_KDC="kdc.${DOMAIN}" fi if [[ -z "${KRB5_ADMIN}" ]]; then if [[ -n "${KRB5_KDC}" ]]; then KRB5_ADMIN=$(printf '%s' "${KRB5_KDC}" | awk -F',' '{print $1}' | xargs) elif [[ -n "${DOMAIN:-}" ]]; then KRB5_ADMIN="kdc.${DOMAIN}" fi fi } primary_kdc() { if [[ -z "${KRB5_KDC}" ]]; then echo "" return 0 fi printf '%s' "${KRB5_KDC}" | awk -F',' '{print $1}' | xargs } is_private_ip() { local ip="$1" if [[ "$ip" =~ ^10\.|^192\.168\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^169\.254\. ]]; then return 0 fi return 1 } default_port_forward_if_local() { if [[ -n "${KRB5_AD_PORT_FORWARD}" ]]; then return fi local kdc_host kdc_host=$(primary_kdc) # Only auto-enable for loopback or localhost by default. # 10.x.x.x and other private ranges are often reachable directly in K3d/Docker. if [[ -n "$kdc_host" ]] && [[ "$kdc_host" == "127.0.0.1" || "$kdc_host" == "localhost" ]]; then KRB5_AD_PORT_FORWARD=1 else KRB5_AD_PORT_FORWARD=0 fi } sync_knoe_kdc_trust() { if [[ "${PROLE_KDC_STANDALONE:-0}" != "1" ]]; then # Default deployment embeds the KDC as a sidecar in `knoe-auth`; do not # attempt to manage a standalone KDC unless explicitly requested. return 0 fi if [[ ! -x "$SCRIPT_DIR/init_kdc.sh" ]]; then return 0 fi local svc_ns svc_ns=$(preferred_service_namespace) SERVICE_NAMESPACE="$svc_ns" \ KRB5_REALM="$KRB5_REALM" \ KRB5_KDC="$KRB5_KDC" \ KRB5_ADMIN="$KRB5_ADMIN" \ KRB5_USER="$KRB5_USER" \ KRB5_PASSWORD="$KRB5_PASSWORD" \ "$SCRIPT_DIR/init_kdc.sh" update || true } ensure_krb5_conf_configmap() { log "Creating/updating ConfigMap knoe-krb5-conf ..." local kdc_val admin_val realm_val domain_val realm_val="$KRB5_REALM" domain_val="${DOMAIN:-}" if [[ -z "$domain_val" && -n "$realm_val" ]]; then domain_val=$(printf '%s' "$realm_val" | tr '[:upper:]' '[:lower:]') fi kdc_val="$KRB5_KDC" admin_val="${KRB5_ADMIN:-}" if [[ -z "$realm_val" || -z "$kdc_val" ]]; then err "ERROR: KRB5_REALM and KRB5_KDC must be set to build krb5.conf." return 1 fi if [[ -z "$admin_val" ]]; then admin_val=$(printf '%s' "$kdc_val" | awk -F',' '{print $1}' | xargs) fi local TMP TMP=$(mktemp) cat >"$TMP" </dev/null || true } wait_for_cnpg_pods() { local timeout=${1:-$CNPG_WAIT_TIMEOUT} local start_time start_time=$(date +%s) while true; do if kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" >/dev/null 2>&1; then # Only wait for CNPG *instance* pods to become Ready. The CNPG initdb job pod also # carries the cluster label and is expected to be Completed (never Ready). if kubectl -n "$NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME,cnpg.io/podRole=instance" --no-headers 2>/dev/null | grep -q .; then if kubectl -n "$NAMESPACE" wait --for=condition=Ready pod -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME,cnpg.io/podRole=instance" --timeout=60s >/dev/null 2>&1; then return 0 fi fi fi if (( $(date +%s) - start_time > timeout )); then err "ERROR: Timed out waiting for CNPG pods for cluster '$CNPG_CLUSTER_NAME' in namespace '$NAMESPACE'." return 1 fi log "Waiting for CNPG pods for cluster '$CNPG_CLUSTER_NAME'..." sleep 3 done } patch_cnpg_cluster_for_auth() { log "Patching CNPG Cluster $CNPG_CLUSTER_NAME to enable GSSAPI (best-effort) ..." log "Current Context: $(kubectl config current-context 2>/dev/null || echo 'unknown')" log "Target Namespace: $NAMESPACE" if ! kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" >/dev/null 2>&1; then err "ERROR: Cluster '$CNPG_CLUSTER_NAME' not found in namespace '$NAMESPACE'." return 1 fi # Best-effort patch; schema may vary with CNPG version. # We remove krb_srvname as it is unrecognized in some PG 17 builds. # We revert certificates to default to avoid operator TLS issues. local kerberos_enabled="${KERBEROS_ENABLED:-${ENABLED:-false}}" local pg_hba if [[ "$kerberos_enabled" == "true" || "$kerberos_enabled" == "True" || "$kerberos_enabled" == "1" ]]; then pg_hba="[ \"local all postgres trust\", \"host all postgres all scram-sha-256\", \"host all authenticator all scram-sha-256\", \"host all all all gss include_realm=1 krb_realm=$KRB5_REALM\", \"host all all all scram-sha-256\" ]" else pg_hba="[ \"local all postgres trust\", \"host all postgres all scram-sha-256\", \"host all all all scram-sha-256\" ]" fi if ! kubectl -n "$NAMESPACE" patch cluster "$CNPG_CLUSTER_NAME" --type merge -p "{ \"spec\": { \"postgresql\": { \"parameters\": {\"krb_srvname\": null}, \"pg_hba\": $pg_hba }, \"certificates\": { \"serverCASecret\": null, \"clientCASecret\": null } } }"; then err "Note: patch failed, but cluster exists. It may need adjustment for your CNPG version." fi } reload_cnpg_cluster() { if command -v kubectl >/dev/null 2>&1 && kubectl cnpg >/dev/null 2>&1; then log "Reloading CNPG Cluster $CNPG_CLUSTER_NAME with kubectl cnpg ..." kubectl cnpg reload -n "$NAMESPACE" "$CNPG_CLUSTER_NAME" || true return 0 fi local primary primary=$(kubectl -n "$NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" -o json 2>/dev/null \ | jq -r '.items[] | select(.metadata.labels["cnpg.io/instanceRole"]=="primary" or .metadata.labels["cnpg.io/role"]=="primary" or .metadata.labels["role"]=="primary") | .metadata.name' \ | head -n1) if [[ -z "$primary" ]]; then err "Unable to detect primary pod for CNPG cluster '$CNPG_CLUSTER_NAME'; skipping reload." return 1 fi log "Reloading PostgreSQL configuration in $primary ..." kubectl -n "$NAMESPACE" exec "$primary" -c postgres -- sh -c 'pg_ctl reload -D "${PGDATA:-/var/lib/postgresql/data}"' || true } apply_krb5_env_to_cnpg() { local conf_path="${1:-/projected/krb5/krb5.conf}" local conf_hash="" local conf conf=$(get_krb5_conf) if [[ -n "$conf" ]]; then conf_hash=$(printf '%s' "$conf" | sha256_stdin) fi if ! kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" >/dev/null 2>&1; then return 1 fi local current_env updated_env current_env=$(kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" -o json 2>/dev/null | jq -c '.spec.env // []') updated_env=$(printf '%s' "$current_env" | jq -c \ --arg name "KRB5_CONFIG" --arg value "$conf_path" \ --arg hash_name "PROLE_KRB5_CONF_SHA" --arg hash_value "$conf_hash" ' map(select(.name != $name and .name != $hash_name)) + [{"name": $name, "value": $value}] + (if $hash_value == "" then [] else [{"name": $hash_name, "value": $hash_value}] end) ') kubectl -n "$NAMESPACE" patch cluster "$CNPG_CLUSTER_NAME" --type merge -p "{\"spec\":{\"env\":$updated_env}}" >/dev/null 2>&1 || true } apply_krb5_conf_mount_to_cnpg() { if ! kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" >/dev/null 2>&1; then err "ERROR: Cluster '$CNPG_CLUSTER_NAME' not found in namespace '$NAMESPACE'." return 1 fi if ! cnpg_supports_projected_volume_template; then err "CNPG CRD does not support spec.projectedVolumeTemplate; falling back to in-pod krb5.conf update." return 1 fi local conf conf=$(get_krb5_conf) if [[ -z "$conf" ]]; then err "Missing krb5.conf data in ConfigMap knoe-krb5-conf." return 1 fi log "Patching CNPG Cluster $CNPG_CLUSTER_NAME to mount krb5.conf via projected volume ..." if ! kubectl -n "$NAMESPACE" patch cluster "$CNPG_CLUSTER_NAME" --type merge -p "{ \"spec\": { \"projectedVolumeTemplate\": { \"sources\": [ {\"configMap\": {\"name\": \"knoe-krb5-conf\", \"items\": [{\"key\": \"krb5.conf\", \"path\": \"krb5/krb5.conf\"}]}} ] } } }" >/dev/null 2>&1; then err "Note: unable to apply krb5.conf mount via patch." return 1 fi } apply_krb5_conf_to_cnpg_pods() { local conf conf=$(get_krb5_conf) if [[ -z "$conf" ]]; then err "Missing krb5.conf data in ConfigMap knoe-krb5-conf." return 1 fi local pods pods=$(kubectl -n "$NAMESPACE" get pods -l "cnpg.io/cluster=$CNPG_CLUSTER_NAME" -o jsonpath='{.items[*].metadata.name}' 2>/dev/null || true) if [[ -z "$pods" ]]; then err "No CNPG pods found for cluster '$CNPG_CLUSTER_NAME' in namespace '$NAMESPACE'." return 1 fi for pod in $pods; do log "Updating /controller/krb5.conf in pod $pod ..." if printf '%s' "$conf" | kubectl -n "$NAMESPACE" exec -i "$pod" -c postgres -- sh -c 'tee /controller/krb5.conf >/dev/null'; then log "[OK] Updated /controller/krb5.conf in $pod" else err "[WARN] Unable to write /controller/krb5.conf in $pod." err " Consider mounting ConfigMap knoe-krb5-conf into the CNPG pods or updating the image." fi done } apply_krb5_conf_to_test_pod() { local conf pod conf=$(get_krb5_conf) if [[ -z "$conf" ]]; then return 0 fi # Look for the 'auth' pod (MIT KDC or test pod) pod=$(kubectl -n "$NAMESPACE" get pods -l "app=auth" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) if [[ -z "$pod" ]]; then # Fallback to 'dog' (legacy name) pod=$(kubectl -n "$NAMESPACE" get pods -l "app=dog" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) fi if [[ -z "$pod" ]]; then return 0 fi log "Updating /etc/krb5.conf in test pod $pod ..." if printf '%s' "$conf" | kubectl -n "$NAMESPACE" exec -i "$pod" -- sh -c 'tee /etc/krb5.conf >/dev/null'; then log "[OK] Updated /etc/krb5.conf in $pod" else err "[WARN] Unable to write /etc/krb5.conf in $pod." fi } apply_ad_forwarder() { local kdc_host kdc_host=$(primary_kdc) if [[ -z "$kdc_host" ]]; then err "KRB5_KDC is required to set up the AD port forwarder." exit 1 fi local host_net_block="" local dns_policy_block="" if [[ "$KRB5_AD_PROXY_HOST_NETWORK" == "1" ]]; then host_net_block=" hostNetwork: true" dns_policy_block=" dnsPolicy: ClusterFirstWithHostNet" fi local tcp_cmd="" local udp_cmd="" local port for port in $KRB5_AD_TCP_PORTS; do local target_port=$port if [[ "$KRB5_AD_PROXY_HOST_NETWORK" == "1" ]]; then target_port=$((port + 10000)) fi tcp_cmd+="socat -d -d TCP-LISTEN:${target_port},fork,reuseaddr TCP:${kdc_host}:${port} & " done for port in $KRB5_AD_UDP_PORTS; do local target_port=$port if [[ "$KRB5_AD_PROXY_HOST_NETWORK" == "1" ]]; then target_port=$((port + 10000)) fi udp_cmd+="socat -d -d UDP-LISTEN:${target_port},fork,reuseaddr UDP:${kdc_host}:${port} & " done local forward_cmd="${tcp_cmd}${udp_cmd}wait" log "Applying AD DC forwarder '${KRB5_AD_PROXY_NAME}' in namespace '${KRB5_AD_NAMESPACE}' (target ${kdc_host}) ..." cat <- ${forward_cmd} ports: $( for port in $KRB5_AD_TCP_PORTS; do local target_port=$port if [[ "$KRB5_AD_PROXY_HOST_NETWORK" == "1" ]]; then target_port=$((port + 10000)) fi printf " - containerPort: %s\n protocol: TCP\n" "$target_port" done for port in $KRB5_AD_UDP_PORTS; do local target_port=$port if [[ "$KRB5_AD_PROXY_HOST_NETWORK" == "1" ]]; then target_port=$((port + 10000)) fi printf " - containerPort: %s\n protocol: UDP\n" "$target_port" done ) --- apiVersion: v1 kind: Service metadata: name: ${KRB5_AD_SERVICE_NAME} namespace: ${KRB5_AD_NAMESPACE} spec: selector: app: ${KRB5_AD_PROXY_NAME} ports: $( for port in $KRB5_AD_TCP_PORTS; do local target_port=$port if [[ "$KRB5_AD_PROXY_HOST_NETWORK" == "1" ]]; then target_port=$((port + 10000)) fi name="tcp-${port}" printf " - name: %s\n port: %s\n targetPort: %s\n protocol: TCP\n" "$name" "$port" "$target_port" done for port in $KRB5_AD_UDP_PORTS; do local target_port=$port if [[ "$KRB5_AD_PROXY_HOST_NETWORK" == "1" ]]; then target_port=$((port + 10000)) fi name="udp-${port}" printf " - name: %s\n port: %s\n targetPort: %s\n protocol: UDP\n" "$name" "$port" "$target_port" done ) EOF wait_for_ad_forwarder_ready "$KRB5_AD_NAMESPACE" "$KRB5_AD_PROXY_NAME" "$KRB5_AD_SERVICE_NAME" 120 } cleanup_ad_forwarder() { log "Removing AD forwarder resources (if present)..." kubectl -n "$KRB5_AD_NAMESPACE" delete service "$KRB5_AD_SERVICE_NAME" --ignore-not-found kubectl -n "$KRB5_AD_NAMESPACE" delete deployment "$KRB5_AD_PROXY_NAME" --ignore-not-found } ensure_kdc_network_allow() { local kdc_host cidr ns kdc_host=$(primary_kdc) if [[ -z "$kdc_host" ]]; then kdc_host="${AD_DC_IP:-${KDC_ANSIBLE_DETECTED:-${KDC_AUTO_DETECTED:-}}}" fi if [[ -z "$kdc_host" ]]; then return 0 fi if ! is_ip_address "$kdc_host"; then local fallback_ip="${AD_DC_IP:-${KDC_ANSIBLE_DETECTED:-${KDC_AUTO_DETECTED:-}}}" if [[ -n "$fallback_ip" ]] && is_ip_address "$fallback_ip"; then kdc_host="$fallback_ip" log "Using AD DC IP for KDC egress policy: $kdc_host" else log "Skipping KDC egress policy (no IP for KDC: $kdc_host)." return 0 fi fi cidr="${kdc_host}/32" ns=$(preferred_service_namespace) ensure_namespace_named "$ns" log "Ensuring KDC egress policy in namespace '${ns}' ..." cat </dev/null 2>&1; then norm=$(knoe_normalize_mode "$mode") else norm="$mode" fi if [[ "$norm" != "k3d" ]]; then return 0 fi if [[ -x "$SCRIPT_DIR/k3d_route_fix.sh" ]]; then log "Applying k3d route fix (best-effort) ..." "$SCRIPT_DIR/k3d_route_fix.sh" >/dev/null 2>&1 || err "WARN: k3d route fix failed; continuing." fi } initialize() { if [[ "${KNOE_MODE:-}" == "min" ]]; then log "Minimal mode: skipping Kerberos Kubernetes resource initialization." return 0 fi ensure_tools ensure_namespace resolve_krb5_defaults normalize_child_realm_preference prefer_cfg_realm_for_test resolve_krb5_password # Support both names for the toggle from knoe.cfg local enabled="${KERBEROS_ENABLED:-${ENABLED:-false}}" if [[ "$enabled" == "false" || "$enabled" == "0" || "$enabled" == "False" ]]; then log "Kerberos is disabled (KERBEROS_ENABLED=$enabled). Skipping initialization." # Cleanup any existing Kerberos config if it was previously enabled kubectl -n "$NAMESPACE" delete configmap knoe-krb5-conf --ignore-not-found return 0 fi if [[ -z "${KRB5_REALM}" || -z "${KRB5_KDC}" ]]; then err "ERROR: KRB5_REALM and KRB5_KDC must be set for Kerberos initialization." err " Set KRB5_REALM/KRB5_KDC or REALM/DOMAIN in env or config." exit 1 fi sync_knoe_kdc_trust ensure_krb5_conf_configmap if apply_krb5_conf_mount_to_cnpg; then apply_krb5_env_to_cnpg "/projected/krb5/krb5.conf" wait_for_cnpg_pods "$CNPG_WAIT_TIMEOUT" else wait_for_cnpg_pods "$CNPG_WAIT_TIMEOUT" apply_krb5_conf_to_cnpg_pods apply_krb5_env_to_cnpg "/controller/krb5.conf" fi patch_cnpg_cluster_for_auth || true reload_cnpg_cluster || true log "Kerberos initialization complete." } run_test() { ensure_tools ensure_namespace resolve_krb5_defaults normalize_child_realm_preference prefer_cfg_realm_for_test resolve_krb5_password KRB5_USER="${KERBEROS_TEST_ADMIN_USER:-administrator}" if [[ -z "${KRB5_REALM}" || -z "${KRB5_KDC}" || -z "${KRB5_USER}" || -z "${KRB5_PASSWORD}" ]]; then err "ERROR: Missing Kerberos configuration. Ensure KRB5_REALM, KRB5_KDC, KRB5_USER, KRB5_PASSWORD are set." exit 1 fi local mode mode="${KNOE_MODE:-}" local prefer_parent="${KERBEROS_TEST_PREFER_PARENT_REALM:-1}" if _knoe_bool_true "$prefer_parent" && [[ -n "${PROLE_PARENT_REALM:-}" ]]; then KRB5_REALM="${PROLE_PARENT_REALM}" REALM="${PROLE_PARENT_REALM}" elif _knoe_bool_true "$prefer_parent" && [[ -n "${KRB5_REALM:-}" && "${KRB5_REALM}" == *.* ]]; then local derived_parent="${KRB5_REALM#*.}" KRB5_REALM="${derived_parent}" REALM="${derived_parent}" fi local test_use_child="${PROLE_USE_CHILD_REALM:-}" if _knoe_bool_true "$prefer_parent"; then test_use_child=0 fi local ad_forward_explicit=0 if [[ -n "${KRB5_AD_PORT_FORWARD}" ]]; then ad_forward_explicit=1 fi local cleanup_after_test=0 default_port_forward_if_local if [[ "$mode" == "k3s" && "$ad_forward_explicit" -eq 0 ]]; then KRB5_AD_PORT_FORWARD=0 fi local normalized="" if command -v knoe_normalize_mode >/dev/null 2>&1; then normalized=$(knoe_normalize_mode "$mode") else normalized="$mode" fi if [[ "$normalized" == "k3s" || "$normalized" == "k3d" ]]; then ensure_kdc_network_allow || true maybe_apply_k3d_route_fix fi local effective_kdc="$KRB5_KDC" if [[ "${KRB5_AD_PORT_FORWARD}" == "1" ]]; then ensure_namespace_named "$KRB5_AD_NAMESPACE" local forwarder_preexisting=0 if kubectl -n "$KRB5_AD_NAMESPACE" get deploy "$KRB5_AD_PROXY_NAME" >/dev/null 2>&1; then forwarder_preexisting=1 fi apply_ad_forwarder if [[ "$KRB5_AD_SERVICE_NAME" == *.* ]]; then effective_kdc="${KRB5_AD_SERVICE_NAME}" else effective_kdc="${KRB5_AD_SERVICE_NAME}.${KRB5_AD_NAMESPACE}.svc.cluster.local" fi if [[ "$mode" == "k3d" && "$forwarder_preexisting" -eq 0 ]]; then cleanup_after_test=1 trap cleanup_ad_forwarder EXIT fi fi log "Using KDC endpoint for test: ${effective_kdc}" KRB5_KDC="$effective_kdc" if [[ "${KRB5_AD_PORT_FORWARD}" == "1" ]]; then KRB5_ADMIN="$effective_kdc" fi ensure_krb5_conf_configmap apply_krb5_conf_to_test_pod local kerberos_check_script="$SCRIPT_DIR/../scripts/validation/check_kerberos.sh" if [[ ! -x "$kerberos_check_script" ]]; then kerberos_check_script="$SCRIPT_DIR/init_kerberos_test.sh" fi if [[ -x "$kerberos_check_script" ]]; then local test_samba_dns="${SAMBA_DNS_SERVER:-}" if [[ "${KRB5_AD_PORT_FORWARD}" == "1" ]]; then test_samba_dns="$effective_kdc" fi local kdc_ns kdc_ns=$(preferred_service_namespace) run_kerberos_test_loop() { local test_attempt=1 local test_max_attempts=2 local checker_mode_args=() if [[ -n "$mode" ]]; then checker_mode_args=(--mode "$mode") fi while true; do if PROLE_USE_CHILD_REALM="$test_use_child" \ KRB5_REALM="$KRB5_REALM" KRB5_KDC="$effective_kdc" KRB5_ADMIN="$KRB5_ADMIN" \ KRB5_USER="${KERBEROS_TEST_ADMIN_USER:-administrator}" KRB5_PASSWORD="$KRB5_PASSWORD" \ SAMBA_ADMIN_USER="${KERBEROS_TEST_ADMIN_USER:-administrator}" SAMBA_ADMIN_PASSWORD="$KRB5_PASSWORD" \ SAMBA_DNS_SERVER="$test_samba_dns" \ "$kerberos_check_script" "${checker_mode_args[@]}" test; then return 0 fi if [[ $test_attempt -ge $test_max_attempts ]]; then err "Kerberos test failed after ${test_attempt} attempts." return 1 fi err "Kerberos test failed; resetting '${PROLE_KDC_NAME}' pod and retrying..." kubectl -n "$kdc_ns" delete pod -l "app=${PROLE_KDC_NAME}" --ignore-not-found >/dev/null 2>&1 || true kubectl -n "$kdc_ns" rollout status deploy/"$PROLE_KDC_NAME" --timeout=120s >/dev/null 2>&1 || true test_attempt=$((test_attempt + 1)) done } local tried_parent=0 while true; do if run_kerberos_test_loop; then break fi if [[ $tried_parent -eq 0 && -n "${PROLE_CHILD_REALM:-}" && -n "${PROLE_PARENT_REALM:-}" && "${KRB5_REALM}" == "${PROLE_CHILD_REALM}" ]]; then err "Kerberos test failed for child realm '${PROLE_CHILD_REALM}'; retrying with parent realm '${PROLE_PARENT_REALM}'..." KRB5_REALM="${PROLE_PARENT_REALM}" PROLE_USE_CHILD_REALM=0 ensure_krb5_conf_configmap || true apply_krb5_conf_to_test_pod || true tried_parent=1 continue fi exit 1 done else err "Kerberos validation script not found (expected $SCRIPT_DIR/../scripts/validation/check_kerberos.sh)." exit 1 fi if [[ "$cleanup_after_test" -eq 1 ]]; then trap - EXIT cleanup_ad_forwarder || true fi } status() { ensure_tools log "--- init_kerberos status ---" log "Namespace: $NAMESPACE" log "CNPG cluster: $CNPG_CLUSTER_NAME" log "KRB5_REALM: ${KRB5_REALM:-}" log "KRB5_KDC: ${KRB5_KDC:-}" log "KRB5_USER: ${KRB5_USER:-}" log "AD forwarder enabled: ${KRB5_AD_PORT_FORWARD:-}" log "AD forwarder namespace: ${KRB5_AD_NAMESPACE:-}" log "AD forwarder deployment: $KRB5_AD_PROXY_NAME" log "AD forwarder service: $KRB5_AD_SERVICE_NAME" if kubectl -n "$NAMESPACE" get configmap knoe-krb5-conf >/dev/null 2>&1; then log "[OK] ConfigMap knoe-krb5-conf present" else log "[MISSING] ConfigMap knoe-krb5-conf" fi if kubectl -n "$KRB5_AD_NAMESPACE" get deploy "$KRB5_AD_PROXY_NAME" >/dev/null 2>&1; then log "[OK] AD forwarder deployment present" else log "[INFO] AD forwarder deployment not present" fi if kubectl -n "$KRB5_AD_NAMESPACE" get svc "$KRB5_AD_SERVICE_NAME" >/dev/null 2>&1; then log "[OK] AD forwarder service present" else log "[INFO] AD forwarder service not present" fi } case "$ACTION" in initialize|configure|update|reload) initialize ;; test) run_test ;; status) status ;; cleanup) ensure_tools ensure_namespace cleanup_ad_forwarder ;; *) err "Usage: $0 {initialize|configure|update|reload|test|status|cleanup}" >&2 exit 2 ;; esac