Новинка Виртуальный VDS сервер в Нидерландах от 490р
Выберите продукт

Linux ACL и setgid для deploy directory: деплой без chmod 777

Разберём безопасную схему прав для каталога деплоя: кто владеет файлами, как работают setgid и ACL, какие права нужны Nginx/Apache и как настроить CI так, чтобы не возвращаться к chmod 777.
Linux ACL и setgid для deploy directory: деплой без chmod 777

Проблема обычно выглядит безобидно: 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.

Диаграмма владельцев, групп, ACL и setgid в каталоге сайта

Как узнать пользователя 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 -S

RHEL-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 acl

RHEL-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. Смысл один: код приложения доступен на чтение, каталоги данных доступны на запись только тем процессам, которым это требуется.

FastFox VDS
Облачный VDS-сервер
Виртуальные серверы с быстрым запуском и гибкой конфигурацией от 390₽ / мес
Доступные локации
Россия Нидерланды

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.php

Apache 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-деплоя с корректными правами для Nginx и Apache

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 исчезнет из аварийных инструкций.

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

Вам будет интересно

Linux sudoers.d и visudo: безопасный sudo для deploy user OpenAI Статья написана AI (GPT 5)

Linux sudoers.d и visudo: безопасный sudo для deploy user

Показываем, как выдать deploy user только нужные sudo-права для выкладки приложения: создать правило в sudoers.d, проверить его че ...
Linux capabilities: как слушать 80 и 443 без запуска приложения от root OpenAI Статья написана AI (GPT 5)

Linux capabilities: как слушать 80 и 443 без запуска приложения от root

Показываем, как разрешить приложению bind privileged ports 80 и 443 без запуска от root: через setcap, cap_net_bind_service и syst ...
Docker Compose healthcheck и depends_on: чем заменить sleep при ожидании базы OpenAI Статья написана AI (GPT 5)

Docker Compose healthcheck и depends_on: чем заменить sleep при ожидании базы

Если контейнер приложения стартует раньше PostgreSQL, MySQL или Redis, не спешите добавлять sleep. Разберём, как правильно описать ...