Что это. Одна страница «первой минуты», собранная из девяти статей серии «Эксплуатация VPS / DevOps-база». Держите её на сервере (
~/incident.md), в терминале или распечатанной рядом с рабочим местом. Она отвечает на вопрос «сайт/сервер не дышит — что делать и в каком порядке», не заставляя вспоминать восемь теорий. Для кого. Самописный VPS с WordPress: Ubuntu/Debian, nginx + PHP-FPM + MariaDB, один администратор. Не для. Полного сценария взлома — если по ходу увидите чужие процессы/подменённые файлы, переходите в «Шаг 6» ниже и в статью про реагирование.
Три золотых правила (нарушение любого стоит дороже самой аварии)
- Не перезагружайте машину первым шагом. Рестарт теряет состояние и улики и почти всегда маскирует причину. Идите по опроснику этажей (Шаг 2).
- Держите второй SSH-сеанс открытым, пока новый вход не подтверждён. Любая правка
sshd/ufw/ядра без проверки нового входа = остаться за дверью. - Меняйте один слой за раз и проверяйте симптом (
curl 443) после каждого действия. «Переписали nginx + php.ini + ребут» не говорит, что помогло и что сломало.
Шаг 0 · Пульс машины (60 секунд)
# SSH заходит? (нет → консоль провайдера, дальше ничего не трогаем)
uptime; free -h; df -h /; df -i /; systemctl --failed --no-pager
# что слушает и открыто наружу
sudo ss -tulpn | grep -E 'LISTEN|UNCONN'
sudo ufw status numbered 2>/dev/null | headКуда проваливаться по первому «не ок»:
| Увидели | Это значит | Идите |
|---|---|---|
df -h / ≈ 100% или df -i / ≈ 100% | кончилось место/иноды → тихая смерть записи | Развилка A |
free → available≈0, --failed с базой/php | OOM-killer кого-то сожрал | Развилка B |
systemctl --failed / юнит рестарт-шторм | сервис лежит или пересоздаётся | Шаг 2, этаж |
лишнее слушает (3306, :9100, чужой порт) | периметр дыряв или чужой процесс | Развилка E / 6 |
load↑ при пустом CPU (wa↑) | тормозит диск/база, не процессор | Развилка A/B |
Шаг 1 · Отвечает ли фронт (30 секунд)
curl -sSI --max-time 8 https://example.test/ | head -n 3| Ответ | Где сломалось | Идите |
|---|---|---|
502 | nginx жив, мёртв PHP-FPM или база | Шаг 2 → этаж php/база, Развилка B/C |
500 | PHP жив, WordPress падает внутри (плагин/тема/запрос) | error-лог PHP + slowlog (Шаг 2, этаж php) |
| таймаут / «тишина» | сокет/пул не отвечает или диск/память «полошили» всё | Развилка A/B |
200/301 | фронт живой, жалоба не в «лежит» | деградация — см. SLO/медленные запросы (Развилка C) |
Шаг 2 · Опросник по этажам (идите сверху вниз, стоп на «не ок»)
Цепочка запроса: браузер → DNS → nginx → [сокет пула] → PHP-FPM → MariaDB → диск/память/CPU.
# Этаж nginx (фронт)
sudo systemctl is-active nginx; sudo nginx -t
sudo journalctl -u nginx -p err --since -15m --no-pager | tail
# Этаж сокет → php-fpm
ls -l /run/php/
sudo systemctl status php8.2-fpm --no-pager | head -n 6
sudo tail -n 30 /var/log/php8.2-fpm.log # «reached pm.max_children»/«out of memory»
sudo tail -n 20 /var/log/php-fpm/example-slow.log # что именно тормозит в коде
# Этаж база
sudo systemctl is-active mariadb; sudo mysqladmin ping
sudo mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Max_used_connections'; SHOW VARIABLES LIKE 'max_connections';"
sudo mysql -e "SELECT ID,USER,TIME,STATE,LEFT(INFO,50) q FROM information_schema.PROCESSLIST WHERE COMMAND='Query' ORDER BY TIME DESC LIMIT 5;"Как только слой «не ок» — это ваш подозреваемый корень; глубже не лезьте, идите в нужную развилку.
Развилки по типу аварии (четыре частых + атака)
Развилка A · «Кончился диск / иноды»
df -h / # байты | df -i / # иноды (свалка мелких файлов при 30% занято)
sudo du -xh --max-depth=1 / 2>/dev/null | sort -h | tail
sudo du -xh --max-depth=1 /var/log 2>/dev/null | sort -h | tailЧистим без удаления открытых дескрипторов:
sudo journalctl --vacuum-size=100M
sudo rm -f /var/log/*.gz /var/log/*/*.[0-9] # ротированные номера/архивы, НЕ текущие .log
# текущий открытый лог — притереть на месте, а не rm:
: > /var/log/nginx/access.log/boot100% (apt не ставит) → удалить старые ядра:apt-get -s autoremove --purgeсимуляцией, затем аккуратно (текущее + одно запасное не трогать).- иноды (свалка) → чистить старые сессии:
find /var/lib/php/sessions -type f -mtime +1 -delete. - ⛔ Никогда
rmу файла, который демон держит открытым — место не освободится до сигнала; используйте: >и/илиUSR1/reload.
Развилка B · «Сервер подзавис и один процесс сожрал память» (OOM)
sudo dmesg -T | grep -iE 'out of memory|oom-kill|killed process' | tail
ps -o rss=,comm= -C php-fpm8.2 | awk '{s+=$1} END{printf "php-fpm суммарно: %.0f МБ\n", s/1024}'
free -hKilled process ... (mariadbd) / (php-fpm) — ядро прирезало, база «сама не падала». Порядок подъёма и быстрая помощь:
sudo systemctl start mariadb && sleep 2 && sudo mysqladmin ping # сначала база
sudo systemctl reload php8.2-fpm # потом php
# временно притормозить аппетит пула, пока пересчитываете:
# pm.max_children ↓ в /etc/php/8.2/fpm/pool.d/www.conf, затем reload fpmКорень — «сумма хотелок > физическая память»: pm.max_children × RSS + innodb_buffer_pool_size + Opcache > RAM. Считать одним бюджетом; на 1 ГБ VPS обязателен swap.
Развилка C · «Слишком много соединений / висящий запрос»
# зависший долгий запрос найти и снять (НЕ рестарт сервиса)
sudo mysql -e "SELECT ID,TIME,STATE,LEFT(INFO,50) FROM information_schema.PROCESSLIST WHERE COMMAND='Query' ORDER BY TIME DESC LIMIT 5;"
sudo mysql -e "KILL <Id>;" # только прочитав Info — не обрывать чужой незавершённый импортERROR 1040: Too many connections→ не рестарт базы, а связкаmax_connections↔ суммарные дети всех пулов ↔wait_timeout(закрывать уснувшие).ss -s/ss -tan | awk '{print $1}' | sort | uniq -c→ многоSYN-RECV= SYN-flood (периметр:ufw limit/fail2ban/WAF); многоTIME_WAIT= норма/keepalive; забитыйnf_conntrack_count→_max= поднять лимит/снизить шум.- «502 из пустого пула» при нормальной памяти →
reached pm.max_children: либо снять висящие (выше), либо реально много параллельных. ⚠️ подниматьmax_children«чтобы не было 502» — получить OOM: потолок 502 и потолок памяти — одна задача.
Развилка D · «Поправил/обновил — не поднялось» (быстрый откат по слоям)
# проверить синтаксис ДО reload и после падения
sudo nginx -t; sudo php-fpm8.2 -t; sudo sshd -t # если правили ssh — ДО рестарта!
sudo systemd-analyze verify /etc/systemd/system/<юнит>.service
sudo journalctl -u <юнит> -p err --since -10m --no-pager | tail # первая строка = причинаОткатывать ровно подозрительный слой:
- nginx/php
.d-конфиг → вернуть.bak/drop-in →-t→reload(не restart фронта). - sshd → не трогать основной файл, убрать битый
sshd_config.d/99-битый.conf; живой второй сеанс не закрывать. - systemd-юнит →
systemctl revert <юнит>(сброс override к пакетному). /etc/*вообще →etckeeper/gitв/etc(git checkout HEAD~1 -- файл).- пакет →
apt-get install pkg=<версия> --allow-downgrades+reloadсервиса. - ядро грузится криво → GRUB из консоли провайдера выбрать старую запись; держать запасное ядро.
- виновник — ночное авто-обновление →
grepпо/var/log/apt/history.logи/var/log/unattended-upgrades/.
Развилка E · «Это не сбой» → точка ветвления на атаку
Если на этажах видны незнакомые пользователи/процессы, подменённые файлы .php в загрузках, исходящий трафик-флуд, пустой/срезанный auth.log, чужой IP входа, новые systemd-юниты → не «чиним и перезапускаем» (затрёте следы и вернёте атакующего). Переходите в Шаг 6 и статью «Реагирование на инциденты: что делать, если сайт взломали». Отличить «OOM из-за кода» от «OOM из-за майнера» — тоже там.
Шаг 3 · Снять закупорку (не «свалить всё»)
KILL <Id> висящего · reload где можно вместо restart · вернуть конфиг из .bak/git · откатить пакет/ядро по необходимости. После каждого действия — curl -sSI https://example.test/ | head (не чиним вслепую; проверяем, что симптом ушёл).
Шаг 4 · Зафиксировать (1 минута, критично)
echo "$(date -u +%FT%TZ) нашёл=<этаж+признак> сделал=<что> откатил=<если>" >> ~/incident-log
Плюс контекст: «после обновления» → apt history; «в 3 ночи» → unattended log; OOM → вывод dmesg -T. Без записи «через неделя машина странно сконфигурирована и никто не помнит почему».
Шаг 5 · Не допустить завтра
Каждый инцидент должен оставить за собой предохранитель — см. «Чек-лист предотвращения» ниже по статье, из которой выросла развилка (диск/иноды → logrotate + df -i алерт; OOM → пересчёт пула + warn на available; соединения → max_connections/wait_timeout + PROCESSLIST-проба; конфиг → etckeeper/бэкап-конфига + -t-привычка; «узнали от пользователя» → порог+heartbeat).
Шаг 6 · Если похоже на взлом (замена сценария)
Изолировать (закрыть наружу доступ, отозвать скомпрометированные ключи/сессии) → зафиксировать (копии логов, снимок подозрительных файлов, снимок процесса) → потом чинить; единственный надёжный путь «вернуть чистое» — восстановление из заведомо чистого бэкапа (файлы из архива, база из проверенного дампа). Дальше — сценарий реагирования (см. ссылку в конце).
Чек-лист предотвращения — по девяти статьям серии
(держать «уже настроенным», чтобы развилки срабатывали редко, а инцидент не стал «узнали от пользователя»)
1 · Анатомия VPS (вход, права, swap, часы)
- не закрывали root-сессию, пока не проверили новый вход в отдельном окне (запасная дверь до подтверждения)
- `sshd -t` перед `systemctl restart ssh` (иначе опечатка в конфиге = на морозе)
- права `~/.ssh` 700, `authorized_keys` 600; публичный ключ не обрезан; passphrase на приватном
- swap-файл прописан в `/etc/fstab` (иначе исчезнет после ребута → первая же ООМ-авария)
- один часовой пояс на всю систему (UTC в ОС + местный в cron = задачи не в тот час)
- бэкап приватного ключа в менеджере паролей; права на файлы, созданные root через sudo, под владельца
2 · systemd (юниты, enable, journal)
- `ExecStart` абсолютным путём (из `command -v`), без shell-конструкций (`cd`/`&&`/`|`)
- `daemon-reload` после правки файла; `enable` + `[Install]/WantedBy` для автостарта; `After=` не равен зависимости (нужен `Wants=`/`Requires=`)
- `WorkingDirectory` задан; `User=` не забыт (иначе root-owned артефакты, которые PHP не прочтёт)
- `Restart=on-failure` (не `always`, иначе не останавливается по `stop`); `RestartSec`/`StartLimitBurst` + потолок `MemoryMax` против рестарт-шторма
- правки через `systemctl edit` (drop-in), не в `/lib/…` (затрёт `apt upgrade`)
3 · Nginx (фронт, TLS, лимиты тела)
- `root` совпадает с реальным путём сайта; `try_files … ?$args` (иначе `?preview`/`?p=` теряются)
- `fastcgi_pass` на unix-сокет (не TCP-порт); `SCRIPT_FILENAME` абсолютный (иначе 502 «Primary script unknown»)
- `cgi.fix_pathinfo=0` + `try_files … =404` (против combo-исполнения из uploads); `^~ /wp-content/uploads/` + deny PHP там
- TLS от Let’s Encrypt с автопродлением (certbot-таймер); порт 80 открыт для продления
- `client_max_body_size` согласован с PHP-лимитами (`> post_max_size > upload_max_filesize`)
4 · PHP-FPM (пул, память, Opcache, slowlog)
- `pm.max_children` посчитан по `free`/RSS, суммарно по всем пулам (+ Opcache-сегмент в бюджет)
- `pm.max_requests` задан (пересоздавать детей, не давая утечке разбухать); режим `static`/`dynamic`/`ondemand` по задаче
- правки в `…/fpm/php.ini` (не `…/cli/php.ini`); `php_admin_value` там, где плагину нельзя менять лимит
- `opcache.validate_timestamps` осознанно (=0 требует сброса кэша после деплоя)
- `slowlog` на существующий путь с правами пользователя пула; `listen.mode 0660` + верный owner/group (не 0666)
5 · MariaDB (bind, InnoDB, бэкап, PROCESSLIST)
- не `DB_USER=’root’` в wp-config; пользователь под один сайт; `bind-address` сокетом, 3306 наружу закрыт
- `innodb_buffer_pool_size` посчитан с учётом PHP-детей на той же машине (не «по учебнику 70%»)
- `mysqldump —single-transaction —routines —triggers —events` (без блокировки и не теряя триггеры)
- бэкап на другом диске/хосте, не рядом с базой; проверка восстановления прогонялась реально
- «БД висит» → `SHOW PROCESSLIST`/`KILL <Id>`, а не рестарт сервиса
6 · Firewall / SSH-харднинг (периметр)
- `ufw allow 22` до `ufw enable`; 80/443 открыты (80 нужен для продления certbot); исходящий трафик не закрыт (`apt`/NTP/DNS)
- fail2ban на актуальные фильтры (invalid-user, а не «Failed password» при выключенном пароле); `ignoreip` на свои IP/монитор
- overrides в `.local`/`.d`, не в `jail.conf`/`sshd_config` напрямую (затрёт обновление); один источник правил (не ufw+Docker+nft вразнобой)
- штатный аудит `ss -tulpn`; нестандартный SSH-порт — шумоподавление, не «защита»
7 · Обновления (кандидат, unattended, откат)
- помните `apt upgrade` ≠ применено (демоны на старом коде до перезапуска) — `needrestart`/`/var/run/reboot-required`
- unattended только `-security`; `Automatic-Reboot` либо с окном+мониторингом, либо выключен
- `hold` на ядре/PHP с планом вручную закрыть CVE (не навсегда без плана); мажорная PHP ставится рядом (второй сокет = путь отката)
- `reload` где достаточно (nginx/php-fpm), не `restart`; дамп базы перед мажорным апгрейдом БД
- `apt-get -s autoremove` симуляцией (не удалить нужное ядро); staging не открыт наружу с боевым дампом
8 · Мониторинг ОС (пять метрик, logrotate, алерты)
- читают `available`, не `free`; видят высокий `wa`= диск/база, а не «мало CPU»; смотрят `df -i` рядом с `df -h`
- `logrotate` для nginx/PHP slowlog + лимит journal (`SystemMaxUse`); `postrotate` с `kill -USR1` nginx / reload fpm
- алерты двух типов: threshold с `for`-гистерезисом (не орёт на касание) + heartbeat/dead man’s switch (ловит «монитор упал вместе с сервером»)
- приёмник heartbeat и Prometheus — не на той же машине; порог привязан к действию и SLO; будит ночью только critical (остальное warning/дашборд)
9 · Диагностика (этот лист)
- идёте по опроснику этажей, а не «ребут первым шагом»
- при «база/php внезапно умер» читают `dmesg -T`/`journalctl -k`; при «500 при свободном диске» — `df -i`
- `KILL` только прочитав `Info`; откатывают один слой, правят конфиг с `-t` и вторым SSH-сеансом
- при признаках взлома меняют сценарий (фиксация→изоляция→чинить), фиксируют «что делали», ставят предохранитель (шаг 5)
Конвенции листа
- Простым языком. Любая аббревиатура при первом употреблении объясняется в статьях серии; здесь предполагается, что вы их уже читали.
- Ubuntu/Debian. Имена пакетов/юнитов/сокетов/логов (
php8.2-fpmvs8.3,mariadbvsmysql,sshvssshd) и поведениеapt-postinst условны — сверяйте фактические черезsystemctl list-units,php -v,mariadb --version,ls /run/php/,logrotate -d. - Домены/IP/цифры условные:
example.test(зарезервированный тест-домен, наружу не резолвится),203.0.113.x(TEST-NET),staging.test; пороги (85%/70%/10m) — ориентиры, калибруйте под бюджет своей машины. - Правило всей серии в аварии: приёмник алертов/сам монитор/консоль отката — не на той же машине, что сторожите; второй SSH-сеанс жив, пока не подтверждён новый вход; боевой не трогаем, пока не проверили на копии.
👤 Зарегистрируйтесь, чтобы добавить материал в закладки.
