gendesign/ops/selectel-audit.sh
bot-backend 1a37c7e0f9
All checks were successful
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m10s
CI / backend-tests (pull_request) Successful in 17m38s
feat(ci): деплой проверяет, к тому ли хосту подключился
Сегодня подлинность хоста не проверяется нигде: deploy-tradein.yml явно
ставил StrictHostKeyChecking=no, а во всех пяти вызовах appleboy/ssh-action
не задан fingerprint. Пока Forgejo, раннеры и прод жили на одной машине,
это было почти бесплатно. После переезда 30.08 тот же канал идёт через
интернет и уносит GHCR_PAT, OPENAI_API_KEY, OBJECTIVE_API_KEY,
GLITCHTIP_BACKEND_DSN и сам DEPLOY_SSH_KEY.

Вызовов оказалось пять, а не четыре: deploy-obsidian.yml тоже ходит на
DEPLOY_HOST с DEPLOY_SSH_KEY. Отпечаток добавлен во все.

Механизм инертен по конструкции самого действия, а не нашей ветвлёнкой:
appleboy/ssh-action@v1.0.3 -> drone-ssh 1.7.3 -> easyssh-proxy v1.5.0, где
hostKeyCallback = ssh.InsecureIgnoreHostKey() и сверка включается только при
непустом Fingerprint. Незаданный секрет = сегодняшнее поведение бит в бит.

Но инертность, безопасная для мержа, опасна для самого окна: забытый секрет
означал бы зелёный деплой через интернет без всякой проверки. Поэтому перед
каждым ssh-шагом стоит преамбула, которая при пустом секрете печатает
::warning с перечислением того, что именно едет по этому каналу. Warning, а
не отказ: секрета сегодня нет ни у кого, отказ сломал бы деплой в момент
мержа.

Ручной ssh в "Resolve deployed base SHA" отпечаток принять не может, ему
отдан отдельный секрет с known_hosts по той же схеме. Заодно расхождение
ключа перестало быть неотличимым от недоступного хоста: раньше 2>/dev/null
глотал "Host key verification failed" и печаталось обычное сообщение про
фолбэк на полную пересборку. Свойство fail-safe сохранено - при любой ошибке
RAW_SHA пустеет и включается build-all.

ops/selectel-ci-access.sh - аддитивная выдача CI-доступа на Selectel по
решению владельца "сейчас только добавляем, удаляем после переезда": ставит
отдельный ключ робота с from="адрес раннера", добавляет узкое правило ufw,
не трогая общее, и печатает план сужения, который сам не выполняет. Ни одной
команды, способной отрезать текущую сессию: sshd не перезапускается, default
policy ufw не трогается, в authorized_keys только дозапись.

Отзыв старого ключа, сужение правил и уборка секретов - в #3075, после
подтверждённого переезда.

Refs #3029, #3075, #3057, #2989
2026-08-24 19:16:56 +03:00

111 lines
6.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
# Аудит нового выделенного сервера ДО того, как на него что-либо поставлено.
#
# Только чтение — ничего не меняет. Задача одна: собрать факты, по которым
# принимается решение «работаем на этой установке» или «переставляем ОС».
#
# Главный вопрос — 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 "--- файрвол ---"
# verbose + numbered вместо голого `ufw status`: по verbose видно политику по
# умолчанию, по numbered — порядок правил и их номера. И то и другое нужно,
# чтобы после ops/selectel-ci-access.sh проверить две вещи сразу: что узкое
# правило для адреса раннера (#3029) добавилось и что общее правило для ssh
# пока живо (до переезда оно снимается ТОЛЬКО вручную и осознанно).
ufw status verbose 2>/dev/null || echo "ufw не установлен"
ufw status numbered 2>/dev/null
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
echo "--- ключи доступа (тела ключей НЕ печатаются) ---"
# Чем проверяется результат ops/selectel-ci-access.sh (#3029): видно, сколько
# ключей у учётки, чем они ограничены и который из них CI-шный. Печатаются
# только отпечаток, комментарий и опции — по ним ключ опознаётся, но
# воспроизвести его нельзя, поэтому вывод аудита остаётся безопасным для
# пересылки. Отпечаток тут тот же, что пишет sshd в auth.log при входе, так
# что строки отсюда и из журнала сопоставляются напрямую.
for ak in /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys; do
[ -f "$ak" ] || continue
echo " $ak:"
n=0
while IFS= read -r akline; do
case "$akline" in ''|'#'*) continue ;; esac
n=$((n+1))
akfp="$(printf '%s\n' "$akline" | ssh-keygen -lf - 2>/dev/null | awk '{$1=""; sub(/^ /,""); print}')"
akopt="$(printf '%s\n' "$akline" | awk '{o="";for(i=1;i<=NF;i++){if($i ~ /^(ssh-|ecdsa-sha2-|sk-)/)break; o=o (o==""?"":" ") $i} print (o==""?"(без ограничений)":o)}')"
printf ' %d) %s\n опции: %s\n' "$n" "${akfp:-<не разобран>}" "$akopt"
done < "$ak"
if [ "$n" -eq 0 ]; then echo " (пусто)"; fi
done
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'