После того как утром я разобрался с падениями воркеров nginx, обнаружилась вторая проблема: загрузка вложений в XenForo AMS перестала работать. Браузер показывал 500, файлы не прикреплялись. Корень оказался в systemd-харденинге PHP-FPM, который тихо смонтировал весь каталог /usr в режиме read-only — прямо туда, где лежит сайт.
При попытке прикрепить изображение к статье в AMS браузерная консоль показывала:
Одновременно в интерфейсе XenForo Admin CP появилось предупреждение о минимальных требованиях:
Первая очевидная гипотеза — неправильные права на директории. Проверяю владельца и режим:
Директории принадлежат www-data и доступны на запись. Ставлю 755 с правильным владельцем — не помогает. Ставлю 777 как требует XenForo — тоже не помогает. Проверяю что PHP реально думает об этих директориях:
PHP из командной строки от имени www-data говорит YES, а веб-запросы всё равно падают. Это ключевое противоречие: CLI и PHP-FPM под одним пользователем ведут себя по-разному.
Вместо того чтобы продолжать гадать по коду ошибки HTTP, смотрю в журнал исключений XenForo:
В логе чётко написано:
Вот это другой разговор. Не "permission denied" — а именно "read-only file system". Это значит файловая система примонтирована только для чтения. Иду проверять диск:
Ни одной строки про ошибки диска или remount. df -h показывает /dev/sda4 смонтирован нормально, 27% использования, места полно.
Методично прохожу оставшиеся варианты.
immutable-флаг (chattr):
Флага i нет — директория не заморожена.
AppArmor:
Пусто. AppArmor не блокирует.
open_basedir в PHP и nginx:
Закомментирован, не задан нигде.
Остаётся одна не проверенная вещь — ограничения на уровне systemd-юнита PHP-FPM. Ubuntu активно использует systemd-харденинг в пакетных юнитах:
Вывод:
Нашёл. ProtectSystem=full — это директива systemd, которая монтирует /usr, /boot и /etc в режиме read-only внутри namespace процесса. Цель правильная: если PHP-процесс скомпрометирован через уязвимость, он не сможет модифицировать системные файлы или записать бэкдор.
Проблема в том, что сайт расположен в /usr/www/ — прямо внутри /usr. PHP-FPM работает в изолированном namespace, где весь /usr read-only. На уровне файловой системы права 777 и владелец www-data — но это не имеет значения, потому что ядро блокирует запись на уровне mount namespace ещё до проверки прав.
Именно поэтому sudo -u www-data php из CLI писало нормально: CLI-процесс запускается в обычном namespace без ограничений systemd. А PHP-FPM через systemctl — в изолированном.
До этого утра загрузка вложений работала. Причина появления проблемы — цепочка событий:
Таким образом, два инцидента связаны: падение nginx спровоцировало появление второй проблемы.
Правильный способ кастомизации systemd-юнита — override-файл. Он не затирается при обновлении пакета, потому что хранится отдельно от оригинального юнита.
Создаю override вручную (через systemctl edit можно, но надёжнее явно):
ReadWritePaths — директива, которая при активном ProtectSystem добавляет исключения: указанные пути остаются доступными на запись, несмотря на общий запрет.
Применяю:
Проверяю что применилось:
Проверяю загрузку вложений в AMS — работает.
1. Контролировать изменения в systemd-юнитах при обновлениях
При обновлении PHP-пакетов проверять что изменилось в юнитах:
2. Проверять ProtectSystem после обновления
После любого обновления PHP:
Если там есть что-то новое и жёсткое — сразу обновлять override.
3. Переехать на /var/www/
Стандартное расположение веб-данных — /var/www/, а не /usr/www/. Директива ProtectSystem=full намеренно не трогает /var, потому что это место для изменяемых данных (variable data). Если перенести сайт на /var/www/, проблема исчезнет без каких-либо override-файлов — PHP-FPM получит доступ туда автоматически.
Миграция требует изменений в конфигах nginx и config.php XenForo, но делает конфигурацию стандартной и устраняет зависимость от ручных правок юнитов.
4. Разделять диагностику CLI и FPM
Если PHP из командной строки работает, а из веб-запроса нет — это почти всегда разница в окружении процесса, а не в правах файловой системы. Первым делом проверять systemctl cat для ограничений юнита и AppArmor для MAC-политик. Не тратить время на chmod 777.
Пятиминутный инцидент превратился в полноценную диагностику из-за того, что симптом ("нет прав") указывал совсем в другую сторону от причины (mount namespace). Ключевые точки:
ProtectSystem=full — это хорошая практика безопасности, и отключать её не нужно. Нужно правильно размещать данные сайта: в /var/www/, а не в /usr/www/.
Симптомы
При попытке прикрепить изображение к статье в AMS браузерная консоль показывала:
Bash:
POST https://sysadmin.guru/attachments/upload?type=ams_article&context[article_id]=13 500 (Internal Server Error)
Одновременно в интерфейсе XenForo Admin CP появилось предупреждение о минимальных требованиях:
Bash:
Каталог /usr/www/sysadmin/httpdocs/data должен быть доступен для записи. Измените права доступа на 0777.
Каталог /usr/www/sysadmin/httpdocs/internal_data должен быть доступен для записи.
Ложный след: права доступа
Первая очевидная гипотеза — неправильные права на директории. Проверяю владельца и режим:
Bash:
ls -la /usr/www/sysadmin/httpdocs/ | grep -E "data|internal"
Директории принадлежат www-data и доступны на запись. Ставлю 755 с правильным владельцем — не помогает. Ставлю 777 как требует XenForo — тоже не помогает. Проверяю что PHP реально думает об этих директориях:
Bash:
sudo -u www-data php /tmp/check.php
/usr/www/sysadmin/httpdocs/data: is_writable=YES
/usr/www/sysadmin/httpdocs/internal_data: is_writable=YES
PHP из командной строки от имени www-data говорит YES, а веб-запросы всё равно падают. Это ключевое противоречие: CLI и PHP-FPM под одним пользователем ведут себя по-разному.
Точная ошибка из XenForo exception log
Вместо того чтобы продолжать гадать по коду ошибки HTTP, смотрю в журнал исключений XenForo:
Bash:
tail -50 /usr/www/sysadmin/httpdocs/internal_data/logs/$(ls -t /usr/www/sysadmin/httpdocs/internal_data/logs/ | head -1)
В логе чётко написано:
Bash:
ErrorException: [E_WARNING] fopen(/usr/www/sysadmin/httpdocs/internal_data/attachments/0/252-427b9be7e75ee3a6c99eee83777b68ae.data):
Failed to open stream: Read-only file system
Вот это другой разговор. Не "permission denied" — а именно "read-only file system". Это значит файловая система примонтирована только для чтения. Иду проверять диск:
Bash:
dmesg | grep -iE "error|readonly|read-only|remount|ext4" | tail -30
Ни одной строки про ошибки диска или remount. df -h показывает /dev/sda4 смонтирован нормально, 27% использования, места полно.
Исключаем другие причины
Методично прохожу оставшиеся варианты.
immutable-флаг (chattr):
Bash:
lsattr -d /usr/www/sysadmin/httpdocs/internal_data/
--------------e------- /usr/www/sysadmin/httpdocs/internal_data/
Флага i нет — директория не заморожена.
AppArmor:
Bash:
aa-status | grep -i php
dmesg | grep -i "apparmor.*DENIED"
Пусто. AppArmor не блокирует.
open_basedir в PHP и nginx:
Bash:
grep -r "open_basedir" /etc/php/8.4/fpm/php.ini
grep -r "PHP_VALUE\|open_basedir" /etc/nginx/sites-enabled/
Закомментирован, не задан нигде.
Находим причину: systemd ProtectSystem=full
Остаётся одна не проверенная вещь — ограничения на уровне systemd-юнита PHP-FPM. Ubuntu активно использует systemd-харденинг в пакетных юнитах:
Bash:
systemctl cat php8.4-fpm | grep -iE "ReadOnly|ProtectSystem|ProtectHome|InaccessiblePaths|PrivateTmp"
Вывод:
Bash:
ProtectSystem=full
Нашёл. ProtectSystem=full — это директива systemd, которая монтирует /usr, /boot и /etc в режиме read-only внутри namespace процесса. Цель правильная: если PHP-процесс скомпрометирован через уязвимость, он не сможет модифицировать системные файлы или записать бэкдор.
Проблема в том, что сайт расположен в /usr/www/ — прямо внутри /usr. PHP-FPM работает в изолированном namespace, где весь /usr read-only. На уровне файловой системы права 777 и владелец www-data — но это не имеет значения, потому что ядро блокирует запись на уровне mount namespace ещё до проверки прав.
Именно поэтому sudo -u www-data php из CLI писало нормально: CLI-процесс запускается в обычном namespace без ограничений systemd. А PHP-FPM через systemctl — в изолированном.
Почему проблема появилась именно сейчас
До этого утра загрузка вложений работала. Причина появления проблемы — цепочка событий:
1. Пакет php8.4-fpm при одном из обновлений получил более жёсткий systemd-юнит с ProtectSystem=full
2. Новый юнит вступает в силу только после перезапуска сервиса
3. PHP-FPM давно не перезапускался — сервер работал без reboot, процессы жили месяцами
4. Утром nginx начал падать с SIGSEGV из-за несовместимого модуля. При устранении этой проблемы nginx перезапускали — и вместе с ним перезапустился PHP-FPM, подхватив новый юнит с ограничениями
Таким образом, два инцидента связаны: падение nginx спровоцировало появление второй проблемы.
Решение
Правильный способ кастомизации systemd-юнита — override-файл. Он не затирается при обновлении пакета, потому что хранится отдельно от оригинального юнита.
Создаю override вручную (через systemctl edit можно, но надёжнее явно):
Bash:
mkdir -p /etc/systemd/system/php8.4-fpm.service.d/
cat > /etc/systemd/system/php8.4-fpm.service.d/override.conf << 'EOF'
[Service]
ReadWritePaths=/usr/www
EOF
ReadWritePaths — директива, которая при активном ProtectSystem добавляет исключения: указанные пути остаются доступными на запись, несмотря на общий запрет.
Применяю:
Bash:
systemctl daemon-reload
systemctl restart php8.4-fpm
Проверяю что применилось:
Bash:
systemctl show php8.4-fpm | grep ReadWrite
ReadWritePaths=/usr/www
Проверяю загрузку вложений в AMS — работает.
Что делать чтобы не повторилось
1. Контролировать изменения в systemd-юнитах при обновлениях
При обновлении PHP-пакетов проверять что изменилось в юнитах:
Bash:
# Посмотреть что изменится перед применением
apt-get --simulate upgrade php8.4-fpm
# После обновления сравнить с override
systemd-delta --type=extended | grep php
2. Проверять ProtectSystem после обновления
После любого обновления PHP:
Bash:
systemctl cat php8.4-fpm | grep -iE "ProtectSystem|ProtectHome|ReadOnly|InaccessiblePaths"
Если там есть что-то новое и жёсткое — сразу обновлять override.
3. Переехать на /var/www/
Стандартное расположение веб-данных — /var/www/, а не /usr/www/. Директива ProtectSystem=full намеренно не трогает /var, потому что это место для изменяемых данных (variable data). Если перенести сайт на /var/www/, проблема исчезнет без каких-либо override-файлов — PHP-FPM получит доступ туда автоматически.
Миграция требует изменений в конфигах nginx и config.php XenForo, но делает конфигурацию стандартной и устраняет зависимость от ручных правок юнитов.
4. Разделять диагностику CLI и FPM
Если PHP из командной строки работает, а из веб-запроса нет — это почти всегда разница в окружении процесса, а не в правах файловой системы. Первым делом проверять systemctl cat для ограничений юнита и AppArmor для MAC-политик. Не тратить время на chmod 777.
Итог
Пятиминутный инцидент превратился в полноценную диагностику из-за того, что симптом ("нет прав") указывал совсем в другую сторону от причины (mount namespace). Ключевые точки:
— XenForo exception log дал точную ошибку: "Read-only file system", а не "Permission denied"
— Тест is_writable() из CLI показал YES, хотя из FPM было NO — это разорвало гипотезу про права
— systemctl cat php8.4-fpm показал ProtectSystem=full — причину нашли
— Решение: ReadWritePaths=/usr/www в systemd override
ProtectSystem=full — это хорошая практика безопасности, и отключать её не нужно. Нужно правильно размещать данные сайта: в /var/www/, а не в /usr/www/.