#!/usr/bin/env bash # ============================================================================= # Linux Patch Manager — Initial Host Setup Script # ============================================================================= # Run as root on the Ubuntu 24.04 Patch Manager host. # This script: # - Creates the service user/group # - Creates required directories with correct permissions # - Installs PostgreSQL if not present # - Creates the database and user # - Copies configuration and binaries # - Installs systemd units # - Generates initial Ed25519 JWT keys # - Generates internal CA and CA-signed web server certificate # ============================================================================= set -euo pipefail RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' info() { echo -e "${GREEN}[INFO]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; } [[ $EUID -ne 0 ]] && error "This script must be run as root." SERVICE_USER="patch-manager" SERVICE_GROUP="patch-manager" CONFIG_DIR="/etc/patch-manager" LOG_DIR="/var/log/patch-manager" DATA_DIR="/opt/patch-manager" FRONTEND_DIR="/usr/share/patch-manager/frontend" BIN_DIR="/usr/local/bin" BACKUP_DIR="/var/backups/patch-manager" DB_NAME="patch_manager" DB_USER="patch_manager" SYSTEMD_DIR="/etc/systemd/system" info "=== Linux Patch Manager Setup ===" # ----------------------------------------------------------------------- # 1. Create service user # ----------------------------------------------------------------------- info "Creating service user '${SERVICE_USER}'..." if ! id "${SERVICE_USER}" &>/dev/null; then useradd --system --no-create-home --shell /usr/sbin/nologin \ --comment "Linux Patch Manager service account" \ "${SERVICE_USER}" info "User '${SERVICE_USER}' created." else warn "User '${SERVICE_USER}' already exists, skipping." fi # ----------------------------------------------------------------------- # 2. Create required directories # ----------------------------------------------------------------------- info "Creating directories..." mkdir -p \ "${CONFIG_DIR}/ca" \ "${CONFIG_DIR}/certs" \ "${CONFIG_DIR}/jwt" \ "${CONFIG_DIR}/tls" \ "${LOG_DIR}" \ "${DATA_DIR}" \ "${FRONTEND_DIR}" \ "${BACKUP_DIR}" chown -R "${SERVICE_USER}:${SERVICE_GROUP}" \ "${CONFIG_DIR}" \ "${LOG_DIR}" \ "${DATA_DIR}" \ "${FRONTEND_DIR}" chmod 750 "${CONFIG_DIR}/ca" "${CONFIG_DIR}/jwt" chmod 700 "${BACKUP_DIR}" info "Directories created." # ----------------------------------------------------------------------- # 3. Install PostgreSQL 16 if not present # ----------------------------------------------------------------------- if ! command -v psql &>/dev/null; then info "Installing PostgreSQL 16..." apt-get update -qq apt-get install -y postgresql-16 systemctl enable --now postgresql else info "PostgreSQL already installed: $(psql --version)" fi # ----------------------------------------------------------------------- # 4. Create database and user # ----------------------------------------------------------------------- info "Creating database '${DB_NAME}' and user '${DB_USER}'..." DB_PASSWORD=$(openssl rand -base64 32) sudo -u postgres psql -v ON_ERROR_STOP=1 </dev/null || echo "localhost") HOSTNAME_SHORT=$(hostname -s 2>/dev/null || echo "localhost") # Get the host's primary IP address for SAN HOST_IP=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}' || echo "127.0.0.1") # Generate ECDSA P-256 private key for web server openssl ecparam -genkey -name prime256v1 -noout -out "${TLS_KEY}" # Generate CSR with SANs openssl req -new -key "${TLS_KEY}" -out "${TLS_CSR}" \ -subj "/CN=${HOSTNAME_FQDN}/O=Patch Manager" \ -addext "subjectAltName=DNS:${HOSTNAME_FQDN},DNS:${HOSTNAME_SHORT},DNS:localhost,IP:${HOST_IP},IP:127.0.0.1,IP:::1" # Sign with the internal CA openssl x509 -req -in "${TLS_CSR}" -CA "${CA_CERT}" -CAkey "${CA_KEY}" \ -CAcreateserial -days 365 -out "${TLS_CERT}" \ -extfile <(printf "subjectAltName=DNS:${HOSTNAME_FQDN},DNS:${HOSTNAME_SHORT},DNS:localhost,IP:${HOST_IP},IP:127.0.0.1,IP:::1\nextendedKeyUsage=serverAuth") # Clean up CSR rm -f "${TLS_CSR}" chown "${SERVICE_USER}:${SERVICE_GROUP}" "${TLS_CERT}" "${TLS_KEY}" chmod 644 "${TLS_CERT}" chmod 600 "${TLS_KEY}" info "CA-signed web server certificate generated for ${HOSTNAME_FQDN}." else warn "TLS certificate already exists at ${TLS_CERT}, skipping." fi # ----------------------------------------------------------------------- # 7. Install systemd units # ----------------------------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Install systemd target TARGET_SRC="${SCRIPT_DIR}/../systemd/patch-manager.target" if [[ -f "${TARGET_SRC}" ]]; then cp "${TARGET_SRC}" "${SYSTEMD_DIR}/patch-manager.target" info "Installed systemd target: patch-manager.target" fi # Install service units for unit in patch-manager-web.service patch-manager-worker.service; do SRC="${SCRIPT_DIR}/../systemd/${unit}" if [[ -f "${SRC}" ]]; then cp "${SRC}" "${SYSTEMD_DIR}/${unit}" info "Installed systemd unit: ${unit}" else warn "Systemd unit not found: ${SRC}" fi done # Install backup script BACKUP_SRC="${SCRIPT_DIR}/backup.sh" if [[ -f "${BACKUP_SRC}" ]]; then cp "${BACKUP_SRC}" "${BIN_DIR}/backup.sh" chmod 700 "${BIN_DIR}/backup.sh" info "Installed backup script to ${BIN_DIR}/backup.sh" fi systemctl daemon-reload info "systemd units installed and daemon reloaded." # ----------------------------------------------------------------------- # 8. Run seed migration (default admin account) # ----------------------------------------------------------------------- SEED_MIGRATION="${SCRIPT_DIR}/../migrations/002_seed_admin.sql" if [[ -f "${SEED_MIGRATION}" ]]; then info "Running seed migration for default admin account..." sudo -u postgres psql -d "${DB_NAME}" -f "${SEED_MIGRATION}" 2>/dev/null || \ warn "Seed migration already applied or failed (may be idempotent)." else warn "Seed migration not found: ${SEED_MIGRATION}" fi # ----------------------------------------------------------------------- # 9. Install backup cron job # ----------------------------------------------------------------------- CRON_LINE="0 2 * * * /usr/local/bin/backup.sh >> /var/log/patch-manager/backup.log 2>&1" if crontab -l 2>/dev/null | grep -qF "backup.sh"; then warn "Backup cron job already installed, skipping." else (crontab -l 2>/dev/null; echo "${CRON_LINE}") | crontab - info "Nightly backup cron installed (02:00 daily)." fi # ----------------------------------------------------------------------- # Done # ----------------------------------------------------------------------- info "=== Setup complete ===" info "Next steps:" echo " 1. Build and install binaries: cargo build --release" echo " cp target/release/pm-web target/release/pm-worker ${BIN_DIR}/" echo " 2. Build and install frontend: scripts/build-frontend.sh" echo " 3. Review ${CONFIG_DEST}" echo " 4. Enable services:" echo " systemctl enable --now patch-manager-web patch-manager-worker" echo " 5. (Optional) Set GPG_RECIPIENT in backup.sh for encrypted backups"