From 0d27eb7463aa408fedcdd8023d5b9cf756fd54c9 Mon Sep 17 00:00:00 2001 From: Sinisa Madzar Date: Mon, 1 Jun 2026 07:37:36 +0200 Subject: [PATCH] Add bootstrap-vps-ssh and full server install README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move SSH bootstrap from rubix; wget from public rubix-deploy on PC - README steps 0–5 and updates match former rubix install flow --- README.md | 83 +++++++- bootstrap-vps-ssh.sh | 490 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 568 insertions(+), 5 deletions(-) create mode 100755 bootstrap-vps-ssh.sh diff --git a/README.md b/README.md index 4698d8b..ffc40b3 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,21 @@ # rubix-deploy -Bootstrap **step 1** for new Rubix servers (install app from Gitea release). +New Rubix server — full install flow. **Repository:** https://gitea.dialer.work/swissdatabase/rubix-deploy -**Clone:** `git clone https://gitea.dialer.work/swissdatabase/rubix-deploy.git` +App releases: https://gitea.dialer.work/swissdatabase/rubix/releases -## On a new VPS +## 0) SSH on a new VPS (from your PC) + +```bash +wget -O bootstrap-vps-ssh.sh \ + https://gitea.dialer.work/swissdatabase/rubix-deploy/raw/branch/main/bootstrap-vps-ssh.sh +chmod +x bootstrap-vps-ssh.sh +./bootstrap-vps-ssh.sh +ssh +``` + +## 1) Download Rubix (on the VPS) ```bash wget -O install-rubix.sh \ @@ -14,6 +24,69 @@ chmod +x install-rubix.sh sudo ./install-rubix.sh ``` -The script: `apt update` + `apt upgrade`, installs tools, asks for token, creates `/home/www/callcenter`, downloads **rubix**. +The script will: -**Step 2+** (`.env`, Docker, `up.sh`): [swissdatabase/rubix](https://gitea.dialer.work/swissdatabase/rubix) README. +1. Ask for your **Gitea token** (if `GITEA_TOKEN` is not already exported) +2. Create **`/home/www/callcenter`** +3. Download and unpack the latest **rubix** release there + +Optional: specific version `sudo ./install-rubix.sh v1.1.10` +Optional: other path `sudo RUBIX_INSTALL_PATH=/opt/rubix ./install-rubix.sh` + +## 2) Configure `.env` and start stack + +```bash +cd /home/www/callcenter/deploy/docker +cp .env.example .env +nano .env +``` + +Set at least: `GITEA_REGISTRY_PULL_TOKEN` (same token as step 1), `MYSQL_ROOT_PASSWORD`, DB users/passwords, `COMPOSE_PROFILES`, domains, `RUBIX_STORAGE_ROOT`. + +Install Docker if not present: + +```bash +apt-get install -y docker.io docker-compose-plugin +systemctl enable --now docker +echo '' | docker login gitea.dialer.work -u --password-stdin +``` + +Start: + +```bash +cd /home/www/callcenter/deploy/docker +sudo ./up.sh +``` + +Single service: `sudo ./up.sh mysql` / `cms` / `asterisk` + +## 3) SQL migrations (fresh database) + +```bash +cd /home/www/callcenter/deploy +sudo RUBIX_MYSQL_VIA_DOCKER=1 MYSQL_DOCKER_CONTAINER=rubix-mysql \ + ./apply_sql_migrations.sh +``` + +Use your `RUBIX_MYSQL_CONTAINER` from `.env`. Existing server: restore MySQL datadir under `RUBIX_MYSQL_DATA` instead. + +## 4) CMS HTTPS (when DNS is ready) + +```bash +cd /home/www/callcenter/deploy/docker +docker-cms-certbot +``` + +## 5) Dialer + +```bash +docker-asterisk +/etc/init.d/callcenter2 restart +``` + +## Updates (app already installed) + +```bash +cd /home/www/callcenter/deploy +./rubix_deploy_from_release.sh vX.Y.Z +``` diff --git a/bootstrap-vps-ssh.sh b/bootstrap-vps-ssh.sh new file mode 100755 index 0000000..f331a1d --- /dev/null +++ b/bootstrap-vps-ssh.sh @@ -0,0 +1,490 @@ +#!/bin/bash +# First-time VPS bootstrap from your workstation: +# password login → deploy user + SSH key → test key → disable password auth → apt upgrade +# → optional ~/.ssh/config alias on this machine +# +# Run locally (not on the VPS): +# wget -O bootstrap-vps-ssh.sh \ +# https://gitea.dialer.work/swissdatabase/rubix-deploy/raw/branch/main/bootstrap-vps-ssh.sh +# chmod +x bootstrap-vps-ssh.sh +# ./bootstrap-vps-ssh.sh +# +# Needs: ssh, optional sshpass (apt install sshpass) + +set -euo pipefail + +SSH_CONFIG="${HOME}/.ssh/config" +SSH_DIR="${HOME}/.ssh" +MARKER_BEGIN="# --- RUBIX-VPS-BEGIN" +MARKER_END="# --- RUBIX-VPS-END" + +prompt() { + local var_name="$1" + local text="$2" + local default="${3:-}" + local value="" + if [[ -n "${default}" ]]; then + read -r -p "${text} [${default}]: " value + value="${value:-${default}}" + else + read -r -p "${text}: " value + fi + printf -v "${var_name}" '%s' "${value}" +} + +prompt_secret() { + local var_name="$1" + local text="$2" + local value="" + read -r -s -p "${text}: " value + echo "" + printf -v "${var_name}" '%s' "${value}" +} + +prompt_yes_no() { + local var_name="$1" + local text="$2" + local default="${3:-y}" + local hint="Y/n" + [[ "${default}" == "y" ]] && hint="Y/n" || hint="y/N" + local answer="" + read -r -p "${text} [${hint}]: " answer + answer="${answer:-${default}}" + case "${answer}" in + y|Y|yes|YES) printf -v "${var_name}" '%s' "yes" ;; + *) printf -v "${var_name}" '%s' "no" ;; + esac +} + +have_sshpass() { + command -v sshpass >/dev/null 2>&1 +} + +# Install sshpass on the machine running this script (your PC), not on the VPS. +ensure_local_sshpass() { + if have_sshpass; then + return 0 + fi + + echo "[rubix-vps] sshpass not found — installing locally (one sudo prompt) ..." + + local run_as=(bash -c) + if [[ "$(id -u)" -ne 0 ]]; then + if ! command -v sudo >/dev/null 2>&1; then + echo "[rubix-vps] WARN: need sudo to install sshpass (run: sudo apt install sshpass)." >&2 + return 1 + fi + run_as=(sudo bash -c) + fi + + if command -v apt-get >/dev/null 2>&1; then + "${run_as[@]}" 'apt-get update -qq && apt-get install -y sshpass' + elif command -v apt >/dev/null 2>&1; then + "${run_as[@]}" 'apt update -qq && apt install -y sshpass' + elif command -v dnf >/dev/null 2>&1; then + "${run_as[@]}" 'dnf install -y sshpass' + elif command -v yum >/dev/null 2>&1; then + "${run_as[@]}" 'yum install -y sshpass' + elif command -v pacman >/dev/null 2>&1; then + "${run_as[@]}" 'pacman -Sy --noconfirm sshpass' + elif command -v zypper >/dev/null 2>&1; then + "${run_as[@]}" 'zypper install -y sshpass' + else + echo "[rubix-vps] WARN: no supported package manager for auto-install of sshpass." >&2 + return 1 + fi + + if have_sshpass; then + echo "[rubix-vps] sshpass installed." + return 0 + fi + echo "[rubix-vps] WARN: sshpass install failed." >&2 + return 1 +} + +# Stop ssh-agent from offering keys (avoids "Too many authentication failures" on password login). +password_ssh_common_opts() { + printf '%s\n' \ + StrictHostKeyChecking=accept-new \ + IdentitiesOnly=yes \ + IdentityAgent=none \ + PubkeyAuthentication=no \ + PreferredAuthentications=password,keyboard-interactive \ + NumberOfPasswordPrompts=3 +} + +ssh_password() { + local target="$1" + shift + local -a base=(ssh -p "${VPS_PORT}") + local opt + while IFS= read -r opt; do + base+=(-o "${opt}") + done < <(password_ssh_common_opts) + if [[ -n "${ROOT_PASSWORD:-}" ]] && have_sshpass; then + sshpass -p "${ROOT_PASSWORD}" "${base[@]}" "${target}" "$@" + else + "${base[@]}" "${target}" "$@" + fi +} + +scp_password() { + local src="$1" + local dest="$2" + local -a base=(scp -P "${VPS_PORT}") + local opt + while IFS= read -r opt; do + base+=(-o "${opt}") + done < <(password_ssh_common_opts) + if [[ -n "${ROOT_PASSWORD:-}" ]] && have_sshpass; then + sshpass -p "${ROOT_PASSWORD}" "${base[@]}" "${src}" "${dest}" + else + "${base[@]}" "${src}" "${dest}" + fi +} + +ssh_key() { + local user="$1" + shift + ssh -o BatchMode=yes \ + -o StrictHostKeyChecking=accept-new \ + -o PasswordAuthentication=no \ + -o IdentitiesOnly=yes \ + -i "${PRIVATE_KEY_FILE}" \ + -p "${VPS_PORT}" \ + "${user}@${VPS_HOST}" \ + "$@" +} + +test_key_login() { + local user="$1" + ssh_key "${user}" "echo ok" >/dev/null 2>&1 +} + +remote_env() { + printf 'DEPLOY_USER=%q KEEP_ROOT=%q INSTALL_ROOT_KEY=%q' \ + "${DEPLOY_USER}" "${KEEP_ROOT}" "${INSTALL_ROOT_KEY}" +} + +update_local_ssh_config() { + local alias="$1" + local host="$2" + local port="$3" + local user="$4" + local key_file="$5" + + mkdir -p "${SSH_DIR}" + chmod 700 "${SSH_DIR}" + touch "${SSH_CONFIG}" + chmod 600 "${SSH_CONFIG}" + + local block + block="$(cat < "${tmp}" || true + fi + { + cat "${tmp}" + echo "" + printf '%s\n' "${block}" + } > "${SSH_CONFIG}.new" + mv "${SSH_CONFIG}.new" "${SSH_CONFIG}" + rm -f "${tmp}" + echo "[rubix-vps] wrote Host ${alias} → ${SSH_CONFIG}" +} + +print_key_summary() { + local alias="$1" + echo "" + echo "[rubix-vps] SSH key for Host alias \"${alias}\":" + echo " Private key : ${PRIVATE_KEY_FILE}" + echo " Public key : ${PUBLIC_KEY_FILE}" + echo " ssh config : Host ${alias} → IdentityFile ${PRIVATE_KEY_FILE}" + echo "" + echo " authorized_keys line:" + sed 's/^/ /' "${PUBLIC_KEY_FILE}" + echo "" +} + +pick_ssh_key() { + local alias="$1" + local choice="" + local key_path="${SSH_DIR}/${alias}.ed25519" + echo "" + echo "SSH key for ~/.ssh/config Host \"${alias}\":" + echo " Naming: ${key_path} (+ ${key_path}.pub)" + echo "" + echo " 1) Use existing public key" + echo " 2) Generate new ed25519 key pair at ${key_path}" + read -r -p "Choice [2]: " choice + choice="${choice:-2}" + + case "${choice}" in + 1) + local default_pub="${SSH_DIR}/id_ed25519.pub" + prompt PUBLIC_KEY_FILE "Path to public key (.pub)" "${default_pub}" + if [[ ! -f "${PUBLIC_KEY_FILE}" ]]; then + echo "Missing: ${PUBLIC_KEY_FILE}" >&2 + exit 1 + fi + local priv="${PUBLIC_KEY_FILE%.pub}" + if [[ -f "${priv}" ]]; then + PRIVATE_KEY_FILE="${priv}" + else + prompt PRIVATE_KEY_FILE "Path to matching private key" "${SSH_DIR}/id_ed25519" + fi + ;; + *) + if [[ -f "${key_path}" ]]; then + prompt_yes_no OVERWRITE "Key ${key_path} exists. Overwrite?" "n" + if [[ "${OVERWRITE}" != "yes" ]]; then + echo "Aborted." >&2 + exit 1 + fi + rm -f "${key_path}" "${key_path}.pub" + fi + local comment="rubix-vps-${alias}-$(date +%Y%m%d)" + echo "" + echo "[rubix-vps] Generating ${key_path} (comment: ${comment}) ..." + ssh-keygen -t ed25519 -f "${key_path}" -C "${comment}" -N "" + PRIVATE_KEY_FILE="${key_path}" + PUBLIC_KEY_FILE="${key_path}.pub" + ;; + esac + + if [[ ! -f "${PRIVATE_KEY_FILE}" ]]; then + echo "Private key not found: ${PRIVATE_KEY_FILE}" >&2 + exit 1 + fi + chmod 600 "${PRIVATE_KEY_FILE}" 2>/dev/null || true + print_key_summary "${alias}" +} + +remote_install_users_and_keys() { + ssh_password "${INITIAL_USER}@${VPS_HOST}" \ + "$(remote_env)" bash -s <<'REMOTE' +set -euo pipefail + +if [[ "$(id -u)" -ne 0 ]]; then + echo "Remote: must run as root." >&2 + exit 1 +fi + +PUBKEY="$(tr -d '\r\n' < /tmp/rubix-bootstrap.pub)" + +if ! id "${DEPLOY_USER}" >/dev/null 2>&1; then + useradd -m -s /bin/bash "${DEPLOY_USER}" + echo "Created user ${DEPLOY_USER}" +else + echo "User ${DEPLOY_USER} already exists" +fi + +if getent group sudo >/dev/null 2>&1; then + usermod -aG sudo "${DEPLOY_USER}" +elif getent group wheel >/dev/null 2>&1; then + usermod -aG wheel "${DEPLOY_USER}" +fi + +install_key_for_user() { + local u="$1" + local home_dir + home_dir="$(getent passwd "${u}" | cut -d: -f6)" + local ssh_dir="${home_dir}/.ssh" + mkdir -p "${ssh_dir}" + chmod 700 "${ssh_dir}" + touch "${ssh_dir}/authorized_keys" + if ! grep -qxF "${PUBKEY}" "${ssh_dir}/authorized_keys" 2>/dev/null; then + echo "${PUBKEY}" >> "${ssh_dir}/authorized_keys" + fi + chown -R "${u}:${u}" "${ssh_dir}" + chmod 600 "${ssh_dir}/authorized_keys" +} + +install_key_for_user "${DEPLOY_USER}" + +if [[ "${INSTALL_ROOT_KEY}" == "yes" ]]; then + install_key_for_user root +fi + +echo "Keys installed for ${DEPLOY_USER} (and root if requested)." +REMOTE +} + +remote_harden_and_upgrade() { + ssh_password "${INITIAL_USER}@${VPS_HOST}" \ + "$(remote_env)" bash -s <<'REMOTE' +set -euo pipefail + +SSHD="/etc/ssh/sshd_config" +cp -a "${SSHD}" "${SSHD}.rubix-backup-$(date +%Y%m%d%H%M%S)" + +set_sshd_option() { + local key="$1" + local value="$2" + if grep -qE "^[#[:space:]]*${key}[[:space:]]" "${SSHD}"; then + sed -i "s/^[#[:space:]]*${key}[[:space:]].*/${key} ${value}/" "${SSHD}" + else + echo "${key} ${value}" >> "${SSHD}" + fi +} + +set_sshd_option PasswordAuthentication no +set_sshd_option KbdInteractiveAuthentication no +set_sshd_option ChallengeResponseAuthentication no +set_sshd_option PubkeyAuthentication yes +set_sshd_option UsePAM no + +if [[ "${KEEP_ROOT}" == "yes" ]]; then + set_sshd_option PermitRootLogin prohibit-password +else + set_sshd_option PermitRootLogin no +fi + +if sshd -t 2>/dev/null || /usr/sbin/sshd -t 2>/dev/null; then + : +else + latest="$(ls -t ${SSHD}.rubix-backup-* 2>/dev/null | head -1)" + [[ -n "${latest}" ]] && cp -a "${latest}" "${SSHD}" + echo "sshd config test failed — restored backup" >&2 + exit 1 +fi + +if systemctl list-units --type=service --all 2>/dev/null | grep -q 'sshd\.service'; then + systemctl reload sshd +elif systemctl list-units --type=service --all 2>/dev/null | grep -q 'ssh\.service'; then + systemctl reload ssh +else + service ssh reload 2>/dev/null || service sshd reload +fi + +if command -v apt-get >/dev/null 2>&1; then + export DEBIAN_FRONTEND=noninteractive + apt-get update -y + apt-get upgrade -y + apt-get install -y locales + if [[ -f /etc/locale.gen ]]; then + sed -i '/en_US.UTF-8 UTF-8/s/^# *//' /etc/locale.gen + grep -q '^en_US.UTF-8 UTF-8' /etc/locale.gen || echo 'en_US.UTF-8 UTF-8' >> /etc/locale.gen + locale-gen en_US.UTF-8 2>/dev/null || locale-gen + fi + update-locale LANG=en_US.UTF-8 LC_CTYPE=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 2>/dev/null || true + mkdir -p /var/lib/cloud/instance + touch /var/lib/cloud/instance/locale-check.skip 2>/dev/null || true +elif command -v dnf >/dev/null 2>&1; then + dnf -y upgrade +elif command -v yum >/dev/null 2>&1; then + yum -y update +fi + +rm -f /tmp/rubix-bootstrap.pub +echo "SSH hardened and packages upgraded." +REMOTE +} + +main() { + echo "=== Rubix VPS first-time SSH bootstrap (run on your PC) ===" + echo "" + + ensure_local_sshpass || true + + prompt VPS_HOST "VPS hostname or IP" + prompt VPS_PORT "SSH port" "22" + prompt INITIAL_USER "First-login user (usually root)" "root" + prompt DEPLOY_USER "Deploy/sudo user to create" "deployer" + if [[ "${DEPLOY_USER}" == "root" ]]; then + echo "WARN: Using root as deploy user — prefer a sudo user (e.g. deployer) for daily SSH." >&2 + fi + prompt SSH_ALIAS "Local ~/.ssh/config Host alias (e.g. rubix-server-5)" "${DEPLOY_USER}-${VPS_HOST}" + + prompt_yes_no KEEP_ROOT "Keep root SSH login (key-only, no password)" "yes" + prompt_yes_no INSTALL_ROOT_KEY "Install same public key for root" "${KEEP_ROOT}" + + if have_sshpass; then + prompt_secret ROOT_PASSWORD "Initial ${INITIAL_USER} password (entered once via sshpass)" + else + echo "" + echo "[rubix-vps] sshpass unavailable — ssh/scp will prompt for the VPS password each time." + echo "" + prompt_secret ROOT_PASSWORD "Initial ${INITIAL_USER} password (empty = type when ssh asks)" + fi + + mkdir -p "${SSH_DIR}" + chmod 700 "${SSH_DIR}" + pick_ssh_key "${SSH_ALIAS}" + + if [[ ! -f "${PUBLIC_KEY_FILE}" ]]; then + PUBLIC_KEY_FILE="${PRIVATE_KEY_FILE}.pub" + fi + if [[ ! -f "${PUBLIC_KEY_FILE}" ]]; then + echo "Public key not found: ${PUBLIC_KEY_FILE}" >&2 + exit 1 + fi + + echo "" + echo "[rubix-vps] Testing password login ..." + ssh_password "${INITIAL_USER}@${VPS_HOST}" "echo connected && uname -a" + + echo "[rubix-vps] Uploading public key ..." + scp_password "${PUBLIC_KEY_FILE}" "${INITIAL_USER}@${VPS_HOST}:/tmp/rubix-bootstrap.pub" + + echo "[rubix-vps] Creating ${DEPLOY_USER} and installing authorized_keys ..." + remote_install_users_and_keys + + echo "[rubix-vps] Testing key login (before disabling passwords) ..." + if ! test_key_login "${DEPLOY_USER}"; then + echo "ERROR: Key login as ${DEPLOY_USER} failed. Password auth still enabled — fix keys and retry." >&2 + exit 1 + fi + echo "[rubix-vps] Key login OK for ${DEPLOY_USER}" + + if [[ "${INSTALL_ROOT_KEY}" == "yes" && "${DEPLOY_USER}" != "root" ]]; then + if test_key_login root; then + echo "[rubix-vps] Key login OK for root" + else + echo "[rubix-vps] WARN: root key login failed (deploy user works)" >&2 + fi + fi + + echo "[rubix-vps] Disabling password auth, reloading sshd, running apt upgrade ..." + remote_harden_and_upgrade + + echo "[rubix-vps] Testing key login after hardening ..." + if ! test_key_login "${DEPLOY_USER}"; then + echo "ERROR: Key login failed after hardening. Use provider console; restore /etc/ssh/sshd_config.rubix-backup-*" >&2 + exit 1 + fi + + prompt_yes_no WRITE_CONFIG "Add Host ${SSH_ALIAS} to ${SSH_CONFIG}" "yes" + if [[ "${WRITE_CONFIG}" == "yes" ]]; then + update_local_ssh_config "${SSH_ALIAS}" "${VPS_HOST}" "${VPS_PORT}" "${DEPLOY_USER}" "${PRIVATE_KEY_FILE}" + echo "" + echo "Next login:" + echo " ssh ${SSH_ALIAS}" + else + echo "" + echo "Manual login:" + echo " ssh -i ${PRIVATE_KEY_FILE} -p ${VPS_PORT} ${DEPLOY_USER}@${VPS_HOST}" + fi + + echo "" + echo "Done." +} + +main "$@"