Compare commits
No commits in common. "42d9eb5c1f647b9c36ea67f40804412081e0ba9a" and "7f6dbc9a597905dccfb193e4eacd51277c359184" have entirely different histories.
42d9eb5c1f
...
7f6dbc9a59
3 changed files with 0 additions and 310 deletions
1
.gitattributes
vendored
1
.gitattributes
vendored
|
|
@ -1 +0,0 @@
|
||||||
*.sh text eol=lf
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# Аудит нового выделенного сервера ДО того, как на него что-либо поставлено.
|
|
||||||
#
|
|
||||||
# Только чтение — ничего не меняет. Задача одна: собрать факты, по которым
|
|
||||||
# принимается решение «работаем на этой установке» или «переставляем ОС».
|
|
||||||
#
|
|
||||||
# Главный вопрос — RAID. На AR32-NVMe два диска по 1 ТБ. Если они отданы
|
|
||||||
# по отдельности или собраны в RAID0, смерть одного диска = потеря всего,
|
|
||||||
# а память на этой платформе non-ECC. Переставить ОС можно только пока
|
|
||||||
# сервер пустой, поэтому проверка идёт первой и до всего остального.
|
|
||||||
#
|
|
||||||
# Запуск: ssh <host> 'bash -s' < ops/selectel-audit.sh
|
|
||||||
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
section() { printf '\n=== %s ===\n' "$1"; }
|
|
||||||
|
|
||||||
section "Идентификация"
|
|
||||||
. /etc/os-release 2>/dev/null && echo "OS: ${PRETTY_NAME:-unknown}"
|
|
||||||
echo "Kernel: $(uname -r)"
|
|
||||||
echo "Hostname: $(hostname)"
|
|
||||||
echo "Uptime: $(uptime -p 2>/dev/null || true)"
|
|
||||||
|
|
||||||
section "CPU / память"
|
|
||||||
lscpu 2>/dev/null | grep -E '^(Model name|CPU\(s\)|Thread|Core|Socket|CPU max MHz)' || nproc
|
|
||||||
free -h | head -2
|
|
||||||
echo "--- ECC (пусто = не-ECC либо недоступно из ОС) ---"
|
|
||||||
dmidecode -t memory 2>/dev/null | grep -iE 'error correction|^\s+Type:' | sort -u | head -5 \
|
|
||||||
|| echo "dmidecode недоступен (нужен root)"
|
|
||||||
|
|
||||||
section "ДИСКИ — ключевой вопрос"
|
|
||||||
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,MODEL
|
|
||||||
echo "--- программный RAID ---"
|
|
||||||
if [ -f /proc/mdstat ]; then cat /proc/mdstat; else echo "/proc/mdstat отсутствует"; fi
|
|
||||||
echo "--- аппаратный RAID / NVMe ---"
|
|
||||||
lspci 2>/dev/null | grep -iE 'raid|nvme' || echo "контроллеров RAID не видно"
|
|
||||||
nvme list 2>/dev/null || echo "nvme-cli не установлен"
|
|
||||||
echo "--- LVM ---"
|
|
||||||
pvs 2>/dev/null; vgs 2>/dev/null; lvs 2>/dev/null
|
|
||||||
echo "--- монтирование ---"
|
|
||||||
df -hT -x tmpfs -x devtmpfs
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo ">>> ВЕРДИКТ ПО ДИСКАМ читать так:"
|
|
||||||
echo ">>> raid1 / mirror в mdstat → зеркало есть, можно работать"
|
|
||||||
echo ">>> raid0 / linear / дисков нет → зеркала НЕТ, переставить ОС до установки чего-либо"
|
|
||||||
echo ">>> один диск в df, второй пуст → второй не используется, решить как задействовать"
|
|
||||||
|
|
||||||
section "Сеть"
|
|
||||||
ip -brief addr show | grep -v '^lo'
|
|
||||||
ip route | head -5
|
|
||||||
echo "--- DNS ---"
|
|
||||||
grep -E '^nameserver' /etc/resolv.conf 2>/dev/null | head -3
|
|
||||||
|
|
||||||
section "Что уже слушает"
|
|
||||||
ss -tulpn 2>/dev/null | grep LISTEN || netstat -tulpn 2>/dev/null | grep LISTEN
|
|
||||||
|
|
||||||
section "Безопасность — текущее состояние"
|
|
||||||
echo "--- sshd (значимые директивы) ---"
|
|
||||||
sshd -T 2>/dev/null | grep -E '^(permitrootlogin|passwordauthentication|pubkeyauthentication|port|permitemptypasswords)' \
|
|
||||||
|| grep -E '^\s*(PermitRootLogin|PasswordAuthentication|Port)' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/* 2>/dev/null
|
|
||||||
echo "--- файрвол ---"
|
|
||||||
ufw status 2>/dev/null || echo "ufw не установлен"
|
|
||||||
nft list ruleset 2>/dev/null | head -20 || iptables -S 2>/dev/null | head -20
|
|
||||||
echo "--- пользователи с shell ---"
|
|
||||||
awk -F: '$7 !~ /(nologin|false)$/ {print $1" "$7}' /etc/passwd
|
|
||||||
|
|
||||||
section "Swap / sysctl"
|
|
||||||
swapon --show || echo "swap отсутствует"
|
|
||||||
sysctl vm.swappiness vm.overcommit_memory 2>/dev/null
|
|
||||||
|
|
||||||
section "Уже установленное ПО"
|
|
||||||
for b in docker containerd git curl ufw fail2ban-server unattended-upgrade; do
|
|
||||||
if command -v "$b" >/dev/null 2>&1; then
|
|
||||||
printf '%-22s %s\n' "$b" "$($b --version 2>/dev/null | head -1)"
|
|
||||||
else
|
|
||||||
printf '%-22s %s\n' "$b" "НЕ установлен"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
section "Время"
|
|
||||||
timedatectl 2>/dev/null | head -5 || date
|
|
||||||
|
|
||||||
printf '\n=== Аудит закончен ===\n'
|
|
||||||
|
|
@ -1,225 +0,0 @@
|
||||||
#!/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}"
|
|
||||||
|
|
||||||
# Версии на текущем прод-хосте — ориентир для сверки, НЕ то, что ставится.
|
|
||||||
# Репозиторий Docker отдаёт последнюю версию; закреплять старую ради совпадения
|
|
||||||
# значило бы отказаться от исправлений безопасности. Расхождение печатается
|
|
||||||
# предупреждением, чтобы оно не прошло незамеченным (22.08.2026 приехали
|
|
||||||
# docker 29.7.2 и compose 5.5.0 против 29.4.1 / v5.1.3 на Beget).
|
|
||||||
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"
|
|
||||||
# Имя начинается с 00, а не с 60, намеренно: sshd берёт ПЕРВОЕ встреченное
|
|
||||||
# значение директивы, а Ubuntu кладёт рядом 50-cloud-init.conf с
|
|
||||||
# PasswordAuthentication yes. При имени 60-* парольный вход молча остаётся
|
|
||||||
# включённым, и `sshd -t` при этом рапортует, что всё в порядке.
|
|
||||||
rm -f /etc/ssh/sshd_config.d/60-hardening.conf
|
|
||||||
cat > /etc/ssh/sshd_config.d/00-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 "конфиг валиден"
|
|
||||||
|
|
||||||
# Синтаксическая валидность ничего не говорит о том, какое значение победило.
|
|
||||||
# Проверяем итог так, как его увидит sshd после перезапуска.
|
|
||||||
eff_pass="$(sshd -T 2>/dev/null | awk '/^passwordauthentication /{print $2}')"
|
|
||||||
eff_root="$(sshd -T 2>/dev/null | awk '/^permitrootlogin /{print $2}')"
|
|
||||||
echo "итог: passwordauthentication=$eff_pass permitrootlogin=$eff_root"
|
|
||||||
[ "$eff_pass" = "no" ] || die "PasswordAuthentication остался '$eff_pass' — какой-то файл в sshd_config.d перебивает наш; НЕ перезапускаю"
|
|
||||||
|
|
||||||
cat <<EOF
|
|
||||||
|
|
||||||
============================================================
|
|
||||||
Настройка завершена. sshd ещё НЕ перезапущен намеренно.
|
|
||||||
|
|
||||||
Прежде чем перезапускать, в ОТДЕЛЬНОМ окне проверь вход по ключу:
|
|
||||||
|
|
||||||
ssh -p $SSH_PORT $ADMIN_USER@\$(hostname -I | awk '{print \$1}')
|
|
||||||
|
|
||||||
Только когда вход по ключу работает:
|
|
||||||
|
|
||||||
systemctl reload ssh
|
|
||||||
|
|
||||||
Если перезапустить раньше и ключ не подойдёт, парольный вход уже
|
|
||||||
выключён — останется только IP-KVM за 1320 руб.
|
|
||||||
============================================================
|
|
||||||
EOF
|
|
||||||
Loading…
Add table
Reference in a new issue