prole/etc/init_cloudnative_pg.sh

253 lines
8.4 KiB
Bash
Executable File
Raw 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_cloudnative_pg.sh
# Purpose:
# - Distribute administrator ed25519 key pair to CloudNativePG 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_primary_domain.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:-}
NAMESPACE=${NAMESPACE:-default}
CNPG_CLUSTER_NAME=${CNPG_CLUSTER_NAME:-prole-db}
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_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"
return 0
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 delete secret cnpg-admin-key -n "$NAMESPACE" >/dev/null 2>&1 || true
kubectl create secret generic cnpg-admin-key -n "$NAMESPACE" \
--from-file=admin.key="$ADMIN_PRIV" \
--from-file=admin.pub="$ADMIN_PUB" \
>/dev/null
}
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" <<EOF
[libdefaults]
default_realm = $REALM
dns_lookup_realm = true
dns_lookup_kdc = true
[realms]
$REALM = {
kdc = $kdc_val
admin_server = $admin_val
}
[domain_realm]
.$DOMAIN = $REALM
$DOMAIN = $REALM
EOF
kubectl -n "$NAMESPACE" create configmap prole-krb5-conf --from-file=krb5.conf="$TMP" --dry-run=client -o yaml | kubectl apply -f -
rm -f "$TMP"
}
generate_tls_if_missing() {
local secret_name="${CNPG_CLUSTER_NAME}-tls"
if kubectl -n "$NAMESPACE" get secret "$secret_name" >/dev/null 2>&1; then
echo "TLS secret $secret_name already exists; skipping generation."
return 0
fi
echo "Generating self-signed TLS (ed25519) for CNPG server ..."
local TMPD
TMPD=$(mktemp -d)
openssl genpkey -algorithm ED25519 -out "$TMPD/ca.key"
openssl req -x509 -new -key "$TMPD/ca.key" -out "$TMPD/ca.crt" -days 365 -subj "/CN=Prole CNPG CA"
openssl genpkey -algorithm ED25519 -out "$TMPD/server.key"
openssl req -new -key "$TMPD/server.key" -out "$TMPD/server.csr" -subj "/CN=${CNPG_CLUSTER_NAME}.${NAMESPACE}.svc.cluster.local" -addext "subjectAltName=DNS:${CNPG_CLUSTER_NAME}.${NAMESPACE}.svc.cluster.local,DNS:*.${CNPG_CLUSTER_NAME}.${NAMESPACE}.svc.cluster.local,DNS:*.${NAMESPACE}.svc.cluster.local"
openssl x509 -req -in "$TMPD/server.csr" -CA "$TMPD/ca.crt" -CAkey "$TMPD/ca.key" -CAcreateserial -out "$TMPD/server.crt" -days 365 -extfile <(printf "subjectAltName=DNS:${CNPG_CLUSTER_NAME}.${NAMESPACE}.svc.cluster.local,DNS:*.${CNPG_CLUSTER_NAME}.${NAMESPACE}.svc.cluster.local,DNS:*.${NAMESPACE}.svc.cluster.local")
kubectl -n "$NAMESPACE" create secret tls "$secret_name" --cert="$TMPD/server.crt" --key="$TMPD/server.key" >/dev/null
kubectl -n "$NAMESPACE" create configmap "${CNPG_CLUSTER_NAME}-ca" --from-file=ca.crt="$TMPD/ca.crt" --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 TLS and GSSAPI (best-effort) ..."
# Best-effort patch; schema may vary with CNPG version. We set SSL on and basic pg_hba entries.
kubectl -n "$NAMESPACE" patch cluster "$CNPG_CLUSTER_NAME" --type merge -p "{
\"spec\": {
\"postgresql\": {
\"parameters\": {\"ssl\": \"on\", \"krb_srvname\": \"postgres\"},
\"pg_hba\": [
\"hostssl all all all cert clientcert=verify-full\",
\"host all all all gss include_realm=1 krb_realm=$REALM\"
]
},
\"certificates\": { \"serverSecret\": \"${CNPG_CLUSTER_NAME}-tls\" }
}
}" >/dev/null || echo "Note: patch may need adjustment for your CNPG version."
}
initialize() {
ensure_tools
fetch_admin_keys_from_bao_or_local
apply_cnpg_admin_secret
ensure_krb5_conf_configmap
generate_tls_if_missing
patch_cnpg_cluster_for_auth
echo "Initialization complete for CNPG + Kerberos + cert artifacts."
}
update_reload() {
initialize
}
case "$ACTION" in
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 {start|stop|status|restart|initialize|update|reload}" >&2
exit 2
;;
esac