Подробный технический журнал: как я обновил рабочий сервер 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. Я сохранил не только удачные команды, но и реальные неполадки, проверки и причины принятых решений.
Содержание
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 — помогла мне избежать потери данных.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.
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
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 года. Для устройства компонентов использовал документацию разработчиков:
- Ubuntu Server — обновление релиза
- Percona Server 8.4 — checklist обновления
- Percona Server 8.4 — несовместимые изменения
- Manticore Search — устройство бинарного журнала
- Nginx — официальная документация
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.