Ошибка 403 Forbidden NGINX: как найти причину и исправить

NGINX отвечает 403 Forbidden nginx, когда получил запрос и разобрал его, но отказался отдавать содержимое: это не «файла нет» – тогда был бы код 404, и не «нужен вход» – это 401 или 407.

403 forbidden nginx как исправить? Короткий план: сначала error.log называет причину без гаданий, а дальше – конкретная правка: индекс, права, автоиндекс, SELinux, запрет в конфиге или ошибка в alias.

  1. Прочитайте error.log – часто он прямо называет причину.
  2. Проверьте права доступа и бит x на пути к файлу.
  3. Убедитесь, что есть индексный файл или включен автоиндекс.
  4. На RHEL-совместимых системах проверьте SELinux.
  5. Поищите запрет deny в конфиге.
  6. Проверьте root и alias, если путь выглядит верным, а сервер все равно отказывает.

Вадим Дворниковавтор материала

Что означает 403 Forbidden в NGINX

«403 Forbidden» – код состояния HTTP: сервер получил запрос, разобрал его и отказался выполнять дальше. NGINX отдает 403 сам, когда запрет стоит в конфигурации, в правах доступа или в правиле deny.

Что означает 403 Forbidden в NGINX

Тот же код 403 может прийти и от приложения за NGINX – PHP-FPM или другого upstream, которому NGINX передал запрос через proxy_pass или fastcgi_pass. Разница не во внешнем виде ошибки, а в источнике: NGINX ее не формировал, а просто передал ответ дальше. Как отличить один случай от другого по логам – в следующем разделе.

С чего начать: error.log

Первый шаг – не правки конфига, а строка в логе. Она называет причину точно, без гаданий:

sudo tail -n 50 /var/log/nginx/error.log

Прежде чем что-то менять, проверьте, что сам конфиг синтаксически цел:

sudo nginx -t
Строка в error.logПричинаЧто читать дальше
directory index of "..." is forbiddenнет индексного файла, автоиндекс выключен«Нет индексного файла», «Листинг каталога»
open() "..." failed (13: Permission denied)права доступа, владелец файла – или SELinux на RHEL-совместимых системах«Права доступа и владелец», «SELinux и AppArmor»
access forbidden by ruleзапрет deny в конфиге«Запрет в конфиге: deny, allow и скрытые файлы»
рядом с этим запросом в error.log нет ни одной строки из этой таблицыотказал не NGINX, а приложение за нимлог самого приложения (PHP-FPM, другой upstream)

Строка open() "..." failed (13: Permission denied) означает, что ядро Linux отказало в доступе. Обычно дело в правах или владельце файла, но на RHEL-совместимых системах ту же строку пишет и отказ SELinux – ядро возвращает NGINX один и тот же код ошибки в обоих случаях. Первая проверка:

sudo ausearch -m avc -ts recent

Есть записи – причина в SELinux, идите в раздел «SELinux и AppArmor». Но пустой ответ SELinux не исключает: часть отказов подавляют правила dontaudit, и в аудит они не попадают, а если auditd не запущен, отказы уходят не в ausearch, а в journalctl. Надежная проверка – временно перевести SELinux в мягкий режим и повторить запрос:

sudo setenforce 0
# повторите запрос к сайту
sudo setenforce 1

Ошибка пропала при выключенном SELinux – дело было в нем: переходите к разделу «SELinux и AppArmor» и настраивайте контекст, а не отключайте защиту насовсем. Ошибка осталась – причина в правах Linux, идите в «Права доступа и владелец».

Если же рядом с этим запросом в error.log NGINX нет ни одной строки из таблицы выше, а браузер все равно получил 403 – отказал не сам NGINX. В access.log код 403 будет, а строки-причины рядом нет: значит, ответ сформировал PHP-FPM или другой upstream. Причину ищите в его собственном логе, например /var/log/php-fpm/www-error.log.

Где лежат конфиги и какой из них ваш

Главный файл – /etc/nginx/nginx.conf, он подключает остальные. В Ubuntu 22.04/24.04 и Debian 12/13 сайты лежат в /etc/nginx/sites-available/, а рабочие ссылки – в /etc/nginx/sites-enabled/. В Rocky Linux и AlmaLinux 9/10 конфиги сайтов – в /etc/nginx/conf.d/*.conf, каталога sites-available там нет.

Раскладка зависит от дистрибутива и способа установки, поэтому нужный файл проще найти по имени домена:

sudo grep -R server_name /etc/nginx

Команда покажет файл и строку, где объявлен ваш блок server. Ubuntu 16.04 и CentOS 7 официально сняты с поддержки – в 2021 и 2024 году соответственно, – и на них проще обновить систему, чем донастраивать NGINX.

Нет индексного файла

Директива index задает, какие файлы NGINX ищет в каталоге и в каком порядке:

index index.html index.htm index.php;

Файлы проверяются слева направо. Не найден ни один – и при выключенном автоиндексе NGINX вернет 403 со строкой directory index of "..." is forbidden в логе.

Имена файлов регистрозависимы: index.html и Index.html – разные файлы для NGINX, хотя выглядят одинаково на экране. Свое имя добавляют в конец той же строки:

index index.html index.htm index.php index.py;

Листинг каталога (autoindex)

Директивы autoindex и autoindex_exact_size пишутся строчными буквами – регистр здесь важен так же, как у путей. Значение по умолчанию – off: без индексного файла NGINX просто отказывает, а не показывает список файлов каталога.

location / {
  autoindex on;
  autoindex_exact_size off;
}

Директивы можно поставить и на весь сайт, и на один location, например только на /myfiles: тогда листинг откроется в одном разделе, а не по всему сайту.

Автоиндекс показывает список всех файлов каталога любому посетителю. На боевом сайте включать его по умолчанию не стоит – так наружу уходят резервные копии и служебные файлы.

Права доступа и владелец

Посмотреть текущие права каталога сайта:

ls -la /var/www/example.com
drwx------ 3 deploy deploy 4096 Sep 10 09:00 .
drwxr-xr-x 4 root   root   4096 Sep 10 09:00 ..
-rw-r--r-- 1 deploy deploy  512 Sep 10 09:00 index.html

В этом примере каталог закрыт от всех, кроме владельца – drwx------, права 700 вместо нужных 755. Именно это и дает 403 при верных на вид правах файла внутри. Стандартная схема:

ОбъектПраваЗачем
Каталог сайта755владелец пишет, остальные читают и заходят внутрь
Обычный файл (html, css, js)644владелец пишет, остальные только читают
Приватный ключ TLS600читает только владелец файла

Отдельная причина 403, которую легко пропустить: NGINX должен иметь бит x на всех каталогах пути до корня сайта, а не только на самом корневом каталоге. Без него рабочий процесс не может зайти внутрь пути, даже если права на конечный файл верные. Проверить всю цепочку одной командой:

namei -l /var/www/example.com/index.html
f: /var/www/example.com/index.html
drwxr-xr-x root   root   /
drwxr-xr-x root   root   var
drwxr-xr-x root   root   www
drwx------ deploy deploy example.com
-rw-r--r-- deploy deploy index.html

Ищите строку без буквы x в правах каталога – в этом примере разрыв на example.com. Исправить права каталогов и файлов раздельно:

sudo find /var/www/example.com -type d -exec chmod 755 {} \;
sudo find /var/www/example.com -type f -exec chmod 644 {} \;

Рабочему процессу нужен доступ на чтение, а не владение файлами. Раздавать chown -R nginx:nginx на весь каталог сайта – плохая практика: владельцем обычно остается пользователь деплоя. При правах 755/644 файлы и так читают все, и группа NGINX ничего не меняет; добавлять его в группу владельца стоит, только если каталоги закрыты от посторонних – 750 для каталогов и 640 для файлов. Тогда группа читает файлы, а остальные – нет:

# Debian, Ubuntu – рабочий процесс от www-data
sudo usermod -aG deploy www-data

# Rocky Linux, AlmaLinux и другие RHEL-совместимые – от nginx
sudo usermod -aG deploy nginx

Имя пользователя берется из директивы user в nginx.conf – подробнее в следующем разделе. Команда с чужим именем ответит ошибкой «user 'nginx' does not exist» на Debian и Ubuntu, где такого пользователя нет вовсе.

Группа обновится только у новых рабочих процессов – они читают /etc/group заново при старте. Полный restart для этого не нужен: обычный reload останавливает старые рабочие процессы и поднимает новые, уже с обновленной группой:

sudo systemctl reload nginx

Под каким пользователем работает NGINX

Источник правды – директива user в nginx.conf. При сборке из исходников по умолчанию это nobody, но пакеты дистрибутивов ее меняют: в Debian и Ubuntu – www-data, в Rocky Linux, AlmaLinux и других RHEL-совместимых системах – nginx.

Проверить, от чьего имени процесс работает на самом деле, можно командой:

ps -ef | grep nginx

Пример вывода:

root      1044     1  0 09:12 ?        00:00:00 nginx: master process /usr/sbin/nginx
www-data  1045  1044  0 09:12 ?        00:00:00 nginx: worker process
www-data  1046  1044  0 09:12 ?        00:00:00 nginx: worker process

Мастер-процесс запущен от root, а рабочие процессы, которые и читают файлы сайта, – от пользователя из директивы user.

SELinux и AppArmor

На Rocky Linux, AlmaLinux и других RHEL-совместимых системах включен SELinux, и он может отказать в доступе даже при верных правах Linux. Текущий режим показывает команда:

getenforce
Enforcing

Контекст каталога смотрят так:

ls -Zd /var/www/example.com
unconfined_u:object_r:default_t:s0 /var/www/example.com

Контекст default_t в этом выводе неверный: каталоги сайта должны быть в контексте httpd_sys_content_t – его использует и Apache, и NGINX. Команда chcon меняет контекст сразу, но только до ближайшей перемаркировки файловой системы (restorecon или autorelabel): после нее контекст возвращается к прежнему, и 403 – вместе с ним. Постоянный способ – добавить правило в политику и применить его:

sudo semanage fcontext -a -t httpd_sys_content_t "/var/www/example.com(/.*)?"
sudo restorecon -Rv /var/www/example.com

Первая команда запоминает контекст для каталога и всего, что появится в нем позже, вторая – применяет его к уже существующим файлам.

Для проксирования на upstream нужен еще один булев переключатель. К 403 он отношения не имеет: без него NGINX не сможет соединиться с upstream и вернет 502, а не 403 – включайте его только при этой ошибке.

sudo setsebool -P httpd_can_network_connect on

Флаг -P делает изменение постоянным – без него оно пропадет после перезагрузки сервера.

Не оставляйте setenforce 0 постоянным решением. Это временно отключает всю защиту SELinux на сервере – годится для диагностики, а не для боевой настройки.

В Debian и Ubuntu штатный пакет NGINX не поставляет собственный профиль AppArmor, поэтому эта система редко становится причиной 403. Заподозрили ее – ищите отказы командой:

sudo journalctl -k | grep -i apparmor

Запись со словом DENIED подтвердит, что дело именно в AppArmor, а не в обычных правах.

Запрет в конфиге: deny, allow и скрытые файлы

Еще одна причина 403 – прямой запрет в конфигурации:

location / {
  deny 192.168.1.100;
  allow all;
}

Строка в логе для этого случая – та же access forbidden by rule, что уже была в таблице выше. Так же закрывают доступ к скрытым файлам и служебным путям – правилом по регулярному выражению:

location ~ /\. {
  deny all;
}

Базовая авторизация работает иначе: без верных учетных данных она отдает 401, а не 403, и оставляет в логе другую отметку. Увидели 403 рядом с auth_basic – причина не в самой авторизации, а в соседнем правиле, которое сработало раньше, например в deny.

Найти правила deny, allow и auth_basic в своих конфигах помогает поиск по ключевым словам:

sudo grep -R "deny\|allow\|auth_basic" /etc/nginx

Ошибки в root и alias

Директива alias заменяет путь location целиком, а не добавляет к нему, поэтому в ней важен каждый слеш.

Первая ошибка – location заканчивается слешем, а alias – нет:

location /files/ {
  alias /data/uploads;
}

Запрос /files/report.pdf склеивается без разделителя – получается путь /data/uploadsreport.pdf. Такого файла нет, и NGINX отдает 404, а не 403: искать здесь отказ доступа бессмысленно, путь просто не существует.

Обратная ошибка опаснее: location без завершающего слеша при alias со слешем.

location /files {
  alias /data/uploads/;
}

NGINX сопоставляет такой location по началу строки, а не по целому сегменту пути, и запрос вроде /files../etc/passwd может увести alias за пределы каталога uploads. Это не типичная причина именно вашего 403: обычный запрос вроде /files/report.pdf такой location обслужит штатно, а утечка проявляется только на специально составленном пути и обычно дает не 403, а содержимое чужого файла или 404, если пути не существует. Опасность здесь другая: рядом с настоящей проблемой сайт может молча отдавать файлы за пределами каталога. Правильно – слеш на обеих сторонах:

location /files/ {
  alias /data/uploads/;
}

Директива root работает иначе – она добавляет путь location к своему значению, поэтому такой ошибки со слешем у нее нет. Две частые причины 403 остаются за самим значением пути:

  • root указывает на каталог уровнем выше настоящего корня сайта – сравните значение root в конфиге с реальным расположением файлов: grep -R root /etc/nginx/sites-enabled покажет путь из конфига, а ls -la по нему – лежат ли там ожидаемые файлы сайта.
  • symlink в корне сайта ведет на раздел, к которому у процесса NGINX нет доступа – проверьте цель ссылки и права на нее той же командой, что и в разделе «Права доступа и владелец»: namei -l /var/www/example.com/index.html покажет разрыв на самой ссылке или на каталоге, куда она ведет.

Проверка и перезапуск

Перед любым перезапуском проверяют синтаксис:

sudo nginx -t

Только после успешной проверки перезагружают конфигурацию:

sudo systemctl reload nginx

Или тот же результат другой командой:

sudo nginx -s reload

reload заставляет NGINX перечитать конфиг и поднять новые рабочие процессы, не разрывая уже открытые соединения. restart останавливает процесс целиком и запускает заново – на боевом сайте это короткая, но заметная пауза.

Чек-лист: 403 за пять минут

  1. Прочитайте error.log и найдите причину по таблице выше.
  2. Проверьте синтаксис конфига (sudo nginx -t) и найдите свой файл по домену: grep -R server_name /etc/nginx.
  3. Проверьте индексный файл и его имя в точности, включая регистр.
  4. Проверьте права и бит x по всему пути: namei -l /путь/к/файлу, при ошибке – chmod 755 на каталоги и chmod 644 на файлы.
  5. Узнайте пользователя NGINX (ps -ef | grep nginx) и на RHEL-совместимых системах проверьте SELinux: getenforce, ls -Zd, при ошибке контекста – semanage fcontext и restorecon.
  6. Поищите запрет в конфиге: grep -R "deny\|allow\|auth_basic" /etc/nginx.
  7. Сверьте root и alias с реальным расположением файлов и целью symlink: namei -l.
  8. Перезагрузите конфигурацию: sudo systemctl reload nginx.

Совет редакции: начинайте с error.log, а не с правки конфига наугад – строка в логе экономит несколько кругов проверок вслепую.

Комментарии

Оставьте свой комментарий
Sergei

Выполняю: sudo chown -R nginx:nginx *
И получаю в ответ: chown: invalid user: ‘nginx:nginx’
nginx установлен последний для ubuntu, работает процесс, но получаю 403 Forbidden

Проверил /etc/passwd и /etc/group - пользователя и группы nginx не существует.

Как это так, подскажите?

Андрей Е.
➞ Sergei

у тебя ответ: chown: invalid user: ‘nginx:nginx’
возможно ты используешь root, и поэтому надо так:
sudo chown -R root:root *