lightweight-server/ochenstarik-server-2.sh
ochenstarik-ui 9f06756cef
Some checks failed
Bash syntax / syntax (push) Failing after 11s
Bash syntax / syntax (pull_request) Failing after 2s
fix: close installer exposure and pin verified payloads
2026-10-23 12:33:19 +07:00

447 lines
15 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
set -Eeuo pipefail
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))
}
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
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 "Сохранённый SSH-порт некорректен: $saved_port"
fi
fi
while :; do
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-порт должен быть числом от 1 до 65535"
done
}
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
}
discover_current_ssh_ports() {
local managed_file="${1:-$MANAGED_PORTS_CONFIG}"
local client_ip client_port server_ip server_port port
local -A seen_ports=()
if [[ -n "${SSH_CONNECTION:-}" ]]; then
read -r client_ip client_port server_ip server_port <<< "$SSH_CONNECTION"
if is_valid_port "${server_port:-}"; then
printf '%s\n' "$((10#$server_port))"
return 0
fi
fi
if command -v ss >/dev/null 2>&1; then
while IFS= read -r port; do
is_valid_port "$port" || continue
port="$((10#$port))"
[[ -z "${seen_ports[$port]:-}" ]] || continue
printf '%s\n' "$port"
seen_ports["$port"]=1
done < <(ss -H -ltnp 2>/dev/null | awk '$NF ~ /sshd/ { address=$4; sub(/^.*:/, "", address); if (address ~ /^[0-9]+$/) print address }')
fi
if ((${#seen_ports[@]} == 0)) && [[ ! -e "$managed_file" ]]; then
# First run from sudo or a local console: preserve the conventional SSH
# port when no session metadata or listening socket is discoverable.
printf '22\n'
fi
}
load_managed_rules() {
local rule port protocol
MANAGED_RULES=()
RULE_SEEN=()
PREVIOUS_MANAGED_RULES=()
if [[ ! -e "$MANAGED_PORTS_CONFIG" ]]; then
return 0
fi
[[ -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"
PREVIOUS_MANAGED_RULES=("${MANAGED_RULES[@]}")
}
add_default_rules() {
local saved_ssh_port current_ssh_port
local -a current_ssh_ports=()
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"
add_managed_rule "$((10#$saved_ssh_port))/tcp"
add_managed_rule 443/tcp
# Keep the port carrying this setup session open during an SSH migration.
mapfile -t current_ssh_ports < <(discover_current_ssh_ports)
for current_ssh_port in "${current_ssh_ports[@]}"; do
add_managed_rule "$current_ssh_port/tcp"
done
}
confirm_legacy_rules() {
local rule answer current_ssh_port
local -a legacy_rules=() retained_rules=()
local -a current_ssh_ports=()
local -A defaults=() seen=()
defaults["$((10#$SSH_PORT))/tcp"]=1
defaults[443/tcp]=1
mapfile -t current_ssh_ports < <(discover_current_ssh_ports)
for current_ssh_port in "${current_ssh_ports[@]}"; do defaults["$current_ssh_port/tcp"]=1; done
for rule in "${MANAGED_RULES[@]}"; do
if [[ -n "${defaults[$rule]:-}" ]]; then
retained_rules+=("$rule")
seen["$rule"]=1
else
legacy_rules+=("$rule")
fi
done
((${#legacy_rules[@]})) || return 0
printf '\nНайдены дополнительные правила UFW из прежней конфигурации; их назначение неизвестно:\n'
printf ' %s\n' "${legacy_rules[@]}"
read -rp 'Сохранить весь этот список? [y/N]: ' answer \
|| die "Ввод прерван; правила UFW не изменены"
if [[ "$answer" == y || "$answer" == Y ]]; then
retained_rules+=("${legacy_rules[@]}")
for rule in "${legacy_rules[@]}"; do seen["$rule"]=1; done
fi
MANAGED_RULES=("${retained_rules[@]}")
RULE_SEEN=()
for rule in "${!seen[@]}"; do RULE_SEEN["$rule"]=1; 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
}
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
}
restore_ipv6_stack_if_managed() {
[[ -e "$IPV6_SYSCTL_FILE" ]] || return 0
enable_ipv6_systemwide
}
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 status
for rule in "${PREVIOUS_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
status="$(LANG=C ufw status)" || die "Не удалось проверить состояние UFW после удаления старых правил"
for rule in "${PREVIOUS_MANAGED_RULES[@]}"; do
if awk -v rule="$rule" \
'$1 == rule && ($2 == "ALLOW" || ($2 == "(v6)" && $3 == "ALLOW")) { found=1 } END { exit !found }' \
<<< "$status"; then
die "Старое правило UFW ${rule} всё ещё разрешено; новая конфигурация не сохранена"
fi
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
restore_ipv6_stack_if_managed
set_ufw_ipv6_setting yes
delete_managed_ufw_rules
else
enable_ipv6_systemwide
[[ "$IP_MODE" != ipv6 ]] || verify_ipv6_connectivity
set_ufw_ipv6_setting yes
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
ufw reload >/dev/null 2>&1 || true
[[ ! -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 PREVIOUS_MANAGED_RULES=()
declare -A RULE_SEEN=()
ACTION=""
SSH_PORT=""
IP_MODE=""
select_action
if [[ "$ACTION" == install ]]; then
choose_ssh_port
choose_ip_mode
export DEBIAN_FRONTEND=noninteractive
log "Обновляю операционную систему"
apt-get update
apt-get upgrade -y
log "Устанавливаю обязательные пакеты SSH, брандмауэра и защиты"
apt-get install -y \
sudo ufw fail2ban curl ca-certificates openssl openssh-server logrotate iproute2
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
PREVIOUS_MANAGED_RULES=("${MANAGED_RULES[@]}")
confirm_legacy_rules
add_default_rules
apply_ip_mode_and_firewall
save_configuration
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
PREVIOUS_MANAGED_RULES=("${MANAGED_RULES[@]}")
confirm_legacy_rules
add_default_rules
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
apply_ip_mode_and_firewall
save_configuration
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 '\nГотово. Следующий этап: ochenstarik-server-user-3.sh\n'
printf 'Этап 3 перенесёт SSH на порт %s.\n' "$SSH_PORT"
printf 'Не удаляйте правило порта 22, пока не проверите вход по порту %s.\n' "$SSH_PORT"