Что нового

Обновление Ubuntu 24.04 до 26.04 LTS: Percona 8.4, PHP, почта и iptables

Изображение ChatGPT 30 окт. 2026 г., 00_50_06.png


Подробный технический журнал: как я обновил рабочий сервер sysadmin.guru с Ubuntu 24.04.5 до Ubuntu 26.04.1 LTS, перевёл PHP и Dovecot на пакеты для новой ОС, обновил Percona Server 8.0 до 8.4 LTS, восстановил совместимость почтовой системы и настроил iptables с постоянными блокировками Fail2ban.

В этом руководстве я описываю не абстрактную установку «с нуля», а реальное обслуживание работающей виртуальной машины Proxmox VE 25–26 октября 2026 года. На ней размещаются XenForo с AMS, Nginx, PHP-FPM, Percona Server, Manticore Search, Postfix, Dovecot, Roundcube, PostfixAdmin и OpenDKIM. Задача была обновить ОС и компоненты без потери данных, восстановить работоспособность поиска после ошибки бинарного журнала, перевести почту на совместимую аутентификацию MySQL, а уже после этого усилить Firewall. Я сохранил не только удачные команды, но и реальные неполадки, проверки и причины принятых решений.

Перед началом: все действия с релизом Ubuntu, Percona и правилами Firewall потенциально прерывают доступ к сайту и почте. У меня была полная резервная копия VM в Proxmox Backup Server и доступ к консоли гипервизора. Не копируйте IP-адреса, пароли, имена пользователей и сетевые разрешения с чужого сервера — в примерах используйте собственные значения.


Содержание
1. Исходная инфраструктура и план обновления
  Какие зависимости я заранее выписал
  Что я увидел до миграции
  Почему я сначала не доверял только apt-cache и systemctl
2. Резервные копии и безопасный порядок действий
  Разделил резервные копии по назначению
  Почему опасно откатывать только пакет MySQL
3. Обновление Ubuntu 24.04.5 до 26.04.1 LTS
  Сначала сделал обычное пакетное обновление Ubuntu 24.04
  Какие вопросы задал do-release-upgrade
  Как мастер обработал сторонние пакеты
  После reboot я проверил именно новый релиз
4. Восстановление Manticore: несовместимый binlog и повторный upgrade
  Фактическая неисправность: несовместимая версия binlog
  Почему нельзя было просто удалить binlog.0001
  Временно установил Manticore 25.0.0
  Проверил данные и сбросил RAM-чанк на диск
  Снова поставил 29.9.0 и проверил индекс
5. Сторонние репозитории после смены релиза
  Почему нельзя выполнять массовую замену noble на resolute
  Отдельно контролировал сценарии downgrade
6. PHP 8.4 и PHP 8.5: переход на пакеты Ubuntu 26.04
  37 установленных PHP-пакетов пришлось рассматривать как один набор
  Проверил два разных FPM-сокета и обработчик FastCGI
  Почему временная ошибка при запуске Nginx не была ошибкой PHP
  Предупреждения OPcache не смешивал с падением сервиса
7. Dovecot 2.4.5 и совместимость почты
  Обновил именно сборки Dovecot, а не основную ветку
  Где я получил проблему с правами на dovecot.conf
  Проверка почты включала реальные протоколы и доставку
8. Подготовка почтовых SQL-пользователей к MySQL 8.4
  Как разделил доступ на чтение и изменение
  Шифрование соединения Postfix MySQL
  Почему старый пользователь остался заблокированным
  PHP-клиенты MySQL и Postfix — два независимых случая
9. Проверка Percona Server перед обновлением до 8.4 LTS
  Перенос InnoDB Redo Log на современный параметр
  Почему первый вход в mysql от root не сработал
  Что именно значили 17 предупреждений Upgrade Checker
  Почему размер Redo Log зафиксировал явно
  Удалил несовместимую опцию заранее
10. Обновление Percona 8.0.46 → 8.4.11-11
   Проверка памяти и почты после Percona
   Какие сведения подтвердили именно обновление данных
   Три базы и контроль приложений
   Почему я не стал срочно выключать swap
   Почтовые службы после работы с MySQL
11. Проверка портов и существующих правил
   Как я проверил административный IP
12. Подготовка единого firewall.sh
   Что находится внутри правил
13. Безопасное применение правил без потери SSH
   Как я настроил автозапуск
14. Установка Fail2ban и постоянные баны
15. Глобальные постоянные блокировки IPv4 и IPv6
16. Подключение защиты Nginx
   Почему у меня сначала не работал фильтр сканирования
   Исправление регулярного выражения Fail2ban
17. Исключение административных адресов из постоянных банов
18. Четыре работающих jail: итоговые настройки
19. Просмотр и ручное управление постоянными блокировками
20. Финальный reboot и контрольный список
21. Практический чек-лист диагностики, если после обновления что-то не заработало
   Сайт возвращает 502 или PHP upstream недоступен
   Postfix принимает SMTP, но не принимает адресата
   IMAP-сервис active, но клиент не может авторизоваться
   MySQL после перехода 8.4 не стартует
   После активации Firewall пропал SSH
22. Версии и границы проведённой работы
23. Источники и документация
24. Итог и что осталось на будущее



1. Исходная инфраструктура и план обновления​


Изначально работала Ubuntu 24.04.5 LTS (noble) на виртуальной машине с диском 50 ГБ (отдельный /boot 500 МБ, swap-раздел 5 ГБ, корневой раздел около 44,5 ГБ) и примерно 7,1 ГиБ доступной RAM. Под нагрузкой находились одновременно сайт, поиск и полноценный почтовый сервер. До обновления использовались Percona Server 8.0.46, PHP 8.4.27 и 8.5.12, Dovecot 2.4.5 в сборке для Ubuntu 24.04, Manticore Search 29.9.0. PHP и Dovecot устанавливались из сторонних репозиториев; это важно при смене релиза ОС.

Я разбил работы на четыре блока: (1) резервные копии и обновление Ubuntu; (2) согласование сторонних репозиториев и работа PHP/почты; (3) переход Percona 8.0 на 8.4 LTS; (4) Firewall и Fail2ban. Между блоками проверял сайт, базы, поиск и почтовые службы.

Начальную диагностику выполнял командами:

Bash:
lsb_release -a
uname -r
free -h
df -hT
dpkg --audit
apt-mark showhold
apt-cache policy percona-server-server php8.4-fpm php8.5-fpm dovecot-core manticore
systemctl is-active mysql nginx postfix dovecot manticore
ss -lntup


Какие зависимости я заранее выписал​


Я сразу отказался от принципа «обновлю всё разом, потом разберусь». На такой VM любое изменение может повлиять на другой сервис. XenForo обращается к MySQL и Manticore Search; веб-сервер передаёт PHP-запросы через Unix-сокеты PHP-FPM; Postfix использует SQL-карты адресатов, доменов и ящиков; Dovecot тоже запрашивает данные учётных записей из MySQL; PostfixAdmin пишет в ту же почтовую базу. Сбой SQL-аутентификации иногда выглядит как неисправность почтового протокола, а отсутствующий PHP-сокет — как неисправность самого Nginx.

Для исходного плана я разделил сервисы на четыре группы: ОС и пакеты; данные и поиск; почтовая авторизация; публикация сайта и сетевые правила. Последовательно проверял active службы, но никогда не считал это окончательным тестом приложения.

Bash:
# Фиксирую текущую ОС, ядро и объём дисков
cat /etc/os-release
uname -r
lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINTS
df -hT
free -h

# Смотрю ошибки системы и активные сокеты
systemctl --failed --no-pager
ss -lntup

# Фиксирую фактические версии пакетов
dpkg-query -W -f='${binary:Package} ${Version}\n' \
  percona-server-server nginx dovecot-core manticore \
  php8.4-fpm php8.5-fpm

Что я увидел до миграции​


На исходной Ubuntu 24.04.5 LTS lsblk показывал диск /dev/sda на 50 ГБ. Отдельный /boot занимал 500 МБ, swap — 5 ГБ, корень — 44,5 ГБ. Свободного места на корневом разделе было достаточно, но отдельный небольшой /boot заставлял отдельно следить за количеством старых ядер и размером initramfs.

Перед обновлением стояли Percona 8.0.46-37, Dovecot 2.4.5 в сборке +ubuntu24.04, PHP 8.4.27 и PHP 8.5.12 из PPA Ondřej/Sury, Manticore 29.9.0 из отдельной ветки пакетов manticoresearch_jammy. Эти сведения понадобились затем для понимания, почему APT может считать downgrade установку правильного пакета новой Ubuntu.

Почему я сначала не доверял только apt-cache и systemctl​


Команда apt-cache policy показывает доступные версии, но не гарантирует совместимость бинарных форматов данных. Пример — binlog Manticore: пакет установлен и служба существует, а движок отказывается стартовать после чтения журнала. Аналогично результат systemctl is-active mysql не гарантирует поддержку старого способа аутентификации приложений после перехода MySQL на 8.4.

2. Резервные копии и безопасный порядок действий​


Перед обновлением создал полный PBS-бэкап виртуальной машины. Это принципиально важно: при обновлении MySQL до 8.4 внутренний словарь данных меняется, и простой откат DEB-пакетов до 8.0 не восстанавливает прежний формат базы.

Помимо копии VM, сохранил конфигурации MySQL, перечень пакетов и логическую копию пользовательских баз sysadmin, postfix и roundcubemail. Для MySQL использовал mysqldump с транзакционным снимком:

Bash:
mkdir -p /root/percona84-prep

mysqldump -u root -p \
  --single-transaction --quick \
  --routines --triggers --events \
  --hex-blob --set-gtid-purged=OFF \
  --databases sysadmin postfix roundcubemail \
  | gzip -1 > /root/percona84-prep/mysql-before-84.sql.gz

gzip -t /root/percona84-prep/mysql-before-84.sql.gz
sha256sum /root/percona84-prep/mysql-before-84.sql.gz

Оговорка: такая команда не является полноценной физической копией экземпляра MySQL, не сохраняет все системные учётные записи и не гарантирует согласованности нетранзакционных таблиц. Мой основной план восстановления — PBS-бэкап VM. На рабочей системе я не использовал apt autoremove наугад и проверял пакетные операции перед установкой.


Разделил резервные копии по назначению​


В качестве точки аварийного отката я использовал полноценную резервную копию VM в Proxmox Backup Server (PBS). Она сохраняет не только базы данных, но и конфигурацию, систему пакетов, пользователей и системные зависимости. На уровне гостевой Ubuntu отдельно сохранял текстовые конфигурации и логические дампы, чтобы в случае отдельной ошибки не восстанавливать весь сервер.

Bash:
# Рабочий каталог диагностики; права только владельцу
install -d -m 700 /root/upgrade-2604

dpkg-query -W > /root/upgrade-2604/packages.before.txt
apt-mark showmanual > /root/upgrade-2604/manual.before.txt
systemctl list-unit-files --type=service > /root/upgrade-2604/services.before.txt

# Состав доступных репозиториев с осторожностью к приватным URL
cp -a /etc/apt/sources.list /root/upgrade-2604/ 2>/dev/null || true
cp -a /etc/apt/sources.list.d /root/upgrade-2604/

# Конфигурации; после бэкапа не публикую эти файлы,
# поскольку в них могут быть учётные данные
cp -a /etc/mysql /root/upgrade-2604/mysql-conf
cp -a /etc/dovecot /root/upgrade-2604/dovecot-conf
cp -a /etc/postfix /root/upgrade-2604/postfix-conf
cp -a /etc/nginx /root/upgrade-2604/nginx-conf

Архив конфигураций с реальными SQL-паролями я не отправляю в публичную статью, а хранилище PBS должно быть защищено отдельно от VM. Целостность дампа gzip -t — только минимальная проверка файла; наиболее надёжный контроль бэкапа — тест восстановления на отдельной VM.

Почему опасно откатывать только пакет MySQL​


Переход между 8.0 и 8.4 затрагивает внутренний словарь данных. Когда новый MySQL уже изменил datadir, установка старого DEB не возвращает состояние данных до обновления. В моём случае для аварийного отката предусматривался полный PBS restore согласованного состояния, а не apt install percona-server-server=8.0... поверх уже обновлённого словаря. Этот вопрос я решил до первого запуска Percona 8.4, а не после него.

3. Обновление Ubuntu 24.04.5 до 26.04.1 LTS​


На исходной системе проверил пакетную базу и доступность обновлений. Сам переход выполнил штатным do-release-upgrade в предусмотренное окно обслуживания:

Bash:
sudo apt update
sudo apt upgrade
sudo do-release-upgrade

В процессе обновления учитывал изменения системных конфигураций и особенности внешних репозиториев. После завершения и перезагрузки получил Ubuntu 26.04.1 LTS (resolute), ядро 7.0.0-38-generic. Проверил:

Bash:
cat /etc/os-release
uname -r
dpkg --audit
apt update
systemctl --failed
systemctl is-active nginx mysql postfix dovecot manticore

Сайт XenForo отвечал HTTP 200. Но успешный старт служб после do-release-upgrade ещё не означал завершённой миграции: часть сторонних пакетов оставалась в сборках для Ubuntu 24.04, а старые источники APT могли быть отключены.


Сначала сделал обычное пакетное обновление Ubuntu 24.04​


Прежде чем запускать do-release-upgrade, я привёл текущую систему в актуальное состояние обычным apt upgrade. В выводе были сотни обновляемых пакетов, включая компоненты ядра, PHP, Percona, Manticore и Dovecot из сторонних репозиториев. Важно различать эти две операции: apt upgrade внутри Ubuntu 24.04 не превращает систему в 26.04 — он подтягивает новые сборки в рамках уже подключённых источников.

Bash:
apt update
apt list --upgradable
apt upgrade

dpkg --audit
apt-mark showhold
systemctl --failed --no-pager

При проверке apt update я видел уведомление Missing Signed-By для старого Ubuntu-источника. Это повод впоследствии привести deb822/ключи репозиториев в порядок, а не основание немедленно менять подписные ключи во время большого обслуживания. При дальнейших APT-операциях я следил за списком REMOVED и не запускал apt autoremove автоматически.

Какие вопросы задал do-release-upgrade​


Штатный мастер обнаружил Ubuntu 26.04 LTS Resolute Raccoon. Поскольку я работал по SSH, он предупредил об опасности потери удалённого доступа и предложил временный дополнительный SSH-демон на порту 1022. Я согласился с запуском резервного демона, но помнил, что существующий Firewall не обязан разрешать этот порт.

Код:
Continue running under SSH?
This session appears to be running under ssh.
If you continue, an additional ssh daemon will be started at port '1022'.

Нюанс: sshd на 1022 не спасёт, если порт запрещён INPUT-правилами. Я не открываю 1022/tcp всему Интернету автоматически: при реальной необходимости можно временно добавить доступ только для административного IP, убедившись, что правило будет удалено после обновления. На практике лучшей страховкой оставалась консоль Proxmox.

Как мастер обработал сторонние пакеты​


В списке Foreign Packages Installed мастер отдельно перечислил Dovecot 2.4.5, PHP 8.5 и многие расширения из PPA ondrej/php. Он предложил продолжить обновление несмотря на неофициальные пакеты — я осознанно согласился, предварительно зафиксировав их версии. Позже сторонние репозитории нужно было восстановить для resolute или оставить выключенными, если поддерживаемого пакета пока не существовало.

Во время расчёта изменений появлялись No candidate ver: linux-image-6.8... для старых версий ядер. Это сообщения APT о кандидатах из прежних репозиториев, а не самостоятельное доказательство повреждения установленного ядра. После обновления я проверил /boot, новое ядро и системные службы.

После reboot я проверил именно новый релиз​


Bash:
cat /etc/os-release
lsb_release -a
uname -r
findmnt /boot
df -h / /boot
dpkg --audit
apt-mark showhold
systemctl --failed --no-pager

На рабочей VM в итоге получилась Ubuntu 26.04.1 LTS, ядро 7.0.0-38-generic. Для apt-cache policy принципиально было видеть правильный новый codename resolute. Установка ОС завершилась, но миграция приложений на этом только начиналась.

4. Восстановление Manticore: несовместимый binlog и повторный upgrade​


Эта авария произошла при предварительном пакетном обслуживании Ubuntu 24.04 — ещё до штатного перехода на релиз 26.04. После восстановления я повторно проверил Manticore уже на новой ОС. Ниже показываю и аварию, и её решение, и итоговую проверку. Я проверил активность процесса, локальные порты 9306/9308/9312 и индекс xf, который возвращал 94 документа.

Bash:
systemctl is-active manticore nginx php8.4-fpm php8.5-fpm mysql
ss -lntp | grep -E ':(9306|9308|9312|3306)[[:space:]]' || true

curl --noproxy '*' \
  --resolve sysadmin.guru:443:127.0.0.1 \
  -sS -o /dev/null -w 'HTTP: %{http_code}\n' \
  https://sysadmin.guru/login/

Сайт вернул HTTP 200. Дополнительно тестировал авторизацию, поиск по форуму, PostfixAdmin и Roundcube. Такие функциональные проверки обязательны: active в systemd сам по себе не доказывает работоспособность сайта и отправки почты.


Фактическая неисправность: несовместимая версия binlog​


Это была одна из самых неприятных проблем обслуживания и главный пропуск первой редакции статьи. После обычного пакетного обновления Manticore 29.9.0 попытался прочитать существующий бинарный журнал RT-таблицы xf и аварийно завершился. Перезапуск не помогал: служба несколько раз подряд получала exit-code и дошла до Start request repeated too quickly.

Код:
FATAL: binlog: log /var/lib/manticore/binlog/binlog.0001 is v.15,
binary is v.16; recovery requires previous binary version

В том же журнале были предупреждения MCL requires a CPU that supports SSE 4.2 и POPCNT. Проверка /proc/cpuinfo не показала соответствующих флагов в VM. Это отдельное ограничение возможностей Manticore на представленной виртуальной CPU, однако непосредственной причиной отказа запуска была именно несовместимая версия binlog, что ясно указано в строке FATAL.

Bash:
systemctl status manticore.service --no-pager -l
journalctl -u manticore.service -b -n 100 --no-pager
tail -n 100 /var/log/manticore/searchd.log

grep -m1 '^flags' /proc/cpuinfo | grep -oE 'sse4_2|popcnt' || true
ls -lah /var/lib/manticore/binlog/

Почему нельзя было просто удалить binlog.0001​


В данном случае RT-таблица xf могла содержать изменения, уже подтверждённые в binlog, но ещё не перенесённые из RAM-чанка в дисковое состояние. Удалив binlog.0001, я бы мог лишиться части этих изменений. Прежде чем менять пакет, я сделал полный снимок текущего /var/lib/manticore и конфигурации.

Bash:
systemctl stop manticore
pgrep -a searchd || true

du -sh /var/lib/manticore
df -h /root /var/lib/manticore

install -d -m 700 /root/manticore-recovery

tar -C /var/lib -cpf \
  /root/manticore-recovery/manticore-data.tar \
  manticore
cp -a /etc/manticoresearch /root/manticore-recovery/config

Данные занимали около 8,1 МБ, а активный binlog — примерно 1,2 МБ. На диске хватало свободного места для сохранения исходного состояния и развёртывания более старой версии.

Временно установил Manticore 25.0.0​


Из доступных в репозитории пакетов я выбрал 25.0.0-26032712-ce3c27828 — версию, способную прочитать старый binlog. Сначала скачал пакет и проверил симуляцию понижения версии:

Bash:
cd /root/manticore-recovery

wget -O manticore_25.deb \
 'https://repo.manticoresearch.com/repository/manticoresearch_jammy/dists/jammy/main/binary-amd64/manticore_25.0.0-26032712-ce3c27828_amd64.deb'

apt -s install --allow-downgrades ./manticore_25.deb

В плане значилось 1 downgraded, 0 to remove. После проверки я выполнил установку. Здесь важная тонкость: в моём конкретном выводе APT итоговая загрузка прошла из того же репозитория, хотя команда ссылалась на локальный DEB. Это не мешало процедуре, но при повторении следует проверять SHA256 файла и источник скачивания, а также версию пакета перед установкой.

Bash:
apt install --allow-downgrades ./manticore_25.deb
systemctl reset-failed manticore
systemctl start manticore
systemctl status manticore --no-pager -l

Версия 25.0.0 запустилась. В searchd.log появилась запись о воспроизведении binlog.0001, включая 11 commits для таблицы xf. Это и было реальным восстановлением накопленных изменений.

Код:
binlog: replaying log /var/lib/manticore/binlog/binlog.0001
binlog: table xf: recovered from tid 590 to tid 601
binlog: replay stats: 11 commits, 1 tables
accepting connections

Проверил данные и сбросил RAM-чанк на диск​


Проверил SQL-интерфейс Manticore по локальному порту 9306 и таблицу XenForo:

Bash:
mysql --protocol=TCP -h 127.0.0.1 -P 9306 \
 -e 'SHOW TABLES;'

mysql --protocol=TCP -h 127.0.0.1 -P 9306 \
 -e 'SELECT COUNT(*) FROM xf;'

По результатам таблица xf имела тип rt и содержала 94 записи. Затем я явно выполнил:

Bash:
mysql --protocol=TCP -h 127.0.0.1 -P 9306 \
 -e 'FLUSH TABLE xf;'

systemctl stop manticore
pgrep -a searchd || true
ls -lah /var/lib/manticore/binlog/

После корректной остановки каталог binlog уже не содержал прежнего binlog.0001: остался лишь маленький binlog.meta. Это позволило повторно установить новую версию без необходимости воспроизводить несовместимый бинарный журнал.

Снова поставил 29.9.0 и проверил индекс​


Bash:
apt install --only-upgrade manticore
systemctl start manticore
systemctl status manticore --no-pager -l

mysql --protocol=TCP -h 127.0.0.1 -P 9306 \
 -e 'SHOW TABLES; SELECT COUNT(*) FROM xf;'

Итог: версия 29.9.0-26091108-58e88d5ab, служба active (running), в systemd отображался статус healthy, индекс xf снова имел 94 записи. Предупреждения об отсутствии SSE 4.2 и POPCNT на представленной CPU оставались отдельным ограничением отдельных функций, но не мешали данному поисковому индексу обслуживаться. Именно эта последовательность — резервная копия, предыдущий бинарник, replay, FLUSH TABLE, корректная остановка, повторный upgrade — помогла мне избежать потери данных.

Нельзя применять такой downgrade наугад. Он допустим только при подтверждённой совместимости формата данных, наличии резервной копии и проверке реальных таблиц. Для другого релиза Manticore способ восстановления может отличаться.


5. Сторонние репозитории после смены релиза​


После обновления я отдельно проверил источники APT и происхождение установленных пакетов. Для меня было важно не смешивать библиотеки и демоны из noble с новой Ubuntu 26.04 без осознанного решения.

Bash:
grep -RnsE 'noble|resolute|sury|dovecot|percona|manticore' \
  /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null || true

apt-cache policy php8.4-fpm php8.5-fpm dovecot-core percona-server-server manticore
apt-get -s --no-remove upgrade

Репозитории PHP, Dovecot и позднее Percona привёл в соответствие с новой ОС. Для Dovecot применял репозиторий ветки ce-2.4-latest/ubuntu/resolute; для PHP — доступные сборки Sury под resolute; для Percona 8.4 — https://repo.percona.com/ps-84-lts/apt. После проблем с binlog я временно откатил Manticore с 29.9.0 на 25.0.0, восстановил журнал изменений и затем вновь установил 29.9.0. Окончательная версия совпала с первоначальной, но это было не просто тестирование, а реальное восстановление поискового движка.

Важно: здесь нельзя давать универсальную команду замены noble на resolute во всех файлах: некоторые поставщики используют собственные правила именования веток и путей. Я проверял apt-cache policy и симуляции APT для каждого компонента.


Почему нельзя выполнять массовую замену noble на resolute​


В моей системе одновременно присутствовали источники Ubuntu, Percona, PPA PHP, Dovecot CE и Manticore. У каждого проекта своя схема каталогов: путь Manticore содержал jammy даже на Ubuntu 24.04 и не превращался автоматически в resolute. Поэтому однострочная sed -i 's/noble/resolute/g' /etc/apt/sources.list.d/* здесь была бы опасной: можно получить несуществующие ветки и смешать неподдерживаемые сборки.

Bash:
# Список реальных источников APT
find /etc/apt/sources.list.d -maxdepth 1 -type f -print

grep -RnsE 'noble|resolute|ondrej|dovecot|percona|manticore' \
  /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null || true

apt-cache policy php8.4-fpm php8.5-fpm dovecot-core \
                 percona-server-server manticore

# Контроль того, что APT планирует удалить или понизить
apt-get -s --no-remove upgrade

После миграции я увидел отключённые записи PPA PHP для noble, Dovecot для noble и прежнюю стороннюю ветку Manticore. Часть затем восстановил под resolute. В случае PHP и Dovecot меня интересовали не только версии движков, но и суффиксы пакетов вида ubuntu26.04. То, что сервис работает после смены ОС на старой сборке, не означает, что пакетная система находится в правильном состоянии.

Отдельно контролировал сценарии downgrade​


При смене источника APT иногда считает правильную сборку Ubuntu 26.04 более старой, чем пакет с большим суффиксом +ubuntu24.04.1+deb.sury.org+1. Для миграции такое понижение номера DEB не равно переходу назад с PHP 8.5 на PHP 8.4. Я всегда читал подробные строки Inst в симуляции, смотрел целевую ветку resolute и проверял список планируемых удалений.

6. PHP 8.4 и PHP 8.5: переход на пакеты Ubuntu 26.04​


Веб-сервер использует два PHP-FPM: 8.4 и 8.5. После обновления Ubuntu я перевёл установленные пакеты PHP на сборки репозитория для resolute. Итоговые ветки остались PHP 8.4.27 и PHP 8.5.12. При этом APT мог указывать downgrade из-за схемы нумерации версий Sury: это не означало переход с PHP 8.5 обратно на старую основную ветку, а означало замену Ubuntu 24.04-сборки на сборку для Ubuntu 26.04.

Перед операцией проверял список пакетов и планы зависимостей:

Bash:
apt-cache policy php8.4-fpm php8.5-fpm
apt-get -s install php8.4-fpm php8.5-fpm
php8.4 -v
php8.5 -v
systemctl is-active php8.4-fpm php8.5-fpm

После обновления проверил конфигурации пулов, реальные сокеты и директивы Nginx:

Bash:
grep -RnsE '^[[:space:]]*listen[[:space:]]*=' /etc/php/8.4/fpm/pool.d /etc/php/8.5/fpm/pool.d
ss -xlpn | grep -E 'php|fpm'
nginx -T 2>&1 | grep fastcgi_pass

На моей VM рабочими оставались сокеты /tmp/php8.4-fpm.sock и /tmp/php8.5-fpm.sock. После одной из перезагрузок Nginx кратковременно выдавал ошибки подключения к /tmp/php8.5-fpm.sock: Nginx стартовал, когда PHP-FPM ещё не создал сокет. Позже сокет появился, а /login/ вернул HTTP 200.

Чтобы улучшить порядок старта, добавил drop-in:

Bash:
install -d -m 755 /etc/systemd/system/nginx.service.d
cat > /etc/systemd/system/nginx.service.d/20-php-fpm-order.conf <<'EOF_SYSTEMD'
[Unit]
Wants=php8.4-fpm.service php8.5-fpm.service
After=php8.4-fpm.service php8.5-fpm.service
EOF_SYSTEMD

systemctl daemon-reload
systemctl show nginx.service -p After -p Wants

Это помогает с порядком запуска, но не является полной проверкой готовности сокета; по журналам PHP-FPM также оставались предупреждения OPcache preload для классов League, которые я не стал исправлять в ходе сетевой миграции.


37 установленных PHP-пакетов пришлось рассматривать как один набор​


У меня было 37 PHP-пакетов с модулями для обеих версий. Частичная замена нескольких пакетов могла привести к конфликту ABI расширений и несовпадению версии PHP-FPM с php*-mysql, php*-redis, php*-imagick и другими модулями. Я подготовил отдельный скрипт симуляции миграции и просил APT подобрать соответствующие ubuntu26.04-сборки для всего установленного набора.

Код:
=== НАЙДЕНО PHP-ПАКЕТОВ: 37 ===
1 upgraded, 4 newly installed, 37 downgraded, 1 to remove and 1 not upgraded.

Слово downgraded относилось к версии Debian-пакета, а не к основной версии PHP: целевые сборки оставались php8.4 8.4.27 и php8.5 8.5.12. В симуляции появлялись пакеты ImageMagick 7 и libzip5, а imagemagick-6-common подлежал удалению. До реального применения я отдельно удостоверился, что такой переход не затрагивает необходимую библиотеку сайта.

Bash:
# Смотрю не только сам php-fpm, но и его расширения
apt-cache policy php8.4-fpm php8.5-fpm php8.4-mysql \
                 php8.5-mysql php8.5-imagick

php8.4 -v
php8.5 -v
php8.4 -m
php8.5 -m
php-fpm8.4 -t
php-fpm8.5 -t

Важно: строка 37 downgraded в чужой статье не должна превращаться в слепую команду apt --allow-downgrades upgrade для любой системы. Я применял точечный, заранее просмотренный план для фактически установленных пакетов.

Проверил два разных FPM-сокета и обработчик FastCGI​


В конфигурации задействованы и PHP 8.4, и 8.5. Поэтому после пакетной миграции проверил не только CLI-версию, но и службу, сокет пула, права на сокет и реально используемые Nginx upstream:

Bash:
systemctl status php8.4-fpm php8.5-fpm --no-pager

grep -RnsE '^[[:space:]]*listen[[:space:]]*=' \
  /etc/php/8.4/fpm/pool.d /etc/php/8.5/fpm/pool.d

ss -xlpn | grep -E 'php8\.[45]|php-fpm' || true
nginx -T 2>&1 | grep -E 'fastcgi_pass|server_name'

Итоговые пути моих сокетов — /tmp/php8.4-fpm.sock и /tmp/php8.5-fpm.sock. Я не менял их на стандартные /run/php/php8.x-fpm.sock без перенастройки Nginx: это гарантированно дало бы connect() to unix:... failed.

Почему временная ошибка при запуске Nginx не была ошибкой PHP​


После перезагрузки сайт вначале сообщал о неудачном подключении к /tmp/php8.5-fpm.sock, но спустя некоторое время сокет появился, а /login/ возвращал HTTP 200. Я сопоставил время сообщений Nginx и время активации php8.5-fpm.service в journald. Это был race condition во время boot, а не признак повреждённого PHP-пакета.

Поэтому создал drop-in /etc/systemd/system/nginx.service.d/20-php-fpm-order.conf с After и Wants, затем проверил systemctl show nginx.service -p After -p Wants. Wants запускает PHP-FPM вместе с Nginx, After задаёт порядок старта. Однако само по себе After не даёт гарантии готовности FastCGI сокета: это граница соответствующей меры, которую честно указал в журнале обслуживания.

Предупреждения OPcache не смешивал с падением сервиса​


Оставались предупреждения OPcache preload о классах League\Event и League\Flysystem. Они требуют отдельного разбора preload-скрипта приложения, но не являлись причиной кратковременной недоступности FastCGI-сокета. В момент обслуживания я не стал менять конфигурацию preload, чтобы не расширять количество одновременно затронутых подсистем.

7. Dovecot 2.4.5 и совместимость почты​


Версию Dovecot я сохранил — 2.4.5, но обновил восемь установленных пакетов со сборок +ubuntu24.04 на +ubuntu26.04: core, imapd, pop3d, lmtpd, mysql, ldap, sieve и managesieved.

Перед операцией проверил конфигурацию и симуляцию:

Bash:
doveconf -n >/dev/null && echo 'Dovecot config OK'
apt-get -s --no-remove install --only-upgrade \
  dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd \
  dovecot-mysql dovecot-ldap dovecot-sieve dovecot-managesieved

Затем установил обновления и проверил IMAP/POP3, SQL-аутентификацию и доставку. На одном из промежуточных шагов изменение доступа к основному /etc/dovecot/dovecot.conf нарушило доставку почты: процесс с менее привилегированным пользователем не мог читать конфигурацию. Я вернул основной конфигурации доступ на чтение, а чувствительные SQL-реквизиты вынес в отдельный файл /etc/dovecot/sg-mysql-private.conf с правами 600 root:root, подключаемый через !include_try.

Важный урок — после редактирования SQL-настроек нельзя ограничиваться проверкой doveconf -n. Нужны реальная проверка входа в Thunderbird и тест доставки письма. Они выявляют ошибки, которые не обнаруживает синтаксическая проверка.


Обновил именно сборки Dovecot, а не основную ветку​


После перехода системы на Ubuntu 26.04 у меня оставался Dovecot 2.4.5. Однако установленный набор был собран для 24.04. Я проверил синтаксис, активный список пакетов и отдельно обновил восемь компонентов до версии 2:2.4.5-2+ubuntu26.04 из ветки ce-2.4-latest/ubuntu/resolute.

Bash:
doveconf -n >/dev/null && echo 'Конфигурация Dovecot OK'

apt-get -s --no-remove install --only-upgrade \
 dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd \
 dovecot-mysql dovecot-ldap dovecot-sieve dovecot-managesieved

План показал Upgrading: 8, Installing: 0, Removing: 0. Только после этого я выполнил реальную команду обновления, сделал systemctl daemon-reload из-за изменившихся unit-файлов и проверил, что doveconf -n снова проходит.

Где я получил проблему с правами на dovecot.conf​


При реорганизации конфигурации обнаружилась важная тонкость: управляющие и дочерние процессы Dovecot могут работать с разными правами. После ошибочного ужесточения доступа к основному /etc/dovecot/dovecot.conf часть процессов не могла его прочитать, что отражалось на доставке. Я вернул корректную читаемость основного конфигурационного файла, а SQL-секреты оставил в отдельном закрытом файле /etc/dovecot/sg-mysql-private.conf под root:root с режимом 0600.

Я не рекомендую менять права рекурсивно командой chmod -R 600 /etc/dovecot: файлам конфигурации, каталогам и служебным процессам нужны разные режимы доступа. Конкретный способ хранения SQL-секретов должен соответствовать модели доступа и синтаксису установленной версии Dovecot.

Bash:
# Смотрю доступ к каждому компоненту пути
namei -l /etc/dovecot/dovecot.conf
stat -c '%A %U:%G %n' /etc/dovecot/dovecot.conf

doveconf -n >/dev/null
journalctl -u dovecot -b -n 100 --no-pager
ss -lntp | grep -E ':(110|143|993|995|4190)[[:space:]]' || true

Проверка почты включала реальные протоколы и доставку​


Синтаксически корректный Dovecot может не принимать пароль из-за ошибки SQL-запроса, неправильно выбранного host пользователя или несовместимого плагина MySQL. Я проверял фактическую почтовую авторизацию и получение письма в клиенте. Отдельно контролировал PostfixAdmin, поскольку работа почтового клиента не гарантирует возможность создания нового ящика через веб-панель. После обновления MySQL повторил эти проверки ещё раз.

8. Подготовка почтовых SQL-пользователей к MySQL 8.4​


Одним из самых ответственных этапов стала подготовка учётных записей MySQL. В MySQL 8.4 старый mysql_native_password отключён по умолчанию, поэтому я заранее перевёл рабочие почтовые подключения на caching_sha2_password. Я не стал включать устаревший плагин для совместимости ценой безопасности.

В итоге использовал разделение полномочий:

  • postfix_ro — чтение базы postfix для SQL-карт Postfix, с локальным TCP-подключением и TLS;
  • dovecot_ro — чтение postfix через локальное соединение Dovecot;
  • postfixadmin_rw — изменение объектов в базе postfix через PostfixAdmin;
  • старый postfixadmin@% — заблокирован и не используется рабочими сервисами.

Все рабочие учётные записи перевёл на современный способ аутентификации. Пример структуры прав (пароли необходимо задать самостоятельно; существующие аккаунты без проверки не пересоздавать):

SQL:
CREATE USER 'postfix_ro'@'127.0.0.1'
  IDENTIFIED WITH caching_sha2_password BY 'REPLACE_WITH_STRONG_SECRET'
  REQUIRE SSL;
GRANT SELECT ON postfix.* TO 'postfix_ro'@'127.0.0.1';

CREATE USER 'dovecot_ro'@'localhost'
  IDENTIFIED WITH caching_sha2_password BY 'REPLACE_WITH_STRONG_SECRET';
GRANT SELECT ON postfix.* TO 'dovecot_ro'@'localhost';

CREATE USER 'postfixadmin_rw'@'localhost'
  IDENTIFIED WITH caching_sha2_password BY 'REPLACE_WITH_STRONG_SECRET';
GRANT ALL PRIVILEGES ON postfix.* TO 'postfixadmin_rw'@'localhost';

На практике я также проверил совместимость клиента Postfix с libmysqlclient и caching_sha2_password, работу Dovecot SQL, вход в PostfixAdmin, создание ящика и отправку/получение тестовых писем. Важно не копировать реальные пароли в публичную статью и не оставлять их в истории shell.


Как разделил доступ на чтение и изменение​


Первоначально у меня использовалась слишком широкая учётная запись 'postfixadmin'@'%' с устаревшим плагином mysql_native_password. Для новой Percona 8.4 это плохая точка отказа сразу нескольких компонентов. Вместо включения deprecated-плагина я ввёл отдельные учётные записи, привязанные к ожидаемому способу подключения, и проверил их разрешения.

SQL:
SELECT user, host, plugin, account_locked
FROM mysql.user
ORDER BY user, host;

SHOW GRANTS FOR 'postfix_ro'@'127.0.0.1';
SHOW GRANTS FOR 'dovecot_ro'@'localhost';
SHOW GRANTS FOR 'postfixadmin_rw'@'localhost';

При этом 'user'@'localhost' и 'user'@'127.0.0.1' — для MySQL не всегда одна и та же учётная запись. Unix-сокет и TCP по localhost выбирают сопоставление пользователей по-разному. Поэтому я сопоставлял поле hosts в SQL-конфигурации сервиса с хостом в MySQL. Нельзя создавать только @localhost, а затем удивляться ошибке доступа при подключении -h 127.0.0.1.

Шифрование соединения Postfix MySQL​


Для postfix_ro@127.0.0.1 я использовал REQUIRE SSL, потому что соединение выполняется по TCP и должен быть совместимый TLS-клиент. Для dovecot_ro@localhost применял локальное соединение. Это не универсальная обязательная комбинация: если читатель изменит протокол и host, права и TLS-условия нужно сверить заново.

Для функционального теста я предпочитал не печатать пароль в командной строке или URL. Проверял соединение интерактивно под выбранным пользователем, SELECT по реальным таблицам Postfix и права на изменение отдельно в PostfixAdmin. Результат SELECT 1 не проверяет работающую SQL-карту получателей, поэтому дополнительно отправлял тестовое письмо и наблюдал за журналом Postfix.

Почему старый пользователь остался заблокированным​


Устаревшую учётную запись 'postfixadmin'@'%' я заблокировал, не удаляя в том же окне обслуживания. Это позволяло сохранить свидетельства прежней конфигурации и откатную документацию без работающей устаревшей авторизации. Удаление потребовало бы отдельной сверки, что ни один скрипт и ни один почтовый компонент больше не использует эту запись.

SQL:
SELECT user, host, plugin, account_locked
FROM mysql.user
WHERE user IN ('postfixadmin','postfix_ro',
               'dovecot_ro','postfixadmin_rw');

PHP-клиенты MySQL и Postfix — два независимых случая​


Postfix подключается через свои MySQL-карты и связанную клиентскую библиотеку, Dovecot — через модуль dovecot-mysql, PHP-приложения — через mysqli/PDO. Даже если PHP-админка успешно вошла в базу, это не доказывает, что Postfix умеет аутентифицироваться caching_sha2_password. Поэтому каждый сервис я тестировал отдельно, а не пытался подтвердить миграцию одной общей командой mysql -u root.

9. Проверка Percona Server перед обновлением до 8.4 LTS​


До миграции использовалась Percona Server 8.0.46-37. Я подключил репозиторий ps-84-lts для Ubuntu 26.04 и сначала проверил план установки через APT:

Bash:
apt-cache policy percona-server-server percona-server-client percona-server-common
apt-get -s --no-remove install \
  percona-server-server percona-server-client percona-server-common

Симуляция показывала обновление трёх пакетов без удаления системных зависимостей. Далее использовал MySQL Shell Upgrade Checker из mysqlsh. Для локальной конфигурации клиент запускался с --no-defaults, иначе мешали опции из общего клиентского файла.

Bash:
mysqlsh --no-defaults --py --sqlc \
  --user=root --socket=/var/run/mysqld/mysqld.sock

В Python-режиме MySQL Shell запустил:

Python:
util.check_for_server_upgrade(None, {
    'targetVersion': '8.4.8',
    'configPath': '/etc/mysql/my.cnf'
})

Проверка дала 0 ошибок, 17 предупреждений и 2 уведомления. Среди предупреждений — изменение значений системных переменных по умолчанию, заблокированная устаревшая учётная запись, а также внешний ключ PostfixAdmin. Для таблицы postfix.vacation обнаружился дополнительный неуникальный индекс email при существующем PRIMARY KEY(email). Мы проверили определение таблицы и отсутствие сиротских строк, поэтому не стали удалять ограничения на работающем сервере ради предупреждения.

Примечание: в проверке указан targetVersion = 8.4.8, а реально устанавливалась 8.4.11. Это было предварительное сканирование совместимости семейства 8.4, а не утверждение о проверке каждой особенности конкретной сборки.

Перенос InnoDB Redo Log на современный параметр​


До обновления в /etc/mysql/mysql.conf.d/mysqld.cnf был старый параметр:

INI:
innodb_log_file_size = 1G

С учётом двух групп журналов фактическая ёмкость составляла 2 ГиБ. Это подтвердил запрос Innodb_redo_log_capacity_resized = 2147483648. Чтобы в Percona 8.4 сохранить тот же размер, я заменил старую запись на:

INI:
innodb_redo_log_capacity = 2G

Также удалил устаревшую строку skip-character-set-client-handshake = 1, несовместимую с новым поколением MySQL. После обеих правок проверил конфигурацию ещё старым бинарным файлом, а после установки — новым:

Bash:
mysqld --validate-config --user=mysql

Единственное оставшееся предупреждение secure-log-path не препятствовало запуску; его не смешивал с изменением версии сервера.


Почему первый вход в mysql от root не сработал​


На раннем этапе простое выполнение mysql -e "SELECT VERSION();" вернуло ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO). Это не ошибка обновления сервера: у root в MySQL просто не был настроен беспарольный локальный вход с текущими настройками клиента. Я не сбрасывал ради диагностики административный пароль и не менял плагины root. Использовал штатный доступ с подходящей конфигурацией и интерактивным вводом пароля:

Bash:
mysql -u root -p -e 'SELECT VERSION();'

Если есть конфигурационный файл с доступом к БД, он должен оставаться непубличным и с ограниченными правами. В статье я не публикую реальные учётные данные и не копирую вывод SHOW CREATE USER вместе с секретами.

Что именно значили 17 предупреждений Upgrade Checker​


MySQL Shell Upgrade Checker работал как предварительная диагностическая проверка совместимости целевой ветки 8.4, а не как модуль, самостоятельно исправляющий базы. Результат — 0 ошибок, 17 предупреждений, 2 уведомления. Я отдельно просмотрел замечания о конфигурационных параметрах, плагине аутентификации, изменениях значений по умолчанию и особенностях индексов. 0 errors означало отсутствие найденных блокирующих проблем в рамках возможностей конкретного набора проверок, но не гарантировало, что все клиенты Postfix/Dovecot автоматически продолжат работать.

В таблице postfix.vacation существовали PRIMARY KEY(email) и дополнительный индекс email. Я проверил структуру и ссылочную целостность, не удалял индекс наугад во время обновления. Оповещение о потенциальной проблеме внешнего ключа — повод исследовать схему и реальные данные, а не немедленно выполнять DDL без тестового стенда и планов восстановления.

Почему размер Redo Log зафиксировал явно​


Старый параметр innodb_log_file_size = 1G в текущей конфигурации давал эффективную redo-ёмкость 2147483648 байт (2 ГиБ). При переходе на 8.4 я не захотел полагаться на вывод размера из устаревших параметров. Заменил его на innodb_redo_log_capacity = 2G. Это устранило одно из предупреждений и сохранило ожидаемый объём redo-журнала. Не следует механически принимать 2 ГиБ как оптимальное значение для любого сервера: оно было выбрано исходя из моего прежнего состояния и нагрузок.

Удалил несовместимую опцию заранее​


Строка skip-character-set-client-handshake = 1 была унаследована от старой конфигурации и удалена до запуска MySQL 8.4. Здесь есть важная последовательность: новая версия может изменить datadir, а потом отказаться запускаться из-за неизвестной опции. Поэтому конфигурацию проверял, а резервную копию сделал до установки новых серверных пакетов.

10. Обновление Percona 8.0.46 → 8.4.11-11​


После полного PBS-бэкапа и подготовки приложений выполнил обновление пакетов Percona до 8.4.11-11-1.resolute. Для основных версий MySQL предпочитаю сначала проверять операцию на изолированной копии VM и предусматривать остановку служб в окно обслуживания. На работающей системе не следует проводить обновление без готового плана восстановления из бэкапа.

После запуска новой версии проверил:

Bash:
mysql --version
systemctl status mysql --no-pager -l
mysqld --validate-config --user=mysql

dpkg-query -W -f='${binary:Package} ${Version}\n' \
  percona-server-server percona-server-client percona-server-common

А через SQL:

SQL:
SELECT VERSION(), @@version_comment;
SHOW GLOBAL VARIABLES LIKE 'innodb_redo_log_capacity';

SELECT user, host, plugin, account_locked
FROM mysql.user
ORDER BY user, host;

SELECT TABLE_SCHEMA, COUNT(*) AS tables_count
FROM information_schema.TABLES
WHERE TABLE_SCHEMA IN ('sysadmin', 'postfix', 'roundcubemail')
  AND TABLE_TYPE='BASE TABLE'
GROUP BY TABLE_SCHEMA;

Результат: Percona Server 8.4.11-11, Redo Log 2147483648 байт, mysql_native_password DISABLED, все действующие приложения используют caching_sha2_password. Базы остались доступны: 313 таблиц в sysadmin, по 17 в postfix и roundcubemail.

В /var/log/mysql/error.log увидел сообщения:

Код:
Data dictionary upgrade from version '80023' to '80300' completed.
Server upgrade from '80046' to '80411' started.
Server upgrade from '80046' to '80411' completed.
ready for connections. Version: '8.4.11-11'

Это стало главным подтверждением успешного обновления системного словаря и запуска новой версии.

Проверка памяти и почты после Percona​


Первый вывод systemctl показал высокий объём памяти MySQL и занятую подкачку. Я проверил free -h, vmstat 1 5, /proc/pressure/memory и сообщения OOM в журнале. При занятом swap около 2,2 ГиБ активной интенсивной подкачки si/so не было; доступной RAM оставалось около 2,6 ГиБ. Поэтому я не стал принудительно выполнять swapoff -a или срочно менять 5-гигабайтный InnoDB Buffer Pool.

Сразу после обновления Postfix и Dovecot оказались остановлены после служебных действий. Я запустил их и проверил порты 25, 465, 587, 993, 995 и очередь почты. Дополнительно исправил владельца обычного файла /var/spool/postfix/etc/resolv.conf на root:root, устранив предупреждение not owned by root. Сообщение о compatibility_level оставил без принудительного изменения.


Какие сведения подтвердили именно обновление данных​


После установки пакетов Percona 8.4.11-11 я не ограничился mysql --version, потому что эта команда показывает версию клиента. Проверил сервер через подключение SELECT VERSION() и изучил /var/log/mysql/error.log. В журнале были последовательные сообщения о переводе data dictionary 80023 → 80300, затем о завершённом серверном обновлении 80046 → 80411, и после этого ready for connections. Это реальные признаки успешной миграции движка, а не просто удачно поставленного DEB-пакета.

Bash:
# Версии DEB и работающей службы
mysql --version
dpkg-query -W -f='${binary:Package} ${Version}\n' \
  percona-server-server percona-server-client percona-server-common
systemctl status mysql --no-pager -l

# Последние события обновления словаря и сервера
grep -E 'Data dictionary upgrade|Server upgrade|ready for connections' \
  /var/log/mysql/error.log | tail -n 20

Три базы и контроль приложений​


После обновления сохранились sysadmin (форум), postfix (виртуальная почта) и roundcubemail (данные веб-почты). По диагностике таблиц я получил соответственно 313, 17 и 17 базовых таблиц. Это не абсолютный тест целостности каждой записи, поэтому параллельно смотрел сайт, панель PostfixAdmin, Roundcube и реальную почтовую авторизацию.

SQL:
SELECT VERSION(), @@version_comment;
SHOW GLOBAL VARIABLES LIKE 'innodb_redo_log_capacity';
SHOW GLOBAL VARIABLES LIKE 'innodb_buffer_pool_size';

SELECT TABLE_SCHEMA, COUNT(*) AS table_count
FROM information_schema.TABLES
WHERE TABLE_SCHEMA IN ('sysadmin','postfix','roundcubemail')
  AND TABLE_TYPE='BASE TABLE'
GROUP BY TABLE_SCHEMA;

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

Почему я не стал срочно выключать swap​


После запуска MySQL 8.4 у меня был InnoDB Buffer Pool около 5 ГиБ, RAM около 7,1 ГиБ и ранее занятый swap. Я проверил vmstat 1 5, free -h, /proc/pressure/memory и журнал OOM. В момент проверки активных si/so и OOM-событий не обнаружил. Поэтому оставил настройки без резкой переконфигурации, не выполнял swapoff -a и не уменьшал Buffer Pool только из-за числа в колонке used.

Bash:
free -h
vmstat 1 5
cat /proc/pressure/memory
journalctl -k -b --no-pager | grep -iE 'out of memory|oom-killer|killed process' || true

Почтовые службы после работы с MySQL​


После остановки и обновления БД я дополнительно проверил, не остались ли остановленными Postfix и Dovecot. Просмотрел их status, журналы, TCP-порты и очередь сообщений. В конфигурации Postfix встречалось предупреждение о владельце /var/spool/postfix/etc/resolv.conf; после проверки этого файла владелец обычного конфигурационного файла был приведён к root:root. Сообщение Postfix compatibility_level не стал механически менять без отдельного анализа совместимости.

Bash:
systemctl is-active postfix dovecot
postfix check
postqueue -p
ss -lntp | grep -E ':(25|465|587|993|995)[[:space:]]' || true
journalctl -u postfix -u dovecot -b -n 100 --no-pager

11. Проверка портов и существующих правил​


Начал с диагностики:

Bash:
iptables -V
iptables -S
ip6tables -S
nft list ruleset
ss -lntup
sshd -T | grep -E '^(port|listenaddress) '
systemctl is-enabled netfilter-persistent nftables 2>/dev/null || true

На моём сервере SSH слушал 3022/tcp. Публично должны были остаться доступны:

  • 80/tcp, 443/tcp — Nginx, XenForo и веб-почта;
  • 25/tcp, 465/tcp, 587/tcp — Postfix;
  • 110/tcp, 143/tcp, 993/tcp, 995/tcp — Dovecot;
  • 3022/tcp — SSH, но только с доверенных адресов;
  • отдельные административные и мониторинговые порты — исключительно с разрешённых IP.

Порт 4190 у Dovecot прослушивался, но в мои публичные разрешения iptables не входил. Percona (3306), Manticore (9306, 9308, 9312) и OpenDKIM слушали 127.0.0.1; разрешать их извне я не стал. Локальный DNS systemd-resolved также не требовал внешнего доступа.

Почтовые порты 110, 143 и 995 сознательно оставил: пока нельзя гарантировать, что ими не пользуются клиенты. Fail2ban защищает их от перебора паролей, хотя сам по себе не делает незашифрованный протокол безопасным. Для подключений клиентов я предпочитаю TLS и защищённые варианты почтовых протоколов.

Как я проверил административный IP​


Переменная $SSH_CONNECTION в моей root-сессии оказалась пустой, поскольку я перешёл в неё из другой учётной записи. Поэтому посмотрел реальные соединения:

Bash:
ss -tnp state established '( sport = :3022 )'
ps -ef | grep '[s]shd'

В моём случае активное SSH-подключение шло с 198.51.100.10. Также имелся второй доверенный адрес 198.51.100.11. Перед использованием этого примера обязательно подставьте свои адреса: ошибочное правило для SSH может полностью отрезать удалённое администрирование.

12. Подготовка единого firewall.sh​


Чтобы не размещать в тексте статьи длинный Python-код управления блокировками, я подготовил два локальных рабочих архива (в публичный комплект они не вложены, поскольку содержат специфические правила моего сервера):

  • sg-firewall-permanent.zip — первоначальная конфигурация iptables, пользовательское действие Fail2ban и unit-файлы systemd;
  • sg-firewall-v2.zip — проверенное обновление до глобальных IPv4/IPv6-блокировок и фильтры Nginx.

Первый архив разворачивает единый файл /etc/firewall/firewall.sh. Мой рабочий скрипт поддерживает четыре действия: check, apply, ban IP, unban IP. Он проверяет адреса через Python ipaddress, блокирует изменение файла на время записи с помощью flock и атомарно сохраняет правила. Повторное добавление уже сохранённого IP не создаёт дубликат.

Я перенёс архив на сервер и распаковал:

Bash:
mkdir -p /root/sg-firewall-permanent
unzip -q /root/sg-firewall-permanent.zip -d /root/sg-firewall-permanent

find /root/sg-firewall-permanent -type f
bash -n /root/sg-firewall-permanent/etc/firewall/firewall.sh

Затем сохранил действующие правила и установил конфигурационные файлы:

Bash:
install -d -m 700 /root/firewall-backup /etc/firewall
install -d -m 755 /etc/fail2ban/action.d /etc/fail2ban/jail.d
install -d -m 755 /etc/systemd/system/fail2ban.service.d

iptables-save > /root/firewall-backup/iptables.v4.before
ip6tables-save > /root/firewall-backup/iptables.v6.before
chmod 600 /root/firewall-backup/iptables.*.before

install -m 700 \
  /root/sg-firewall-permanent/etc/firewall/firewall.sh \
  /etc/firewall/firewall.sh

install -m 644 \
  /root/sg-firewall-permanent/etc/fail2ban/action.d/sg-permanent-iptables.conf \
  /etc/fail2ban/action.d/

install -m 644 \
  /root/sg-firewall-permanent/etc/fail2ban/jail.d/99-sg-mail.local \
  /etc/fail2ban/jail.d/

install -m 644 \
  /root/sg-firewall-permanent/etc/systemd/system/sg-firewall.service \
  /etc/systemd/system/

install -m 644 \
  /root/sg-firewall-permanent/etc/systemd/system/fail2ban.service.d/10-sg-firewall.conf \
  /etc/systemd/system/fail2ban.service.d/

systemctl daemon-reload
/etc/firewall/firewall.sh check

Здесь нельзя механически копировать разрешённые адреса: до применения я сверил правила SSH, мониторинга и FTP внутри firewall.sh со своей инфраструктурой. Подтверждением успешной проверки стала строка:

Код:
FIREWALL CONFIG: OK

Что находится внутри правил​


Для IPv4 у меня установлены INPUT DROP, FORWARD DROP и OUTPUT ACCEPT. Я разрешил loopback, существующие соединения, ICMP, веб- и почтовые порты, а также адресные административные исключения. Остальной входящий трафик отклоняется.

Для IPv6 также действуют INPUT DROP и FORWARD DROP, но разрешены loopback, нужный ICMPv6, уже установленные соединения и публичные веб-порты 80/443. Почтовые порты IPv6 специально не открывал. ICMPv6 не следует бездумно полностью отключать: он нужен для нормальной работы IPv6.

13. Безопасное применение правил без потери SSH​


Я не стал сразу делать Firewall постоянным. Сначала настроил автоматический откат старых правил через пять минут:

Bash:
systemd-run \
  --unit=sg-fw-rollback \
  --on-active=5m \
  /bin/bash -c \
  '/usr/sbin/iptables-restore -w < /root/firewall-backup/iptables.v4.before; /usr/sbin/ip6tables-restore -w < /root/firewall-backup/iptables.v6.before'

systemctl list-timers --all | grep sg-fw-rollback

Применил новый файл:

Bash:
/etc/firewall/firewall.sh apply
iptables -S
ip6tables -S
systemctl is-active ssh nginx postfix dovecot mysql

После этого я открыл новую сессию PuTTY на TCP 3022. Проверять только старую сессию нельзя: она может продолжать работать по правилу ESTABLISHED, даже если новое подключение запрещено.

Убедившись, что вход, сайт и почта доступны, отключил автоматический откат:

Bash:
systemctl stop sg-fw-rollback.timer

Как я настроил автозапуск​


На сервере ранее использовался netfilter-persistent. Чтобы после перезагрузки старые правила из /etc/iptables/rules.v4 не конкурировали с новым файлом, я отключил только его автозагрузку:

Bash:
systemctl disable netfilter-persistent.service
systemctl enable --now sg-firewall.service

systemctl is-enabled sg-firewall.service
systemctl is-enabled netfilter-persistent.service
systemctl status sg-firewall.service --no-pager

Я не выполнял systemctl stop netfilter-persistent, так как поведение остановки зависит от установленной конфигурации и может затронуть действующие правила. Отдельную службу nftables.service также не включал.

14. Установка Fail2ban и постоянные баны​


Проверил пакетную установку и установил Fail2ban с поддержкой journald:

Bash:
apt-get update
apt-get -s --no-remove --no-install-recommends install fail2ban python3-systemd
apt-get install --no-remove --no-install-recommends fail2ban python3-systemd

fail2ban-client -t
systemctl enable --now fail2ban
fail2ban-client status

Моим ключевым требованием были постоянные, а не временные блокировки. Поэтому в jail я использовал:

INI:
findtime = 30m
maxretry = 10
bantime = -1
action = sg-permanent-iptables

В обычной конфигурации Fail2ban bantime = -1 означает бессрочный бан, но не записывает его автоматически в мой файл. Для этого в первом архиве есть собственное действие:

INI:
[Definition]
actionstart =
actionstop =
actioncheck =
actionban = /etc/firewall/firewall.sh ban <ip>
actionunban =

Именно команда actionban добавляет правило в работающий iptables и одновременно записывает его в firewall.sh. Пустые actionstop и actionunban не дают обычной остановке Fail2ban автоматически удалить постоянные блокировки. Ручную разблокировку я вынес в отдельные команды.

На первом этапе включил два jail: postfix-sasl и dovecot. Они анализируют ошибки авторизации в journald. SSH-jail оставил выключенным: SSH уже ограничен по доверенным адресам в iptables, а дополнительная бессрочная автоматическая блокировка источника администрирования мне не нужна.

Bash:
fail2ban-client status postfix-sasl
fail2ban-client status dovecot
iptables -S SG_F2B_MAIL

Fail2ban сразу обнаружил перебор SMTP-паролей. Два посторонних адреса оказались заблокированы, а соответствующие правила появились и в файле, и в ядре. При этом первоначальная цепочка SG_F2B_MAIL распространялась только на почтовые порты. Позднее я сделал её глобальной.

15. Глобальные постоянные блокировки IPv4 и IPv6​


Мне было важно, чтобы IP, пойманный на подборе паролей, больше не мог обращаться и к другим публичным портам сервера, включая сайт. Для этого я подготовил вторую редакцию Firewall.

Распаковал второй архив:

Bash:
mkdir -p /root/sg-firewall-v2
unzip -q /root/sg-firewall-v2.zip -d /root/sg-firewall-v2

python3 /root/sg-firewall-v2/upgrade-firewall.py --check

Проверка сообщила:

Код:
READY: compatible file, two ban sections will be available

Перед обновлением сохранил текущий файл и уже действующие баны:

Bash:
cp -a /etc/firewall/firewall.sh \
  /root/firewall-backup/sg-fw-v2-before.sh

iptables-save > /root/firewall-backup/sg-fw-v2-before.v4
ip6tables-save > /root/firewall-backup/sg-fw-v2-before.v6
chmod 600 /root/firewall-backup/sg-fw-v2-before.v4 \
          /root/firewall-backup/sg-fw-v2-before.v6

python3 /root/sg-firewall-v2/upgrade-firewall.py --apply
bash -n /etc/firewall/firewall.sh
/etc/firewall/firewall.sh check

Обновляющий скрипт проверяет структуру старого файла, сохраняет административные разрешения и уже накопленные блокировки. Для IPv4 он ставит переход в общую цепочку до разрешения ESTABLISHED,RELATED:

Bash:
-A INPUT -i lo -j ACCEPT
-A INPUT -j SG_F2B_MAIL
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

Имя цепочки SG_F2B_MAIL я оставил ради совместимости с прежними правилами, но теперь она блокирует все входящие IPv4-подключения, а не только почтовые. Для IPv6 добавлена отдельная цепочка SG_F2B6_ALL и отдельный раздел сохранения адресов в том же файле.

Как и прежде, я применил обновление через таймер отката:

Bash:
systemd-run \
  --unit=sg-fw-v2-rollback \
  --on-active=5m \
  /bin/bash /root/sg-firewall-v2/rollback-firewall.sh

/etc/firewall/firewall.sh apply
iptables -S INPUT
iptables -S SG_F2B_MAIL
ip6tables -S INPUT
ip6tables -S SG_F2B6_ALL

После проверки нового SSH-подключения остановил таймер:

Bash:
systemctl stop sg-fw-v2-rollback.timer

Важно: глобальный постоянный бан гораздо жёстче бана отдельного порта. Он может разорвать уже существующие подключения и отрезать администратору SSH. Поэтому здесь особенно важны корректные ignoreip, защищённые адреса в управляющем скрипте, резервная консоль и возможность отката.

16. Подключение защиты Nginx​


Для Nginx я добавил два дополнительных фильтра:

  • sg-nginx-probes — повторяющиеся обращения к характерным для сканирования путям: /.env, /.git/config, /wp-login.php, /xmlrpc.php, /vendor/phpunit/, /cgi-bin/;
  • nginx-http-auth — многократные ошибки пароля в штатной HTTP Basic Authentication Nginx.

Я не стал блокировать навсегда за обычные HTTP 404 и за обычную форму входа XenForo: при этом слишком легко получить ложные срабатывания. Оба Nginx jail первоначально оставил выключенными, пока не убедился, что журналы содержат настоящие адреса клиентов.

Установил файлы из архива:

Bash:
install -m 644 \
  /root/sg-firewall-v2/etc/fail2ban/filter.d/sg-nginx-probes.conf \
  /etc/fail2ban/filter.d/

install -m 644 \
  /root/sg-firewall-v2/etc/fail2ban/jail.d/99-sg-nginx.local \
  /etc/fail2ban/jail.d/

Почему у меня сначала не работал фильтр сканирования​


В конфигурации Nginx для sysadmin.guru была директива access_log off;. Поэтому обычный /var/log/nginx/access.log оставался пустым, а Fail2ban не имел данных для анализа.

Чтобы не записывать вообще каждый просмотр форума, я настроил отдельный условный журнал. Внутри секции http у меня подключён каталог /etc/nginx/conf.d/*.conf. Создал файл:

Bash:
cat > /etc/nginx/conf.d/sg-security-log.conf <<'EOF_MAP'
map $request_uri $sg_security_probe {
    default 0;
    ~*^/(?:\.env|\.git/(?:config|HEAD)|wp-login\.php|xmlrpc\.php|vendor/phpunit/|cgi-bin/) 1;
}
EOF_MAP

nginx -t

Далее нашёл фактически загруженный виртуальный хост с помощью nginx -T. У меня это оказался /etc/nginx/sites-available/default, на который указывает символьная ссылка в sites-enabled. Сделал резервную копию:

Bash:
CFG=$(readlink -f /etc/nginx/sites-enabled/default)
cp -a "$CFG" /root/nginx-default-before-security-log.conf

В HTTPS/основном блоке server сайта заменил только его собственную строку:

NGINX:
access_log off;

на:

NGINX:
access_log /var/log/nginx/security-access.log combined if=$sg_security_probe;

Остальные access_log off; в location бездумно не заменял. Это важно: вложенный location может переопределять журналирование родительского server, поэтому такой фильтр охватывает только те запросы, которые действительно попали в файл безопасности. Полное покрытие всех location я не заявляю.

Bash:
nginx -t && systemctl reload nginx

curl --noproxy '*' \
  --resolve sysadmin.guru:443:127.0.0.1 \
  -sS -o /dev/null -w 'HTTP: %{http_code}\n' \
  'https://sysadmin.guru/.env.sg-f2b-test'

tail -n 5 /var/log/nginx/security-access.log

Мой тест вернул HTTP: 403, а журнал записал запрос с IP 127.0.0.1. При проверке из браузера Nginx показал реальный внешний адрес клиента, а не адрес обратного прокси. Если сайт расположен за CDN или reverse proxy, до включения пожизненных банов нужно отдельно и строго настроить доверенные прокси и real_ip_header; иначе можно заблокировать сам прокси.

Исправление регулярного выражения Fail2ban​


Первоначальный фильтр не распознавал корректную запись. Причина — Fail2ban извлекает дату из строки access.log перед применением регулярного выражения. Я заменил проверку даты в квадратных скобках на \[\]. Итоговый файл /etc/fail2ban/filter.d/sg-nginx-probes.conf у меня такой:

INI:
[Definition]
failregex = ^<HOST> \S+ \S+ \[\] "(?:GET|POST|HEAD) /(?:\.env\S*|\.git/(?:config|HEAD)\S*|wp-login\.php\S*|xmlrpc\.php\S*|vendor/phpunit/\S*|cgi-bin/\S*) HTTP/[0-9.]+" [0-9]{3} \S+
ignoreregex =

В секции [sg-nginx-probes] файла /etc/fail2ban/jail.d/99-sg-nginx.local указал:

INI:
logpath = /var/log/nginx/security-access.log

Протестировал:

Bash:
fail2ban-regex \
  /var/log/nginx/security-access.log \
  /etc/fail2ban/filter.d/sg-nginx-probes.conf

Результат был 1 matched, 0 missed. Только после этой проверки перешёл к включению веб-jail.

17. Исключение административных адресов из постоянных банов​


При первом включении веб-jail я заметил, что в списке ignoreip присутствовали только 127.0.0.0/8 и ::1, хотя в шаблоне конфигурации ожидались административные адреса. При глобальном бессрочном бане это опасно: можно лишиться SSH за собственные тестовые запросы.

Я немедленно добавил исключения в работающие jail:

Bash:
for jail in postfix-sasl dovecot sg-nginx-probes; do
    for ip in 198.51.100.10 198.51.100.11 198.51.100.20 198.51.100.21; do
        fail2ban-client set "$jail" addignoreip "$ip"
    done
done

После этого сохранил исключения в /etc/fail2ban/jail.d/zz-sg-trusted.local, явно для каждого jail. Ниже мои адреса приведены только для примера — их нужно заменить собственными доверенными IP:

INI:
[postfix-sasl]
ignoreip = 127.0.0.1/8 ::1 198.51.100.10 198.51.100.11 198.51.100.20 198.51.100.21

[dovecot]
ignoreip = 127.0.0.1/8 ::1 198.51.100.10 198.51.100.11 198.51.100.20 198.51.100.21

[sg-nginx-probes]
ignoreip = 127.0.0.1/8 ::1 198.51.100.10 198.51.100.11 198.51.100.20 198.51.100.21

[nginx-http-auth]
ignoreip = 127.0.0.1/8 ::1 198.51.100.10 198.51.100.11 198.51.100.20 198.51.100.21

Затем проверил:

Bash:
fail2ban-client -t
fail2ban-client reload
fail2ban-client get sg-nginx-probes ignoreip
fail2ban-client get postfix-sasl ignoreip
fail2ban-client get dovecot ignoreip

Все нужные адреса появились в исключениях. Отдельно в коде firewall.sh предусмотрен отказ от автоматической блокировки защищённых административных и мониторинговых IP — это второй уровень защиты от ошибки в jail.

18. Четыре работающих jail: итоговые настройки​


После проверки журналов и исключений я включил оба веб-jail. Финальные параметры у меня следующие:

INI:
# /etc/fail2ban/jail.d/99-sg-nginx.local
[sg-nginx-probes]
enabled = true
filter = sg-nginx-probes
backend = polling
logpath = /var/log/nginx/security-access.log
maxretry = 15
findtime = 30m
bantime = -1
action = sg-permanent-iptables

[nginx-http-auth]
enabled = true
filter = nginx-http-auth
backend = polling
logpath = /var/log/nginx/error.log
maxretry = 20
findtime = 30m
bantime = -1
action = sg-permanent-iptables

Здесь postfix-sasl и dovecot продолжают использовать по 10 неудачных авторизаций за 30 минут, с бессрочной блокировкой. Веб-сканирование — 15 подходящих запросов за 30 минут. HTTP Basic — 20 ошибок за 30 минут.

Защита HTTP Basic нужна для административных разделов Nginx, где я использовал auth_basic "Admin Login" и файл /etc/nginx/auth. В качестве обезличенных примеров таких разделов приведены /private-panel/, /private-panel-2/ и /private-panel-3/. Этот фильтр не защищает обычную форму логина XenForo — для неё требуются отдельные меры на уровне приложения.

Я проверил работу стандартного фильтра на заведомо неверном пользователе, не раскрывая рабочие пароли:

Bash:
# На сервере: запрос через localhost с правильным Host/SNI
curl --noproxy '*' \
  --resolve sysadmin.guru:443:127.0.0.1 \
  -k -u 'sg_f2b_test_invalid:wrong-password' \
  -o /dev/null -w 'HTTP: %{http_code}\n' \
  https://sysadmin.guru/private-panel-2/

grep -E 'password mismatch|was not found in' \
  /var/log/nginx/error.log | tail -n 10

fail2ban-regex \
  /var/log/nginx/error.log \
  /etc/fail2ban/filter.d/nginx-http-auth.conf

В журнале появились две записи user "sg_f2b_test_invalid" was not found, и Fail2ban распознал обе: Failregex: 2 total. Из-за корректных ignoreip мой собственный адрес не был заблокирован.

После применения настроек:

Bash:
fail2ban-client -t
fail2ban-client reload
fail2ban-client status

Я получил четыре активных jail:

Код:
dovecot, nginx-http-auth, postfix-sasl, sg-nginx-probes

19. Просмотр и ручное управление постоянными блокировками​


Списки блокировок доступны без обращения к базе Fail2ban:

Bash:
# Постоянные IPv4-баны в едином файле
sed -n \
  '/^# SG_F2B_BANS_BEGIN$/,/^# SG_F2B_BANS_END$/p' \
  /etc/firewall/firewall.sh

# Действующие IPv4-правила
iptables -S SG_F2B_MAIL

# Действующие IPv6-правила
ip6tables -S SG_F2B6_ALL

# Статистика обнаружения
fail2ban-client status postfix-sasl
fail2ban-client status dovecot
fail2ban-client status sg-nginx-probes
fail2ban-client status nginx-http-auth

При срабатывании Fail2ban создаёт строку вида:

Bash:
-A SG_F2B_MAIL -s 203.0.113.7/32 -j DROP

Здесь 203.0.113.7 — демонстрационный IP, а не настоящий атакующий. Для IPv6 используются цепочка SG_F2B6_ALL и маска /128.

Ручное добавление постоянной блокировки:

Bash:
/etc/firewall/firewall.sh ban 203.0.113.7

Для полной разблокировки IP, ранее заблокированного Fail2ban, очищаю и его состояние, и постоянное правило:

Bash:
fail2ban-client unban 203.0.113.7
/etc/firewall/firewall.sh unban 203.0.113.7

Команда fail2ban-client unban в одиночку здесь недостаточна: наше пользовательское actionunban намеренно пустое, и сохранённая строка в firewall.sh останется. Править управляемые разделы файла вручную при работающем Fail2ban я не рекомендую.

20. Финальный reboot и контрольный список​


После всех изменений я сохранил резервные копии конфигураций, убедился в наличии резервного SSH-подключения и ещё раз выполнил:

Bash:
/etc/firewall/firewall.sh check
fail2ban-client -t
nginx -t
systemctl is-enabled sg-firewall fail2ban
systemctl list-timers --all | grep sg-fw || true

Откатных таймеров не осталось. После обычной перезагрузки VM выполнил:

Bash:
uptime -p
systemctl is-active \
  sg-firewall fail2ban mysql nginx \
  php8.4-fpm php8.5-fpm postfix dovecot manticore

fail2ban-client ping
fail2ban-client status
iptables -S SG_F2B_MAIL
ip6tables -S SG_F2B6_ALL

sed -n '/^# SG_F2B_BANS_BEGIN$/,/^# SG_F2B_BANS_END$/p' \
  /etc/firewall/firewall.sh

/etc/firewall/firewall.sh check
journalctl -b -u sg-firewall -u fail2ban -p warning --no-pager

Результат: все девять проверенных служб запустились и были active, Fail2ban ответил pong, все четыре jail восстановились, старые постоянные IP-баны остались и в iptables, и в едином файле, FIREWALL CONFIG: OK, новых warning/error в журнале Firewall и Fail2ban не было.


21. Практический чек-лист диагностики, если после обновления что-то не заработало​


Этот раздел я добавил специально для повторного использования руководства. В ходе обновления проблемы проявлялись в разных слоях системы, и по одному сообщению failed причину определить нельзя.

Сайт возвращает 502 или PHP upstream недоступен​


Первым делом проверяю имя сокета в Nginx, существование Unix-файла, состояние нужного пула и время запуска служб. Если upstream указывает на /tmp/php8.5-fpm.sock, наличие /run/php/php8.5-fpm.sock ему не поможет.

Bash:
nginx -t
systemctl status nginx php8.4-fpm php8.5-fpm --no-pager -l
journalctl -b -u nginx -u php8.4-fpm -u php8.5-fpm --no-pager | tail -n 100
ss -xlpn | grep -E 'php.*fpm' || true
nginx -T 2>&1 | grep fastcgi_pass

Postfix принимает SMTP, но не принимает адресата​


Тогда проверяю не только Postfix, но и SQL-карты. Для неисправности SQL-клиента и неправильного user@host возможна ситуация, когда сам демон слушает порт 25, но не может подтвердить виртуальный домен или почтовый ящик.

Bash:
systemctl status postfix mysql --no-pager -l
postfix check
postqueue -p
journalctl -u postfix -b -n 100 --no-pager

При проверке SQL-карт избегаю вывода реальных паролей из /etc/postfix/mysql-*.cf в публичный чат или статью. Если пароль подозревается скомпрометированным, его нужно сменить на сервере и в соответствующей настройке приложения, а не просто удалить из опубликованного лога.

IMAP-сервис active, но клиент не может авторизоваться​


Проверяю doveconf -n, права на пути конфигурации, конкретный SQL-host и плагин аутентификации MySQL. Особенно важно сравнить localhost и 127.0.0.1 при поиске нужной учётной записи в MySQL. После проверки синтаксиса обязательно тестирую вход почтовым клиентом.

Bash:
systemctl status dovecot --no-pager -l
doveconf -n >/dev/null && echo 'Dovecot config OK'
namei -l /etc/dovecot/dovecot.conf
journalctl -u dovecot -b -n 100 --no-pager

MySQL после перехода 8.4 не стартует​


Здесь прежде всего смотрю error.log и проверяю неизвестные устаревшие параметры конфигурации. Если datadir уже обновился до 8.4, не пытаюсь просто поставить назад пакет 8.0. Для возврата использую заранее подготовленную и проверенную резервную копию всего согласованного состояния.

Bash:
systemctl status mysql --no-pager -l
journalctl -u mysql -b -n 100 --no-pager
tail -n 100 /var/log/mysql/error.log
mysqld --validate-config --user=mysql

После активации Firewall пропал SSH​


До любых изменений держу консоль Proxmox. Проверяю, включён ли текущий адрес в разрешения для нужного SSH-порта, нет ли собственного IP в сохранённых банах и сработал ли таймер восстановления. Глобальный Fail2ban-бан идёт раньше разрешения установленных соединений, поэтому нельзя рассчитывать, что открытая SSH-сессия обязательно выживет при ошибке настройки.

Bash:
iptables -S INPUT
iptables -S SG_F2B_MAIL
ss -lntp | grep ':3022'
/etc/firewall/firewall.sh check
systemctl status sg-firewall fail2ban --no-pager

Самый важный принцип: я проверяю здоровье конкретного приложения на его собственном уровне — SQL-запросом, HTTP-ответом, почтовой доставкой, а не только зелёным статусом systemd.

22. Версии и границы проведённой работы​


  • Ubuntu — 24.04.5 LTS → 26.04.1 LTS; ядро после обновления — 7.0.0-38-generic.
  • PHP — обе ветки 8.4.27 и 8.5.12, миграция набора пакетов на сборки ubuntu26.04, без смены основной ветки.
  • Dovecot — ветка 2.4.5, восемь пакетов пересобраны для нового релиза.
  • Percona Server — 8.0.46-37 → 8.4.11-11, системный словарь обновился; рабочие SQL-пользователи переведены на caching_sha2_password.
  • Manticore — восстановление binlog через 25.0.0, затем повторная установка 29.9.0; RT-индекс xf = 94 записи.
  • Веб — Nginx и два PHP-FPM; отдельные предупреждения о дублирующихся server_name и OPcache preload не смешивал с основной миграцией.
  • Сеть — iptables-nft, единый /etc/firewall/firewall.sh, постоянные глобальные блокировки IPv4/IPv6 и четыре jail Fail2ban.
  • Результат — после reboot активны девять проверенных служб, fail2ban-client ping возвращает pong, все четыре jail присутствуют, постоянные блокировки сохранены.

23. Источники и документация​


В этой статье я прежде всего опирался на собственные логи команд и результаты проверок на sysadmin.guru за 25–26 октября 2026 года. Для устройства компонентов использовал документацию разработчиков:


24. Итог и что осталось на будущее​


За один цикл обслуживания я обновил Ubuntu 24.04.5 до 26.04.1, перевёл PHP и Dovecot на пакеты под новый релиз, восстановил Manticore после несовместимого binlog, обновил Percona 8.0 до 8.4.11 LTS, подготовил почтовые SQL-учётные записи под caching_sha2_password, проверил XenForo, Roundcube, PostfixAdmin и доставку почты. Затем ввёл единый Firewall на iptables с Fail2ban, который сохраняет глобальные бессрочные IP-блокировки и восстанавливается после reboot.

Даже после такой большой миграции не всё требует немедленной «оптимизации»: я осознанно оставил почтовые порты 110/143/995 (они могут использоваться клиентами), не удалял заблокированную старую учётную запись MySQL без отдельного окна, не менял размер InnoDB Buffer Pool лишь из-за занятого swap.

Главный вывод: обновление операционной системы — это не только do-release-upgrade. Для работающего сервера важнее весь путь: резервные копии, происхождение пакетов, совместимость приложений с базой, права и способы SQL-аутентификации, тесты почтовых клиентов, безопасное переключение сетевых правил и обязательный контроль после перезагрузки.

См. также: Ubuntu Server, Percona Server 8.4, Fail2ban, Nginx.
Об авторе
Guru
Василий, cистемный админ /gnu/linux/windows/macos/mikrotik/troubleshooter, создатель сайта
Интересуюсь всем что делает инфраструктуру быстрой и надёжной
Открыт к общению и проектам, написать мне можно через форму или в личном сообщении

❗ Если есть пожелания по обзору какого-либо вопроса не представленного на сайте - пиши в комментариях

Комментарии

Нет комментариев для отображения.

Информация о статье

Автор
Guru
Время чтения статьи
41 мин чтения
Просмотры
20
Посл. обновление

Ещё в Linux

Ещё от Guru

Поделиться этой статьёй

Назад
Верх