#!/usr/bin/env bash set -euo pipefail # init_cloudnative_pg.sh # Purpose: # - Distribute administrator ed25519 key pair to CloudNative‑PG as a Kubernetes Secret for cert auth # - Configure Kerberos (GSSAPI) using external Kerberos KDC # - Patch CNPG cluster to enable TLS and GSSAPI where possible # # Usage: # ./init_cloudnative_pg.sh start|stop|status|restart # ./init_cloudnative_pg.sh initialize # create/update k8s secrets/configs and patch CNPG # ./init_cloudnative_pg.sh update|reload # re-apply/patch # # Requirements: # - init_openbao.sh has been run (OpenBao running in k8s) # - $PROLE_HOME/env.sh or $HOME/.prole/env.sh defining PROLE_SERVICE # Initialize SCRIPT_DIR SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) # Load env without leaking our positional args to the env script (some env.sh may `exec "$@"`). __PROLE_SAVED_ARGS=("$@") if [[ -n "${PROLE_HOME:-}" && -f "$PROLE_HOME/env.sh" ]]; then set -- # shellcheck disable=SC1090 source "$PROLE_HOME/env.sh" elif [[ -f "$HOME/.prole/env.sh" ]]; then set -- # shellcheck disable=SC1090 source "$HOME/.prole/env.sh" else echo "ERROR: Missing env. Provide PROLE_HOME/env.sh or ~/.prole/env.sh" >&2 exit 1 fi set -- "${__PROLE_SAVED_ARGS[@]}" unset __PROLE_SAVED_ARGS if [[ -z "${PROLE_SERVICE:-}" ]]; then echo "ERROR: PROLE_SERVICE is not defined in env." >&2 exit 1 fi ACTION=${1:-} CNPG_CLUSTER_NAME=${2:-${CNPG_CLUSTER_NAME:-prole-db}} NAMESPACE=${NAMESPACE:-prole} OPENBAO_NAME=${OPENBAO_NAME:-openbao} REALM=${REALM:-PROLE.ORG} DOMAIN=${DOMAIN:-prole.org} KRB5_KDC=${KRB5_KDC:-} KRB5_ADMIN=${KRB5_ADMIN:-} CNPG_MANIFEST="$SCRIPT_DIR/../k8s/prole/prole-db.yaml" SECRETS_DIR="$PROLE_SERVICE/secrets" ADMIN_PRIV="$SECRETS_DIR/admin_ed25519.key" ADMIN_PUB="$SECRETS_DIR/admin_ed25519.pub" OPENBAO_TOKEN_FILE="$SECRETS_DIR/openbao-root-token" ensure_tools() { for t in kubectl curl openssl base64 jq; do command -v "$t" >/dev/null || { echo "Missing required tool: $t" >&2; exit 1; } done } # Resolve OpenBao URL: prefer explicit env, then localhost port-forward, then cluster DNS bao_service_url() { if [[ -n "${PROLE_OPENBAO_URL:-}" ]]; then echo "$PROLE_OPENBAO_URL" return 0 fi # Prefer standard local port-forward managed by etc/init_port_forwards.sh if curl -sS "http://127.0.0.1:18200/v1/sys/health" >/dev/null 2>&1; then echo "http://127.0.0.1:18200" return 0 fi echo "http://$OPENBAO_NAME.$NAMESPACE.svc.cluster.local:8200" } fetch_admin_keys_and_db_pass_from_bao_or_local() { local token url if [[ -f "$OPENBAO_TOKEN_FILE" ]]; then token=$(cat "$OPENBAO_TOKEN_FILE") else token="" fi url=$(bao_service_url) if [[ -n "$token" ]]; then echo "Attempting to read admin key pair from OpenBao kv/prole/admin ..." if curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -e '.data.data' >/dev/null 2>&1; then local priv_b64 pub_b64 priv_b64=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -r '.data.data.admin_private_key_b64') pub_b64=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/admin" | jq -r '.data.data.admin_public_key_b64') printf "%s" "$priv_b64" | base64 -d >"$ADMIN_PRIV" printf "%s" "$pub_b64" | base64 -d >"$ADMIN_PUB" chmod 0600 "$ADMIN_PRIV" fi echo "Attempting to read database password from OpenBao kv/prole/db ..." if curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/db" | jq -e '.data.data' >/dev/null 2>&1; then local db_pass db_pass=$(curl -sS -H "X-Vault-Token: $token" "$url/v1/kv/data/prole/db" | jq -r '.data.data.password') if [[ -n "$db_pass" ]]; then echo "Updating database user secret 'prole-db-user' from OpenBao ..." kubectl create secret generic prole-db-user -n "$NAMESPACE" \ --from-literal=username=prole \ --from-literal=password="$db_pass" \ --dry-run=client -o yaml | kubectl apply -f - echo "Updating database superuser secret 'prole-db-superuser' from OpenBao ..." kubectl create secret generic prole-db-superuser -n "$NAMESPACE" \ --from-literal=username=postgres \ --from-literal=password="$db_pass" \ --dry-run=client -o yaml | kubectl apply -f - fi fi fi if [[ -f "$ADMIN_PRIV" && -f "$ADMIN_PUB" ]]; then echo "Using local admin key pair at $SECRETS_DIR" return 0 fi echo "ERROR: Could not obtain admin key pair from OpenBao and no local files found." >&2 exit 1 } apply_cnpg_admin_secret() { echo "Creating/updating Secret cnpg-admin-key ..." kubectl create secret generic cnpg-admin-key -n "$NAMESPACE" \ --from-file=admin.key="$ADMIN_PRIV" \ --from-file=admin.pub="$ADMIN_PUB" \ --dry-run=client -o yaml | kubectl apply -f - } ensure_krb5_conf_configmap() { echo "Creating/updating ConfigMap prole-krb5-conf ..." local kdc_val admin_val if [[ -n "$KRB5_KDC" ]]; then kdc_val="$KRB5_KDC" admin_val=${KRB5_ADMIN:-$(echo "$KRB5_KDC" | cut -d, -f1)} else kdc_val="kdc.$DOMAIN" admin_val="kdc.$DOMAIN" fi local TMP TMP=$(mktemp) cat >"$TMP" </dev/null 2>&1; then echo "CA secret $ca_secret_name already exists; skipping generation." return 0 fi echo "Generating self-signed CA (RSA 4096) for CNPG ..." local TMPD TMPD=$(mktemp -d) openssl genrsa -out "$TMPD/ca.key" 4096 openssl req -x509 -new -key "$TMPD/ca.key" -out "$TMPD/ca.crt" -days 3650 -subj "/CN=Prole CNPG CA" kubectl -n "$NAMESPACE" create secret generic "$ca_secret_name" \ --from-file=ca.crt="$TMPD/ca.crt" \ --from-file=ca.key="$TMPD/ca.key" \ --dry-run=client -o yaml | kubectl apply -f - rm -rf "$TMPD" } patch_cnpg_cluster_for_auth() { echo "Patching CNPG Cluster $CNPG_CLUSTER_NAME to enable GSSAPI and fix config (best-effort) ..." # 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. kubectl -n "$NAMESPACE" patch cluster "$CNPG_CLUSTER_NAME" --type merge -p "{ \"spec\": { \"postgresql\": { \"parameters\": {\"krb_srvname\": null}, \"pg_hba\": [ \"local all postgres trust\", \"host all postgres all scram-sha-256\", \"host all all all gss include_realm=1 krb_realm=$REALM\", \"host all all all scram-sha-256\" ] }, \"certificates\": { \"serverCASecret\": null, \"clientCASecret\": null } } }" >/dev/null || echo "Note: patch may need adjustment for your CNPG version." } initialize() { ensure_tools # Ensure port-forward is running for OpenBao (dependency) echo "Ensuring port-forward for OpenBao is active ..." "$SCRIPT_DIR/init_port_forwards.sh" restart openbao fetch_admin_keys_and_db_pass_from_bao_or_local apply_cnpg_admin_secret ensure_krb5_conf_configmap # generate_tls_if_missing patch_cnpg_cluster_for_auth # Ensure port-forward is running for Postgres (local access) echo "Ensuring port-forward for Postgres is active ..." "$SCRIPT_DIR/init_port_forwards.sh" restart postgres echo "Initialization complete for CNPG + Kerberos + cert artifacts." } update_reload() { initialize } case "$ACTION" in recreate) ensure_tools "$0" delete "$CNPG_CLUSTER_NAME" "$0" create "$CNPG_CLUSTER_NAME" ;; create) ensure_tools echo "Installing CloudNative-PG operator ..." kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.27/releases/cnpg-1.27.0.yaml echo "Creating CloudNative-PG cluster and resources for '$CNPG_CLUSTER_NAME' ..." kubectl apply -k "$SCRIPT_DIR/../k8s/prole" # Ensure prole-index-html exists for prole deployment readiness probe if ! kubectl get configmap prole-index-html -n prole >/dev/null 2>&1; then echo "Creating prole-index-html configmap..." printf "

Prole

" > /tmp/index.html kubectl create configmap prole-index-html --from-file=/tmp/index.html -n prole rm /tmp/index.html fi # Ensure prole-nginx-tls exists (self-signed for dev) if ! kubectl get secret prole-nginx-tls -n prole >/dev/null 2>&1; then echo "Generating self-signed prole-nginx-tls for development..." openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout /tmp/nginx-tls.key -out /tmp/nginx-tls.crt \ -subj "/CN=prole.org" >/dev/null 2>&1 kubectl create secret tls prole-nginx-tls --key /tmp/nginx-tls.key --cert /tmp/nginx-tls.crt -n prole rm /tmp/nginx-tls.key /tmp/nginx-tls.crt fi echo "Initializing and patching cluster ..." initialize ;; delete) ensure_tools echo "Deleting all resources for '$CNPG_CLUSTER_NAME' ..." kubectl delete -k "$SCRIPT_DIR/../k8s/prole" --ignore-not-found ;; start) ensure_tools if [[ ! -f "$CNPG_MANIFEST" ]]; then echo "ERROR: CNPG manifest not found at $CNPG_MANIFEST" >&2 exit 1 fi echo "Starting CloudNative-PG cluster from $CNPG_MANIFEST ..." kubectl apply -f "$CNPG_MANIFEST" ;; stop) ensure_tools if [[ ! -f "$CNPG_MANIFEST" ]]; then echo "ERROR: CNPG manifest not found at $CNPG_MANIFEST" >&2 exit 1 fi echo "Stopping CloudNative-PG cluster using $CNPG_MANIFEST ..." kubectl delete -f "$CNPG_MANIFEST" --ignore-not-found ;; status) ensure_tools echo "--- CloudNative-PG Cluster Status ($CNPG_CLUSTER_NAME) ---" if kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" >/dev/null 2>&1; then kubectl -n "$NAMESPACE" get cluster "$CNPG_CLUSTER_NAME" echo "" echo "CNPG Plugin Status:" if kubectl cnpg version >/dev/null 2>&1; then kubectl cnpg status "$CNPG_CLUSTER_NAME" -n "$NAMESPACE" else echo "Note: 'kubectl cnpg' plugin not found; skipping detailed status." fi else echo "Cluster '$CNPG_CLUSTER_NAME' not found in namespace '$NAMESPACE'." fi ;; restart) ensure_tools "$0" stop "$0" start ;; initialize) initialize ;; update|reload) update_reload ;; *) echo "Usage: $0 {create|delete|recreate|start|stop|status|restart|initialize|update|reload} [dbname]" >&2 exit 2 ;; esac