Add backup automation and IP family selection

This commit is contained in:
Ochenstarik 2026-07-14 15:06:26 +07:00
parent c57639f734
commit 5e64d33a35
5 changed files with 875 additions and 56 deletions

View file

@ -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-уведомления и автоматические расписания резервного копирования являются необязательными этапами.
## Безопасность

View file

@ -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"

View file

@ -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" <<EOF
[Unit]
Description=Ochenstarik server backup (%i)
After=local-fs.target
[Service]
Type=oneshot
ExecStart=${BACKUP_RUNNER} %i
Nice=10
IOSchedulingClass=idle
UMask=0077
TimeoutStartSec=infinity
EOF
cat > "${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

View file

@ -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() {

View file

@ -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@<server-ip>\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'