Решение проблем

403 Forbidden в nginx: почему и как исправить

403 Forbidden в nginx значит, что nginx нашёл файл, но не отдаёт его. Пять причин: нет индекса, права доступа, SELinux, правило deny или это не nginx.

Опубликовано ~8 мин чтения

403 Forbidden от nginx значит, что nginx нашёл путь, но отказывается его отдавать. Лог ошибок называет причину в одной строке: directory index of … is forbidden — нет индексного файла, (13: Permission denied) — права файловой системы или SELinux, а access forbidden by rule — сработало правило allow/deny. Сначала читайте лог, исправляйте эту одну причину и перезагружайте конфигурацию.

быстрое решение
sudo tail -n 20 /var/log/nginx/error.log
# исправьте то, что показал лог, затем
sudo nginx -t && sudo systemctl reload nginx

Что говорит лог ошибок nginx?

/var/log/nginx/error.log — путь по умолчанию в Debian, Ubuntu, Rocky, Alma и Alpine. Конкретный сайт может писать ошибки в другое место своей строкой error_log (найти её — sudo grep -R error_log /etc/nginx/), а официальный образ Docker пишет в вывод контейнера, поэтому там нужен docker logs <container>. Перезагрузите страницу один раз и сверьтесь с самой новой строкой:

  • directory index of "/var/www/site/" is forbidden — причина 1, нет индексного файла.
  • open() "/var/www/site/index.html" failed (13: Permission denied) — причина 2, или причина 3 на Rocky, Alma и RHEL.
  • access forbidden by rule — причина 4, правило allow/deny.
  • Для этого запроса в логе вообще ничего нет — причина 5: 403, скорее всего, не от nginx.

Это тот же порядок действий «сначала лог», что и для 502 Bad Gateway; разница в том, что 403 почти всегда дело в файлах или конфигурации, а не в бэкенде.

Причина 1: есть ли индексный файл?

Запрос каталога — / или /blog/ — заставляет nginx искать файлы, указанные в директиве index, по умолчанию index.html. Если такого файла нет, а показ списков каталогов выключен (это по умолчанию), nginx отвечает 403, а не 404.

проверка
ls -la /var/www/site/                                 # есть ли index.html или index.php?
sudo nginx -T | grep -nE 'root|index|autoindex|try_files'
  • root указывает не на ту папку. Фреймворки отдают файлы из подкаталога — public/ для Laravel, dist/ или build/ для сборки фронтенда. Направьте root туда.
  • PHP-сайт без index.php в списке. Добавьте его первым: index index.php index.html;.
  • Ожидание, что try_files $uri $uri/ =404; вернёт 404. Для существующего каталога это не так: $uri/ совпадает с ним, nginx ищет индексный файл, и без него получается 403. Решение — всё тот же индексный файл, либо для приложения с front controller — index index.php; плюс запасной вариант try_files $uri $uri/ /index.php?$query_string;.

Не включайте autoindex on; только чтобы убрать 403 — это заменяет ошибку на публичный список всех файлов каталога.

Причина 2: может ли nginx прочитать файлы?

Главный процесс nginx работает от root, но воркеры, которые читают ваши файлы, работают от непривилегированного пользователя: www-data в пакетах Debian и Ubuntu, nginx — в Rocky, Alma, Alpine, в пакетах с nginx.org и в официальном образе Docker. Проверьте, а не угадывайте:

какой пользователь читает файлы?
ps -o user=,args= -C nginx         # воркеры покажут нужного пользователя
grep -n '^user' /etc/nginx/nginx.conf

Этому пользователю нужно право на чтение файла и право на выполнение (проход) на каждый каталог выше него. Одной отсутствующей x в любом месте пути достаточно для 403. namei показывает всю цепочку целиком:

проверить каждый каталог на пути
namei -om /var/www/site/index.html
# f: /var/www/site/index.html
#  drwxr-xr-x root  root  /
#  drwxr-xr-x root  root  var
#  drwxr-xr-x root  root  www
#  drwxr-x--- alice alice site          <- нет x для остальных: 403
#  -rw-r--r-- alice alice index.html

# проверка от имени пользователя nginx (nginx вместо www-data на Rocky, Alma, Alpine)
sudo -u www-data cat /var/www/site/index.html > /dev/null && echo readable

Классический случай — сайт внутри /root или домашнего каталога. Новые домашние каталоги в современных Debian и Ubuntu закрыты для остальных пользователей, и nginx не может пройти через них, какими бы открытыми ни были файлы внутри. Чистое решение — отдавать сайт из /var/www или /srv, принадлежащих вашему deploy-пользователю и доступных на чтение всем:

веб-корень, который nginx может прочитать
sudo mkdir -p /var/www/site
sudo chown -R $USER:$USER /var/www/site
find /var/www/site -type d -exec chmod 755 {} +
find /var/www/site -type f -exec chmod 644 {} +

Если сайт обязательно должен остаться в домашнем каталоге, chmod o+x /home/alice пускает nginx проходить через него без возможности листинга — это работает, но тогда любая локальная учётная запись получает доступ к любому файлу внутри, если его собственные права это позволяют. Файлы, скопированные с другого сервера, часто приходят со старыми идентификаторами владельца или слишком строгим режимом. В Docker файлы, смонтированные через bind-mount, должны быть читаемы пользователем nginx внутри контейнера, а не только вами на хосте.

Причина 3: блокирует ли SELinux nginx на Rocky, Alma или RHEL?

Если права доступа выше в порядке, а лог всё ещё пишет (13: Permission denied), смотрите на SELinux. По умолчанию он в режиме enforcing на Rocky, AlmaLinux и RHEL и пускает nginx читать только файлы с меткой веб-контента, такой как httpd_sys_content_t. Файлы, созданные в домашнем каталоге или перемещённые из него через mv, сохраняют старую метку — и получают 403 при идеальных правах Unix.

диагностика
getenforce                         # Enforcing?
ls -Z /srv/site                    # ищем httpd_sys_content_t
sudo ausearch -m avc -ts recent    # отказы, которые залогировал SELinux
пометить каталог навсегда
sudo dnf install -y policycoreutils-python-utils     # даёт semanage
sudo semanage fcontext -a -t httpd_sys_content_t '/srv/site(/.*)?'
sudo restorecon -Rv /srv/site

/var/www/html уже несёт нужную метку по умолчанию, так что для файлов, перемещённых туда, достаточно sudo restorecon -Rv /var/www/html. chcon тоже меняет метку, но последующий полный релейбл её сбрасывает; semanage вместе с restorecon переживает его. Каталогам, в которые приложение пишет, нужна метка httpd_sys_rw_content_t. Не «лечите» это setenforce 0 или отключением SELinux целиком — так вы снимаете защиту со всех служб на машине, чтобы исправить метку одного каталога.

Причина 4: совпадает ли правило deny?

access forbidden by rule значит, что запросу подошла директива allow/deny. Найдите их все в загруженной конфигурации:

найти правило
sudo nginx -T | grep -nE 'deny|allow|satisfy'
  • Белый список, в который вы не попадаете. allow 203.0.113.0/24; deny all; — обычный способ защитить админский путь, и на phpMyAdmin 403 от него — это защита, а не ошибка. За CDN или прокси nginx видит адрес прокси, если не настроены set_real_ip_from и real_ip_header.
  • Блок dotfile-ов, который заодно ловит Let’s Encrypt. location ~ /\. { deny all; } защищает .git и .env, но также совпадает с /.well-known/acme-challenge/, и проверка сертификата падает с 403. Исключите именно этот путь (пример ниже).
  • Базовая авторизация. Неверный пароль с auth_basic возвращает 401, а не 403. 403 на пути с паролем идёт от половины allow/deny в блоке satisfy.
оставить dotfile-ы закрытыми, пропустить ACME
location ^~ /.well-known/acme-challenge/ {
    allow all;
}
location ~ /\.(?!well-known) {
    deny all;
}

Причина 5: действительно ли 403 от nginx?

Если лог ошибок молчит, проверьте, кто на самом деле ответил:

кто отправил 403?
curl -sI https://example.com/path | grep -iE '^(HTTP|server)'
  • CDN или WAF в заголовке server — его файрвол заблокировал запрос, не дойдя до вас. Страница блокировки выглядит иначе, чем простая страница nginx, и правка делается в панели этого сервиса.
  • server: nginx, но страница — от вашего приложения — nginx передал запрос дальше, а 403 вернуло приложение: защитный плагин WordPress, проверка CSRF, middleware. Лог доступа покажет 403, лог ошибок — нет; смотрите логи самого приложения.
  • Тело ответа говорит только Access denied. — PHP-FPM отказал скрипту, чьё имя не кончается на .php, это его правило security.limit_extensions. Лог ошибок nginx покажет это как FastCGI sent in stderr с has been denied (see security.limit_extensions). Правьте location в nginx, чтобы до PHP-FPM доходили только файлы .php, а не расширяйте список.

Что бы вы ни меняли, проверяйте перед перезагрузкой — sudo nginx -t ловит опечатку до того, как она положит сайт, а reload применяет изменение без разрыва соединений. На Alpine используйте rc-service nginx reload.

Частые вопросы

Что значит 403 Forbidden в nginx?

Nginx нашёл запрошенный путь, но отказывается его отдавать. Лог ошибок называет причину в одной строке: directory index of … is forbidden — нет индексного файла, (13: Permission denied) — права файловой системы или SELinux, access forbidden by rule — сработало правило allow/deny. Сначала читайте /var/log/nginx/error.log, потом исправляйте именно эту причину.

Как исправить 403 Forbidden, если нет индексного файла?

Проверьте, что в папке действительно есть index.html или index.php (ls -la), и что директива root в конфигурации указывает на правильную папку — фреймворки часто отдают файлы из подкаталога вроде public/ или dist/. Для PHP-сайта добавьте index.php первым в список index. Не включайте autoindex on только чтобы убрать ошибку — так вы откроете публичный список всех файлов каталога.

Почему SELinux вызывает 403 Forbidden на Rocky или AlmaLinux?

SELinux по умолчанию в режиме enforcing на Rocky, AlmaLinux и RHEL и пускает nginx читать только файлы с меткой веб-контента, такой как httpd_sys_content_t. Файлы, созданные в домашнем каталоге или перемещённые туда через mv, сохраняют старую метку и получают 403 даже при правильных правах Unix. Исправляют это командами sudo semanage fcontext и sudo restorecon, а не отключением SELinux целиком.

Читайте также

Все статьи раздела

504 Gateway Time-out в nginx: как найти медленный бэкенд и исправить ошибку

504 значит, что nginx ждал ответа от PHP-FPM, Node или другого бэкенда и не дождался. Читаем лог, увеличиваем нужный таймаут и чиним медленный запрос.

Permission denied (publickey): почему SSH не принимает ключ и как это исправить

SSH подключился, но сервер отверг ключ. Четыре причины: не тот пользователь, не тот ключ, неверные права или ключ не установлен. Отдельно — GitHub и PuTTY.

ssh-copy-id: как скопировать SSH-ключ на сервер и что делать в Windows

ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host дописывает открытый SSH-ключ в authorized_keys. Как создать ключ, все параметры, замена для Windows и ошибки.

503 Service Unavailable on your server: find who refused and why(англ.)

A 503 means your server is up but refusing work: backend down, PHP-FPM out of workers, a rate limit or maintenance mode. Find the source and add Retry-After.

400 Bad Request: find out what the server rejected, and fix it(англ.)

400 Bad Request means the server refused a malformed or oversized request. Fix cookies that grew too large, HTTP sent to an HTTPS port and broken API calls.