Compare commits
2
Commits
abc62f5704
...
559ad8dc2d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
559ad8dc2d | ||
|
|
0ee7ceae55 |
Executable
+95
@@ -0,0 +1,95 @@
|
|||||||
|
#!/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"
|
||||||
|
|
||||||
|
source <(curl -fsSL "$LIB_URL/build.func")
|
||||||
|
|
||||||
|
# ── 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" <<EOF
|
||||||
|
NEXUS_APP_IP='$NEXUS_APP_IP'
|
||||||
|
DB_NAME='$DB_NAME'
|
||||||
|
DB_USER='$DB_USER'
|
||||||
|
DB_PORT='$DB_PORT'
|
||||||
|
EOF
|
||||||
|
pct push "$CTID" "$tmpf" /root/nexus-db.deploy.env --perms 600
|
||||||
|
rm -f "$tmpf"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── trailing summary ─────────────────────────────────────────────────────────
|
||||||
|
print_app_summary() {
|
||||||
|
local pg_state
|
||||||
|
pg_state=$(pct exec "$CTID" -- systemctl is-active postgresql 2>/dev/null | tr -d '\r\n')
|
||||||
|
cat <<EOF
|
||||||
|
PostgreSQL 16: $IP_CT:$DB_PORT — $pg_state
|
||||||
|
Database: $DB_NAME (owner: $DB_USER, extension: vector)
|
||||||
|
Allowed client: $NEXUS_APP_IP/32 — all other hosts are rejected
|
||||||
|
Credentials + DSN: /root/nexus-db.credentials (inside the LXC)
|
||||||
|
|
||||||
|
Smoke test FROM THE NEXUS LXC (uses the DSN from the credentials file):
|
||||||
|
psql "postgresql://$DB_USER:<password>@$IP_CT:$DB_PORT/$DB_NAME" -c "SELECT extname FROM pg_extension;"
|
||||||
|
|
||||||
|
Negative test from any OTHER host (must fail):
|
||||||
|
psql "postgresql://$DB_USER:<password>@$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
|
||||||
Executable
+154
@@ -0,0 +1,154 @@
|
|||||||
|
#!/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"
|
||||||
Reference in New Issue
Block a user