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
Сервер 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`.
209 lines
9.2 KiB
Bash
209 lines
9.2 KiB
Bash
#!/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
|