Короткий ответ
403 — сервер понял URL и отказал в доступе. Это не 404 (нет объекта) и не 401 (нужна аутентификация). На site.example с корнем /var/www/site.example чаще всего: нет index.php/index.html при выключенном autoindex, каталог не исполняемый для www-data, location ~ /\. { deny all; } режет легитимный путь, Apache Require all denied. Ubuntu по умолчанию без SELinux; если он есть — смотрите audit, не setenforce 0 навсегда. Не делайте chmod -R 777 /var/www/site.example.
Сначала error.log: directory index of "/var/www/site.example/" is forbidden vs open() failed (13: Permission denied).
Симптомы и как отличить
- Браузер:
403 Forbidden, страница nginx/Apache, не приложения. curl -I→ 403, тело короткое.- Соседний URL
/robots.txtможет быть 200.
| Лог | Смысл |
|---|---|
| directory index … forbidden | нет index, autoindex off |
| Permission denied open() | POSIX-права или ACL |
| access forbidden by rule | deny / Require |
| 404 после переноса | другой корень |
| 401 WWW-Authenticate | auth, не 403 |
Возможные причины
indexне содержитindex.php, файл не залит после rsync.- Каталоги 700, владелец
root, nginx workerwww-dataне проходитxна пути/var/www/site.example. location ^~ /wp-admin/ { deny all; }после копипасты hardening.- Apache 2.4: забыли
Require all grantedв<Directory>. - AppArmor nginx/apache2 (Ubuntu) — DENIED в
dmesg. - SELinux
httpd_sys_content_tне на webroot (CentOS-привычка на Ubuntu-хосте с включённым SELinux). try_files $uri $uri/ =403вместо=404в чужом примере.- Листинг запрещён, а запрос идёт на каталог без слэша/index.
Диагностика
1. Код и какой сервер ответил
curl -sI --resolve site.example:443:10.0.20.40 https://site.example/ | head
curl -sI --resolve site.example:443:10.0.20.40 https://site.example/index.php | head
sudo tail -n 30 /var/log/nginx/error.log
sudo tail -n 30 /var/log/apache2/site.example-error.log 2>/dev/nullЗаголовок Server: подскажет фронт.
2. POSIX-путь целиком
namei -l /var/www/site.example/index.php
ps -o user,group,cmd -C nginx
ps -o user,group,cmd -C apache2Каждый каталог на пути должен давать x для www-data. Файл — r. Типичный слом: /var/www 700 root после «укрепления».
3. Index и root
sudo nginx -T 2>/dev/null | grep -E 'server_name site.example|root |index |autoindex'
ls -la /var/www/site.example/Пустой каталог + index index.html без файла = 403 на /.
4. Правила deny/allow
sudo grep -nE 'deny|allow|Satisfy|Require |chmod' /etc/nginx/sites-enabled/* /etc/apache2/sites-enabled/* 2>/dev/null5. MAC (AppArmor / SELinux)
sudo aa-status 2>/dev/null | head
sudo dmesg | grep -iE 'apparmor|DENIED' | tail
getenforce 2>/dev/null
sudo ausearch -m avc -ts recent 2>/dev/null | tailРешение
Сценарий A. Нет index
Положите index.php или поправьте:
index index.php index.html;
root /var/www/site.example;
location / {
try_files $uri $uri/ /index.php?$args;
}Для статики без PHP достаточно index.html. Не включайте autoindex on на проде как «лечение 403».
Сценарий B. Права
sudo chown -R www-data:www-data /var/www/site.example
sudo find /var/www/site.example -type d -exec chmod 755 {} \;
sudo find /var/www/site.example -type f -exec chmod 644 {} \;wp-config.php можно 640, владелец www-data или деплой-пользователь в группе. Каталоги записи (wp-content/uploads) — 775 той же группе, не 777.
Проверьте родителей:
sudo chmod 755 /var /var/wwwСценарий C. nginx deny задел лишнее
Сузьте правило. Блокировать скрытые файлы:
location ~ /\.(?!well-known).* {
deny all;
}Не режьте /.well-known/acme-challenge/ — сломаете certbot.
Сценарий D. Apache Require
<Directory /var/www/site.example>
Options -Indexes +FollowSymLinks
AllowOverride FileInfo
Require all granted
</Directory>-Indexes предотвращает листинг, но / с index будет 200. Require ip 10.0.20.0/24 на всём DocumentRoot закроет интернет — тогда 403 ожидаем.
Сценарий E. AppArmor/SELinux
Ubuntu: профиль nginx в complain для проверки — лучше точечный tweak профиля, не aa-complain навсегда. SELinux: restorecon -Rv /var/www/site.example, контекст httpd_sys_content_t. Не оставляйте Permissive.
Сценарий F. PHP-файл есть, но location ~ \.php$ { deny all; }
После «hardening» сниппета PHP в корне запрещён, а index.php лежит в /var/www/site.example. Тогда / → directory index / try_files на php → 403, не 404. Сверьте порядок location: deny на \.php$ не должен покрывать фронтконтроллер. Запрещайте PHP только в uploads, cache, backup.
На Ubuntu AppArmor профиль nginx редко мешает чтению /var/www; чаще мешает кастомный root /home/deploy/site.example без разрешения профиля. Смотрите dmesg | grep apparmor, переносите сайт в /var/www, не aa-disable nginx навсегда.
Как проверить, что проблема устранена
sudo -u www-data test -r /var/www/site.example/index.php && echo readable
curl -sI --resolve site.example:443:10.0.20.40 https://site.example/ | head
curl -sI --resolve site.example:443:10.0.20.40 https://site.example/no-such | head/ — не 403. Несуществующий путь — 404, не 403 (иначе try_files/error_page перепутаны). PHP может дать 502 — FPM.
Если не помогло
- 403 только на POST:
limit_except GETв location. - 403 с CDN WAF — смотрите ответ
CF-RAY/WAF, не только origin. - Симлинк за пределы root: nginx
disable_symlinks if_not_owner. - PHP-файл открывается как статика 403 из‑за
location ~ \.php$ { deny all; }в чужом сниппете.
Профилактика
- Деплой выставляет владельца и режим, не ручной chmod после каждого rsync.
- Запрет листинга, явный index.
- Hardening allow/deny — в git с ревью, не «скрипт с форума» на весь server.
- Мониторинг всплеска 403: сканер или сломанный деплой.
FAQ
chmod 777 «на час»?
Нет. Это оставляет окно записи. Чините владельца и биты x на каталогах.
Почему ls под root видит файлы, а сайт 403?
Root обходит DAC. Смотрите sudo -u www-data.
SELinux на Ubuntu 24.04?
Пакет не включён по умолчанию. Если getenforce есть и Enforcing — работайте политикой, не отключайте навсегда.
403 на certbot /.well-known
Исключение location для ACME, иначе продление сломается.
Apache AH01630 client denied by server configuration
Это 403 от Require. Сверьте Directory/Location с DocumentRoot после переноса.