#!/usr/bin/env bash # # Базовая подготовка нового сервера перед восстановлением из backup-master. # Основано на vps_installer/setup.sh (https://gitea.hsrv.site/mrleo1nid/vps_installer), # но рассчитано на запуск из `backupctl restore`: # - все параметры приходят через окружение (их выставляет restore по манифесту); # - интерактивны только NEW_USER (по умолчанию DEFAULT_USER) и NEW_PASS; # - apt-пакеты ставятся точных версий со старого сервера, если они есть в репозитории; # - 3x-ui, Nebula, sing-box ставит restore (нужной версии), здесь их нет; # - без перезагрузки — о ней напомнит restore в конце. # # Окружение: # NEW_USER, NEW_PASS, DEFAULT_USER, NEW_HOSTNAME, # INSTALL_BLOCKLIST, BLOCKLIST_URL, HARDEN_SSH (y/n), # INSTALL_CADDY, CADDY_VERSION, INSTALL_TELEGRAF, TELEGRAF_VERSION, # INSTALL_DOCKER, DOCKER_CE_VERSION, DOCKER_CLI_VERSION, COMPOSE_VERSION set -Eeuo pipefail REPO_RAW="https://gitea.hsrv.site/mrleo1nid/vps_installer/raw/branch/main" STATUSPAGE_DIR="/var/www/statuspage" LOG_FILE="/var/log/vps-setup.log" DEFAULT_BLOCKLIST_URL="https://raw.githubusercontent.com/jahlib/somebody-knocked-my-ssh/refs/heads/main/ban.txt" BAN_SCRIPT_URL="https://raw.githubusercontent.com/jahlib/somebody-knocked-my-ssh/refs/heads/main/ban.sh" # 00- — чтобы файл читался раньше 50-cloud-init.conf: sshd берёт ПЕРВОЕ найденное значение SSHD_HARDENING="/etc/ssh/sshd_config.d/00-hardening.conf" export DEBIAN_FRONTEND=noninteractive export NEEDRESTART_MODE=a : "${INSTALL_BLOCKLIST:=y}" "${HARDEN_SSH:=y}" "${INSTALL_CADDY:=n}" "${INSTALL_TELEGRAF:=n}" "${INSTALL_DOCKER:=n}" : "${BLOCKLIST_URL:=$DEFAULT_BLOCKLIST_URL}" # ---------- вывод ---------- if [[ -t 1 ]]; then C_RED=$'\e[31m' C_GRN=$'\e[32m' C_YLW=$'\e[33m' C_BLU=$'\e[1;34m' C_RST=$'\e[0m' else C_RED='' C_GRN='' C_YLW='' C_BLU='' C_RST='' fi step() { echo; echo "${C_BLU}==> $*${C_RST}"; } info() { echo " $*"; } ok() { echo "${C_GRN} ✓ $*${C_RST}"; } warn() { echo "${C_YLW} ! $*${C_RST}"; } die() { echo "${C_RED}ОШИБКА: $*${C_RST}" >&2; exit 1; } trap 'echo "${C_RED}Скрипт упал на строке $LINENO: $BASH_COMMAND${C_RST}" >&2' ERR # ---------- ввод (через /dev/tty) ---------- TTY=/dev/tty have_tty() { [[ -r $TTY && -w $TTY ]] && { : <"$TTY"; } 2>/dev/null; } ask() { # ask VAR "вопрос" [default] local __var=$1 __q=$2 __def=${3:-} __ans [[ -n ${!__var:-} ]] && return 0 have_tty || die "Нет терминала для вопроса «$__q». Задайте переменную $__var." if [[ -n $__def ]]; then read -r -p "$__q [$__def]: " __ans <"$TTY" __ans=${__ans:-$__def} else read -r -p "$__q: " __ans <"$TTY" fi printf -v "$__var" '%s' "$__ans" } ask_password() { # ask_password VAR local __var=$1 p1 p2 [[ -n ${!__var:-} ]] && return 0 have_tty || die "Нет терминала для ввода пароля. Задайте переменную $__var." while true; do read -r -s -p "Пароль для $NEW_USER: " p1 <"$TTY"; echo >"$TTY" read -r -s -p "Повторите пароль: " p2 <"$TTY"; echo >"$TTY" if [[ -z $p1 ]]; then echo "Пароль не может быть пустым." >"$TTY" elif [[ $p1 != "$p2" ]]; then echo "Пароли не совпадают, ещё раз." >"$TTY" elif (( ${#p1} < 8 )); then echo "Слишком короткий, минимум 8 символов." >"$TTY" else break fi done printf -v "$__var" '%s' "$p1" } apt_install() { apt-get install -y -q -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold "$@" } # pkg_spec PKG VERSION — "pkg=version", если такая версия есть в репозитории, иначе "pkg" pkg_spec() { local pkg=$1 ver=${2:-} if [[ -n $ver ]] && apt-cache madison "$pkg" 2>/dev/null | awk -F'|' '{gsub(/ /,"",$2); print $2}' | grep -qxF "$ver"; then echo "$pkg=$ver" else [[ -n $ver ]] && warn "$pkg $ver нет в репозитории — будет установлена последняя версия" >&2 echo "$pkg" fi } # ---------- проверки ---------- [[ $EUID -eq 0 ]] || die "Нужен root" . /etc/os-release [[ ${ID:-} == ubuntu ]] || die "Скрипт рассчитан на Ubuntu (обнаружено: ${PRETTY_NAME:-?})" [[ ${VERSION_ID:-} == 24.04 ]] || warn "Скрипт писался под Ubuntu 24.04, у вас ${PRETTY_NAME}. Продолжаю." ARCH=$(dpkg --print-architecture) CLIENT_IP="" if [[ -n ${SSH_CLIENT:-} ]]; then CLIENT_IP=${SSH_CLIENT%% *} else CLIENT_IP=$(who am i 2>/dev/null | sed -nE 's/.*\(([0-9a-fA-F.:]+)\).*/\1/p' || true) fi # ---------- параметры ---------- step "Параметры подготовки" while true; do ask NEW_USER "Логин пользователя с sudo" "${DEFAULT_USER:-}" if [[ $NEW_USER =~ ^[a-z_][a-z0-9_-]{0,31}$ && $NEW_USER != root ]]; then break; fi echo "Недопустимый логин (строчные латинские буквы, цифры, _ и -; не root)." NEW_USER="" done USER_EXISTS=n if id "$NEW_USER" &>/dev/null; then USER_EXISTS=y info "Пользователь $NEW_USER уже есть — пароль не меняю (задайте NEW_PASS, чтобы сменить)" else ask_password NEW_PASS fi [[ -n ${NEW_HOSTNAME:-} ]] || ask NEW_HOSTNAME "Имя машины (hostname)" "$(hostname)" NEW_HOSTNAME=${NEW_HOSTNAME,,} touch "$LOG_FILE" && chmod 600 "$LOG_FILE" exec > >(tee -a "$LOG_FILE") 2>&1 echo "===== $(date '+%F %T') backup-master provision start =====" >>"$LOG_FILE" # ---------- 1. Обновление системы ---------- step "1. Обновление системы" apt-get update -q apt-get -y -q -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold upgrade apt_install unattended-upgrades curl ca-certificates gnupg cat >/etc/apt/apt.conf.d/20auto-upgrades <<'EOF' APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Unattended-Upgrade "1"; EOF ok "Система обновлена, автообновления безопасности включены" # ---------- 2. Пользователь с sudo ---------- step "2. Пользователь $NEW_USER" if [[ $USER_EXISTS == n ]]; then useradd -m -s /bin/bash "$NEW_USER" ok "Пользователь создан" fi if [[ -n ${NEW_PASS:-} ]]; then echo "$NEW_USER:$NEW_PASS" | chpasswd unset NEW_PASS fi usermod -aG sudo "$NEW_USER" SUDOERS_FILE="/etc/sudoers.d/$NEW_USER" echo "$NEW_USER ALL=(ALL) NOPASSWD:ALL" >"$SUDOERS_FILE.tmp" chmod 440 "$SUDOERS_FILE.tmp" if visudo -c -f "$SUDOERS_FILE.tmp" >/dev/null; then mv "$SUDOERS_FILE.tmp" "$SUDOERS_FILE" ok "sudo без пароля ($SUDOERS_FILE)" else rm -f "$SUDOERS_FILE.tmp" die "visudo отверг файл sudoers" fi # ---------- 3. Имя машины ---------- step "3. Имя машины: $NEW_HOSTNAME" SHORT_HOSTNAME=${NEW_HOSTNAME%%.*} hostnamectl set-hostname "$NEW_HOSTNAME" HOSTS_LINE="127.0.1.1 $NEW_HOSTNAME" [[ $SHORT_HOSTNAME != "$NEW_HOSTNAME" ]] && HOSTS_LINE+=" $SHORT_HOSTNAME" if grep -q '^127\.0\.1\.1[[:space:]]' /etc/hosts; then sed -i "s/^127\.0\.1\.1[[:space:]].*/$HOSTS_LINE/" /etc/hosts else echo "$HOSTS_LINE" >>/etc/hosts fi if [[ -d /etc/cloud/cloud.cfg.d ]]; then echo "preserve_hostname: true" >/etc/cloud/cloud.cfg.d/99-preserve-hostname.cfg fi ok "hostname: $(hostname)" # ---------- 4. iptables legacy ---------- step "4. Переключение на iptables-legacy" apt_install iptables ipset for t in iptables ip6tables arptables ebtables; do if [[ -x /usr/sbin/$t-legacy ]] && update-alternatives --list "$t" &>/dev/null; then update-alternatives --set "$t" "/usr/sbin/$t-legacy" >/dev/null fi done iptables --version | grep -q legacy || die "iptables не переключился на legacy" ok "$(iptables --version)" # ---------- 5. Fail2ban ---------- step "5. Fail2ban" apt_install fail2ban python3-systemd IGNORE_IP="127.0.0.1/8 ::1 192.168.0.0/16 10.0.0.0/8" if [[ -n $CLIENT_IP ]]; then IGNORE_IP+=" $CLIENT_IP" info "Ваш текущий IP $CLIENT_IP добавлен в ignoreip" fi cat >/etc/fail2ban/jail.local </dev/null 2>&1 systemctl restart fail2ban ok "Fail2ban включён" # ---------- 6. UFW ---------- # правила со старого сервера восстановит категория system (ufw reload после распаковки) step "6. Firewall (UFW)" apt_install ufw ufw default deny incoming >/dev/null ufw default allow outgoing >/dev/null ufw allow OpenSSH >/dev/null mkdir -p /run/sshd SSH_PORT=$(sshd -T 2>/dev/null | awk '/^port /{print $2; exit}' || true) if [[ -n $SSH_PORT && $SSH_PORT != 22 ]]; then ufw allow "$SSH_PORT/tcp" >/dev/null fi ufw allow 80/tcp >/dev/null ufw allow 443/tcp >/dev/null ufw --force enable >/dev/null ok "UFW включён: SSH, 80, 443" # ---------- 7. Блокировка IP по списку ---------- if [[ $INSTALL_BLOCKLIST == y ]]; then step "7. Блокировка IP по внешнему списку" curl -fsSL "$BAN_SCRIPT_URL" -o /usr/local/sbin/ban.sh chmod +x /usr/local/sbin/ban.sh if [[ $BLOCKLIST_URL != "$DEFAULT_BLOCKLIST_URL" ]]; then ESCAPED_URL=$(printf '%s' "$BLOCKLIST_URL" | sed 's/[&|\\]/\\&/g') sed -i "s|^URL=.*|URL=\"$ESCAPED_URL\"|" /usr/local/sbin/ban.sh fi bash /usr/local/sbin/ban.sh || warn "ban.sh завершился с ошибкой" cat >/etc/cron.d/ban-blacklist <<'EOF' SHELL=/bin/bash PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin @reboot root sleep 60 && bash /usr/local/sbin/ban.sh >/dev/null 2>&1 0 */6 * * * root bash /usr/local/sbin/ban.sh >/dev/null 2>&1 EOF chmod 644 /etc/cron.d/ban-blacklist ok "ipset blacklist настроен" else step "7. Блокировка IP — пропущено" fi # ---------- 8. Статус-страница ---------- step "8. Статус-страница $STATUSPAGE_DIR" if [[ -f $STATUSPAGE_DIR/index.html ]]; then info "уже есть — не трогаю" else install -d -m 755 "$STATUSPAGE_DIR" curl -fsSL "$REPO_RAW/statuspage/index.html" -o "$STATUSPAGE_DIR/index.html" || warn "статус-страница не скачалась" chmod 644 "$STATUSPAGE_DIR/index.html" 2>/dev/null || true fi # ---------- 9. Docker ---------- if [[ $INSTALL_DOCKER == y ]]; then step "9. Docker + Docker Compose" for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do dpkg -s "$pkg" &>/dev/null && apt-get remove -y -q "$pkg" done install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc echo "deb [arch=$ARCH signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $VERSION_CODENAME stable" \ >/etc/apt/sources.list.d/docker.list apt-get update -q apt_install --allow-downgrades \ "$(pkg_spec docker-ce "${DOCKER_CE_VERSION:-}")" \ "$(pkg_spec docker-ce-cli "${DOCKER_CLI_VERSION:-}")" \ containerd.io docker-buildx-plugin \ "$(pkg_spec docker-compose-plugin "${COMPOSE_VERSION:-}")" systemctl enable --now docker.service containerd.service >/dev/null 2>&1 usermod -aG docker "$NEW_USER" ok "$(docker --version), $(docker compose version | head -n1)" else step "9. Docker — пропущено" fi # ---------- 10. Telegraf ---------- if [[ $INSTALL_TELEGRAF == y ]]; then step "10. Telegraf" mkdir -p /etc/apt/keyrings curl -fsSL https://repos.influxdata.com/influxdata-archive.key \ | gpg --batch --yes --dearmor -o /etc/apt/keyrings/influxdata.gpg echo "deb [signed-by=/etc/apt/keyrings/influxdata.gpg] https://repos.influxdata.com/ubuntu stable main" \ >/etc/apt/sources.list.d/influxdata.list apt-get update -q apt_install --allow-downgrades "$(pkg_spec telegraf "${TELEGRAF_VERSION:-}")" influxdata-archive-keyring # запуск — после восстановления конфига, по состоянию со старого сервера systemctl disable --now telegraf >/dev/null 2>&1 || true ok "$(telegraf --version) установлен" else step "10. Telegraf — пропущено" fi # ---------- 11. Caddy ---------- if [[ $INSTALL_CADDY == y ]]; then step "11. Caddy" apt_install debian-keyring debian-archive-keyring apt-transport-https curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \ | gpg --batch --yes --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \ >/etc/apt/sources.list.d/caddy-stable.list apt-get update -q apt_install --allow-downgrades "$(pkg_spec caddy "${CADDY_VERSION:-}")" ok "Caddy $(caddy version | awk '{print $1}') установлен" else step "11. Caddy — пропущено" fi # ---------- 12. Отключение доступа root ---------- if [[ $HARDEN_SSH == y ]]; then step "12. Отключение доступа root" cat >"$SSHD_HARDENING" <<'EOF' PermitRootLogin no PasswordAuthentication yes KbdInteractiveAuthentication no PubkeyAuthentication yes EOF mkdir -p /run/sshd sshd -t || { rm -f "$SSHD_HARDENING"; die "sshd -t нашёл ошибку в конфигурации"; } systemctl restart ssh passwd -l root >/dev/null ok "Вход root по SSH запрещён, пароль root заблокирован" else step "12. Отключение доступа root — пропущено" fi ok "Базовая подготовка завершена"