Web Server Permissions

The Permissions a Web Server Actually Needs

Apache 129 starts as root to bind port 80, then mod_unixd drops its children to User and Group: www-data on Ubuntu 225 (from /etc/apache2/envvars), apache on Red Hat. The manual says this account "should have no privileges that result in it being able to access files that are not intended to be visible to the outside world". Ubuntu's PHP-FPM pool also runs as www-data, and its socket /run/php/php8.5-fpm.sock is mode 660, www-data:www-data. chmod -R 777 is the classic wrong fix: every local account and every other site's PHP pool can then rewrite your code, and one upload bug lets an attacker overwrite index.php. Separate who deploys from who serves:

An ownership and mode scheme for a PHP application
Path Owner:group Mode Why
Document root and its directories deploy:www-data 2750 Server traverses; setgid keeps the group
PHP, asset and .env files deploy:www-data 640 Server reads but cannot rewrite code
storage/, bootstrap/cache/, uploads/ deploy:www-data 2770 The only places the server writes
Applying the scheme and testing it as the web serverShell
cd /var/www/example.com
sudo chown -R deploy:www-data .
sudo find . -type d -exec chmod 2750 {} +          # setgid + rwxr-x---
sudo find . -type f -exec chmod 640 {} +
sudo chmod 2770 storage storage/logs bootstrap/cache uploads   # the writable islands
sudo -u www-data test -r public/index.php && echo "server can read the entry point"
sudo -u www-data test -w public || echo "server cannot write the code: correct"
sudo -u deploy touch public/new.php                # a file added by the next release
sudo stat -c '%a %U:%G %n' public/index.php public/new.php
Output
server can read the entry point
server cannot write the code: correct
640 deploy:www-data public/index.php
644 deploy:www-data public/new.php

new.php joined group www-data with no chgrp, thanks to the setgid 2, but is 644 because sudo used umask 022: deploy with umask 027. Never let the server execute what it stores in uploads/ (Apache).

When modes are not enough: ACLs

A third identity, such as a backup account that must read everything, needs a POSIX ACL (sudo apt install acl). setfacl -m adds a named entry; -d sets a default ACL that new files inherit.

Giving the backup account read access with an ACLShell
sudo setfacl -R -m u:backup:rX .          # existing files and directories
sudo setfacl -R -d -m u:backup:rX .       # default ACL: inherited by new files
sudo getfacl -c storage | grep backup
sudo chmod 2700 storage                   # a later chmod of the group bits...
sudo getfacl -c storage | grep -E '^(user:backup|mask)'   # ...lowers the mask
sudo setfacl -R -b .                      # remove every ACL again
Output
user:backup:r-x
default:user:backup:r-x
user:backup:r-x         #effective:---
mask::---

The mask:: entry caps every named entry and a chmod of the group bits rewrites it, which is why a recursive chmod seems to erase ACLs. If modes and ACLs look right and access is still denied, check mandatory access control: sudo aa-status (AppArmor 454,622 , Ubuntu) or ls -Z (SELinux 1,634 , Red Hat).