prole/etc/init_kdc.sh
chrisfu d6586cf1d9 kdc: backport init_kdc.sh trust fixes + add reset-repeatable Junie brief
Mirrors the knoe-db commit `ff7546d` patches into the prole copy of
`etc/init_kdc.sh` so a re-run of `install.sh --mode k3s --reset` from
this repo produces a working cross-realm trust without manual cluster
surgery. The k3s cluster is provisioned from this repo, so the source
fix must live here (knoe-db remains canonical for GKE).

Changes to etc/init_kdc.sh:

1. Create BOTH cross-realm krbtgts in MIT, not just the outbound one.
   The inbound `krbtgt/<REALM>@<TRUST_REALM>` (issued by Samba,
   decrypted here) was missing entirely; without it, MIT cannot
   decrypt inbound TGTs and the trust never carries traffic.

2. Pin both cross-realm krbtgts to RC4 (`arcfour-hmac:normal`). AES
   keys depend on salt, and Samba's `<remote_realm>+UPN` salt does
   not match MIT's `<local_realm>+<principal-no-realm>`; RC4 has no
   salt so both sides converge from the password alone. Matches the
   already-pinned Samba side (commit `ad1eced`).

3. Replace the broken "remote kadmin to Samba" reciprocal-trust block
   with a documented no-op pointing at
   `infrastructure/playbooks/kerberos_trust_setup.yml`. Samba AD
   does not accept additions over MIT's kadmin protocol; the block
   always failed with "Missing parameters in krb5.conf required for
   kadmin client".

4. Switch the KDC data volume from emptyDir to a PVC
   (claimName `knoe-kdc-data`, parameterized by
   `$PROLE_KDC_STORAGE_SIZE` and `$PROLE_KDC_STORAGE_CLASS`).
   State now survives pod restarts.

Adds Junie brief `docs/plans/junie/kdc-trust-reset-repeatable.md`
with four TDD acceptance criteria for an end-to-end --reset run on
the prole k3s cluster.

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

1071 lines
36 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 >/dev/null 2>&1; then
base64 --decode
return 0
fi
if base64 -d </dev/null >/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 <host>: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 <<EOF
${PROLE_KDC_TRUST_REALM} = {
kdc = ${KRB5_KDC}
admin_server = ${trust_admin_server}
}
[capaths]
${PROLE_KDC_REALM} = {
${PROLE_KDC_TRUST_REALM} = .
}
${PROLE_KDC_TRUST_REALM} = {
${PROLE_KDC_REALM} = .
}
EOF
)
fi
apply_kdc_manifest() {
log "Applying Knoe KDC '${PROLE_KDC_NAME}' in namespace '${KDC_NAMESPACE}' (realm ${PROLE_KDC_REALM}) ..."
local host_net_block=""
local dns_policy_block=""
if is_truthy "${PROLE_KDC_HOST_NETWORK:-0}"; then
host_net_block=" hostNetwork: true"
dns_policy_block=" dnsPolicy: ClusterFirstWithHostNet"
fi
# PVC storage class: explicit when $PROLE_KDC_STORAGE_CLASS is set;
# otherwise leave blank so the cluster's default StorageClass picks
# the binder (k3s "local-path", GKE "standard", etc.).
local storage_class_block=""
if [[ -n "${PROLE_KDC_STORAGE_CLASS:-}" ]]; then
storage_class_block=" storageClassName: ${PROLE_KDC_STORAGE_CLASS}"
fi
cat <<EOF | kubectl apply -n "$KDC_NAMESPACE" -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: knoe-kdc-config
namespace: ${KDC_NAMESPACE}
data:
krb5.conf: |
[libdefaults]
default_realm = ${PROLE_KDC_REALM}
dns_lookup_realm = false
dns_lookup_kdc = false
[realms]
${PROLE_KDC_REALM} = {
kdc = 127.0.0.1
admin_server = 127.0.0.1
}${trust_block}
kdc.conf: |
[kdcdefaults]
kdc_ports = 88
kdc_tcp_ports = 88
[realms]
${PROLE_KDC_REALM} = {
database_name = /var/lib/krb5kdc/principal
admin_keytab = FILE:/etc/krb5kdc/kadm5.keytab
acl_file = /etc/krb5kdc/kadm5.acl
key_stash_file = /etc/krb5kdc/stash
max_life = 10h 0m 0s
max_renewable_life = 7d 0h 0m 0s
default_principal_flags = +preauth
}
kadm5.acl: |
${admin_acl_principal} *
entrypoint.sh: |
#!/usr/bin/env bash
set -euo pipefail
# Enable verbose debug if requested
if [[ "${PROLE_KDC_DEBUG:-}" == "1" ]]; then
set -x
echo "[DEBUG] Environment snapshot:" >&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 heredoc 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 (<remote_realm> + UPN) does not match MIT's
# (<local_realm> + <principal-no-realm>). 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 <<EOF | kubectl apply -n "$PROLE_AUTH_NAMESPACE" -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: knoe-kdc-config
namespace: ${PROLE_AUTH_NAMESPACE}
data:
krb5.conf: |
[libdefaults]
default_realm = ${PROLE_KDC_REALM}
dns_lookup_realm = false
dns_lookup_kdc = false
[realms]
${PROLE_KDC_REALM} = {
kdc = 127.0.0.1
admin_server = 127.0.0.1
}${trust_block}
kdc.conf: |
[kdcdefaults]
kdc_ports = 88
kdc_tcp_ports = 88
[realms]
${PROLE_KDC_REALM} = {
database_name = /var/lib/krb5kdc/principal
admin_keytab = FILE:/etc/krb5kdc/kadm5.keytab
acl_file = /etc/krb5kdc/kadm5.acl
key_stash_file = /etc/krb5kdc/stash
max_life = 10h 0m 0s
max_renewable_life = 7d 0h 0m 0s
default_principal_flags = +preauth
}
kadm5.acl: |
${admin_acl_principal} *
entrypoint.sh: |
#!/usr/bin/env bash
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
realm="\${PROLE_KDC_REALM:-PROLE.ORG}"
admin_principal="\${PROLE_KDC_ADMIN_PRINCIPAL:-admin/admin}"
if [[ "\${admin_principal}" != *"@"* ]]; then
admin_principal="\${admin_principal}@\${realm}"
fi
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
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:-<unset>}"
log "Knoe KDC service: ${PROLE_KDC_SERVICE:-<unset>}"
log "Knoe KDC image: ${PROLE_KDC_IMAGE:-<unset>}"
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