Link setup choices across server scripts

This commit is contained in:
Ochenstarik 2026-07-14 13:27:31 +07:00
parent feaa85a39b
commit ab7d3f6e03
6 changed files with 188 additions and 40 deletions

View file

@ -6,9 +6,9 @@
| Файл | Назначение | Можно запускать отдельно |
| --- | --- | --- |
| `ochenstarik-server-1.sh` | Создаёт swap-файл 2 ГБ и задаёт `vm.swappiness=20` | Да |
| `ochenstarik-server-2.sh` | Обновляет систему, ставит серверные и офисные пакеты, настраивает часовой пояс и UFW | Да |
| `ochenstarik-server-user-3.sh` | Создаёт администратора, позволяет выбрать SSH-порт, включает fail2ban или отдельно добавляет правила UFW | После установки необходимых пакетов; проще всего сначала запустить скрипт 2 |
| `ochenstarik-server-1.sh` | Предлагает выбрать часовой пояс, создаёт swap-файл 2 ГБ и задаёт `vm.swappiness=20` | Да |
| `ochenstarik-server-2.sh` | Предлагает выбрать будущий SSH-порт, обновляет систему, ставит пакеты и настраивает 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 | Да; зависимости устанавливаются автоматически |
@ -37,11 +37,11 @@ chmod 700 ./*.sh
bash -n ./ИМЯ-СКРИПТА.sh
```
## 1. Настройка swap
## 1. Часовой пояс и настройка swap
Файл: `ochenstarik-server-1.sh`.
Создаёт `/swapfile` размером 2 ГБ, добавляет его в `/etc/fstab` и устанавливает `vm.swappiness=20`. Повторный запуск предусмотрен.
Сначала предлагает ввести часовой пояс в формате IANA, например `Asia/Novosibirsk`, `Europe/Moscow` или `UTC`, и проверяет его через `timedatectl`. Затем создаёт `/swapfile` размером 2 ГБ, добавляет его в `/etc/fstab` и устанавливает `vm.swappiness=20`. Повторный запуск предусмотрен.
```bash
curl -fLO https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main/ochenstarik-server-1.sh
@ -61,12 +61,12 @@ sysctl vm.swappiness
Файл: `ochenstarik-server-2.sh`.
Скрипт обновляет систему, устанавливает OpenSSH, UFW, fail2ban, инструменты обработки документов, изображений, аудио и видео, а также задаёт часовой пояс `Asia/Novosibirsk`.
Скрипт запрашивает SSH-порт, который будет применён на этапе 3, сохраняет его в `/etc/ochenstarik-server/ssh-port.conf`, обновляет систему и устанавливает OpenSSH, UFW, fail2ban, инструменты обработки документов, изображений, аудио и видео.
Открываются TCP-порты:
- `22` — временный SSH-порт;
- `20202` — новый SSH-порт;
- выбранный SSH-порт — будущий порт SSH;
- `80`, `443` — HTTP и HTTPS;
- `2096`, `40000`, `63636` — порты приложений.
@ -93,10 +93,12 @@ timedatectl
1. новая установка или повторная настройка администратора и SSH;
2. только добавление открытых портов в UFW без изменения пользователя и SSH.
В режиме новой установки скрипт интерактивно запрашивает имя администратора, SSH-порт, SSH-ключ и пароль. Затем он:
В режиме новой установки скрипт интерактивно запрашивает имя администратора, SSH-ключ и пароль. SSH-порт автоматически читается из защищённого файла, созданного этапом 2. Если этап 2 не выполнялся, скрипт предложит выбрать порт вручную. Затем он:
- создаёт или обновляет пользователя;
- добавляет пользователя в группу `sudo`;
- назначает пользователю его домашнюю папку и всё содержимое `.ssh`;
- устанавливает права `700` для `.ssh` и `600` для `authorized_keys` и остальных файлов ключей;
- запрещает вход `root` по SSH;
- переносит SSH на выбранный порт и добавляет для него правило UFW;
- настраивает fail2ban;
@ -162,7 +164,7 @@ sudo tail -f /var/log/ochenstarik-ssh-login-telegram.log
Файл: `ochenstarik-server-vpn-5.sh`.
Скрипт устанавливает Xray и необходимые пакеты, принимает прямую ссылку `vless://` либо HTTPS-ссылку подписки 3x-ui и настраивает маршрутизацию через `nftables`. Поддерживается VLESS с транспортом TCP/RAW и REALITY.
Скрипт устанавливает Xray и необходимые пакеты, принимает прямую ссылку `vless://` либо HTTPS-ссылку подписки 3x-ui и настраивает маршрутизацию через `nftables`. Если подписка содержит VLESS-ссылки на нескольких портах, скрипт выводит доступные порты и просит выбрать нужный. Поддерживается VLESS с транспортом TCP/RAW и REALITY.
```bash
curl -fLO https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main/ochenstarik-server-vpn-5.sh

View file

@ -1,7 +1,7 @@
Актуальная инструкция находится в README.md:
https://github.com/ochenstarik-ui/lightweight-server#readme
Можно установить только нужный модуль. Пример для базовой настройки:
Можно установить только нужный модуль. Первый скрипт предлагает выбрать часовой пояс, второй — будущий SSH-порт, а пятый — порт VLESS из подписки. Пример для базовой настройки:
sudo apt-get update
sudo apt-get install -y curl

View file

@ -7,13 +7,34 @@ readonly SWAPPINESS="20"
readonly SYSCTL_FILE="/etc/sysctl.d/60-hermes-swap.conf"
log() { printf '[+] %s\n' "$*"; }
warn() { printf '[!] %s\n' "$*" >&2; }
die() { printf '[x] %s\n' "$*" >&2; exit 1; }
choose_timezone() {
local current_timezone timezone
current_timezone="$(timedatectl show --property=Timezone --value 2>/dev/null || true)"
current_timezone="${current_timezone:-Asia/Novosibirsk}"
printf 'Select an IANA timezone, for example Asia/Novosibirsk, Europe/Moscow or UTC.\n'
while :; do
read -rp "Timezone [${current_timezone}]: " timezone
timezone="${timezone:-$current_timezone}"
if timedatectl list-timezones | grep -Fqx -- "$timezone"; then
timedatectl set-timezone "$timezone"
log "Timezone set to ${timezone}"
return 0
fi
warn "Unknown timezone: $timezone"
done
}
[[ "$EUID" -eq 0 ]] || die "Run this script as root"
for command_name in awk blkid fallocate mkswap swapon sysctl; do
for command_name in awk blkid fallocate grep mkswap swapon sysctl timedatectl; do
command -v "$command_name" >/dev/null 2>&1 || die "Required command not found: $command_name"
done
choose_timezone
log "Configuring ${SWAPSIZE} swap file"
if [[ ! -e "$SWAPFILE" ]]; then
fallocate -l "$SWAPSIZE" "$SWAPFILE"
@ -44,5 +65,6 @@ chmod 644 "$SYSCTL_FILE"
sysctl -p "$SYSCTL_FILE" >/dev/null
log "Server memory setup is complete"
printf 'Timezone: %s\n' "$(timedatectl show --property=Timezone --value)"
swapon --show
printf '\nNow run ochenstarik-server-2.sh as root.\n'

View file

@ -1,13 +1,47 @@
#!/usr/bin/env bash
set -Eeuo pipefail
readonly SSH_PORT="20202"
readonly TIMEZONE="Asia/Novosibirsk"
readonly SSH_PORT_CONFIG="/etc/ochenstarik-server/ssh-port.conf"
readonly DEFAULT_SSH_PORT="20202"
log() { printf '[+] %s\n' "$*"; }
warn() { printf '[!] %s\n' "$*" >&2; }
die() { printf '[x] %s\n' "$*" >&2; exit 1; }
is_valid_port() {
local port="$1"
[[ "$port" =~ ^[0-9]{1,5}$ ]] || return 1
(( 10#$port >= 1 && 10#$port <= 65535 ))
}
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')"
if is_valid_port "$saved_port"; then
default_port="$((10#$saved_port))"
else
warn "Ignoring invalid saved SSH port: ${saved_port:-empty}"
fi
fi
while :; do
read -rp "SSH port for step 3 [${default_port}]: " selected_port
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"
done
}
[[ "$EUID" -eq 0 ]] || die "Run this script as root"
SSH_PORT=""
choose_ssh_port
export DEBIAN_FRONTEND=noninteractive
log "Updating the operating system"
@ -23,11 +57,14 @@ apt-get install -y \
ffmpeg mediainfo p7zip-full unzip zip unrar jq yq csvkit sqlite3 \
python3-pip python3-venv mc
log "Setting timezone to ${TIMEZONE}"
timedatectl set-timezone "$TIMEZONE"
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 "Configuring UFW"
# Keep port 22 open until step 3 moves SSH to 20202 and the new login is tested.
# 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"
@ -41,5 +78,6 @@ log "Installed versions"
printf 'Timezone: %s\n' "$(timedatectl show --property=Timezone --value)"
ufw status verbose
printf '\nDone. Now run ochenstarik-server-3.sh as root.\n'
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"

View file

@ -5,6 +5,7 @@ readonly ENV_FILE="${ENV_FILE:-/root/setup-data/env.txt}"
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"
log() { printf '[+] %s\n' "$*"; }
warn() { printf '[!] %s\n' "$*" >&2; }
@ -151,7 +152,7 @@ add_ufw_ports_interactive() {
}
collect_user_configuration() {
local new_username ssh_port default_ssh_port saved_ssh_port
local new_username ssh_port default_ssh_port saved_ssh_port selected_step2_port
local ssh_public_key pass pass2 candidate_uid
local key_method source_username source_home source_key_file candidate_key key_test
@ -174,13 +175,23 @@ collect_user_configuration() {
done
default_ssh_port="20202"
if [[ -f "$ENV_FILE" ]]; then
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"
selected_step2_port="$(sed -n 's/^SSH_PORT=//p' "$SSH_PORT_CONFIG" | head -n1 | tr -d '\r')"
is_valid_port "$selected_step2_port" \
|| die "Invalid SSH port in $SSH_PORT_CONFIG"
ssh_port="$((10#$selected_step2_port))"
log "Using SSH port ${ssh_port} selected in step 2"
elif [[ -f "$ENV_FILE" ]]; then
saved_ssh_port="$(read_env_value SSH_PORT)"
if is_valid_port "$saved_ssh_port"; then
default_ssh_port="$saved_ssh_port"
fi
fi
if [[ -z "${ssh_port:-}" ]]; then
warn "The SSH port from step 2 was not found; select it now"
while :; do
read -rp "SSH port [${default_ssh_port}]: " ssh_port
ssh_port="${ssh_port:-$default_ssh_port}"
@ -190,6 +201,7 @@ collect_user_configuration() {
fi
warn "SSH port must be a number between 1 and 65535"
done
fi
while :; do
printf '\nSSH key setup:\n'
@ -329,7 +341,7 @@ fi
log "Checking packages installed by step 2"
for command_name in sshd ssh-keygen visudo systemctl; do
for command_name in find sshd ssh-keygen visudo systemctl; do
require_command "$command_name"
done
@ -370,13 +382,28 @@ user_group="$(id -gn "$NEW_USERNAME")"
# every run so an existing user can be fixed by running this script again.
chown "$NEW_USERNAME:$user_group" "$user_home"
chmod 750 "$user_home"
[[ ! -L "$user_home/.ssh" ]] || die "Refusing to use a symbolic link as $user_home/.ssh"
install -d -m 700 -o "$NEW_USERNAME" -g "$user_group" "$user_home/.ssh"
[[ ! -L "$user_home/.ssh/authorized_keys" ]] \
|| die "Refusing to use a symbolic link as $user_home/.ssh/authorized_keys"
touch -- "$user_home/.ssh/authorized_keys"
chown "$NEW_USERNAME:$user_group" "$user_home/.ssh/authorized_keys"
chmod 600 "$user_home/.ssh/authorized_keys"
if ! grep -Fqx -- "$SSH_PUBLIC_KEY" "$user_home/.ssh/authorized_keys"; then
printf '%s\n' "$SSH_PUBLIC_KEY" >> "$user_home/.ssh/authorized_keys"
fi
chown -R --no-dereference "$NEW_USERNAME:$user_group" "$user_home/.ssh"
find "$user_home/.ssh" -xdev -type d -exec chmod 700 {} +
find "$user_home/.ssh" -xdev -type f -exec chmod 600 {} +
id -nG "$NEW_USERNAME" | tr ' ' '\n' | grep -Fqx sudo \
|| die "$NEW_USERNAME was not added to the sudo group"
[[ "$(stat -c '%U:%G' "$user_home/.ssh")" == "$NEW_USERNAME:$user_group" ]] \
|| die "Incorrect owner for $user_home/.ssh"
[[ "$(stat -c '%a' "$user_home/.ssh")" == 700 ]] \
|| die "Incorrect permissions for $user_home/.ssh"
[[ "$(stat -c '%U:%G' "$user_home/.ssh/authorized_keys")" == "$NEW_USERNAME:$user_group" ]] \
|| die "Incorrect owner for authorized_keys"
[[ "$(stat -c '%a' "$user_home/.ssh/authorized_keys")" == 600 ]] \
|| die "Incorrect permissions for authorized_keys"
if command -v restorecon >/dev/null 2>&1; then
restorecon -RF "$user_home/.ssh" || warn "SELinux context could not be restored"

View file

@ -81,7 +81,9 @@ printf '\n'
TMP_DIR="$(mktemp -d)"
chmod 700 "$TMP_DIR"
RAW_FILE="${TMP_DIR}/subscription.txt"
LINKS_FILE="${TMP_DIR}/vless-links.txt"
LINK_FILE="${TMP_DIR}/vless-link.txt"
PORTS_FILE="${TMP_DIR}/vless-ports.txt"
GENERATED_CONFIG="${TMP_DIR}/config.json"
INSTALLER_FILE="${TMP_DIR}/install-release.sh"
@ -98,36 +100,93 @@ fi
unset SUBSCRIPTION_INPUT
chmod 600 "$RAW_FILE"
python3 - "$RAW_FILE" "$LINK_FILE" <<'PY'
python3 - "$RAW_FILE" "$LINKS_FILE" "$PORTS_FILE" <<'PY'
import base64
import pathlib
import sys
import urllib.parse
raw = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8").strip()
def find_vless(text: str):
result = []
for line in text.replace("\r", "").split("\n"):
line = line.strip()
if line.startswith("vless://"):
return line
return None
result.append(line)
return result
link = find_vless(raw)
if not link:
links = find_vless(raw)
if not links:
try:
padded = raw.replace("-", "+").replace("_", "/")
padded += "=" * (-len(padded) % 4)
decoded = base64.b64decode(padded, validate=False).decode("utf-8")
except Exception as exc:
raise SystemExit(f"Подписка не является Base64/VLESS: {exc}")
link = find_vless(decoded)
links = find_vless(decoded)
if not link:
links = list(dict.fromkeys(links))
if not links:
raise SystemExit("В подписке не найдена ссылка vless://")
pathlib.Path(sys.argv[2]).write_text(link, encoding="utf-8")
ports = []
for link in links:
try:
port = urllib.parse.urlsplit(link).port
except ValueError as exc:
raise SystemExit(f"Некорректный порт в VLESS-ссылке: {exc}")
if port is None:
raise SystemExit("В одной из VLESS-ссылок отсутствует порт")
if port not in ports:
ports.append(port)
pathlib.Path(sys.argv[2]).write_text("\n".join(links) + "\n", encoding="utf-8")
pathlib.Path(sys.argv[3]).write_text(
"\n".join(str(port) for port in ports) + "\n", encoding="utf-8"
)
PY
chmod 600 "$LINKS_FILE" "$PORTS_FILE"
mapfile -t SUBSCRIPTION_PORTS < "$PORTS_FILE"
(( ${#SUBSCRIPTION_PORTS[@]} > 0 )) || die "В подписке не найдены доступные порты"
printf 'Доступные порты в подписке: %s\n' "${SUBSCRIPTION_PORTS[*]}"
while :; do
read -rp "Выберите порт подписки [${SUBSCRIPTION_PORTS[0]}]: " SUBSCRIPTION_PORT
SUBSCRIPTION_PORT="${SUBSCRIPTION_PORT:-${SUBSCRIPTION_PORTS[0]}}"
[[ "$SUBSCRIPTION_PORT" =~ ^[0-9]{1,5}$ ]] \
&& (( 10#$SUBSCRIPTION_PORT >= 1 && 10#$SUBSCRIPTION_PORT <= 65535 )) \
|| { warn "Порт должен быть числом от 1 до 65535"; continue; }
SUBSCRIPTION_PORT="$((10#$SUBSCRIPTION_PORT))"
port_found="no"
for available_port in "${SUBSCRIPTION_PORTS[@]}"; do
if [[ "$SUBSCRIPTION_PORT" == "$available_port" ]]; then
port_found="yes"
break
fi
done
[[ "$port_found" == yes ]] && break
warn "Порт ${SUBSCRIPTION_PORT} отсутствует в подписке"
done
python3 - "$LINKS_FILE" "$LINK_FILE" "$SUBSCRIPTION_PORT" <<'PY'
import pathlib
import sys
import urllib.parse
links = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8").splitlines()
selected_port = int(sys.argv[3])
for link in links:
if urllib.parse.urlsplit(link).port == selected_port:
pathlib.Path(sys.argv[2]).write_text(link, encoding="utf-8")
break
else:
raise SystemExit(f"VLESS-ссылка для порта {selected_port} не найдена")
PY
chmod 600 "$LINK_FILE"
log "Выбран порт подписки ${SUBSCRIPTION_PORT}"
VPN_IP="$(python3 - "$LINK_FILE" "$GENERATED_CONFIG" "$TPROXY_PORT" "$SOCKS_PORT" <<'PY'
import json