diff --git a/README.md b/README.md index f018a91..3bf9567 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,12 @@ | Файл | Назначение | Можно запускать отдельно | | --- | --- | --- | | `ochenstarik-server-1.sh` | Предлагает выбрать часовой пояс, создаёт swap-файл 2 ГБ и задаёт `vm.swappiness=20` | Да | -| `ochenstarik-server-2.sh` | Предлагает выбрать будущий SSH-порт, обновляет систему, ставит пакеты и настраивает UFW | Да | +| `ochenstarik-server-2.sh` | Предлагает выбрать будущий SSH-порт и режим IPv4/IPv6, обновляет систему, ставит пакеты и настраивает UFW | Да | | `ochenstarik-server-user-3.sh` | Создаёт администратора, переносит SSH на порт из этапа 2, проверяет права и включает fail2ban | После установки необходимых пакетов; проще всего сначала запустить скрипт 2 | | `ochenstarik-server-tg-4.sh` | Отправляет уведомления в Telegram при успешном входе по SSH | После установки OpenSSH и `curl`; обычно после скрипта 2 или 3 | | `ochenstarik-server-vpn-5.sh` | Устанавливает Xray и направляет системный трафик через VLESS + REALITY | Да; зависимости устанавливаются автоматически | | `ochenstarik-server-panel-warp-6.sh` | Устанавливает 3x-ui и Cloudflare WARP, настраивает и проверяет их порты | Ubuntu 22.04/24.04/26.04 или Debian 12/13 | +| `ochenstarik-server-backup-7.sh` | Создаёт неизменяемый первичный снимок и настраивает выбранные расписания резервного копирования | Да; зависимости устанавливаются автоматически | Файлы `*.env.example` являются только примерами. Не записывайте настоящие пароли, токены и приватные SSH-ключи в Git. @@ -88,7 +89,17 @@ sysctl vm.swappiness Файл: `ochenstarik-server-2.sh`. -Скрипт запрашивает SSH-порт, который будет применён на этапе 3, сохраняет его в `/etc/ochenstarik-server/ssh-port.conf`, обновляет систему и устанавливает OpenSSH, UFW, fail2ban, инструменты обработки документов, изображений, аудио и видео. +При каждом запуске скрипт предлагает три действия: полную установку или обновление, изменение режима IP без переустановки пакетов либо добавление открытых портов. При полной установке он запрашивает SSH-порт для этапа 3, обновляет систему и устанавливает OpenSSH, UFW, fail2ban, инструменты обработки документов, изображений, аудио и видео. + +Для входящих подключений можно выбрать: + +- только IPv4 — IPv6 отключается в UFW и системно через `sysctl`; этот режим рекомендуется, если 3x-ui используется только по IPv4; +- только IPv6 — управляемые скриптом порты открываются лишь для IPv6; +- IPv4 + IPv6 — порты открываются для обоих семейств. + +Выбор сохраняется в `/etc/ochenstarik-server/ip-family.conf`, а список управляемых портов — в `/etc/ochenstarik-server/ufw-managed-ports.conf`. При переходе на режим с одним семейством старые правила этих портов пересоздаются. Скрипт не разрешит отключить семейство адресов, через которое работает текущая SSH-сессия. Для режима IPv6 также проверяются глобальный IPv6-адрес и маршрут по умолчанию. + +В режиме «только IPv6» системный IPv4 остаётся доступен для исходящих соединений и работы менеджера пакетов, но входящие управляемые порты по IPv4 закрываются UFW. Полное переносимое отключение IPv4 в Linux без привязки к конкретной сетевой конфигурации не выполняется. Открываются TCP-порты: @@ -108,6 +119,8 @@ sudo ./ochenstarik-server-2.sh ```bash sudo ufw status verbose +cat /etc/ochenstarik-server/ip-family.conf +cat /proc/sys/net/ipv6/conf/all/disable_ipv6 timedatectl ``` @@ -131,6 +144,8 @@ timedatectl - настраивает fail2ban; - по выбору управляет правилами UFW. +Правила UFW добавляются с учётом режима IPv4/IPv6, сохранённого скриптом 2. Новые порты также записываются в общий список управляемых правил, поэтому последующее переключение IP-режима пересоздаст их корректно. + Для отдельного запуска сначала установите зависимости: ```bash @@ -238,10 +253,58 @@ sudo ./ochenstarik-server-panel-warp-6.sh В UFW открываются и проверяются `80/tcp`, `443/tcp`, выбранный порт панели и выбранный порт подписок. WARP слушает только локально на `127.0.0.1:40000` либо другом выбранном порту. Этот порт намеренно не публикуется через UFW, поскольку локальный proxy не требует аутентификации. +Публичные порты 3x-ui открываются только для семейств адресов, выбранных на этапе 2. В режиме `ipv4` скрипт не создаёт правил IPv6. + Поддерживаются официально доступные пакеты WARP для Ubuntu `jammy`, `noble`, `resolute` и Debian `bookworm`, `trixie` на `amd64` и `arm64`. Официальные источники: [установка 3x-ui](https://github.com/MHSanaei/3x-ui/wiki/Installation), [пакеты Cloudflare WARP](https://pkg.cloudflareclient.com/) и [режимы WARP](https://developers.cloudflare.com/warp-client/warp-modes/). +## 7. Резервные копии сервера + +Файл: `ochenstarik-server-backup-7.sh`. + +Скрипт создаёт полный сжатый снимок корневой файловой системы с сохранением владельцев, ACL и расширенных атрибутов. Виртуальные файловые системы, временные каталоги, swap, подключённые диски и сам каталог бэкапов исключаются. Первичный снимок создаётся сразу, повторно не перезаписывается и не участвует в ротации. Если файловая система поддерживает атрибут `immutable`, первичный архив и его контрольная сумма дополнительно защищаются от случайного удаления. + +При установке можно выбрать несколько расписаний одним вводом, например `1 3`: + +- ежедневное — хранится 7 последних архивов; +- еженедельное — хранится 4 последних архива; +- ежемесячное — хранится 12 последних архивов; +- только первичный снимок без автоматического расписания. + +```bash +curl -fLO https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main/ochenstarik-server-backup-7.sh +chmod 700 ochenstarik-server-backup-7.sh +bash -n ochenstarik-server-backup-7.sh +sudo ./ochenstarik-server-backup-7.sh +``` + +По умолчанию архивы сохраняются в `/var/backups/ochenstarik-server`, но во время установки можно указать абсолютный путь на другом диске. Незавершённые архивы имеют суффикс `.partial` и не попадают в ротацию. Для каждого готового архива создаётся файл `.sha256`. + +Проверка расписаний и ручной запуск: + +```bash +systemctl list-timers "ochenstarik-backup-*" +sudo /usr/local/sbin/ochenstarik-server-backup daily +sudo journalctl -u ochenstarik-backup@daily.service +``` + +Проверка целостности первичного снимка: + +```bash +cd /var/backups/ochenstarik-server/initial +sha256sum -c ./*.sha256 +``` + +Восстанавливайте архив только из rescue-системы или в отдельный пустой каталог, предварительно проверив контрольную сумму. Пример распаковки в подготовленный каталог `/restore`: + +```bash +mkdir -p /restore +zstd -dc /ПУТЬ/К/АРХИВУ.tar.zst | tar --acls --xattrs --numeric-owner -xpf - -C /restore +``` + +Это локальная резервная копия, а не защита от отказа диска или потери сервера. Регулярно переносите готовые архивы и `.sha256` на отдельный сервер или накопитель. Снимок создаётся с работающей системы; для активно изменяющихся баз данных дополнительно используйте штатный экспорт или согласованный снимок самой базы. + ## Полная установка Если нужны все модули, запускайте их по очереди: @@ -253,9 +316,10 @@ sudo ./ochenstarik-server-user-3.sh sudo ./ochenstarik-server-tg-4.sh sudo ./ochenstarik-server-vpn-5.sh sudo ./ochenstarik-server-panel-warp-6.sh +sudo ./ochenstarik-server-backup-7.sh ``` -VPN, 3x-ui, WARP и Telegram-уведомления являются необязательными этапами. +VPN, 3x-ui, WARP, Telegram-уведомления и автоматические расписания резервного копирования являются необязательными этапами. ## Безопасность diff --git a/ochenstarik-server-2.sh b/ochenstarik-server-2.sh index 43d3b09..65c138f 100644 --- a/ochenstarik-server-2.sh +++ b/ochenstarik-server-2.sh @@ -1,83 +1,375 @@ #!/usr/bin/env bash set -Eeuo pipefail -readonly SSH_PORT_CONFIG="/etc/ochenstarik-server/ssh-port.conf" +readonly CONFIG_DIR="/etc/ochenstarik-server" +readonly SSH_PORT_CONFIG="${CONFIG_DIR}/ssh-port.conf" +readonly IP_FAMILY_CONFIG="${CONFIG_DIR}/ip-family.conf" +readonly MANAGED_PORTS_CONFIG="${CONFIG_DIR}/ufw-managed-ports.conf" +readonly IPV6_SYSCTL_FILE="/etc/sysctl.d/99-zz-ochenstarik-disable-ipv6.conf" +readonly UFW_DEFAULTS="/etc/default/ufw" readonly DEFAULT_SSH_PORT="20202" log() { printf '[+] %s\n' "$*"; } warn() { printf '[!] %s\n' "$*" >&2; } die() { printf '[x] %s\n' "$*" >&2; exit 1; } +require_command() { + command -v "$1" >/dev/null 2>&1 || die "Не найдена команда: $1" +} + is_valid_port() { local port="$1" [[ "$port" =~ ^[0-9]{1,5}$ ]] || return 1 - (( 10#$port >= 1 && 10#$port <= 65535 )) + ((10#$port >= 1 && 10#$port <= 65535)) +} + +read_saved_value() { + local file="$1" key="$2" + [[ -e "$file" ]] || return 0 + [[ -f "$file" && ! -L "$file" ]] \ + || die "$file должен быть обычным файлом, а не символической ссылкой" + sed -n "s/^${key}=//p" "$file" | head -n1 | tr -d '\r' +} + +select_action() { + local choice default_choice=1 + + [[ ! -e "$IP_FAMILY_CONFIG" ]] || default_choice=2 + printf 'Выберите действие:\n' + printf ' 1) Новая установка или полное обновление пакетов\n' + printf ' 2) Изменить режим IPv4/IPv6 без установки пакетов\n' + printf ' 3) Добавить открытые порты без установки пакетов\n' + while :; do + read -rp "Номер действия [${default_choice}]: " choice || die "Ввод был прерван" + choice="${choice:-$default_choice}" + case "$choice" in + 1) ACTION=install; return 0 ;; + 2) ACTION=ip-mode; return 0 ;; + 3) ACTION=add-ports; return 0 ;; + *) warn "Введите 1, 2 или 3" ;; + esac + done } choose_ssh_port() { local default_port="$DEFAULT_SSH_PORT" saved_port selected_port - if [[ -e "$SSH_PORT_CONFIG" ]]; then - [[ -f "$SSH_PORT_CONFIG" && ! -L "$SSH_PORT_CONFIG" ]] \ - || die "$SSH_PORT_CONFIG must be a regular non-symlink file" - saved_port="$(sed -n 's/^SSH_PORT=//p' "$SSH_PORT_CONFIG" | head -n1 | tr -d '\r')" + saved_port="$(read_saved_value "$SSH_PORT_CONFIG" SSH_PORT)" + if [[ -n "$saved_port" ]]; then if is_valid_port "$saved_port"; then default_port="$((10#$saved_port))" else - warn "Ignoring invalid saved SSH port: ${saved_port:-empty}" + warn "Сохранённый SSH-порт некорректен: $saved_port" fi fi while :; do - read -rp "SSH port for step 3 [${default_port}]: " selected_port + read -rp "SSH-порт для этапа 3 [${default_port}]: " selected_port \ + || die "Ввод был прерван" selected_port="${selected_port:-$default_port}" if is_valid_port "$selected_port"; then SSH_PORT="$((10#$selected_port))" return 0 fi - warn "SSH port must be a number between 1 and 65535" + warn "SSH-порт должен быть числом от 1 до 65535" done } -[[ "$EUID" -eq 0 ]] || die "Run this script as root" +choose_ip_mode() { + local saved_mode default_choice choice + + saved_mode="$(read_saved_value "$IP_FAMILY_CONFIG" IP_MODE)" + case "$saved_mode" in + ipv4) default_choice=1 ;; + ipv6) default_choice=2 ;; + both) default_choice=3 ;; + *) default_choice=1 ;; + esac + + printf '\nВыберите семейства IP для входящих подключений:\n' + printf ' 1) Только IPv4 — IPv6 отключается системно и в UFW\n' + printf ' 2) Только IPv6 — открытые входящие порты доступны лишь по IPv6\n' + printf ' 3) IPv4 + IPv6\n' + while :; do + read -rp "Режим [${default_choice}]: " choice || die "Ввод был прерван" + choice="${choice:-$default_choice}" + case "$choice" in + 1) IP_MODE=ipv4; return 0 ;; + 2) IP_MODE=ipv6; return 0 ;; + 3) IP_MODE=both; return 0 ;; + *) warn "Введите 1, 2 или 3" ;; + esac + done +} + +add_managed_rule() { + local rule="$1" + [[ -n "${RULE_SEEN[$rule]:-}" ]] && return 0 + MANAGED_RULES+=("$rule") + RULE_SEEN["$rule"]=1 +} + +load_managed_rules() { + local rule port protocol + MANAGED_RULES=() + RULE_SEEN=() + [[ -e "$MANAGED_PORTS_CONFIG" ]] || return 0 + [[ -f "$MANAGED_PORTS_CONFIG" && ! -L "$MANAGED_PORTS_CONFIG" ]] \ + || die "$MANAGED_PORTS_CONFIG должен быть обычным файлом" + + while IFS= read -r rule || [[ -n "$rule" ]]; do + rule="${rule//$'\r'/}" + [[ -n "$rule" && "$rule" != \#* ]] || continue + [[ "$rule" =~ ^([0-9]+)/(tcp|udp)$ ]] \ + || die "Некорректное правило в $MANAGED_PORTS_CONFIG: $rule" + port="${BASH_REMATCH[1]}" + protocol="${BASH_REMATCH[2]}" + is_valid_port "$port" || die "Некорректный порт в $MANAGED_PORTS_CONFIG: $port" + add_managed_rule "$((10#$port))/${protocol}" + done < "$MANAGED_PORTS_CONFIG" +} + +add_default_rules() { + local saved_ssh_port + + saved_ssh_port="${SSH_PORT:-$(read_saved_value "$SSH_PORT_CONFIG" SSH_PORT)}" + is_valid_port "$saved_ssh_port" || saved_ssh_port="$DEFAULT_SSH_PORT" + for rule in \ + 22/tcp "$((10#$saved_ssh_port))/tcp" 80/tcp 2096/tcp 443/tcp \ + 40000/tcp 63636/tcp; do + add_managed_rule "$rule" + done +} + +collect_additional_ports() { + local input normalized token port protocol rule + local -a tokens additions=() + local -A additions_seen=() + + printf '\nВведите порты через пробел или запятую. TCP используется по умолчанию.\n' + printf 'Пример: 80 443/tcp 53/udp 40000\n' + while :; do + read -rp 'Порты: ' input || die "Ввод был прерван" + normalized="${input//,/ }" + read -r -a tokens <<< "$normalized" + additions=() + additions_seen=() + ((${#tokens[@]} > 0)) || { warn "Введите хотя бы один порт"; continue; } + + for token in "${tokens[@]}"; do + if [[ ! "$token" =~ ^([0-9]+)(/(tcp|udp))?$ ]]; then + warn "Некорректное правило: $token" + additions=() + break + fi + port="${BASH_REMATCH[1]}" + protocol="${BASH_REMATCH[3]:-tcp}" + if ! is_valid_port "$port"; then + warn "Порт должен быть от 1 до 65535: $port" + additions=() + break + fi + rule="$((10#$port))/${protocol}" + if [[ -z "${additions_seen[$rule]:-}" ]]; then + additions+=("$rule") + additions_seen["$rule"]=1 + fi + done + ((${#additions[@]} > 0)) && break + done + + for rule in "${additions[@]}"; do + add_managed_rule "$rule" + done +} + +save_configuration() { + local config_file + install -d -m 700 -o root -g root "$CONFIG_DIR" + for config_file in "$SSH_PORT_CONFIG" "$IP_FAMILY_CONFIG" "$MANAGED_PORTS_CONFIG"; do + [[ ! -L "$config_file" ]] || die "Отказ от записи через символическую ссылку: $config_file" + done + printf 'SSH_PORT=%s\n' "$SSH_PORT" > "$SSH_PORT_CONFIG" + printf '%s\n' "${MANAGED_RULES[@]}" > "$MANAGED_PORTS_CONFIG" + chown root:root "$SSH_PORT_CONFIG" "$MANAGED_PORTS_CONFIG" + chmod 600 "$SSH_PORT_CONFIG" "$MANAGED_PORTS_CONFIG" +} + +set_ufw_ipv6_setting() { + local value="$1" + [[ -f "$UFW_DEFAULTS" && ! -L "$UFW_DEFAULTS" ]] \ + || die "Не найден безопасный файл $UFW_DEFAULTS" + if grep -q '^IPV6=' "$UFW_DEFAULTS"; then + sed -i "s/^IPV6=.*/IPV6=${value}/" "$UFW_DEFAULTS" + else + printf '\nIPV6=%s\n' "$value" >> "$UFW_DEFAULTS" + fi +} + +disable_ipv6_systemwide() { + cat > "$IPV6_SYSCTL_FILE" <<'EOF' +# Managed by ochenstarik-server-2.sh +net.ipv6.conf.all.disable_ipv6 = 1 +net.ipv6.conf.default.disable_ipv6 = 1 +net.ipv6.conf.lo.disable_ipv6 = 1 +EOF + chmod 644 "$IPV6_SYSCTL_FILE" + sysctl -p "$IPV6_SYSCTL_FILE" >/dev/null + [[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null || true)" == 1 ]] \ + || die "Не удалось отключить IPv6" +} + +enable_ipv6_systemwide() { + local sysctl_path + local -a sysctl_paths=(/proc/sys/net/ipv6/conf/*/disable_ipv6) + + rm -f -- "$IPV6_SYSCTL_FILE" + for sysctl_path in "${sysctl_paths[@]}"; do + [[ -e "$sysctl_path" ]] || continue + printf '0\n' > "$sysctl_path" || die "Не удалось включить IPv6 через $sysctl_path" + done +} + +check_session_safety() { + local client_ip client_port server_ip server_port + + [[ -n "${SSH_CONNECTION:-}" ]] || return 0 + read -r client_ip client_port server_ip server_port <<< "$SSH_CONNECTION" + if [[ "$IP_MODE" == ipv4 && "$server_ip" == *:* ]]; then + die "Текущая SSH-сессия использует IPv6. Сначала подключитесь по IPv4 или используйте консоль провайдера" + fi + if [[ "$IP_MODE" == ipv6 && "$server_ip" == *.* ]]; then + die "Текущая SSH-сессия использует IPv4. Для режима IPv6 подключитесь по IPv6 или используйте консоль провайдера" + fi +} + +verify_ipv6_connectivity() { + ip -6 address show scope global | grep -q 'inet6' \ + || die "На сервере нет глобального IPv6-адреса" + ip -6 route show default | grep -q '^default' \ + || die "На сервере нет маршрута IPv6 по умолчанию" +} + +delete_managed_ufw_rules() { + local rule port protocol attempt + + for rule in "${MANAGED_RULES[@]}"; do + port="${rule%/*}" + protocol="${rule#*/}" + for attempt in {1..5}; do + ufw --force delete allow "$rule" >/dev/null 2>&1 || break + done + ufw --force delete allow from 0.0.0.0/0 to any port "$port" proto "$protocol" \ + >/dev/null 2>&1 || true + ufw --force delete allow from ::/0 to any port "$port" proto "$protocol" \ + >/dev/null 2>&1 || true + done +} + +allow_rule_for_selected_families() { + local rule="$1" port protocol + port="${rule%/*}" + protocol="${rule#*/}" + + if [[ "$IP_MODE" == ipv4 || "$IP_MODE" == both ]]; then + ufw allow from 0.0.0.0/0 to any port "$port" proto "$protocol" + fi + if [[ "$IP_MODE" == ipv6 || "$IP_MODE" == both ]]; then + ufw allow from ::/0 to any port "$port" proto "$protocol" + fi +} + +apply_ip_mode_and_firewall() { + local rule + + check_session_safety + if [[ "$IP_MODE" == ipv4 ]]; then + delete_managed_ufw_rules + set_ufw_ipv6_setting no + disable_ipv6_systemwide + else + enable_ipv6_systemwide + [[ "$IP_MODE" != ipv6 ]] || verify_ipv6_connectivity + set_ufw_ipv6_setting yes + ufw reload >/dev/null 2>&1 || true + delete_managed_ufw_rules + fi + + ufw default deny incoming + ufw default allow outgoing + for rule in "${MANAGED_RULES[@]}"; do + log "Открываю ${rule} для режима ${IP_MODE}" + allow_rule_for_selected_families "$rule" + done + ufw --force enable + [[ ! -L "$IP_FAMILY_CONFIG" ]] \ + || die "Отказ от записи через символическую ссылку: $IP_FAMILY_CONFIG" + printf 'IP_MODE=%s\n' "$IP_MODE" > "$IP_FAMILY_CONFIG" + chown root:root "$IP_FAMILY_CONFIG" + chmod 600 "$IP_FAMILY_CONFIG" +} + +[[ "$EUID" -eq 0 ]] || die "Запустите этот скрипт от имени root" +declare -a MANAGED_RULES=() +declare -A RULE_SEEN=() +ACTION="" SSH_PORT="" -choose_ssh_port -export DEBIAN_FRONTEND=noninteractive +IP_MODE="" +select_action -log "Updating the operating system" -apt-get update -apt-get upgrade -y +if [[ "$ACTION" == install ]]; then + choose_ssh_port + choose_ip_mode + export DEBIAN_FRONTEND=noninteractive -log "Installing server and document-processing packages" -apt-get install -y \ - sudo ufw fail2ban curl ca-certificates openssl openssh-server logrotate \ - poppler-utils qpdf ghostscript ocrmypdf \ - tesseract-ocr tesseract-ocr-eng tesseract-ocr-rus \ - libreoffice pandoc antiword catdoc imagemagick libimage-exiftool-perl webp \ - ffmpeg mediainfo p7zip-full unzip zip unrar jq yq csvkit sqlite3 \ - python3-pip python3-venv mc + log "Обновляю операционную систему" + apt-get update + apt-get upgrade -y -install -d -m 700 -o root -g root "$(dirname "$SSH_PORT_CONFIG")" -printf 'SSH_PORT=%s\n' "$SSH_PORT" > "$SSH_PORT_CONFIG" -chown root:root "$SSH_PORT_CONFIG" -chmod 600 "$SSH_PORT_CONFIG" -log "Saved SSH port ${SSH_PORT} for step 3" + log "Устанавливаю серверные пакеты и средства обработки документов" + apt-get install -y \ + sudo ufw fail2ban curl ca-certificates openssl openssh-server logrotate iproute2 \ + poppler-utils qpdf ghostscript ocrmypdf \ + tesseract-ocr tesseract-ocr-eng tesseract-ocr-rus \ + libreoffice pandoc antiword catdoc imagemagick libimage-exiftool-perl webp \ + ffmpeg mediainfo p7zip-full unzip zip unrar jq yq csvkit sqlite3 \ + python3-pip python3-venv mc -log "Configuring UFW" -# Keep port 22 open until step 3 moves SSH to the selected port and the new login is tested. -ufw allow "22/tcp" -ufw allow "${SSH_PORT}/tcp" -ufw allow "80/tcp" -ufw allow "2096/tcp" -ufw allow "443/tcp" -ufw allow "40000/tcp" -ufw allow "63636/tcp" -ufw --force enable + for command_name in ip sed sysctl ufw; do + require_command "$command_name" + done + install -d -m 700 -o root -g root "$CONFIG_DIR" + load_managed_rules + add_default_rules + save_configuration + apply_ip_mode_and_firewall + log "SSH-порт ${SSH_PORT} сохранён для этапа 3" +else + for command_name in ip sed sysctl ufw; do + require_command "$command_name" + done + install -d -m 700 -o root -g root "$CONFIG_DIR" + SSH_PORT="$(read_saved_value "$SSH_PORT_CONFIG" SSH_PORT)" + is_valid_port "$SSH_PORT" || SSH_PORT="$DEFAULT_SSH_PORT" + load_managed_rules + add_default_rules -log "Installed versions" -printf 'Timezone: %s\n' "$(timedatectl show --property=Timezone --value)" + if [[ "$ACTION" == ip-mode ]]; then + choose_ip_mode + else + IP_MODE="$(read_saved_value "$IP_FAMILY_CONFIG" IP_MODE)" + case "$IP_MODE" in ipv4|ipv6|both) ;; *) choose_ip_mode ;; esac + collect_additional_ports + fi + save_configuration + apply_ip_mode_and_firewall +fi + +log "Текущая конфигурация" +printf 'Режим IP: %s\n' "$IP_MODE" +printf 'IPv6 в ядре: %s\n' "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null || printf 'недоступно')" ufw status verbose -printf '\nDone. Now run ochenstarik-server-user-3.sh as root.\n' -printf 'Step 3 will move SSH to port %s.\n' "$SSH_PORT" -printf 'Do not remove the UFW rule for port 22 until SSH login on port %s succeeds.\n' "$SSH_PORT" +printf '\nГотово. Следующий этап: ochenstarik-server-user-3.sh\n' +printf 'Этап 3 перенесёт SSH на порт %s.\n' "$SSH_PORT" +printf 'Не удаляйте правило порта 22, пока не проверите вход по порту %s.\n' "$SSH_PORT" diff --git a/ochenstarik-server-backup-7.sh b/ochenstarik-server-backup-7.sh new file mode 100644 index 0000000..1995a69 --- /dev/null +++ b/ochenstarik-server-backup-7.sh @@ -0,0 +1,376 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +readonly DEFAULT_BACKUP_ROOT="/var/backups/ochenstarik-server" +readonly CONFIG_DIR="/etc/ochenstarik-server" +readonly CONFIG_FILE="${CONFIG_DIR}/backup.conf" +readonly BACKUP_RUNNER="/usr/local/sbin/ochenstarik-server-backup" +readonly SERVICE_FILE="/etc/systemd/system/ochenstarik-backup@.service" +readonly TIMER_PREFIX="/etc/systemd/system/ochenstarik-backup" + +log() { printf '[+] %s\n' "$*"; } +warn() { printf '[!] %s\n' "$*" >&2; } +die() { printf '[x] %s\n' "$*" >&2; exit 1; } + +require_command() { + command -v "$1" >/dev/null 2>&1 || die "Не найдена команда: $1" +} + +choose_schedules() { + local answer token + declare -gA SELECTED_SCHEDULES=() + + while :; do + printf '\nВыберите автоматические резервные копии (можно несколько):\n' + printf ' 1) Ежедневные — хранить последние 7\n' + printf ' 2) Еженедельные — хранить последние 4\n' + printf ' 3) Ежемесячные — хранить последние 12\n' + printf ' 4) Все расписания\n' + printf ' 0) Только первичный снимок, без расписания\n' + read -rp "Введите номера через пробел или запятую [4]: " answer \ + || die "Ввод был прерван" + answer="${answer:-4}" + answer="${answer//,/ }" + SELECTED_SCHEDULES=() + + if [[ "$answer" =~ ^[[:space:]]*4[[:space:]]*$ ]]; then + SELECTED_SCHEDULES[daily]=1 + SELECTED_SCHEDULES[weekly]=1 + SELECTED_SCHEDULES[monthly]=1 + return 0 + fi + if [[ "$answer" =~ ^[[:space:]]*0[[:space:]]*$ ]]; then + return 0 + fi + + for token in $answer; do + case "$token" in + 1) SELECTED_SCHEDULES[daily]=1 ;; + 2) SELECTED_SCHEDULES[weekly]=1 ;; + 3) SELECTED_SCHEDULES[monthly]=1 ;; + *) + warn "Неизвестный пункт: $token" + SELECTED_SCHEDULES=() + break + ;; + esac + done + ((${#SELECTED_SCHEDULES[@]} > 0)) && return 0 + warn "Выберите 1, 2, 3, их комбинацию, 4 или 0" + done +} + +choose_backup_root() { + local entered_path + + read -rp "Каталог резервных копий [${DEFAULT_BACKUP_ROOT}]: " entered_path \ + || die "Ввод был прерван" + entered_path="${entered_path:-$DEFAULT_BACKUP_ROOT}" + [[ "$entered_path" == /* ]] || die "Укажите абсолютный путь, начинающийся с /" + BACKUP_ROOT="$(readlink -m -- "$entered_path")" + [[ "$BACKUP_ROOT" =~ ^/[A-Za-z0-9._/-]+$ ]] \ + || die "Путь для бэкапов может содержать только латинские буквы, цифры, /, точку, дефис и подчёркивание" + case "$BACKUP_ROOT" in + /|/proc|/proc/*|/sys|/sys/*|/dev|/dev/*|/run|/run/*|/tmp|/tmp/*) + die "Нельзя хранить резервные копии в $BACKUP_ROOT" + ;; + esac +} + +write_backup_runner() { + cat > "$BACKUP_RUNNER" <<'BACKUP_RUNNER_EOF' +#!/usr/bin/env bash +set -Eeuo pipefail +umask 077 + +readonly CONFIG_FILE="/etc/ochenstarik-server/backup.conf" +readonly LOCK_FILE="/run/lock/ochenstarik-server-backup.lock" +readonly MIN_FREE_RESERVE=$((512 * 1024 * 1024)) + +log() { printf '[+] %s\n' "$*"; } +warn() { printf '[!] %s\n' "$*" >&2; } +die() { printf '[x] %s\n' "$*" >&2; exit 1; } + +human_size() { + numfmt --to=iec-i --suffix=B "$1" +} + +protect_initial_files() { + local file + command -v chattr >/dev/null 2>&1 || return 0 + for file in "$BACKUP_ROOT"/initial/initial-*.tar.zst \ + "$BACKUP_ROOT"/initial/initial-*.tar.zst.sha256; do + [[ -e "$file" ]] || continue + chattr +i -- "$file" 2>/dev/null \ + || warn "Файловая система не поддерживает immutable для $file" + done +} + +check_free_space() { + local root_device backup_device root_used backup_used source_used + local available reserve estimated_required backup_total + + root_device="$(df --output=source / | awk 'NR == 2 { print $1 }')" + backup_device="$(df --output=source "$BACKUP_ROOT" | awk 'NR == 2 { print $1 }')" + root_used="$(df --output=used -B1 / | awk 'NR == 2 { print $1 }')" + backup_total="$(df --output=size -B1 "$BACKUP_ROOT" | awk 'NR == 2 { print $1 }')" + available="$(df --output=avail -B1 "$BACKUP_ROOT" | awk 'NR == 2 { print $1 }')" + source_used="$root_used" + + if [[ "$root_device" == "$backup_device" ]]; then + backup_used="$(du -s -B1 -- "$BACKUP_ROOT" | awk '{ print $1 }')" + ((backup_used < source_used)) && source_used=$((source_used - backup_used)) + fi + + reserve=$((backup_total / 20)) + ((reserve >= MIN_FREE_RESERVE)) || reserve="$MIN_FREE_RESERVE" + estimated_required=$((source_used / 2 + reserve)) + if ((available < estimated_required)); then + die "Недостаточно свободного места: доступно $(human_size "$available"), приблизительно требуется $(human_size "$estimated_required"). Укажите другой диск в $CONFIG_FILE" + fi +} + +rotate_backups() { + local backup_kind="$1" keep_count="$2" index filename + local target_dir="${BACKUP_ROOT}/${backup_kind}" + local -a archives + + mapfile -t archives < <( + find "$target_dir" -maxdepth 1 -type f -name "${backup_kind}-*.tar.zst" \ + -printf '%f\n' | sort -r + ) + for ((index = keep_count; index < ${#archives[@]}; index++)); do + filename="${archives[index]}" + rm -f -- "$target_dir/$filename" "$target_dir/${filename}.sha256" + log "Удалён старый ${backup_kind}-архив: $filename" + done +} + +create_backup() { + local backup_kind="$1" keep_count="$2" timestamp target_dir + local archive temporary checksum checksum_temporary exclude_backup_root + + target_dir="${BACKUP_ROOT}/${backup_kind}" + mkdir -p -m 700 -- "$target_dir" + + if [[ "$backup_kind" == initial ]] && + find "$target_dir" -maxdepth 1 -type f -name 'initial-*.tar.zst' -print -quit | grep -q .; then + log "Первичный снимок уже существует и не будет перезаписан" + protect_initial_files + return 0 + fi + + check_free_space + timestamp="$(date +%Y%m%d-%H%M%S)" + archive="${target_dir}/${backup_kind}-${timestamp}.tar.zst" + temporary="${archive}.partial" + checksum="${archive}.sha256" + checksum_temporary="${checksum}.partial" + exclude_backup_root=".${BACKUP_ROOT}" + trap 'rm -f -- "${temporary:-}" "${checksum_temporary:-}"' EXIT + + [[ ! -e "$archive" && ! -e "$temporary" ]] \ + || die "Архив с таким именем уже существует: $archive" + log "Создаю ${backup_kind}-архив: $archive" + tar \ + --create \ + --file=- \ + --directory=/ \ + --one-file-system \ + --acls \ + --xattrs \ + --xattrs-include='*' \ + --numeric-owner \ + --sparse \ + --ignore-failed-read \ + --warning=no-file-changed \ + --exclude='./proc' \ + --exclude='./proc/*' \ + --exclude='./sys' \ + --exclude='./sys/*' \ + --exclude='./dev' \ + --exclude='./dev/*' \ + --exclude='./run' \ + --exclude='./run/*' \ + --exclude='./tmp' \ + --exclude='./tmp/*' \ + --exclude='./var/tmp' \ + --exclude='./var/tmp/*' \ + --exclude='./mnt' \ + --exclude='./mnt/*' \ + --exclude='./media' \ + --exclude='./media/*' \ + --exclude='./lost+found' \ + --exclude='./swapfile' \ + --exclude="$exclude_backup_root" \ + --exclude="${exclude_backup_root}/*" \ + . | zstd -T0 -3 --quiet -o "$temporary" + + [[ -s "$temporary" ]] || die "Получился пустой архив" + chmod 600 "$temporary" + mv -- "$temporary" "$archive" + ( + cd "$target_dir" + sha256sum "$(basename "$archive")" > "$(basename "$checksum_temporary")" + ) + chmod 600 "$checksum_temporary" + mv -- "$checksum_temporary" "$checksum" + trap - EXIT + + if [[ "$backup_kind" == initial ]]; then + protect_initial_files + else + rotate_backups "$backup_kind" "$keep_count" + fi + log "Готово: $archive ($(human_size "$(stat -c %s "$archive")"))" +} + +[[ "$EUID" -eq 0 ]] || die "Запустите команду от имени root" +[[ -f "$CONFIG_FILE" && ! -L "$CONFIG_FILE" ]] \ + || die "Не найден безопасный файл настроек: $CONFIG_FILE" +[[ "$(stat -c %u "$CONFIG_FILE")" == 0 ]] \ + || die "Файл настроек должен принадлежать root: $CONFIG_FILE" +# shellcheck source=/dev/null +source "$CONFIG_FILE" +[[ "${BACKUP_ROOT:-}" == /* && "$BACKUP_ROOT" != / ]] \ + || die "Некорректный BACKUP_ROOT в $CONFIG_FILE" +mkdir -p -m 700 -- "$BACKUP_ROOT"/{initial,daily,weekly,monthly} + +exec 9>"$LOCK_FILE" +flock -n 9 || die "Другое резервное копирование уже выполняется" + +case "${1:-}" in + initial) create_backup initial 0 ;; + daily) create_backup daily 7 ;; + weekly) create_backup weekly 4 ;; + monthly) create_backup monthly 12 ;; + *) die "Использование: $0 {initial|daily|weekly|monthly}" ;; +esac +BACKUP_RUNNER_EOF + chmod 700 "$BACKUP_RUNNER" +} + +write_systemd_units() { + cat > "$SERVICE_FILE" < "${TIMER_PREFIX}-daily.timer" <<'EOF' +[Unit] +Description=Daily Ochenstarik server backup + +[Timer] +OnCalendar=*-*-* 03:15:00 +Persistent=true +RandomizedDelaySec=10m +Unit=ochenstarik-backup@daily.service + +[Install] +WantedBy=timers.target +EOF + + cat > "${TIMER_PREFIX}-weekly.timer" <<'EOF' +[Unit] +Description=Weekly Ochenstarik server backup + +[Timer] +OnCalendar=Sun *-*-* 04:15:00 +Persistent=true +RandomizedDelaySec=10m +Unit=ochenstarik-backup@weekly.service + +[Install] +WantedBy=timers.target +EOF + + cat > "${TIMER_PREFIX}-monthly.timer" <<'EOF' +[Unit] +Description=Monthly Ochenstarik server backup + +[Timer] +OnCalendar=*-*-01 05:15:00 +Persistent=true +RandomizedDelaySec=10m +Unit=ochenstarik-backup@monthly.service + +[Install] +WantedBy=timers.target +EOF + chmod 644 "$SERVICE_FILE" "${TIMER_PREFIX}-daily.timer" \ + "${TIMER_PREFIX}-weekly.timer" "${TIMER_PREFIX}-monthly.timer" +} + +apply_schedule_selection() { + local schedule timer + + systemctl daemon-reload + for schedule in daily weekly monthly; do + timer="ochenstarik-backup-${schedule}.timer" + if [[ -n "${SELECTED_SCHEDULES[$schedule]:-}" ]]; then + systemctl enable --now "$timer" + else + systemctl disable --now "$timer" >/dev/null 2>&1 || true + fi + done +} + +print_summary() { + local schedule + + printf '\nНастройка резервного копирования завершена.\n' + printf 'Каталог: %s\n' "$BACKUP_ROOT" + printf 'Первичный снимок: создан один раз, автоматическая ротация отключена.\n' + printf 'Активные расписания:' + if ((${#SELECTED_SCHEDULES[@]} == 0)); then + printf ' нет\n' + else + for schedule in daily weekly monthly; do + [[ -n "${SELECTED_SCHEDULES[$schedule]:-}" ]] && printf ' %s' "$schedule" + done + printf '\n' + fi + printf '\nПроверка таймеров:\n systemctl list-timers "ochenstarik-backup-*"\n' + printf 'Ручной запуск:\n sudo %s daily\n' "$BACKUP_RUNNER" + printf 'Проверка архива:\n cd %s/initial && sha256sum -c ./*.sha256\n' "$BACKUP_ROOT" + printf '\nХраните дополнительную копию на другом сервере или диске.\n' +} + +[[ "$EUID" -eq 0 ]] || die "Запустите этот скрипт от имени root" +for command_name in apt-get cat chmod install readlink systemctl; do + require_command "$command_name" +done + +choose_schedules +choose_backup_root + +export DEBIAN_FRONTEND=noninteractive +log "Устанавливаю зависимости" +apt-get update +apt-get install -y coreutils e2fsprogs findutils tar util-linux zstd +for command_name in awk basename chattr df du find flock grep numfmt sha256sum sort stat tar zstd; do + require_command "$command_name" +done + +install -d -o root -g root -m 700 "$CONFIG_DIR" "$BACKUP_ROOT" +[[ ! -L "$CONFIG_FILE" ]] || die "Отказ от записи через символическую ссылку: $CONFIG_FILE" +printf 'BACKUP_ROOT=%q\n' "$BACKUP_ROOT" > "$CONFIG_FILE" +chown root:root "$CONFIG_FILE" +chmod 600 "$CONFIG_FILE" + +write_backup_runner +write_systemd_units + +log "Создаю первичный снимок (при повторном запуске существующий не изменяется)" +"$BACKUP_RUNNER" initial +apply_schedule_selection +print_summary diff --git a/ochenstarik-server-panel-warp-6.sh b/ochenstarik-server-panel-warp-6.sh index 91cdfb1..b114d20 100644 --- a/ochenstarik-server-panel-warp-6.sh +++ b/ochenstarik-server-panel-warp-6.sh @@ -11,6 +11,8 @@ readonly DEFAULT_PANEL_PORT="2053" readonly DEFAULT_SUBSCRIPTION_PORT="2096" readonly DEFAULT_WARP_PORT="40000" readonly SSH_PORT_CONFIG="/etc/ochenstarik-server/ssh-port.conf" +readonly IP_FAMILY_CONFIG="/etc/ochenstarik-server/ip-family.conf" +readonly MANAGED_PORTS_CONFIG="/etc/ochenstarik-server/ufw-managed-ports.conf" TMP_DIR="" @@ -78,6 +80,26 @@ read_saved_ssh_port() { SAVED_SSH_PORT="$((10#$saved_port))" } +read_ip_mode() { + local mode=both + if [[ -e "$IP_FAMILY_CONFIG" ]]; then + [[ -f "$IP_FAMILY_CONFIG" && ! -L "$IP_FAMILY_CONFIG" ]] \ + || die "$IP_FAMILY_CONFIG должен быть обычным файлом" + mode="$(sed -n 's/^IP_MODE=//p' "$IP_FAMILY_CONFIG" | head -n1 | tr -d '\r')" + fi + case "$mode" in ipv4|ipv6|both) printf '%s' "$mode" ;; *) die "Некорректный IP_MODE: $mode" ;; esac +} + +record_managed_ufw_rule() { + local rule="$1" + install -d -m 700 -o root -g root "$(dirname "$MANAGED_PORTS_CONFIG")" + [[ ! -L "$MANAGED_PORTS_CONFIG" ]] || die "$MANAGED_PORTS_CONFIG не должен быть символической ссылкой" + touch "$MANAGED_PORTS_CONFIG" + grep -Fqx -- "$rule" "$MANAGED_PORTS_CONFIG" || printf '%s\n' "$rule" >> "$MANAGED_PORTS_CONFIG" + chown root:root "$MANAGED_PORTS_CONFIG" + chmod 600 "$MANAGED_PORTS_CONFIG" +} + wait_for_tcp_listener() { local port="$1" description="$2" attempt for attempt in {1..20}; do @@ -91,15 +113,30 @@ wait_for_tcp_listener() { } allow_ufw_port() { - local port="$1" description="$2" + local port="$1" description="$2" mode + mode="$(read_ip_mode)" log "UFW: открываю ${port}/tcp (${description})" - ufw allow "${port}/tcp" + record_managed_ufw_rule "${port}/tcp" + if [[ "$mode" == ipv4 || "$mode" == both ]]; then + ufw allow from 0.0.0.0/0 to any port "$port" proto tcp + fi + if [[ "$mode" == ipv6 || "$mode" == both ]]; then + ufw allow from ::/0 to any port "$port" proto tcp + fi } verify_ufw_port() { - local port="$1" - LANG=C ufw status | grep -Eq "(^|[[:space:]])${port}/tcp[[:space:]]+ALLOW" \ - || die "В UFW не найдено разрешающее правило для ${port}/tcp" + local port="$1" mode status + mode="$(read_ip_mode)" + status="$(LANG=C ufw status)" + if [[ "$mode" == ipv4 || "$mode" == both ]]; then + grep -Eq "^${port}/tcp[[:space:]]+ALLOW" <<< "$status" \ + || die "В UFW не найдено IPv4-правило для ${port}/tcp" + fi + if [[ "$mode" == ipv6 || "$mode" == both ]]; then + grep -Eq "^${port}/tcp \(v6\)[[:space:]]+ALLOW" <<< "$status" \ + || die "В UFW не найдено IPv6-правило для ${port}/tcp" + fi } set_xui_setting() { diff --git a/ochenstarik-server-user-3.sh b/ochenstarik-server-user-3.sh index 31fc28f..ef1caa4 100644 --- a/ochenstarik-server-user-3.sh +++ b/ochenstarik-server-user-3.sh @@ -6,6 +6,8 @@ readonly HASH_FILE="${HASH_FILE:-/root/setup-data/password.hash}" readonly SSHD_DROPIN="/etc/ssh/sshd_config.d/00-hermes-hardening.conf" readonly FAIL2BAN_JAIL="/etc/fail2ban/jail.d/hermes.local" readonly SSH_PORT_CONFIG="/etc/ochenstarik-server/ssh-port.conf" +readonly IP_FAMILY_CONFIG="/etc/ochenstarik-server/ip-family.conf" +readonly MANAGED_PORTS_CONFIG="/etc/ochenstarik-server/ufw-managed-ports.conf" log() { printf '[+] %s\n' "$*"; } warn() { printf '[!] %s\n' "$*" >&2; } @@ -64,6 +66,40 @@ is_valid_port() { (( 10#$port >= 1 && 10#$port <= 65535 )) } +read_ip_mode() { + local mode=both + if [[ -e "$IP_FAMILY_CONFIG" ]]; then + [[ -f "$IP_FAMILY_CONFIG" && ! -L "$IP_FAMILY_CONFIG" ]] \ + || die "$IP_FAMILY_CONFIG must be a regular file" + mode="$(sed -n 's/^IP_MODE=//p' "$IP_FAMILY_CONFIG" | head -n1 | tr -d '\r')" + fi + case "$mode" in ipv4|ipv6|both) printf '%s' "$mode" ;; *) die "Invalid IP_MODE: $mode" ;; esac +} + +record_managed_ufw_rule() { + local rule="$1" + install -d -m 700 -o root -g root "$(dirname "$MANAGED_PORTS_CONFIG")" + [[ ! -L "$MANAGED_PORTS_CONFIG" ]] || die "$MANAGED_PORTS_CONFIG must not be a symbolic link" + touch "$MANAGED_PORTS_CONFIG" + grep -Fqx -- "$rule" "$MANAGED_PORTS_CONFIG" || printf '%s\n' "$rule" >> "$MANAGED_PORTS_CONFIG" + chown root:root "$MANAGED_PORTS_CONFIG" + chmod 600 "$MANAGED_PORTS_CONFIG" +} + +allow_ufw_rule() { + local rule="$1" mode port protocol + mode="$(read_ip_mode)" + port="${rule%/*}" + protocol="${rule#*/}" + record_managed_ufw_rule "$rule" + if [[ "$mode" == ipv4 || "$mode" == both ]]; then + ufw allow from 0.0.0.0/0 to any port "$port" proto "$protocol" + fi + if [[ "$mode" == ipv6 || "$mode" == both ]]; then + ufw allow from ::/0 to any port "$port" proto "$protocol" + fi +} + select_action() { local choice @@ -139,7 +175,7 @@ add_ufw_ports_interactive() { for rule in "${rules[@]}"; do log "Allowing ${rule} in UFW" - ufw allow "$rule" + allow_ufw_rule "$rule" done if ! LANG=C ufw status | grep -q '^Status: active'; then @@ -438,9 +474,9 @@ effective_password="$(sshd -T | awk '$1 == "passwordauthentication" { print $2; if [[ "$MANAGE_UFW" == yes ]]; then log "Allowing SSH, HTTPS, and application ports in UFW without resetting existing rules" - ufw allow "${SSH_PORT}/tcp" - ufw allow "443/tcp" - ufw allow "63636/tcp" + allow_ufw_rule "${SSH_PORT}/tcp" + allow_ufw_rule "443/tcp" + allow_ufw_rule "63636/tcp" ufw --force enable else warn "MANAGE_UFW=no: firewall was not enabled or modified" @@ -483,5 +519,19 @@ printf 'Password SSH authentication: %s\n' "$PASSWORD_AUTH" printf 'Passwordless sudo: %s\n' "$PASSWORDLESS_SUDO" printf '\nOpen a NEW terminal and test before closing the current root session:\n' printf 'ssh -p %s %s@\n' "$SSH_PORT" "$NEW_USERNAME" -printf 'After that login succeeds, remove the temporary port 22 rule: sudo ufw delete allow 22/tcp\n' +case "$(read_ip_mode)" in + ipv4) + printf 'After that login succeeds, remove temporary port 22 with:\n' + printf 'sudo ufw delete allow from 0.0.0.0/0 to any port 22 proto tcp\n' + ;; + ipv6) + printf 'After that login succeeds, remove temporary port 22 with:\n' + printf 'sudo ufw delete allow from ::/0 to any port 22 proto tcp\n' + ;; + both) + printf 'After that login succeeds, remove both temporary port 22 rules:\n' + printf 'sudo ufw delete allow from 0.0.0.0/0 to any port 22 proto tcp\n' + printf 'sudo ufw delete allow from ::/0 to any port 22 proto tcp\n' + ;; +esac printf 'To enable Telegram login notifications, run ochenstarik-server-tg-4.sh.\n'