Files
2026-10-06 11:45:11 +03:00

495 lines
22 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
#
# Первоначальная подготовка сервера Ubuntu 24.04 по инструкциям
# https://wiki.hsrv.site/books/pervonacalnaia-nastroika-ubuntu
#
# Скрипт только готовит сервер: базовая защита + установка выбранного ПО.
# Конфиги сервисов (Caddy, 3x-ui, Nebula, Telegraf) не трогаются — их либо
# восстанавливают из бекапа, либо настраивают вручную.
#
# Запуск (от root):
# curl -fsSL https://gitea.hsrv.site/mrleo1nid/vps_installer/raw/branch/main/setup.sh | sudo bash
#
# Все вопросы задаются через /dev/tty, поэтому запуск через пайп работает.
# Ответы можно передать заранее через переменные окружения (тогда вопрос не задаётся):
# NEW_USER, NEW_PASS, NEW_HOSTNAME, INSTALL_BLOCKLIST (y/n), BLOCKLIST_URL,
# INSTALL_CADDY, INSTALL_DOCKER, INSTALL_TELEGRAF, INSTALL_NEBULA, NEBULA_LIGHTHOUSE,
# INSTALL_3XUI, HARDEN_SSH, DO_REBOOT (все y/n)
set -Eeuo pipefail
# ---------- версии (как в вики) ----------
XUI_VERSION="v2.8.11"
NEBULA_VERSION="1.10.3"
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
# ---------- вывод ----------
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
# Что осталось сделать руками — выводится в конце
TODO=()
todo() { TODO+=("$*"); }
# ---------- ввод (через /dev/tty, т.к. stdin занят пайпом curl) ----------
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_yn() { # ask_yn VAR "вопрос" y|n
local __var=$1 __q=$2 __def=$3 __ans
if [[ -z ${!__var:-} ]]; then
have_tty || die "Нет терминала для вопроса «$__q». Задайте переменную $__var."
local hint="[y/N]"; [[ $__def == y ]] && hint="[Y/n]"
read -r -p "$__q $hint: " __ans <"$TTY"
__ans=${__ans:-$__def}
else
__ans=${!__var}
fi
case ${__ans,,} in y|yes|д|да) printf -v "$__var" y ;; *) printf -v "$__var" n ;; esac
}
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 "$@"
}
# ---------- проверки ----------
[[ $EUID -eq 0 ]] || die "Запустите от root: curl -fsSL <url> | sudo bash"
. /etc/os-release
[[ ${ID:-} == ubuntu ]] || die "Скрипт рассчитан на Ubuntu (обнаружено: ${PRETTY_NAME:-?})"
[[ ${VERSION_ID:-} == 24.04 ]] || warn "Скрипт писался под Ubuntu 24.04, у вас ${PRETTY_NAME}. Продолжаю."
ARCH=$(dpkg --print-architecture)
# IP, с которого мы подключены (чтобы не забанить себя в fail2ban)
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 "Логин нового пользователя"
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
CONTINUE_EXISTING=""
ask_yn CONTINUE_EXISTING "Пользователь $NEW_USER уже существует. Сменить ему пароль и продолжить?" n
[[ $CONTINUE_EXISTING == y ]] || die "Отменено."
fi
ask_password NEW_PASS
while true; do
ask NEW_HOSTNAME "Имя машины (hostname, можно FQDN, напр. vps1.example.com)" "$(hostname)"
if [[ ${#NEW_HOSTNAME} -le 253 && $NEW_HOSTNAME =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$ ]]; then
NEW_HOSTNAME=${NEW_HOSTNAME,,}
break
fi
echo "Недопустимое имя (латиница, цифры, дефис, части через точку)."
NEW_HOSTNAME=""
done
ask_yn INSTALL_BLOCKLIST "Блокировать IP по внешнему списку (ipset + cron)?" y
if [[ $INSTALL_BLOCKLIST == y ]]; then
ask BLOCKLIST_URL "URL списка IP" "$DEFAULT_BLOCKLIST_URL"
fi
echo
echo "Дополнительное ПО (ставится без настройки):"
ask_yn INSTALL_CADDY " Caddy?" y
ask_yn INSTALL_DOCKER " Docker + Docker Compose?" n
ask_yn INSTALL_TELEGRAF " Telegraf?" n
ask_yn INSTALL_NEBULA " Nebula $NEBULA_VERSION?" n
if [[ $INSTALL_NEBULA == y ]]; then
ask_yn NEBULA_LIGHTHOUSE " Этот сервер — lighthouse (открыть 4242/udp)?" n
fi
ask_yn INSTALL_3XUI " 3x-ui $XUI_VERSION? (установщик задаст свои вопросы)" n
if [[ $INSTALL_NEBULA == y && $ARCH != amd64 && $ARCH != arm64 ]]; then
die "Nebula: архитектура $ARCH не поддерживается скриптом (только amd64/arm64)"
fi
echo
ask_yn HARDEN_SSH "В конце запретить вход root по SSH и заблокировать пароль root?" y
# Всё дальнейшее пишем ещё и в лог (там будут и учётные данные 3x-ui — только для root)
touch "$LOG_FILE" && chmod 600 "$LOG_FILE"
exec > >(tee -a "$LOG_FILE") 2>&1
echo "===== $(date '+%F %T') vps-setup 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
echo "$NEW_USER:$NEW_PASS" | chpasswd
unset NEW_PASS
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
# иначе cloud-init вернёт старое имя при перезагрузке
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)"
NEED_REBOOT=y # старые nft-правила уйдут только после перезагрузки
# ---------- 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 <<EOF
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
# свои IP, чтобы не забанить себя
ignoreip = $IGNORE_IP
[sshd]
enabled = true
backend = systemd
EOF
systemctl enable fail2ban >/dev/null 2>&1
systemctl restart fail2ban
sleep 2
fail2ban-client status sshd >/dev/null && ok "Fail2ban работает, jail sshd активен" \
|| warn "Fail2ban не отвечает, проверьте: journalctl -u fail2ban"
# ---------- 6. UFW ----------
step "6. Firewall (UFW)"
apt_install ufw
ufw default deny incoming >/dev/null
ufw default allow outgoing >/dev/null
ufw allow OpenSSH >/dev/null
# если SSH висит не на 22 — открываем и его
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
info "Открыт нестандартный SSH-порт $SSH_PORT"
fi
# 80 — для выпуска сертификатов Let's Encrypt (Caddy, 3x-ui), 443 — HTTPS
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: $(ipset list blacklist 2>/dev/null | awk '/Number of entries/{print $4}') записей, обновление каждые 6 ч"
else
step "7. Блокировка IP — пропущено"
fi
# ---------- 8. Статус-страница ----------
step "8. Статус-страница $STATUSPAGE_DIR"
if [[ -f $STATUSPAGE_DIR/index.html ]]; then
info "$STATUSPAGE_DIR/index.html уже есть — не трогаю"
else
install -d -m 755 "$STATUSPAGE_DIR"
curl -fsSL "$REPO_RAW/statuspage/index.html" -o "$STATUSPAGE_DIR/index.html"
chmod 644 "$STATUSPAGE_DIR/index.html"
ok "Заглушка положена в $STATUSPAGE_DIR/index.html"
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 docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable docker.service containerd.service >/dev/null 2>&1
usermod -aG docker "$NEW_USER"
ok "$(docker --version), $(docker compose version | head -n1)"
info "$NEW_USER добавлен в группу docker"
warn "Docker публикует порты в обход UFW — биндите на 127.0.0.1:PORT или фильтруйте в DOCKER-USER"
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 telegraf influxdata-archive-keyring
# доступ telegraf к статусу fail2ban
cat >/etc/sudoers.d/telegraf.tmp <<'EOF'
telegraf ALL=(root) NOPASSWD: /usr/bin/fail2ban-client status
telegraf ALL=(root) NOPASSWD: /usr/bin/fail2ban-client status *
Defaults!/usr/bin/fail2ban-client !logfile, !syslog, !pam_session
EOF
chmod 440 /etc/sudoers.d/telegraf.tmp
if visudo -c -f /etc/sudoers.d/telegraf.tmp >/dev/null; then
mv /etc/sudoers.d/telegraf.tmp /etc/sudoers.d/telegraf
else
rm -f /etc/sudoers.d/telegraf.tmp
warn "sudoers для telegraf не прошёл проверку — пропущен"
fi
# с конфигом из пакета он только сыпет ошибками в лог — запустим после настройки
systemctl disable --now telegraf >/dev/null 2>&1 || true
ok "$(telegraf --version) установлен, сервис выключен до настройки"
todo "Telegraf: заполнить /etc/telegraf/telegraf.conf → telegraf --config /etc/telegraf/telegraf.conf --test → systemctl enable --now telegraf"
else
step "10. Telegraf — пропущено"
fi
# ---------- 11. Nebula ----------
if [[ $INSTALL_NEBULA == y ]]; then
step "11. Nebula $NEBULA_VERSION"
NEBULA_TMP=$(mktemp -d)
curl -fsSL "https://github.com/slackhq/nebula/releases/download/v$NEBULA_VERSION/nebula-linux-$ARCH.tar.gz" \
-o "$NEBULA_TMP/nebula.tar.gz"
tar -xzf "$NEBULA_TMP/nebula.tar.gz" -C /usr/local/bin nebula nebula-cert
chmod +x /usr/local/bin/nebula /usr/local/bin/nebula-cert
nebula -version | grep -q "$NEBULA_VERSION" || die "nebula -version не показывает $NEBULA_VERSION"
mkdir -p /etc/nebula
curl -fsSL "https://raw.githubusercontent.com/slackhq/nebula/v$NEBULA_VERSION/examples/config.yml" \
-o /etc/nebula/config.yml.example
curl -fsSL "https://raw.githubusercontent.com/slackhq/nebula/v$NEBULA_VERSION/examples/service_scripts/nebula.service" \
-o /etc/systemd/system/nebula.service
grep -q '^ExecStart=/usr/local/bin/nebula -config /etc/nebula/config.yml' /etc/systemd/system/nebula.service \
|| warn "В nebula.service неожиданный ExecStart — проверьте пути"
systemctl daemon-reload
rm -rf "$NEBULA_TMP"
if [[ ${NEBULA_LIGHTHOUSE:-n} == y ]]; then
ufw allow 4242/udp >/dev/null
info "Открыт 4242/udp для lighthouse"
fi
ok "$(nebula -version | head -n1), юнит установлен, сервис не запущен (нет сертификатов)"
todo "Nebula: положить ca.crt, <node>.crt, <node>.key и config.yml в /etc/nebula (пример: config.yml.example), chmod 600 /etc/nebula/*.key → systemctl enable --now nebula"
else
step "11. Nebula — пропущено"
fi
# ---------- 12. 3x-ui ----------
# ставим до Caddy: установщику 3x-ui для сертификата Let's Encrypt нужен свободный порт 80
if [[ $INSTALL_3XUI == y ]]; then
step "12. 3x-ui $XUI_VERSION"
XUI_INSTALLER=$(mktemp)
curl -fsSL "https://raw.githubusercontent.com/mhsanaei/3x-ui/$XUI_VERSION/install.sh" -o "$XUI_INSTALLER"
info "Дальше — вопросы установщика 3x-ui (порт панели, SSL)."
XUI_STDIN=/dev/null; have_tty && XUI_STDIN=$TTY
if (cd /tmp && bash "$XUI_INSTALLER" "$XUI_VERSION" <"$XUI_STDIN"); then
XUI_PORT=$(/usr/local/x-ui/x-ui setting -show true 2>/dev/null | grep -Eo 'port: [0-9]+' | awk '{print $2}' | head -n1 || true)
if [[ -n $XUI_PORT ]]; then
ufw allow "$XUI_PORT/tcp" >/dev/null
ok "3x-ui установлен, порт панели $XUI_PORT/tcp открыт в UFW"
else
warn "Не удалось определить порт панели — откройте его: ufw allow <port>/tcp"
fi
info "Логин/пароль/путь панели — выше в выводе установщика (и в $LOG_FILE)"
else
warn "Установщик 3x-ui завершился с ошибкой"
fi
rm -f "$XUI_INSTALLER"
else
step "12. 3x-ui — пропущено"
fi
# ---------- 13. Caddy ----------
if [[ $INSTALL_CADDY == y ]]; then
step "13. 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 caddy
ok "Caddy $(caddy version | awk '{print $1}') установлен (стандартный Caddyfile)"
todo "Caddy: заполнить /etc/caddy/Caddyfile → caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy"
else
step "13. Caddy — пропущено"
fi
# ---------- 14. Отключение доступа root ----------
if [[ $HARDEN_SSH == y ]]; then
step "14. Отключение доступа root"
# вход новым пользователем — по паролю, поэтому PasswordAuthentication включён явно
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 "14. Отключение доступа root — пропущено"
fi
# ---------- итог ----------
step "Итоговая проверка"
info "hostname: $(hostname)"
info "iptables: $(iptables --version)"
info "UFW: $(ufw status | head -n1)"
info "Fail2ban: $(systemctl is-active fail2ban || true)"
if [[ $INSTALL_BLOCKLIST == y ]]; then
info "Blacklist: $(ipset list blacklist 2>/dev/null | awk '/Number of entries/{print $4}') записей"
fi
if [[ $INSTALL_DOCKER == y ]]; then info "Docker: $(systemctl is-active docker || true)"; fi
if [[ $INSTALL_TELEGRAF == y ]]; then info "Telegraf: установлен, выключен"; fi
if [[ $INSTALL_NEBULA == y ]]; then info "Nebula: установлен, ждёт сертификатов"; fi
if [[ $INSTALL_3XUI == y ]]; then info "3x-ui: $(systemctl is-active x-ui || true)"; fi
if [[ $INSTALL_CADDY == y ]]; then info "Caddy: $(systemctl is-active caddy || true)"; fi
info "root: $(passwd -S root | awk '{print $2}') (L = заблокирован)"
info "sshd: $(sshd -T 2>/dev/null | grep -E '^(permitrootlogin|passwordauthentication) ' | tr '\n' ' ')"
info "Лог: $LOG_FILE"
if (( ${#TODO[@]} )); then
step "Осталось настроить (или восстановить из бекапа)"
for t in "${TODO[@]}"; do info "• $t"; done
fi
if [[ ${NEED_REBOOT:-n} == y || -f /var/run/reboot-required ]]; then
echo
warn "Нужна перезагрузка (обновление ядра / сброс старых nft-правил)."
ask_yn DO_REBOOT "Перезагрузить сервер сейчас?" y
if [[ $DO_REBOOT == y ]]; then
ok "Перезагрузка через 5 секунд... Подключайтесь как: ssh $NEW_USER@<адрес>"
sleep 5
reboot
else
info "Не забудьте: sudo reboot"
fi
fi
ok "Готово."