Common causes
- Files or parent directories are not readable or traversable by the web server user
- The directory has no index.php or index.html and directory listing is disabled
- Apache Require all denied, an old Order/Deny rule or an .htaccess IP block
- A security plugin, ModSecurity rule or CDN firewall blocking the request
- SELinux context on RHEL-based systems not set to httpd_sys_content_t
- Files uploaded outside the DocumentRoot or root path configured for the site
How to fix it
- Read the error log entry. Apache logs AH01630 (config deny), AH01276 (no index) or AH00035 (permissions); Nginx logs "directory index ... is forbidden" or "(13: Permission denied)". Each points to a different fix.
- Fix permissions on the whole path. Directories need 755 and files 644, and every parent directory must be traversable. Check the chain with namei -l /var/www/html/index.php.
- Add an index file or index directive. Make sure index.php or index.html exists, and that DirectoryIndex (Apache) or index index.php index.html (Nginx) lists it.
- Review access rules. In Apache 2.4 the site Directory block needs Require all granted. Look in .htaccess for deny from, Require ip or Require not lines that match your IP.
- Check security layers. Temporarily disable the security plugin, look in ModSecurity's audit log, or check the CDN firewall events for blocked requests from your IP.
- Fix SELinux labels. On RHEL, Alma or Rocky run sudo restorecon -Rv /var/www/html. For custom paths add a rule with semanage fcontext -a -t httpd_sys_content_t "/srv/site(/.*)?" and run restorecon.
Shell + Apache config
sudo find /var/www/site -type d -exec chmod 755 {} +
sudo find /var/www/site -type f -exec chmod 644 {} +
namei -l /var/www/site/index.php
<Directory /var/www/site>
AllowOverride All
Require all granted
</Directory> How to stop it happening again
- Deploy with a script that sets ownership and permissions consistently
- Never fix a 403 with chmod 777; it creates a security hole and can cause 500 errors under suEXEC
- Document IP allowlists so they are updated when office IPs change
- Keep SELinux labels correct with semanage rules rather than disabling SELinux