gendesign/ops/selectel-bootstrap.sh
bot-backend 6484116079
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
chore(ops): скрипты аудита и первичной настройки выделенного сервера Selectel
Сервер Poincare (SPB-4, 188.246.224.93) поднят 22.08.2026. Два скрипта под
переезд (#2989, #3027-#3032).

selectel-audit.sh — только чтение. Отвечает на вопрос, который надо закрыть
до установки чего бы то ни было: собраны ли диски в зеркало. Переставить ОС
можно только пока сервер пустой.

Результат первого прогона: RAID1 на всех трёх разделах ([2/2] [UU] на /boot,
swap и /), память non-ECC подтверждена (Error Correction Type: None),
910G свободно из 924G.

selectel-bootstrap.sh — идемпотентная настройка: пользователь с ключом,
ufw, Docker версий текущего прода (29.4.1 / v5.1.3), лимит журналов
контейнеров, sysctl, unattended-upgrades, fail2ban, ужесточение sshd.

Порядок шагов выбран так, чтобы не потерять доступ: правило для SSH
добавляется ДО включения файрвола, а sshd в конце намеренно НЕ
перезапускается — сначала проверяется вход по ключу в отдельном окне.
При выключенном PasswordAuthentication ошибка означала бы IP-KVM за 1320 руб.

Два умолчания выставлены по факту, а не по привычке:

- часовой пояс Europe/Moscow, а не UTC — оба хоста сейчас на MSK, и смена
  пояса сдвинула бы все шесть cron-задач (бэкапы, геокодинг) на три часа;
- swap не создаётся, если он уже есть — установщик Selectel отдаёт раздел
  4.7G в зеркале, добавлять файл поверх незачем при 62G памяти.

.gitattributes: *.sh с LF — иначе скрипт, отредактированный под Windows,
падает на удалённом хосте с `$'\r': command not found`.
2026-08-22 12:49:09 +03:00

209 lines
9.2 KiB
Bash
Raw 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
# Первичная настройка нового выделенного сервера (Selectel AR32-NVMe, Ubuntu 24.04).
#
# Идемпотентен: повторный запуск ничего не ломает и не дублирует.
# Ничего не удаляет и не переразмечает диски.
#
# ВАЖНО: сначала прогнать ops/selectel-audit.sh и убедиться, что диски в зеркале.
# Если зеркала нет — ОС переставляется ДО этого скрипта, иначе всю настройку
# придётся выкидывать вместе с переустановкой.
#
# Порядок внутри выбран так, чтобы не потерять доступ: правило для SSH в файрвол
# добавляется ДО его включения, парольный вход выключается ПОСЛЕ проверки, что
# ключ работает.
#
# Запуск (от root): ssh root@<ip> 'bash -s' < ops/selectel-bootstrap.sh
#
# Переменные окружения:
# ADMIN_USER — создаваемый пользователь (по умолчанию gendesign)
# ADMIN_PUBKEY — публичный ключ для него (обязателен при создании)
# SSH_PORT — порт sshd (по умолчанию 22)
# SWAP_GB — размер файла подкачки (по умолчанию 8)
# TIMEZONE — часовой пояс (по умолчанию Europe/Moscow, как на текущем проде)
set -euo pipefail
ADMIN_USER="${ADMIN_USER:-gendesign}"
ADMIN_PUBKEY="${ADMIN_PUBKEY:-}"
SSH_PORT="${SSH_PORT:-22}"
SWAP_GB="${SWAP_GB:-8}"
# Версии — те же, что на текущем прод-хосте, чтобы поведение compose не разъехалось.
WANT_DOCKER="29.4.1"
WANT_COMPOSE="v5.1.3"
log() { printf '\n\033[1m>>> %s\033[0m\n' "$1"; }
warn() { printf '\033[33m[!] %s\033[0m\n' "$1"; }
die() { printf '\033[31m[x] %s\033[0m\n' "$1" >&2; exit 1; }
[ "$(id -u)" -eq 0 ] || die "запускать от root"
log "0. Проверка предусловий"
if [ ! -f /proc/mdstat ] || ! grep -q 'raid1' /proc/mdstat 2>/dev/null; then
warn "RAID1 в /proc/mdstat не найден."
warn "Память на этой платформе non-ECC, диска два — без зеркала отказ одного"
warn "диска означает потерю базы. Убедись, что это осознанное решение."
warn "Продолжение через 15 секунд, Ctrl-C чтобы прервать."
sleep 15
fi
log "1. Базовые пакеты"
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq \
ca-certificates curl gnupg git ufw fail2ban unattended-upgrades \
htop ncdu jq nvme-cli smartmontools chrony >/dev/null
log "2. Часовой пояс"
# Совпадает с текущим прод-хостом (Beget = Europe/Moscow, проверено 22.08.2026).
# Ставить UTC нельзя без правки crontab: бэкапы и джобы записаны в местном
# времени, смена пояса сдвинула бы все шесть задач на три часа.
timedatectl set-timezone "${TIMEZONE:-Europe/Moscow}"
systemctl enable --now chrony >/dev/null 2>&1 || true
timedatectl | head -3
log "3. Пользователь $ADMIN_USER"
if id "$ADMIN_USER" >/dev/null 2>&1; then
echo "уже существует"
else
[ -n "$ADMIN_PUBKEY" ] || die "ADMIN_PUBKEY не задан — иначе доступа к серверу не будет"
adduser --disabled-password --gecos "" "$ADMIN_USER"
echo "создан"
fi
if [ -n "$ADMIN_PUBKEY" ]; then
install -d -m 700 -o "$ADMIN_USER" -g "$ADMIN_USER" "/home/$ADMIN_USER/.ssh"
touch "/home/$ADMIN_USER/.ssh/authorized_keys"
grep -qxF "$ADMIN_PUBKEY" "/home/$ADMIN_USER/.ssh/authorized_keys" \
|| echo "$ADMIN_PUBKEY" >> "/home/$ADMIN_USER/.ssh/authorized_keys"
chmod 600 "/home/$ADMIN_USER/.ssh/authorized_keys"
chown -R "$ADMIN_USER:$ADMIN_USER" "/home/$ADMIN_USER/.ssh"
echo "ключ на месте"
fi
usermod -aG sudo "$ADMIN_USER"
echo "$ADMIN_USER ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/90-$ADMIN_USER"
chmod 440 "/etc/sudoers.d/90-$ADMIN_USER"
log "4. Файрвол (правило для SSH — до включения)"
ufw --force reset >/dev/null
ufw default deny incoming >/dev/null
ufw default allow outgoing >/dev/null
ufw allow "$SSH_PORT/tcp" comment 'ssh' >/dev/null
ufw allow 80/tcp comment 'http' >/dev/null
ufw allow 443/tcp comment 'https' >/dev/null
ufw --force enable >/dev/null
ufw status verbose
log "5. Docker $WANT_DOCKER + Compose $WANT_COMPOSE"
if ! command -v docker >/dev/null 2>&1; then
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
> /etc/apt/sources.list.d/docker.list
apt-get update -qq
apt-get install -y -qq docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin >/dev/null
fi
usermod -aG docker "$ADMIN_USER"
systemctl enable --now docker >/dev/null
have_docker="$(docker --version | grep -oP '\d+\.\d+\.\d+' | head -1)"
have_compose="$(docker compose version --short 2>/dev/null || echo '?')"
echo "docker $have_docker · compose $have_compose"
[ "$have_docker" = "$WANT_DOCKER" ] || warn "docker $have_docker != $WANT_DOCKER на текущем проде"
case "$have_compose" in "${WANT_COMPOSE#v}"*) ;; *) warn "compose $have_compose != $WANT_COMPOSE" ;; esac
log "6. Журналы Docker — чтобы диск не съело логами"
if [ ! -f /etc/docker/daemon.json ]; then
install -d /etc/docker
cat > /etc/docker/daemon.json <<'JSON'
{
"log-driver": "json-file",
"log-opts": { "max-size": "50m", "max-file": "3" },
"live-restore": true
}
JSON
systemctl restart docker
echo "daemon.json создан"
else
echo "daemon.json уже есть — не трогаю"
fi
log "7. Swap ${SWAP_GB}G"
# Установщик Selectel уже отдаёт swap-раздел (на AR32-NVMe — 4.7G в зеркале md).
# Добавлять файл поверх раздела незачем: памяти 62G, своп тут страховка.
if swapon --show --noheadings 2>/dev/null | grep -q .; then
echo "swap уже есть, файл не создаю:"
swapon --show
elif swapon --show | grep -q '/swapfile'; then
echo "уже подключён"
else
fallocate -l "${SWAP_GB}G" /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=$((SWAP_GB*1024))
chmod 600 /swapfile
mkswap /swapfile >/dev/null
swapon /swapfile
grep -q '^/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
echo "создан"
fi
cat > /etc/sysctl.d/60-gendesign.conf <<'SYSCTL'
# Памяти 64 ГБ — своп только страховка, в него лезть не надо.
vm.swappiness = 10
# Redis просит overcommit, иначе фоновое сохранение может не запуститься.
vm.overcommit_memory = 1
# Больше открытых файлов: два десятка контейнеров + Postgres.
fs.file-max = 2097152
SYSCTL
sysctl --system >/dev/null
sysctl vm.swappiness vm.overcommit_memory
log "8. Автообновления безопасности"
cat > /etc/apt/apt.conf.d/20auto-upgrades <<'APT'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT
systemctl enable --now unattended-upgrades >/dev/null 2>&1 || true
log "9. fail2ban на sshd"
cat > /etc/fail2ban/jail.d/sshd.local <<JAIL
[sshd]
enabled = true
port = $SSH_PORT
maxretry = 5
bantime = 1h
findtime = 10m
JAIL
systemctl enable --now fail2ban >/dev/null
systemctl is-active fail2ban
log "10. Ужесточение sshd"
cat > /etc/ssh/sshd_config.d/60-hardening.conf <<SSHD
Port $SSH_PORT
PermitRootLogin prohibit-password
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitEmptyPasswords no
MaxAuthTries 4
ClientAliveInterval 60
SSHD
sshd -t || die "конфиг sshd невалиден — НЕ перезапускаю, доступ сохранён"
echo "конфиг валиден"
cat <<EOF
============================================================
Настройка завершена. sshd ещё НЕ перезапущен намеренно.
Прежде чем перезапускать, в ОТДЕЛЬНОМ окне проверь вход по ключу:
ssh -p $SSH_PORT $ADMIN_USER@\$(hostname -I | awk '{print \$1}')
Только когда вход по ключу работает:
systemctl reload ssh
Если перезапустить раньше и ключ не подойдёт, парольный вход уже
выключён — останется только IP-KVM за 1320 руб.
============================================================
EOF