Every web server and PHP process runs as a Linux user. That web server user decides which files can be read, which can be changed, and how much damage a hacked plugin can do. Most "Permission denied" errors and many security incidents come down to getting this wrong. This guide explains the users involved and safe permission patterns.
Before you start
- Root or sudo access to a VPS.
- Basics of
chmodandchown: Linux file permissions.
Who runs what
| Software | Default user |
|---|---|
| Apache on Debian/Ubuntu | www-data |
| Apache on AlmaLinux/Rocky | apache |
| Nginx | www-data (Debian) or nginx (RHEL family) |
| OpenLiteSpeed | nobody |
| PHP-FPM pool | Whatever user = says in the pool (default same as web server) |
The master process starts as root to bind ports 80/443, then workers drop to the unprivileged user. Check with ps -eo user,comm | grep -E 'nginx|apache|httpd|php-fpm|lsphp'.
Two users matter: web server and PHP
- The web server user must read static files (CSS, images) and be able to traverse directories.
- The PHP user must read code and write only where the app needs: uploads, cache, sessions.
When both are the same shared user (e.g. www-data) for every site, any compromised site can modify every other site. Running each site's PHP as its own user fixes that: PHP-FPM pools per site.
A safe permission pattern for one site
Site owner siteuser, PHP pool running as siteuser, web server www-data in the siteuser group:
sudo chown -R siteuser:siteuser /var/www/example.com
sudo find /var/www/example.com -type d -exec chmod 750 {} \;
sudo find /var/www/example.com -type f -exec chmod 640 {} \;
sudo usermod -aG siteuser www-data- Directories
750: owner full, group can read and enter, others nothing. - Files
640: owner read/write, group (the web server) read-only. - Config files with secrets (
wp-config.php,.env) can be600when PHP runs as the owner. - Restart the web server after changing its group membership.
When PHP runs as the shared web server user
If you must keep a single www-data pool, give the code to a different owner and allow writes only where needed:
sudo chown -R deploy:www-data /var/www/example.com
sudo find /var/www/example.com -type d -exec chmod 750 {} \;
sudo find /var/www/example.com -type f -exec chmod 640 {} \;
sudo chmod -R g+w /var/www/example.com/wp-content/uploadsPHP can then read the code but not change it, which blocks many attacks that try to inject code into existing files. Updates must be run as deploy.
Never use 777
chmod 777 lets every user on the system write the file. It "fixes" permission errors by removing all protection and is a common entry point for malware. Find and fix such files:
find /var/www -perm -0002 -type f -printHow hosting panels isolate users
On cPanel servers each account is a separate Linux user. PHP runs as that user (suEXEC-style), so files are typically 644 and directories 755 owned by the account. On MdmHost, CloudLinux CageFS additionally gives every account its own virtual filesystem, so one account cannot even see others' files.
SELinux on AlmaLinux and Rocky
Even with correct Unix permissions, SELinux can block access. Web content needs the httpd_sys_content_t label, and writable folders httpd_sys_rw_content_t:
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/example.com/uploads(/.*)?"
sudo restorecon -Rv /var/www/example.comTroubleshooting
- 403 on static files: the web server user can't traverse a parent directory. Check every level with
namei -l /var/www/example.com/public/index.html. - Plugin or upload can't write: PHP user lacks write permission on that folder only. Grant it there, not site-wide.
- Correct permissions but still denied on RHEL: SELinux context. Check
ls -Zand the audit log.
FAQ: web server users and permissions
Should files be owned by www-data?
Not all of them. Ownership by www-data lets any PHP code on the server rewrite them. Prefer a separate owner.
What permissions does WordPress need?
Typically 644/755 when PHP runs as the file owner, with uploads writable.
Is suEXEC still used?
The idea is: modern setups achieve it with per-user PHP-FPM pools or LiteSpeed's per-user lsphp.
