Files
proxmox-scripts/install/nexus-db-install.sh
T
l.kirchner 559ad8dc2d 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
2026-06-11 15:56:05 +02:00

155 lines
6.8 KiB
Bash
Executable File

#!/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
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" <<EOF
$HBA_MARKER — change via card, not by hand (nexus-hub K-102)
# TYPE DATABASE USER ADDRESS METHOD
local all postgres peer
local all all peer
host $DB_NAME $DB_USER $NEXUS_APP_IP/32 scram-sha-256
host all all 0.0.0.0/0 reject
host all all ::/0 reject
EOF
msg_ok "pg_hba.conf restricted to $NEXUS_APP_IP/32"
else
msg_warn "pg_hba.conf already managed, skipping"
fi
systemctl enable postgresql >/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
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" <<EOF
nexus-db — PostgreSQL $PG_MAJOR for nexus (Family Knowledge Hub)
Host: $IP_SELF:$DB_PORT
Database: $DB_NAME
Role: $DB_USER (NOSUPERUSER NOCREATEDB NOCREATEROLE, sole owner)
Password: $DB_PASS
DSN for /etc/nexus/env on the nexus LXC (NEXUS_DATABASE_URL):
postgresql+psycopg://$DB_USER:$DB_PASS@$IP_SELF:$DB_PORT/$DB_NAME
Access policy (pg_hba): only $NEXUS_APP_IP/32 may connect; all other
hosts are rejected. Local socket stays peer-auth for maintenance:
pct exec <CTID> -- 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"