Короткий ответ
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/html→drwxrwxrwx;- пользователи UNIX подменяют друг другу upload;
wp-config.php666.
| Права | Когда ок |
|---|---|
1777 на /tmp | sticky, да |
777 на /var/www | нет |
1777 на upload | иногда, с проверкой содержимого |
600 на ключ | да |
Отличие от секретов в git: там содержимое, здесь режим. Часто оба: секреты. SUID на world-writable — критично: SUID.
Возможные причины
- Совет «chmod 777» из поиска.
- Несовпадение uid PHP-FPM и файлов деплоя.
- umask
000в unit или в cron. - Архив распаковали с правами DOS.
- NFS
all_squashи попытка обойти uid. - Контейнер 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,UserWeb:
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/myserviceUser= в 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=0027Cron: явный 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.