Почему Nginx отдаёт 403 или 404, когда «файл точно есть»
Ошибки 403 Forbidden и 404 Not Found в Nginx регулярно выглядят одинаково со стороны пользователя: «ничего не открывается». Но причины обычно приземлённые: Nginx не может прочитать файл, не может пройти по каталогам, строит неправильный путь из-за root/alias, или доступ режет SELinux/AppArmor. Код ответа зависит от того, на каком этапе запрос «сломался».
Чаще всего виновато одно из следующего:
- неверный
rootилиalias(Nginx ищет файл не там); - нет права «прохода» (execute,
x) на одном из каталогов по пути; - не тот пользователь воркеров Nginx, и как следствие — не те права/группы;
- ошибки связки с PHP-FPM (Unix-сокет и его права; неверная подстановка пути к скрипту);
- SELinux или AppArmor блокируют доступ, даже если POSIX-права выглядят корректно.
Ниже — пошаговый чек-лист, который обычно быстрее всего приводит к точной первопричине.
Шаг 1. Начинаем с логов: где именно «сломалось»
Первое, что нужно сделать — увидеть точную причину глазами самого Nginx. Берём error.log и access.log. В типичных установках:
sudo tail -n 200 /var/log/nginx/error.log
sudo tail -n 50 /var/log/nginx/access.log
В error.log ищите маркеры, которые сразу сужают круг:
permission denied— почти всегда права, SELinux/AppArmor или доступ к сокету;open() ... failed (2: No such file or directory)— Nginx реально не нашёл файл по построенному пути (частоroot/alias/try_files);directory index of ... is forbidden— запросили каталог, индекс не найден/не разрешён, листинг запрещён (403);FastCGI sent in stderr: "Primary script unknown"— PHP-FPM не сопоставил путь к скрипту (часто ошибка в пути, связкеroot/aliasили настройках fastcgi-параметров).
Одинаковая первопричина «нет доступа» иногда проявляется как 403, 404 и даже 502. Важно понять, кто именно не получил доступ: Nginx к файлу, Nginx к сокету, PHP-FPM к файлу, или MAC-слой (SELinux/AppArmor) к любому из этих объектов.
Шаг 2. Проверяем, под кем реально работают воркеры Nginx
Права на файлы и каталоги должны быть у воркеров, а не у master-процесса. Master обычно root (чтобы слушать 80/443) — это нормально. Смотрим директиву user и фактические процессы:
sudo nginx -T 2>/dev/null | grep -nE '^*user'
ps -eo user,group,pid,cmd | grep -E 'nginx: worker process' | head
На Debian/Ubuntu часто это www-data, на RHEL-подобных — nginx. Дальше весь разбор прав делаем относительно этого пользователя и его групп.

Шаг 3. Самая частая причина 403: нет «x» на одном из каталогов по пути
Чтобы открыть файл, веб-серверу обычно нужны:
xна каждом каталоге по пути (право «прохода»);rна самом файле.
Проверять удобно командой namei — она покажет права и владельцев на каждом сегменте пути. Пример для /var/www/site/public/index.html:
namei -l /var/www/site/public/index.html
Сразу видно, на каком каталоге пропал execute для нужного пользователя/группы.
Чтобы проверить доступ «глазами» nginx-пользователя, используйте sudo -u (подставьте своего пользователя воркера):
sudo -u www-data test -r /var/www/site/public/index.html && echo OK || echo NO
sudo -u www-data test -x /var/www/site/public && echo OK || echo NO
Если здесь NO, дальше уже не гадание, а чистая механика прав.
Шаг 4. Приводим права в порядок: chmod/chown без фанатизма
«Поставьте 777» — плохая практика: она ухудшает безопасность и иногда не помогает из-за SELinux/AppArmor. Нормальная схема зависит от того, кто пишет в директорию (деплой, приложение, отдельный пользователь), но для типового сайта безопасный минимум такой:
- каталоги:
755; - файлы:
644; - владелец: пользователь проекта/деплоя;
- доступ Nginx — через чтение (мировое) или через группу (если нужно закрыть доступ «для всех»).
Пример: владелец deploy и стандартные режимы (подставьте свой путь):
sudo chown -R deploy:deploy /var/www/site
sudo find /var/www/site -type d -exec chmod 755 {} ;
sudo find /var/www/site -type f -exec chmod 644 {} ;
Если вы даёте доступ через группу (частый кейс на серверах с несколькими проектами), схема обычно такая: общая группа проекта, nginx-пользователь добавлен в эту группу, права 750/640.
sudo usermod -aG sitegrp www-data
sudo chown -R deploy:sitegrp /var/www/site
sudo find /var/www/site -type d -exec chmod 750 {} ;
sudo find /var/www/site -type f -exec chmod 640 {} ;
После изменения групп лучше перезапустить Nginx, чтобы воркеры поднялись с актуальным набором групп:
sudo systemctl restart nginx
Шаг 5. root vs alias: почему из-за одной строки получаем «не туда» и 404
Ошибки связки root/alias — частая причина «файл есть, но 404». Два правила, которые стоит держать в голове:
rootдобавляет к указанному пути весь URI (включая совпавшую частьlocation);aliasподменяет совпавшую частьlocationна указанный путь.
Пример: как ломается с root
Хотим, чтобы /static/app.css отдавался из /var/www/site/static/app.css.
Неправильно:
location /static/ {
root /var/www/site/static;
}
Так Nginx построит путь /var/www/site/static/static/app.css и вернёт 404.
Правильно через root:
location /static/ {
root /var/www/site;
}
Или правильно через alias (обратите внимание на завершающий слеш у пути):
location /static/ {
alias /var/www/site/static/;
}
Как быстро увидеть, куда Nginx реально пытался сходить
Не пытайтесь угадывать: в сообщении open() внутри error.log уже будет полный путь, который Nginx пытался открыть. Это самый быстрый способ проверить, где «склейка» пошла не так.
Шаг 6. 403 «directory index is forbidden»: индексы и try_files
Если запросили каталог (например, /), а индексный файл не найден или недоступен, Nginx вернёт 403 с сообщением directory index ... is forbidden. Типовые причины:
- в
indexзабыли нужный файл (например,index.php); - индекс есть, но нет прав на чтение/проход по пути;
- листинг выключен (по умолчанию), а индекса нет.
Минимально понятная настройка индексов:
server {
index index.php index.html;
}
Для PHP-приложений (особенно с роутингом) почти всегда нужен корректный try_files:
location / {
try_files $uri $uri/ /index.php?$args;
}
Если вы одновременно настраиваете HTTPS и заголовки безопасности, удобно держать под рукой отдельный чек-лист по миграциям и HSTS. См. материал 301-редирект, SSL и HSTS при переезде домена.
Шаг 7. Permission denied при PHP: права на Unix-сокет PHP-FPM
Если PHP подключён через Unix-сокет, ошибка часто выглядит так: connect() to unix:/run/php/php8.2-fpm.sock failed (13: Permission denied). Это означает, что воркер Nginx не может открыть файл сокета.
Проверяем, где сокет и какие у него права
ls -la /run/php/ | grep -E 'fpm\.sock'
stat /run/php/php8.2-fpm.sock
Сопоставьте пользователя воркеров Nginx, владельца/группу сокета и режим (часто 660).
Исправляем правильно: настройки пула PHP-FPM
Права на сокет нужно задавать в конфиге пула, потому что сокет пересоздаётся при перезапуске. Пример для /etc/php/8.2/fpm/pool.d/www.conf:
listen = /run/php/php8.2-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
После правок:
sudo systemctl restart php8.2-fpm
sudo systemctl restart nginx
Если вы разводите пользователей (Nginx один, PHP-FPM другой), рабочая схема — общая группа и режим 0660, а не «всем всё можно».
Шаг 8. SELinux: когда POSIX-права «правильные», но доступа всё равно нет
На системах с SELinux (часто RHEL/Alma/Rocky) можно выставить идеальные chmod/chown, но всё равно получить permission denied. Тогда проверяем SELinux-режим и AVC-отказы.
Как быстро подтвердить, что виноват SELinux
sestatus
Если режим Enforcing, смотрим свежие AVC:
sudo ausearch -m avc -ts recent | tail -n 50
Контекст для статики: httpd_sys_content_t
Контент, который Nginx должен читать, обычно должен иметь тип httpd_sys_content_t. Проверяем:
ls -lZ /var/www/site/public | head
ls -lZ /var/www/site/public/index.html
Если контекст не подходит, задаём постоянное правило и применяем:
sudo semanage fcontext -a -t httpd_sys_content_t "/var/www/site/public(/.*)?"
sudo restorecon -Rv /var/www/site/public
Для директорий с записью (uploads, cache) нужны другие типы, например httpd_sys_rw_content_t, но включать запись стоит только там, где это действительно необходимо.

Шаг 9. AppArmor: «всё верно», но доступ блокируется профилем
На Ubuntu/Debian роль SELinux часто выполняет AppArmor. Симптомы: в error.log — permission denied, а в логах ядра — строки вида apparmor="DENIED" для процесса nginx и конкретного пути к файлу/сокету.
Проверяем, активен ли AppArmor и какие профили загружены:
sudo aa-status
Ищем отказы в журнале ядра:
sudo journalctl -k --since "1 hour ago" | grep -i apparmor | tail -n 50
Дальше правильный путь — привести размещение файлов к ожидаемым директориям или точечно поправить профиль. На проде «просто выключить AppArmor» обычно дороже по рискам, чем один раз аккуратно настроить доступ. Если хотите выстроить подход к «жёсткости» веб-окружения системно, пригодится заметка веб-харденинг с AppArmor и SELinux.
Шаг 10. Частые ловушки, которые маскируются под 403/404
Симлинки и запрет на проход
Если деплой сделан через симлинк (например, current на релиз), Nginx-пользователь всё равно должен иметь x на всех родительских каталогах, а SELinux/AppArmor должны разрешать доступ к целевому пути. Симлинк сам по себе проблему не лечит и не создаёт — её создают права на компоненты пути.
Файлы существуют, но запрос попадает в другой server
Иногда правят один виртуальный хост, а запрос обслуживает другой (например, дефолтный). Тогда «есть файл, но 404» — потому что ищут в другом root. Проверьте server_name, порядок загрузки конфигов и нет ли конфликта default_server. Полезно увидеть активную конфигурацию целиком:
sudo nginx -T 2>/dev/null | less
Ограничения в location: deny/allow, auth_basic
Явный запрет deny all;, ограничения по IP или включённая базовая авторизация обычно дают «честный» 403. Это не про права файловой системы, но в диагностике важно не забывать про политику доступа.
Мини-алгоритм диагностики (чтобы не бегать по кругу)
Смотрите
/var/log/nginx/error.logи выписывайте ключевую строку:open(),connect(),directory index,Primary script unknown.Определяйте пользователя воркеров Nginx и его группы.
Проверяйте проход по пути:
namei -lи тесты черезsudo -u.Если PHP через сокет — сверяйте владельца/группу/режим сокета и параметры
listen.owner,listen.group,listen.modeв пуле PHP-FPM.Если SELinux/AppArmor активны — подтверждайте отказ через AVC или журнал ядра и исправляйте контексты/профили.
Если «No such file» — перепроверяйте
root/aliasи реальный путь из строкиopen().
Что в итоге считать «правильной» настройкой
Стабильная конфигурация — когда согласованы четыре слоя:
Путь: Nginx строит ожидаемый путь (аккуратно с
root/alias).Пользователь: воркеры работают под понятным аккаунтом, права выдаются именно ему.
POSIX-права: есть
xна каталогах иrна файлах; write-доступ только там, где нужен.MAC-слой: SELinux-контексты или AppArmor-профили не блокируют легитимный доступ.
Когда эти слои согласованы, исчезают ситуации «я час меняю chmod, а 403/404 не уходит».
Напоследок: привычки, которые экономят часы
Держите отдельный
publicдля веб-доступа, приватные директории — выше по дереву.Не лечите 403 режимом 777: сначала определите пользователя воркеров и проверьте проход по каталогам.
При
permission deniedна RHEL-семействе сразу смотрите AVC — SELinux часто оказывается причиной.Если используете PHP-FPM сокет, фиксируйте права в конфиге пула, а не вручную на файле сокета.
Если вы размещаете несколько сайтов и важно предсказуемое окружение, часто проще унести каждый проект в отдельную изолированную конфигурацию (пользователь, пул, права). Для таких схем обычно выбирают VDS, чтобы без ограничений настроить пользователей, группы и политики доступа. А для публичных прод-сайтов не забывайте про корректный HTTPS: подобрать и выпустить SSL-сертификаты полезно заранее, чтобы не совмещать миграцию шифрования с отладкой прав.


