Короткий ответ

chmod 777 — не фикс Permission denied, а приглашение дописать web-shell и подменить cron. Найдите world-writable вне sticky-каталогов (/tmp, /var/tmp, /dev/shm), верните каталоги 755/750, файлы 644/640, секреты 600. Запись приложению дайте через владельца/группу, не через other. AppArmor DENIED лечите профилем, не 777. Хост host.example (10.0.20.10), админ admin.

Симптомы и как отличить

  • В тикете «поставили 777, заработало»;
  • ls -ld /var/www/htmldrwxrwxrwx;
  • пользователи UNIX подменяют друг другу upload;
  • wp-config.php 666.
ПраваКогда ок
1777 на /tmpsticky, да
777 на /var/wwwнет
1777 на uploadиногда, с проверкой содержимого
600 на ключда

Отличие от секретов в git: там содержимое, здесь режим. Часто оба: секреты. SUID на world-writable — критично: SUID.

Возможные причины

  1. Совет «chmod 777» из поиска.
  2. Несовпадение uid PHP-FPM и файлов деплоя.
  3. umask 000 в unit или в cron.
  4. Архив распаковали с правами DOS.
  5. NFS all_squash и попытка обойти uid.
  6. Контейнер root пишет 777 volume.

Диагностика

Ограничьте поиск, исключите /proc /sys /run (много ложных):

sudo find / -xdev -type d -perm -0002 ! -path '/tmp' ! -path '/var/tmp' ! -path '/dev/shm' \
  ! -perm -1000 2>/dev/null | head
sudo find /var /opt /home /etc -xdev \( -type f -o -type d \) -perm -0002 2>/dev/null | head -n 80

-perm -0002 — writable for other. Без sticky (! -perm -1000) на каталогах — плохой признак.

stat /var/www/html
namei -l /var/www/html/wp-config.php
umask
sudo systemctl show myservice -p UMask,User

Web:

sudo -u www-data test -w /var/www/html && echo www_can_write_root_doc
sudo -u www-data test -w /var/www/html/wp-content/uploads

Нужна запись в uploads, не в корень CMS.

Решение

Сценарий A. Документы сайта

Владелец деплоя admin или deploy, группа www-data, каталоги 750/2775 точечно на uploads:

sudo chown -R admin:www-data /var/www/html
sudo find /var/www/html -type d -exec chmod 750 {} \;
sudo find /var/www/html -type f -exec chmod 640 {} \;
sudo chown -R www-data:www-data /var/www/html/wp-content/uploads
sudo find /var/www/html/wp-content/uploads -type d -exec chmod 750 {} \;

PHP-FPM pool user = www-data. Не 777. Если DENIED — AppArmor.

Сценарий B. Каталог приложения systemd

sudo chown -R myservice:myservice /var/lib/myservice
sudo chmod 750 /var/lib/myservice

User= в unit: сервис от root. Не 777 «чтобы root и admin оба писали» — группа и 770.

Сценарий C. Shared drop directory

Нужен sticky: chmod 1770 на групповой каталог или 1777 только если модель как /tmp. Без sticky любой пользователь удалит чужие файлы.

Сценарий D. umask

# /etc/systemd/system/myservice.service.d/umask.conf
[Service]
UMask=0027

Cron: явный umask 027 в скрипте. Скрипт не world-writable: cron.

Отдельный проход по «скрытым» 777: upload CMS, wp-content/cache, sessions PHP, spool почты, /var/tmp без sticky после ручной «чистки». find без -xdev уйдёт в bind-mount бэкапов и NFS — либо ограничьте пути, либо пройдите каждый mount явно.

ACL (getfacl) могут давать other:rwx при unix-mode 750. Если видите mask::rwx и default ACL на каталоге сайта — снимите лишнее setfacl -b точечно после проверки, что www-data всё ещё пишет uploads через группу.

Контейнеры: umask процесса PID 1 в образе часто 0022, но entrypoint делает chmod -R 777 /data «для Windows-volume». Исправьте entrypoint, не хостовый cron chmod. Podman/Docker volume на Linux ACL наследуются от каталога хоста: chown UID в контейнере должен совпадать с политикой, не 777.

Samba/create mask = 0777 воспроизводит проблему каждую копию с ноутбука. Ставьте 0640/0750 и force group = www-data осознанно.

После массового chmod перезапустите PHP-FPM/сервис: открытые fd могут держать старые права на уже открытые файлы, но новые create пойдут с новым umask unit. Проверьте UMask в systemctl show.

Не используйте chmod -R a+rwX из «шпаргалки восстановления». Это хуже 777 на файлах (исполняемые секреты). Восстановление из бэкапа с tar --acls --xattrs предпочтительнее.

Связка со скриптами cron: world-writable target job = hijack, даже если дерево сайта уже 750. Один backup.sh 777 перечёркивает обход www.

Для артефактов деплоя (rsync --chmod) задайте явный --chmod=F640,D750, иначе umask ноутбука админа снова размажет 664/777 по /var/www. Проверьте один раз stat после выкладки, не только на staging.

После выкладки CMS плагины снова ставят 777 на cache — включите в мониторинг find /var/www -perm -0002 раз в сутки, не разовый chmod.

Как проверить, что проблема устранена

sudo find /var/www /opt /etc/myservice /var/lib/myservice -xdev -perm -0002 2>/dev/null
stat -c '%a %n' /var/www/html /var/www/html/wp-config.php
sudo -u www-data test -r /var/www/html/index.php && echo read_ok
sudo -u nobody test -w /var/www/html; echo nobody_write:$?

Сайт/сервис отвечают. nobody не пишет в дерево приложения. Секреты не 644.

Если не помогло

  • PHP всё ещё не пишет upload: uid pool, не 777. Смотрите ps -o user -C php-fpm8.3.
  • NFS root_squash: согласуйте uid, не 777 на экспорте.
  • ACL поверх mode (getfacl) — сбросьте лишний mask::rwx для other.
  • Контейнер пересоздаёт volume 777 — исправьте Dockerfile umask/USER.
  • SUID-файл в writable каталоге — снимите SUID немедленно.

Профилактика

  • Запрет 777 в политике деплоя; CI find -perm 777.
  • umask 027 на серверах.
  • Деплой от deploy, runtime от www-data/myservice.
  • Регулярный find world-writable в baseline.

FAQ

755 vs 750 на каталоге сайта

750 если other не должен листить. 755 — листинг содержимого без записи. Секреты всё равно не в docroot.

Нужен ли sticky на /var/tmp

Да, пакетный дефолт 1777. Не меняйте на 777 без sticky.

chmod 666 на сокет приложения

Сокет лучше 660 user:group клиентов. 666 — любой локальный процесс.

Windows-копирование на Samba даёт 777

create mask / directory mask в smb.conf, не «потом chmod».

Можно ли оставить 777 на /opt/app/tmp за NAT

Нет. Локальный RCE www-data достаточен. NAT не модель угроз для mode.