#!/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}" # 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" # ── packages: PGDG repo + PostgreSQL 16 + pgvector ──────────────────────────── setup_base_apt curl ca-certificates gnupg lsb-release configure_ssh_root_login 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 # The server package runs initdb for the `main` cluster on install — its # encoding is frozen there. Make a UTF-8 locale active for THIS process first # so the cluster is never created as SQL_ASCII (issue #8: a pre-fix LXC was # provisioned under LANG=C and ended up SQL_ASCII). ensure_utf8_locale_active en_US.UTF-8 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)" 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" fi if [[ "$(run_psql -c "SELECT 1 FROM pg_database WHERE datname='$DB_NAME'")" != "1" ]]; then # Explicit encoding/collation from template0 — never inherit the cluster # default, which may be SQL_ASCII if initdb ran under a broken locale # (issue #8). template0 is required to override LC_COLLATE/LC_CTYPE. run_psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USER ENCODING 'UTF8' LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8' TEMPLATE template0" # 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 (UTF8, owner $DB_USER, PUBLIC revoked)" else msg_warn "Database $DB_NAME already exists, skipping creation" fi # Encoding guard (issue #8): catch both a freshly mis-created DB and a # pre-existing SQL_ASCII database from an old provisioning. Abort before the # app ever connects — a wrong encoding is DB damage, not a warning. assert_db_encoding_utf8 "$DB_NAME" # 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" < -- su - postgres -c "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"