From 0ee7ceae551bbf6091d0d9e749632e4883779301 Mon Sep 17 00:00:00 2001 From: Lutz Date: Thu, 11 Jun 2026 15:51:04 +0200 Subject: [PATCH 1/2] feat(nexus-db): PostgreSQL 16 + pgvector LXC template (nexus-hub K-102) webapp-pattern ct/install pair: PGDG repo, database nexus with least-privilege owner role, pg_hba allowlist restricted to the nexus app LXC (explicit reject for everything else), pgvector created by the installer, credentials/DSN summary in /root/nexus-db.credentials. Idempotent re-runs keep role/db and do not rotate the password. --- ct/nexus-db.sh | 93 ++++++++++++++++++++++++ install/nexus-db-install.sh | 139 ++++++++++++++++++++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100755 ct/nexus-db.sh create mode 100755 install/nexus-db-install.sh diff --git a/ct/nexus-db.sh b/ct/nexus-db.sh new file mode 100755 index 0000000..cdf92ff --- /dev/null +++ b/ct/nexus-db.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# nexus-db — PostgreSQL 16 (+pgvector) for nexus (Family Knowledge Hub) +# +# Creates an unprivileged Debian 12 LXC that: +# - runs PostgreSQL 16 from the PGDG repo with the pgvector extension +# - hosts database `nexus` owned by a least-privilege role `nexus` +# - accepts connections ONLY from the nexus app LXC (pg_hba allowlist); +# every other host is rejected +# +# Companion card: nexus-hub K-102. Run on a Proxmox VE host: +# bash -c "$(curl -fsSL https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/ct/nexus-db.sh)" + +set -euo pipefail + +APP="nexus-db" +APP_DESCRIPTION="PostgreSQL 16 + pgvector for nexus (access restricted to the nexus LXC)" + +LIB_URL="${LIB_URL:-https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/lib}" +INSTALL_SCRIPT_URL="${INSTALL_SCRIPT_URL:-https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/install/nexus-db-install.sh}" + +# LXC defaults (DB only: small CPU, RAM matters for shared_buffers/cache) +DEFAULT_HOSTNAME="nexus-db" +DEFAULT_DISK="16" +DEFAULT_CORES="2" +DEFAULT_RAM="4096" + +DEFAULT_DB_NAME="nexus" +DEFAULT_DB_USER="nexus" +DEFAULT_DB_PORT="5432" + +# ── app-specific prompts (host TTY; each skipped if the var is preset) ─────── +prompt_app_config() { + echo + echo "── nexus-db configuration ───────────────────────────────────" + # The ONLY host that may connect (pg_hba allowlist) — the nexus app LXC. + if [[ -z "${NEXUS_APP_IP:-}" ]]; then + read -rp "IP of the nexus app LXC (sole allowed client): " NEXUS_APP_IP + fi + [[ -n "${NEXUS_APP_IP:-}" ]] || { msg_err "NEXUS_APP_IP is required (pg_hba allowlist)"; exit 1; } + + DB_NAME="${DB_NAME:-$DEFAULT_DB_NAME}" + DB_USER="${DB_USER:-$DEFAULT_DB_USER}" + DB_PORT="${DB_PORT:-$DEFAULT_DB_PORT}" + + echo " → database: $DB_NAME role: $DB_USER port: $DB_PORT" + echo " → allowed client: $NEXUS_APP_IP/32 (everything else is rejected)" +} + +# ── push gathered config into the container for the installer to consume ───── +push_app_config() { + msg_info "Pushing db config into container..." + local tmpf; tmpf=$(mktemp) + cat >"$tmpf" </dev/null | tr -d '\r\n') + cat <@$IP_CT:$DB_PORT/$DB_NAME" -c "SELECT extname FROM pg_extension;" + + Negative test from any OTHER host (must fail): + psql "postgresql://$DB_USER:@$IP_CT:$DB_PORT/$DB_NAME" -c "SELECT 1;" + + Logs: pct exec $CTID -- journalctl -u postgresql -f +EOF +} + +# ── orchestrate ─────────────────────────────────────────────────────────────── +trap _on_error ERR +preflight_pve +show_header "$APP" "$APP_DESCRIPTION" +prompt_lxc_config +prompt_app_config +resolve_debian_template +create_lxc +push_app_config +bootstrap_install_script "$INSTALL_SCRIPT_URL" +print_summary diff --git a/install/nexus-db-install.sh b/install/nexus-db-install.sh new file mode 100755 index 0000000..b4f9c78 --- /dev/null +++ b/install/nexus-db-install.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# nexus-db installer — runs inside the LXC, called by ct/nexus-db.sh +# +# PostgreSQL 16 from the PGDG repo, pgvector extension, database `nexus` +# with a least-privilege role, network access restricted to the nexus app +# LXC via pg_hba. Idempotent: safe to re-run (existing role/db/extension +# are kept, the password is NOT rotated on re-run). + +set -euo pipefail + +APP="nexus-db" +LIB_URL="${LIB_URL:-https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/lib}" +source <(curl -fsSL "$LIB_URL/install.func") + +[[ "$EUID" -eq 0 ]] || { msg_err "Must run as root"; exit 1; } + +# ── load config pushed in by the host script ───────────────────────────────── +CONF="/root/nexus-db.deploy.env" +[[ -f "$CONF" ]] || { msg_err "$CONF not found (host bootstrap incomplete)"; exit 1; } +set -a; . "$CONF"; set +a + +: "${NEXUS_APP_IP:?missing NEXUS_APP_IP}" +DB_NAME="${DB_NAME:-nexus}" +DB_USER="${DB_USER:-nexus}" +DB_PORT="${DB_PORT:-5432}" + +PG_MAJOR=16 +CRED_FILE="/root/nexus-db.credentials" + +# ── packages: PGDG repo + PostgreSQL 16 + pgvector ──────────────────────────── +setup_base_apt curl ca-certificates gnupg lsb-release + +if [[ ! -f /etc/apt/sources.list.d/pgdg.sources ]] && [[ ! -f /etc/apt/sources.list.d/pgdg.list ]]; then + msg_info "Adding PGDG apt repo..." + apt-get install -y -qq postgresql-common >/dev/null + /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y >/dev/null + msg_ok "PGDG repo added" +else + msg_warn "PGDG repo already present, skipping" +fi + +msg_info "Installing PostgreSQL $PG_MAJOR + pgvector..." +apt-get install -y -qq "postgresql-$PG_MAJOR" "postgresql-$PG_MAJOR-pgvector" >/dev/null +msg_ok "PostgreSQL $(psql --version | awk '{print $3}') installed" + +PG_CONF_DIR="/etc/postgresql/$PG_MAJOR/main" +PG_CONF="$PG_CONF_DIR/postgresql.conf" +PG_HBA="$PG_CONF_DIR/pg_hba.conf" + +# ── network exposure: listen on all interfaces, gate via pg_hba ─────────────── +if ! grep -q "^listen_addresses = '\*'" "$PG_CONF"; then + msg_info "Configuring listen_addresses + port $DB_PORT..." + sed -i "s/^#\?listen_addresses\s*=.*/listen_addresses = '*'/" "$PG_CONF" + sed -i "s/^#\?port\s*=.*/port = $DB_PORT/" "$PG_CONF" + msg_ok "postgresql.conf updated" +else + msg_warn "listen_addresses already configured, skipping" +fi + +# ── pg_hba: ONLY the nexus LXC may connect over the network ────────────────── +# Strategy: replace the default file with an explicit allowlist. Local +# UNIX-socket access stays peer-authenticated for the postgres superuser +# (maintenance), the nexus role may connect from exactly one IP, everyone +# else hits the final reject rule (defense-in-depth on top of "no other +# rule matches"). +HBA_MARKER="# managed by nexus-db-install.sh" +if ! grep -q "$HBA_MARKER" "$PG_HBA"; then + msg_info "Writing restrictive pg_hba.conf..." + cp -a "$PG_HBA" "$PG_HBA.dist" + cat >"$PG_HBA" </dev/null 2>&1 +systemctl restart postgresql + +# ── role + database + extension (idempotent, password kept on re-run) ───────── +run_psql() { runuser -u postgres -- psql -v ON_ERROR_STOP=1 -qAt "$@"; } + +if [[ "$(run_psql -c "SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'")" != "1" ]]; then + msg_info "Creating role $DB_USER + database $DB_NAME..." + DB_PASS="$(openssl rand -base64 32 | tr -d '/+=' | head -c 32)" + run_psql -c "CREATE ROLE $DB_USER LOGIN PASSWORD '$DB_PASS' NOSUPERUSER NOCREATEDB NOCREATEROLE" + msg_ok "Role $DB_USER created (least privilege)" +else + DB_PASS="" + msg_warn "Role $DB_USER already exists, password unchanged" +fi + +if [[ "$(run_psql -c "SELECT 1 FROM pg_database WHERE datname='$DB_NAME'")" != "1" ]]; then + run_psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USER" + # Only the owner may connect — no PUBLIC access. + run_psql -c "REVOKE CONNECT ON DATABASE $DB_NAME FROM PUBLIC" + msg_ok "Database $DB_NAME created (owner $DB_USER, PUBLIC revoked)" +else + msg_warn "Database $DB_NAME already exists, skipping" +fi + +# pgvector: CREATE EXTENSION needs superuser; installed now (per ADR-0002: +# "Extension ab Tag 1 installiert, ungenutzt bis Phase 2"). +run_psql -d "$DB_NAME" -c "CREATE EXTENSION IF NOT EXISTS vector" >/dev/null +msg_ok "Extension vector available in $DB_NAME" + +# ── credentials / DSN summary ───────────────────────────────────────────────── +IP_SELF="$(hostname -I | awk '{print $1}')" +if [[ -n "$DB_PASS" ]]; then + cat >"$CRED_FILE" < -- runuser -u postgres -- psql -d $DB_NAME +EOF + chmod 600 "$CRED_FILE" + msg_ok "Credentials written to $CRED_FILE (chmod 600)" +else + msg_warn "Re-run detected: $CRED_FILE untouched (password not rotated)" +fi + +apt_cleanup +msg_ok "$APP installation finished" -- 2.54.0 From 559ad8dc2d3f9da7e4ba03f4db980090cf4c16d5 Mon Sep 17 00:00:00 2001 From: Lutz Date: Thu, 11 Jun 2026 15:56:05 +0200 Subject: [PATCH 2/2] fix(nexus-db): cross-review findings - source build.func (script was unrunnable without it) - validate DB_NAME/DB_USER/DB_PORT/NEXUS_APP_IP before SQL/pg_hba use - rotate password when role exists but credentials file is missing --- ct/nexus-db.sh | 2 ++ install/nexus-db-install.sh | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/ct/nexus-db.sh b/ct/nexus-db.sh index cdf92ff..61b2488 100755 --- a/ct/nexus-db.sh +++ b/ct/nexus-db.sh @@ -28,6 +28,8 @@ DEFAULT_DB_NAME="nexus" DEFAULT_DB_USER="nexus" DEFAULT_DB_PORT="5432" +source <(curl -fsSL "$LIB_URL/build.func") + # ── app-specific prompts (host TTY; each skipped if the var is preset) ─────── prompt_app_config() { echo diff --git a/install/nexus-db-install.sh b/install/nexus-db-install.sh index b4f9c78..20af92d 100755 --- a/install/nexus-db-install.sh +++ b/install/nexus-db-install.sh @@ -24,6 +24,13 @@ DB_NAME="${DB_NAME:-nexus}" DB_USER="${DB_USER:-nexus}" DB_PORT="${DB_PORT:-5432}" +# Values land verbatim in SQL and pg_hba.conf — accept only safe shapes. +[[ "$DB_NAME" =~ ^[a-z_][a-z0-9_]*$ ]] || { msg_err "DB_NAME must be a plain lowercase identifier: $DB_NAME"; exit 1; } +[[ "$DB_USER" =~ ^[a-z_][a-z0-9_]*$ ]] || { msg_err "DB_USER must be a plain lowercase identifier: $DB_USER"; exit 1; } +[[ "$DB_PORT" =~ ^[0-9]{2,5}$ ]] || { msg_err "DB_PORT must be numeric: $DB_PORT"; exit 1; } +[[ "$NEXUS_APP_IP" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ || "$NEXUS_APP_IP" =~ ^[0-9a-fA-F:]+$ ]] \ + || { msg_err "NEXUS_APP_IP must be a single host IP: $NEXUS_APP_IP"; exit 1; } + PG_MAJOR=16 CRED_FILE="/root/nexus-db.credentials" @@ -92,6 +99,14 @@ if [[ "$(run_psql -c "SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'")" != "1" DB_PASS="$(openssl rand -base64 32 | tr -d '/+=' | head -c 32)" run_psql -c "CREATE ROLE $DB_USER LOGIN PASSWORD '$DB_PASS' NOSUPERUSER NOCREATEDB NOCREATEROLE" msg_ok "Role $DB_USER created (least privilege)" +elif [[ ! -f "$CRED_FILE" ]]; then + # Recovery: role exists but the generated password was never persisted + # (first run died between CREATE ROLE and credentials write). Rotate so + # the credentials file is authoritative again. + msg_warn "Role $DB_USER exists but $CRED_FILE is missing — rotating password" + DB_PASS="$(openssl rand -base64 32 | tr -d '/+=' | head -c 32)" + run_psql -c "ALTER ROLE $DB_USER PASSWORD '$DB_PASS'" + msg_ok "Password rotated" else DB_PASS="" msg_warn "Role $DB_USER already exists, password unchanged" -- 2.54.0