Проблема обычно выглядит безобидно: CI успешно загрузил релиз, но сайт отвечает 403, PHP не может записать кэш, Nginx не видит статику, Apache жалуется на доступ к каталогу. Через пару минут кто-то предлагает быстрый фикс: chmod 777 -R /var/www/site. Сайт оживает, инцидент закрывается, но вместе с ним открывается другая дверь: любой локальный пользователь, скомпрометированный скрипт или ошибочный процесс получает возможность менять файлы приложения.
Правильная схема строится не на полном доступе для всех, а на понятном разделении ролей: кто деплоит, кто читает файлы, кто имеет право писать в uploads/cache/storage, а кто не должен менять код вообще. Для этого в Linux достаточно классических прав, бита setgid и, когда обычных owner/group/other уже мало, linux acl.
chmod 777почти никогда не является решением для веб-каталога. Это диагностический костыль, который маскирует проблему владельца, группы,umaskили ACL.
Что именно ломается в deploy directory
Каталог деплоя, или deploy directory, чаще всего содержит несколько типов данных. Код приложения должен обновляться CI или пользователем deploy. Веб-серверу и PHP-FPM нужно читать этот код. Отдельным каталогам, например storage, var/cache, runtime, uploads, требуется запись со стороны приложения. Уже этого достаточно, чтобы простая команда chown -R www-data:www-data была не лучшей идеей: веб-процесс получает право менять код, хотя для обслуживания запросов ему обычно достаточно чтения.
Вторая частая причина — разные пользователи сервисов. На Debian и Ubuntu Nginx и Apache обычно работают от www-data. В RHEL-based системах Apache чаще использует apache, а Nginx — nginx. PHP-FPM может работать от того же пользователя, что веб-сервер, или от отдельного пользователя сайта. Если CI заходит по SSH как deploy, а файлы после сборки оказываются с группой deploy и правами 600, веб-сервер их не прочитает.
Третья причина — каталоги без execute-бита. Для файла достаточно права чтения, но для прохода по пути каждому каталогу нужен бит x. Поэтому файл /var/www/example.com/current/public/index.php может иметь 644, но если /var/www/example.com закрыт как 700, Nginx или Apache всё равно получит отказ.
Минимальная модель прав: deploy владеет, web читает
Удобная базовая схема для большинства сайтов такая: пользователь deploy владеет файлами и выполняет обновления, группа веб-сервера читает код, а запись веб-процессу разрешена только в специально выделенных каталогах. Тогда компрометация веб-приложения не означает автоматическую возможность переписать весь код проекта.
Для статических файлов и PHP-приложений можно ориентироваться на такие права:
- каталоги кода:
2750или2755, если действительно нужно чтение для прочих пользователей; - файлы кода:
0640или0644; - секреты и env-файлы:
0640, группа должна быть только у процесса, которому реально нужно читать файл; - каталоги записи приложения:
2770с ограниченной группой или ACL; - никаких
777и666в дереве сайта.
Цифра 2 в 2750 и 2770 — это как раз setgid для каталога. Новые файлы и подкаталоги внутри наследуют группу родительского каталога. Это особенно полезно при деплое: CI загружает новый файл, а группа остаётся, например, www-data, nginx или apache, а не случайная основная группа пользователя deploy.

Как узнать пользователя Nginx, Apache и PHP-FPM
Не начинайте настройку с предположений. Сначала проверьте, от кого реально работают процессы. В разных дистрибутивах, панелях управления и образах VDS это может отличаться.
Debian/Ubuntu
ps -eo user,group,comm | grep -E 'nginx|apache2|php-fpm'
grep -R -E '^(user|group)' /etc/php/*/fpm/pool.d 2>/dev/null
apachectl -SRHEL-based: AlmaLinux, Rocky Linux, CentOS Stream, Oracle Linux
ps -eo user,group,comm | grep -E 'nginx|httpd|php-fpm'
grep -R -E '^(user|group)' /etc/php-fpm.d 2>/dev/null
httpd -SВ выводе ищите не master-процесс от root, а worker-процессы. Именно они читают файлы сайта. Если PHP-FPM работает отдельным пулом от пользователя site1, то права на PHP-файлы и каталоги записи должны учитывать не только Nginx или Apache, но и этого пользователя.
Подготовка ACL-инструментов и проверка файловой системы
На современных ext4 и XFS поддержка ACL обычно включена по умолчанию. Но утилиты getfacl и setfacl могут быть не установлены в минимальном образе сервера.
Debian/Ubuntu
sudo apt update
sudo apt install aclRHEL-based и Fedora
sudo dnf install aclПроверьте параметры монтирования. Если в опциях нет слова acl, это не всегда проблема: для ext4 и XFS ACL может быть включён неявно. Важно, чтобы команды getfacl и setfacl работали без ошибки Operation not supported.
findmnt -no FSTYPE,OPTIONS /var/www
getfacl /var/wwwНастройка каталога сайта с setgid
Ниже пример для сайта example.com, где деплой выполняет пользователь deploy, а веб-сервер читает файлы через свою группу. Подставьте группу под вашу ОС и сервис: www-data для типичного Debian/Ubuntu, nginx для Nginx в RHEL-based системах, apache для Apache/httpd.
Debian/Ubuntu: Nginx или Apache с группой www-data
sudo useradd --home-dir /var/www/example.com --shell /bin/bash --create-home deploy
sudo install -d -o deploy -g www-data -m 2750 /var/www/example.com
sudo install -d -o deploy -g www-data -m 2750 /var/www/example.com/current
sudo install -d -o deploy -g www-data -m 2750 /var/www/example.com/shared
sudo find /var/www/example.com -type d -exec chmod 2750 {} +
sudo find /var/www/example.com -type f -exec chmod 0640 {} +RHEL-based: Nginx с группой nginx
sudo useradd --home-dir /var/www/example.com --shell /bin/bash --create-home deploy
sudo install -d -o deploy -g nginx -m 2750 /var/www/example.com
sudo install -d -o deploy -g nginx -m 2750 /var/www/example.com/current
sudo install -d -o deploy -g nginx -m 2750 /var/www/example.com/shared
sudo find /var/www/example.com -type d -exec chmod 2750 {} +
sudo find /var/www/example.com -type f -exec chmod 0640 {} +RHEL-based: Apache/httpd с группой apache
sudo useradd --home-dir /var/www/example.com --shell /bin/bash --create-home deploy
sudo install -d -o deploy -g apache -m 2750 /var/www/example.com
sudo install -d -o deploy -g apache -m 2750 /var/www/example.com/current
sudo install -d -o deploy -g apache -m 2750 /var/www/example.com/shared
sudo find /var/www/example.com -type d -exec chmod 2750 {} +
sudo find /var/www/example.com -type f -exec chmod 0640 {} +Такой вариант уже решает важную часть задачи: новые каталоги внутри дерева наследуют группу веб-сервера благодаря setgid. Но если CI или сборщик создаёт файлы с правами 0600, группа всё равно не сможет их прочитать. Здесь помогают правильный umask, параметры rsync и default ACL.
Default ACL: чтобы новые файлы сразу получали правильные права
setgid наследует группу, но не заставляет новые файлы иметь нужные права. Например, приложение может создать файл с 600, и группа веб-сервера не прочитает его. Default ACL задаёт права, которые будут применяться к новым объектам внутри каталога.
Пример для Debian/Ubuntu с группой www-data:
sudo setfacl -R -m u:deploy:rwX,g:www-data:rX,m:rwX /var/www/example.com
sudo find /var/www/example.com -type d -exec setfacl -d -m u:deploy:rwX,g:www-data:rX,m:rwX {} +
getfacl /var/www/example.comДля Nginx на RHEL-based системах замените группу:
sudo setfacl -R -m u:deploy:rwX,g:nginx:rX,m:rwX /var/www/example.com
sudo find /var/www/example.com -type d -exec setfacl -d -m u:deploy:rwX,g:nginx:rX,m:rwX {} +Для Apache/httpd на RHEL-based системах:
sudo setfacl -R -m u:deploy:rwX,g:apache:rX,m:rwX /var/www/example.com
sudo find /var/www/example.com -type d -exec setfacl -d -m u:deploy:rwX,g:apache:rX,m:rwX {} +Буква X в ACL важна: она ставит execute-бит только каталогам и тем файлам, у которых он уже был. Это безопаснее, чем массово делать исполняемыми все файлы. Параметр m:rwX задаёт ACL mask. Если mask слишком узкая, права в getfacl могут выглядеть правильными, но в колонке effective окажутся урезанными.
Каталоги, куда приложению действительно нужна запись
Для uploads, runtime, cache и sessions не нужно открывать весь сайт. Создайте отдельные каталоги и выдайте запись только там. Например, для Debian/Ubuntu:
sudo install -d -o deploy -g www-data -m 2770 /var/www/example.com/shared/uploads
sudo install -d -o deploy -g www-data -m 2770 /var/www/example.com/shared/cache
sudo setfacl -m u:deploy:rwx,g:www-data:rwx,m:rwx /var/www/example.com/shared/uploads
sudo setfacl -d -m u:deploy:rwx,g:www-data:rwx,m:rwx /var/www/example.com/shared/uploads
sudo setfacl -m u:deploy:rwx,g:www-data:rwx,m:rwx /var/www/example.com/shared/cache
sudo setfacl -d -m u:deploy:rwx,g:www-data:rwx,m:rwx /var/www/example.com/shared/cacheЕсли PHP-FPM работает не от www-data, а от отдельного пользователя, например site1, выдавайте запись именно этому пользователю или его группе. Nginx сам по себе часто не пишет в uploads PHP-приложения; это делает PHP-FPM. Поэтому при ошибке записи смотрите пользователя PHP-пула, а не только пользователя Nginx.
sudo setfacl -m u:site1:rwx,u:deploy:rwx,m:rwx /var/www/example.com/shared/uploads
sudo setfacl -d -m u:site1:rwx,u:deploy:rwx,m:rwx /var/www/example.com/shared/uploadsДля Laravel это могут быть storage и bootstrap/cache, для Symfony — var, для WordPress — wp-content/uploads. Смысл один: код приложения доступен на чтение, каталоги данных доступны на запись только тем процессам, которым это требуется.
Nginx permissions: что нужно веб-серверу
Nginx должен иметь execute-доступ ко всем каталогам на пути и read-доступ к файлам, которые он отдаёт сам: HTML, CSS, JS, изображения, иногда PHP-файлы для передачи в FastCGI. Для PHP важно не путать права Nginx и PHP-FPM. Nginx проверяет и передаёт путь, а читает и исполняет скрипт уже PHP-FPM.
Минимальный фрагмент server block может выглядеть так:
server {
listen 80;
server_name example.com;
root /var/www/example.com/current/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php-fpm.sock;
}
}Если Nginx отдаёт 403, проверьте не только права файла, но и весь путь:
namei -l /var/www/example.com/current/public/index.php
sudo -u www-data test -x /var/www/example.com
sudo -u www-data test -r /var/www/example.com/current/public/index.phpДля RHEL-based систем замените www-data на nginx, если Nginx работает от этого пользователя:
sudo -u nginx test -x /var/www/example.com
sudo -u nginx test -r /var/www/example.com/current/public/index.phpApache permissions: Require, Options и права файловой системы
У Apache есть два уровня доступа. Первый — права файловой системы: может ли пользователь www-data или apache пройти по пути и прочитать файл. Второй — правила в конфигурации: Require all granted, Options, AllowOverride. Ошибка 403 может быть на любом из этих уровней.
Пример виртуального хоста для Apache 2.4:
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/example.com/current/public
<Directory /var/www/example.com/current/public>
Require all granted
AllowOverride None
Options FollowSymLinks
</Directory>
</VirtualHost>Проверки для Debian/Ubuntu:
namei -l /var/www/example.com/current/public/index.php
sudo -u www-data test -r /var/www/example.com/current/public/index.php
apachectl configtest
systemctl reload apache2Проверки для RHEL-based систем:
namei -l /var/www/example.com/current/public/index.php
sudo -u apache test -r /var/www/example.com/current/public/index.php
httpd -t
systemctl reload httpdЕсли выбираете стек для нового проекта, полезно заранее понимать различия процессов и конфигурации — мы разбирали это в материале Nginx vs Apache для современных сайтов. А в уже работающем проекте сначала добейтесь правильных Unix permissions и ACL, затем проверяйте особенности веб-сервера.
Если SELinux включён, файловые права могут быть корректными, но доступ всё равно будет запрещён политикой. В этом случае смотрите audit-логи и контексты. Не смешивайте проблемы: сначала проверьте владельцев, группы, execute-биты и ACL, затем переходите к SELinux-контекстам.

CI deploy: как не сломать права при каждой поставке
CI часто портит права не из злого умысла, а из-за поведения инструментов. rsync -a сохраняет права из рабочей директории раннера. Архивы могут распаковываться с правами, которые были у файлов на машине сборки. Git checkout зависит от umask пользователя. Поэтому правила прав должны быть частью деплой-процесса, а не ручной операцией после инцидента.
Для пользователя deploy задайте разумный umask. Значение 027 означает: владелец получает полный доступ, группа — чтение и проход по каталогам, остальные — ничего.
echo 'umask 027' | sudo tee -a /home/deploy/.profile
sudo chown deploy:deploy /home/deploy/.profileПример rsync-деплоя, который явно выставляет права каталогам и файлам:
rsync -az --delete --delay-updates --chmod=D2750,F0640 public/ deploy@example.com:/var/www/example.com/current/public/Если вы используете релизы с атомарным переключением symlink, права лучше готовить до переключения current. Тогда веб-сервер либо видит старый рабочий релиз, либо новый уже с корректными правами.
ssh deploy@example.com 'mkdir -p /var/www/example.com/releases/2026-07-06-120000'
rsync -az --delete --delay-updates --chmod=D2750,F0640 public/ deploy@example.com:/var/www/example.com/releases/2026-07-06-120000/public/
ssh deploy@example.com 'ln -sfn /var/www/example.com/releases/2026-07-06-120000 /var/www/example.com/current'В GitLab CI или GitHub Actions идея та же: раннер подключается отдельным пользователем, не root, не пользователем веб-сервера, и не выполняет chmod 777 после копирования. Упрощённый фрагмент job:
deploy:
stage: deploy
script:
- rsync -az --delete --delay-updates --chmod=D2750,F0640 public/ deploy@$HOST:/var/www/example.com/current/public/
- ssh deploy@$HOST 'find /var/www/example.com/current -type d -exec chmod 2750 {} +'
- ssh deploy@$HOST 'find /var/www/example.com/current -type f -exec chmod 0640 {} +'Если проекту нужны исполняемые файлы, например CLI-скрипты в bin, не делайте исполняемым всё дерево. Выдайте execute точечно:
ssh deploy@example.com 'chmod 0750 /var/www/example.com/current/bin/console'Типовые ошибки и как их диагностировать
Самый быстрый инструмент для разбора 403 — namei -l. Он показывает каждый компонент пути, и сразу видно, где нет execute-бита или владелец не тот.
namei -l /var/www/example.com/current/public/app.cssДальше смотрите ACL. Обычный ls -l показывает знак + после прав, если у файла есть расширенные ACL, но не раскрывает их полностью.
ls -la /var/www/example.com/current/public
getfacl /var/www/example.com/current/public
getfacl /var/www/example.com/current/public/app.cssПроверить, остались ли в дереве небезопасные world-writable файлы, можно так:
sudo find /var/www/example.com -perm -0002 -lsЕсли такие файлы нашлись, не спешите просто убрать бит записи для others. Сначала выясните, почему они появились: распаковка архива, npm build, composer script, upload от приложения, неправильный umask, ручная команда в CI. Иначе после следующего деплоя проблема вернётся.
Чтобы быстро вернуть дерево к базовой безопасной схеме, используйте команды, соответствующие вашей группе веб-сервера. Для Debian/Ubuntu:
sudo chown -R deploy:www-data /var/www/example.com
sudo find /var/www/example.com -type d -exec chmod 2750 {} +
sudo find /var/www/example.com -type f -exec chmod 0640 {} +
sudo setfacl -R -m u:deploy:rwX,g:www-data:rX,m:rwX /var/www/example.com
sudo find /var/www/example.com -type d -exec setfacl -d -m u:deploy:rwX,g:www-data:rX,m:rwX {} +Для Nginx на RHEL-based системах замените группу на nginx, для Apache — на apache. После этого отдельно верните запись в каталоги данных, например uploads и cache.
Когда ACL лучше групп, а когда достаточно setgid
setgid хорош, когда у вас простая модель: один владелец деплоя и одна группа чтения. Это прозрачно, легко проверяется через ls -l и редко удивляет администраторов. ACL нужны, когда участников больше: несколько CI-пользователей, отдельный PHP-FPM user, веб-серверу нужно только чтение, воркерам очередей нужна запись в один каталог, а SFTP-пользователю нужен доступ только к uploads.
Минус ACL — их хуже видно при беглом просмотре. Если команда не привыкла проверять getfacl, можно долго искать причину отказа. Поэтому документируйте схему прямо в runbook проекта: кто владеет деревом, какая группа у web, какие каталоги writable, какие команды используются после деплоя.
Практичный чек-лист перед продакшеном
- Пользователь CI не является
rootи не совпадает с пользователем веб-сервера. - Код принадлежит
deploy, а группа соответствует процессу, которому нужно читать файлы. - На каталогах включён
setgid, чтобы группа не терялась после новых релизов. - Default ACL настроены там, где новые файлы должны сразу получать права для группы или пользователя.
- Запись веб-приложению разрешена только в uploads/cache/storage/runtime, а не во всём дереве.
- CI явно задаёт права через
--chmod,umaskили post-deploy команды. - В дереве сайта нет объектов с
777,666и неожиданным владельцем. - Nginx/Apache конфигурация указывает на правильный
rootилиDocumentRoot, а не на родительский закрытый каталог. - После деплоя выполняется быстрая проверка чтения от имени пользователя веб-сервера.
Итоговая схема
Для безопасного деплоя не нужен магический набор прав. Нужна дисциплина: отделить пользователя деплоя от пользователя веб-сервера, выдать чтение через группу или ACL, включить setgid на каталогах и разрешить запись только там, где приложение действительно пишет данные. Тогда nginx permissions, apache permissions и ci deploy перестают быть набором случайных исправлений и становятся воспроизводимой частью инфраструктуры.
Если после каждого релиза вам приходится чинить 403 вручную, проблема почти всегда не в Nginx или Apache, а в том, что права не описаны как часть деплоя. Один раз настройте владельцев, setgid, default ACL и umask — и команда chmod 777 исчезнет из аварийных инструкций.


