Add backup automation and IP family selection
This commit is contained in:
parent
c57639f734
commit
5e64d33a35
5 changed files with 875 additions and 56 deletions
70
README.md
70
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-уведомления и автоматические расписания резервного копирования являются необязательными этапами.
|
||||
|
||||
## Безопасность
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
376
ochenstarik-server-backup-7.sh
Normal file
376
ochenstarik-server-backup-7.sh
Normal 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
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
Loading…
Reference in a new issue