#!/bin/sh # Install or upgrade chess-server (rc.d service `chessd`) with PostgreSQL # inside a FreeBSD jail. Run as root inside the jail from a copy of this # repository: # # CHESS_BINARY=/tmp/chess-server TRUSTED_PROXIES= \ # sh deploy/freebsd/setup-jail.sh # # Prerequisites: # - postgresql18-server, postgresql18-client, and stockfish packages # - the jail has its own System V shared memory (deploy/freebsd/host.sh) # - the release binary, built with `make server-freebsd` (or `make server` # on FreeBSD) # # Inputs (environment): # CHESS_BINARY required: chess-server binary to install # TRUSTED_PROXIES address(es) the host nginx connects from, stored as # chessd_trusted_proxies in rc.conf. Without it, every # proxied client shares one rate-limit bucket. # SPLIT_PRIVILEGES yes|no (default no); see deploy/postgresql/setup.sql # ENABLE_LOG_ROTATION yes|no (default no): newsyslog entry for the log # ENABLE_BACKUP yes|no (default no): nightly pg_dump via cron # # The service layout comes from rc.conf (chessd_*) with the defaults of # deploy/freebsd/rc.d/chessd: account chess, home /home/chess, binary # ~/bin/chess-server, log /var/log/chessd.log, 0.0.0.0:8080. # # Steps (idempotent; rerun to upgrade): # 1. PostgreSQL: initdb only if no cluster exists; pg_hba.conf allows only # postgres and chess over the Unix socket (peer); TCP listener disabled # 2. role, database, and schema from deploy/postgresql/setup.sql (first run) # 3. stop chessd; install the binary (previous kept as .prev) and the rc.d # script (previous copy saved under /var/backups) # 4. JWT key; schema migration; rc.conf settings # 5. optional log rotation and backups # 6. start chessd and check /health # Files of a previous SQLite release are left untouched; remove them once the # new release is verified. set -eu PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin umask 022 here=$(cd "$(dirname "$0")" && pwd) repo=$(cd "$here/../.." && pwd) : "${CHESS_BINARY:?set CHESS_BINARY to the chess-server binary to install}" TRUSTED_PROXIES=${TRUSTED_PROXIES:-} SPLIT_PRIVILEGES=${SPLIT_PRIVILEGES:-no} ENABLE_LOG_ROTATION=${ENABLE_LOG_ROTATION:-no} ENABLE_BACKUP=${ENABLE_BACKUP:-no} # System locations; overridable only to test the script. RC_DIR=${RC_DIR:-/usr/local/etc/rc.d} RC_BACKUP_DIR=${RC_BACKUP_DIR:-/var/backups} NEWSYSLOG_DIR=${NEWSYSLOG_DIR:-/usr/local/etc/newsyslog.conf.d} CRON_DIR=${CRON_DIR:-/usr/local/etc/cron.d} SBIN_DIR=${SBIN_DIR:-/usr/local/sbin} BACKUP_DIR=${BACKUP_DIR:-/var/db/postgres/backups} log() { printf '==> %s\n' "$*"; } die() { printf 'setup-jail: %s\n' "$*" >&2; exit 1; } # SQL from stdin, run as the postgres superuser against database $1. pg_sql() { su -m postgres -c "psql -X -A -t -q -v ON_ERROR_STOP=1 -d $1" } # rc.conf value of $1, or the default $2. rc_value() { _value=$(sysrc -n "$1" 2>/dev/null) || _value="" if [ -n "$_value" ]; then echo "$_value"; else echo "$2"; fi } yes_no() { case $2 in yes | no) ;; *) die "$1 must be yes or no" ;; esac } # --- preflight ----------------------------------------------------------------- [ "$(id -u)" -eq 0 ] || die "run as root" [ "$(uname -s)" = FreeBSD ] || die "this script targets FreeBSD" [ -f "$CHESS_BINARY" ] || die "CHESS_BINARY not found: $CHESS_BINARY" for f in "$repo/deploy/postgresql/setup.sql" "$here/rc.d/chessd" "$repo/deploy/postgresql/chess-backup.sh"; do [ -f "$f" ] || die "missing $f; run from a complete repository copy" done yes_no SPLIT_PRIVILEGES "$SPLIT_PRIVILEGES" yes_no ENABLE_LOG_ROTATION "$ENABLE_LOG_ROTATION" yes_no ENABLE_BACKUP "$ENABLE_BACKUP" command -v postgres >/dev/null 2>&1 || die "PostgreSQL is not installed (pkg install postgresql18-server postgresql18-client)" pg_major=$(postgres --version | sed -n 's/^postgres (PostgreSQL) \([0-9][0-9]*\).*/\1/p') [ -n "$pg_major" ] && [ "$pg_major" -ge 17 ] || die "PostgreSQL 17 or newer is required (found: $(postgres --version))" command -v stockfish >/dev/null 2>&1 || log "WARNING: stockfish not found in PATH; computer moves will fail" user=$(rc_value chessd_user chess) group=$(rc_value chessd_group chess) [ "$user" = chess ] || die "chessd_user is $user; peer authentication requires the OS account and database role to both be chess" if ! id "$user" >/dev/null 2>&1; then log "creating account $user (home /home/$user, no shell, no password)" pw useradd -n "$user" -c "Chess server" -d "/home/$user" -m -s /usr/sbin/nologin -w no fi # chessd_home: rc.conf, else the account's home. rc.d/chessd defaults to # /home/chess, so any other home is recorded in rc.conf. account_home=$(pw usershow "$user" | cut -d: -f9) home=$(rc_value chessd_home "$account_home") if [ "$home" != /home/chess ] && [ -z "$(rc_value chessd_home "")" ]; then sysrc -q chessd_home="$home" >/dev/null fi bin=$(rc_value chessd_bin "$home/bin/chess-server") host=$(rc_value chessd_host 0.0.0.0) port=$(rc_value chessd_port 8080) dsn=$(rc_value chessd_dsn 'postgres:///chess?host=/tmp') key=$(rc_value chessd_jwt_key "$home/jwt.key") case $(rc_value chessd_flags "") in *-storage-path*) die "rc.conf chessd_flags contains -storage-path, which no longer exists; remove it (sysrc -x chessd_flags) and rerun" ;; esac # --- PostgreSQL ------------------------------------------------------------------- pgdata=$(rc_value postgresql_data "/var/db/postgres/data$pg_major") if [ ! -f "$pgdata/PG_VERSION" ]; then log "initializing PostgreSQL cluster in $pgdata" sysrc -q postgresql_enable=YES >/dev/null sysrc -q postgresql_initdb_flags="--encoding=UTF8 --locale-provider=builtin --builtin-locale=C.UTF-8 --auth-local=peer --auth-host=reject" >/dev/null service postgresql initdb || die "initdb failed; a shared memory error means the jail needs sysvshm=new (deploy/freebsd/host.sh)" fi sysrc -q postgresql_enable=YES >/dev/null if ! service postgresql status >/dev/null 2>&1; then log "starting PostgreSQL" service postgresql start fi # Only the postgres superuser and the chess role, each over the Unix socket # with peer authentication. A cluster initialized with FreeBSD's default flags # trusts every local connection; this replaces that. hba="$pgdata/pg_hba.conf" if ! grep -q '^# Managed by chess setup-jail.sh' "$hba" 2>/dev/null; then [ -f "$hba.orig" ] || cp -p "$hba" "$hba.orig" log "writing $hba (original kept as pg_hba.conf.orig)" cat >"$hba" <<'HBA' # Managed by chess setup-jail.sh; edits are kept on rerun. # TYPE DATABASE USER METHOD local all postgres peer local chess chess peer HBA chown postgres:postgres "$hba" chmod 600 "$hba" service postgresql reload fi # Unix socket only. ALTER SYSTEM writes postgresql.auto.conf and leaves # postgresql.conf untouched; the setting needs a restart. if [ -n "$(echo 'SHOW listen_addresses' | pg_sql postgres)" ]; then log "disabling the PostgreSQL TCP listener" pg_sql postgres <<'SQL' ALTER SYSTEM SET listen_addresses = ''; ALTER SYSTEM SET unix_socket_directories = '/tmp'; SQL service postgresql restart if [ -n "$(echo 'SHOW listen_addresses' | pg_sql postgres)" ]; then log "WARNING: listen_addresses is still set, from a source that overrides postgresql.auto.conf (postgresql_flags?); PostgreSQL still listens on TCP" fi fi have_role=$(echo "SELECT 1 FROM pg_roles WHERE rolname = 'chess'" | pg_sql postgres) have_db=$(echo "SELECT 1 FROM pg_database WHERE datname = 'chess'" | pg_sql postgres) case "$have_role$have_db" in 11) log "role and database chess exist; provisioning unchanged" ;; "") log "provisioning role, database, and schema (split privileges: $SPLIT_PRIVILEGES)" split=false [ "$SPLIT_PRIVILEGES" = yes ] && split=true su -m postgres -c "psql -X -q -d postgres -v split=$split -f -" <"$repo/deploy/postgresql/setup.sql" ;; *) die "only one of role chess and database chess exists; inspect with: su -m postgres -c psql" ;; esac # --- chessd -------------------------------------------------------------------- if service chessd onestatus >/dev/null 2>&1; then log "stopping chessd" service chessd onestop fi bindir=$(dirname "$bin") [ -d "$bindir" ] || install -d -o root -g wheel -m 0755 "$bindir" # Root-owned: the service account cannot replace its own executable file. if [ -f "$bin" ] && cmp -s "$CHESS_BINARY" "$bin"; then # Same content, possibly the installed file itself (a rerun with # CHESS_BINARY pointing at it): install(1) refuses to copy a file onto # itself, so only normalize ownership and mode. chown root:wheel "$bin" chmod 0555 "$bin" else if [ -f "$bin" ]; then log "keeping the previous binary as $bin.prev" cp -p "$bin" "$bin.prev" fi install -o root -g wheel -m 0555 "$CHESS_BINARY" "$bin" fi [ -d "$RC_DIR" ] || install -d "$RC_DIR" if [ -f "$RC_DIR/chessd" ] && ! cmp -s "$here/rc.d/chessd" "$RC_DIR/chessd"; then # Saved outside rc.d: rc(8) would run a second copy found there. [ -d "$RC_BACKUP_DIR" ] || install -d -m 0750 "$RC_BACKUP_DIR" saved="$RC_BACKUP_DIR/chessd.rc.$(date -u +%Y%m%dT%H%M%SZ)" cp -p "$RC_DIR/chessd" "$saved" log "previous rc.d script saved as $saved" fi install -o root -g wheel -m 0555 "$here/rc.d/chessd" "$RC_DIR/chessd" if [ ! -f "$key" ]; then log "generating JWT signing key $key" (umask 077 && openssl rand -base64 48 >"$key") fi chown "$user:$group" "$key" chmod 600 "$key" log "creating or migrating the chess schema" if [ "$SPLIT_PRIVILEGES" = yes ]; then # The postgres account migrates as chess_owner. It may be unable to reach # the chess home, so it runs a temporary copy of the binary. migrator=$(mktemp /tmp/chess-migrate.XXXXXX) install -o root -g wheel -m 0555 "$CHESS_BINARY" "$migrator" su -m postgres -c "$migrator db init -dsn \"dbname=chess user=postgres options='-c role=chess_owner -c search_path=chess'\"" || { rm -f "$migrator"; die "schema migration failed"; } rm -f "$migrator" else su -m "$user" -c "$bin db init -dsn '$dsn'" fi sysrc -q chessd_enable=YES >/dev/null if [ -n "$TRUSTED_PROXIES" ]; then sysrc -q chessd_trusted_proxies="$TRUSTED_PROXIES" >/dev/null fi [ -n "$(rc_value chessd_trusted_proxies "")" ] || log "WARNING: chessd_trusted_proxies is unset; behind nginx every client shares one rate limit" # Settings of the SQLite-era rc.d script that nothing reads any more. for stale in chessd_storage_path chessd_dir; do if sysrc -n "$stale" >/dev/null 2>&1; then sysrc -q -x "$stale" >/dev/null log "removed unused rc.conf setting $stale" fi done # --- optional maintenance ------------------------------------------------------------- logs=$(rc_value chessd_logs /var/log/chessd.log) if [ "$ENABLE_LOG_ROTATION" = yes ]; then # Daily or at 1 MB, keep 7; SIGHUP makes daemon(8) (-H) reopen the log. [ -d "$NEWSYSLOG_DIR" ] || install -d "$NEWSYSLOG_DIR" printf '%s\n' \ "# Managed by chess setup-jail.sh" \ "$logs $user:$group 640 7 1000 @T00 JC /var/run/chessd.pid 1" \ >"$NEWSYSLOG_DIR/chessd.conf" log "log rotation: $NEWSYSLOG_DIR/chessd.conf" fi if [ "$ENABLE_BACKUP" = yes ]; then # Nightly pg_dump at 03:30 as postgres, keeping 14 days. [ -d "$SBIN_DIR" ] || install -d "$SBIN_DIR" install -o root -g wheel -m 0555 "$repo/deploy/postgresql/chess-backup.sh" "$SBIN_DIR/chess-backup" [ -d "$BACKUP_DIR" ] || install -d -o postgres -g postgres -m 0700 "$BACKUP_DIR" [ -d "$CRON_DIR" ] || install -d "$CRON_DIR" printf '%s\n' \ "# Managed by chess setup-jail.sh" \ "SHELL=/bin/sh" \ "PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin" \ "30 3 * * * postgres CHESS_BACKUP_DIR=$BACKUP_DIR $SBIN_DIR/chess-backup" \ >"$CRON_DIR/chess-backup" log "backups: $CRON_DIR/chess-backup -> $BACKUP_DIR" fi # --- start and verify ----------------------------------------------------------------- log "starting chessd" service chessd start health_host=$host [ "$health_host" = 0.0.0.0 ] && health_host=127.0.0.1 url="http://$health_host:$port/health" health="" for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do if health=$(fetch -q -T 2 -o - "$url" 2>/dev/null); then break fi sleep 2 done case $health in *'"storage":"ok"'*) log "healthy: $health" ;; *) die "no healthy response from $url (got: ${health:-nothing}); see $logs, and check that the jail firewall allows loopback" ;; esac cat <