Что нового

Chmod 777 не помогает: когда права в порядке, а файловая система всё равно read-only

После того как утром я разобрался с падениями воркеров nginx, обнаружилась вторая проблема: загрузка вложений в XenForo AMS перестала работать. Браузер показывал 500, файлы не прикреплялись. Корень оказался в systemd-харденинге PHP-FPM, который тихо смонтировал весь каталог /usr в режиме read-only — прямо туда, где лежит сайт.

Симптомы​


При попытке прикрепить изображение к статье в AMS браузерная консоль показывала:
1780993913154.png

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/.
Об авторе
Guru
Василий, cистемный админ /gnu/linux/windows/macos/mikrotik/troubleshooter, создатель сайта
Интересуюсь всем что делает инфраструктуру быстрой и надёжной
Открыт к общению и проектам, написать мне можно через форму или в личном сообщении

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

Комментарии

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

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

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

Ещё в Размышления системного администратора

Ещё от Guru

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

Назад
Верх