服务器Web目录权限怎么设置?Nginx、Apache与PHP运行用户配置

网站被挂马、被上传 WebShell、被读取到配置文件里的数据库密码——这类事故的根源,十有八九是 Web 目录权限设置不当。最常见的错误配置是 chmod -R 777 /var/www。看似"解决了一切权限报错",实际上等于告诉系统上任何一个进程:这个目录你可以随便读写执行。一个很小的上传漏洞,就可能演变成整个站点被控、进而服务器被控。

Web 目录权限是个看起来简单、实际很容易踩坑的话题,因为它牵涉三方:文件属主、文件权限位、以及 Web 服务和 PHP 以什么用户身份运行。这三者要配合起来看,而不是孤立地 chmod 777。原则只有一句话:该写的目录尽量少、权限尽量小、运行用户尽量低。

这篇文章按"运行用户是谁、目录该给什么权限、上传目录怎么单独处理、Nginx/Apache/PHP 怎么配、常见错误怎么修"的顺序展开,给出可直接落地的权限方案和命令。涉及 chmod、chown 的批量操作都是高危操作,改错会让网站 500 或让文件泄露,动手前请先备份权限(getfacl)并确认可回滚。

一、先搞清楚:Web 进程以谁的身份运行

权限配置的第一步不是敲 chmod,而是确认运行用户。Linux 上,进程能对文件做什么,取决于进程的有效用户(effective UID)和文件权限位的组合。所以你必须先知道 Nginx 的 worker 进程、Apache 的进程、PHP 的 FPM 进程分别以谁运行。

# 查看 nginx worker 进程的运行用户
ps -eo pid,user,comm | grep nginx
# 或看主配置里的 user 指令
grep -E "^\s*user" /etc/nginx/nginx.conf

# 查看 Apache 运行用户
ps -eo pid,user,comm | grep -E "httpd|apache2"
grep -E "^\s*(User|Group)" /etc/httpd/conf/httpd.conf      # RHEL 系
grep -E "^\s*(User|Group)" /etc/apache2/envvars            # Debian 系

# 查看 php-fpm 池的运行用户
grep -E "^\s*user|^\s*group" /etc/php-fpm.d/www.conf       # RHEL 系
grep -E "^\s*user|^\s*group" /etc/php/*/fpm/pool.d/www.conf  # Debian 系

不同发行版和组件默认用的用户不同,下面这张表是常见默认值(经验总结,实际以你的配置为准):

组件 RHEL/CentOS/Rocky Debian/Ubuntu 说明
Nginx worker nginx www-data 主进程通常由 root 启动再降权
Apache apache www-data 由 User/Group 指令指定
PHP-FPM apache 或 nginx www-data 看 pool 配置,建议独立用户
网站文件属主 部署用户 部署用户 建议非 root、非 Web 用户

关键认知:Nginx 和 PHP-FPM 通常是不同的用户。Nginx 负责把静态文件和 .php 请求处理掉,静态文件由 Nginx 用户读,PHP 脚本由 PHP-FPM 用户执行。如果你把文件属主设成 Nginx 用户却让 PHP 去写,就会出现权限问题;反之亦然。所以现代架构里,更推荐的做法是让 Nginx 和 PHP-FPM 使用同一个用户(或者把 PHP-FPM 用户加进 Nginx 用户组)来简化协作,同时把权限收紧在"只读 + 少数可写目录"。

二、目录与文件的基础权限方案

先记住一条原则性区别:目录的 x 位表示"能否进入/遍历",文件的 x 位表示"能否执行"。所以目录和文件的权限基准不一样。

推荐的基准(经验值,适用于绝大多数 PHP/静态站点):

  • 目录:755(属主读写执行,组和其他只读+遍历)
  • 文件:644(属主读写,组和其他只读)
  • 属主:网站的部署/运维账号(比如 deploy),组:Web 运行组(www-data 或 nginx)

这样 Web 进程作为"组"或"其他"用户只能读、不能写,杜绝了"通过 Web 漏洞往代码目录写文件"的可能。

# 设定属主和属组(注意 -R 会递归,先确认路径正确)
chown -R deploy:www-data /var/www/html        # Debian/Ubuntu
chown -R deploy:nginx    /var/www/html        # RHEL/CentOS/Rocky

# 目录统一 755,文件统一 644(两条命令分别处理,顺序不能反)
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;

# 备份当前权限,便于回滚(重要!)
getfacl -R /var/www/html > /root/www_acl_backup_$(date +%F).txt

这里必须强调两条命令的顺序:先设目录、再设文件。因为 find -type f 不会影响目录,但要小心不要用 chmod -R 755 一把梭——那会把所有文件也变成可执行,虽然对 PHP 无害,但会让静态文件、上传文件带上 x 位,扩大风险。

三、可写目录:把"能写"限制到最小

网站总有些目录需要写权限,比如上传目录、缓存目录、日志目录、session 目录。核心思路是把可写范围压缩到最小,并且禁止在可写目录里执行脚本。

以常见的 PHP 应用为例,典型可写目录是:

  • wp-content/uploads/(WordPress 上传目录)
  • storage/、cache/、logs/、sessions/(Laravel 等框架)
  • runtime/(ThinkPHP 等)

对这些目录,给予 Web 用户写权限,但不给执行权限(对目录来说,去掉 x 会导致无法进入,所以目录仍需要 x;这里的"不给执行"是针对脚本文件层面,见下一节的 Nginx/Apache 配置)。权限可以设成 775 并把属组设为 Web 组:

# 上传目录:属主给部署用户,属组给 Web 用户,组可写
chown -R deploy:www-data /var/www/html/wp-content/uploads
chmod -R 775 /var/www/html/wp-content/uploads

# 只对目录和文件分别设权限,保持一致性
find /var/www/html/storage -type d -exec chmod 775 {} \;
find /var/www/html/storage -type f -exec chmod 664 {} \;

注意 664(组可写、其他只读)比 644 多了组写权限,适用于需要组内共享写的场景。不要用 777,因为那会额外给"其他"用户写权限——系统上任何服务(包括被攻陷的无关进程)都能往里写。

一个进阶但很有用的技巧是利用 setgid 位保证新建文件继承属组。如果上传目录的属组会变化,可以给目录设 setgid:

# 目录属组设为 www-data,并设 setgid(2xxx),新建文件自动继承该组
chmod 2775 /var/www/html/wp-content/uploads

setgid 目录的好处是:无论谁在这里建文件,文件的组都自动是目录的组,避免出现"新建文件属组不对导致 Web 读不到"的经典问题。

四、禁止在可写/上传目录执行脚本

这是防 WebShell 最关键的一招。攻击者上传了 .php 马儿,即使文件落地成功,只要你让 Nginx/Apache 不在上传目录里执行 PHP,它也只能变成一段无害的静态文本。

Nginx + PHP-FPM 的配置方式是在上传目录的 location 里拒绝 .php:

# 在 server 块中,针对上传目录做保护
location ~* ^/wp-content/uploads/.*\.php$ {
    deny all;
    return 403;
}

# 或者更通用地拒绝常见脚本后缀
location ~* /(uploads|files|images|cache)/.*\.(php|php5|phtml|jsp|asp|aspx|sh)$ {
    deny all;
}

Apache 则用 .htaccess 或在虚拟主机里配置:

# 在 /var/www/html/wp-content/uploads/.htaccess 中

    Require all denied


# 或全局关闭该目录的 PHP 引擎(老式 mod_php,需 php_flag 支持)
php_flag engine off

需要说明的是,php_flag engine off 只对 mod_php 有效;用 PHP-FPM 时应该用上面的"拒绝后缀"方式,因为 PHP 执行已交给 FPM,Apache 的 php_flag 管不到。

另外,把上传目录设置成 Nginx 的 internal; 或加上 X-Content-Type-Options: nosniff 头,能进一步降低风险。还可以给整个站点加上安全响应头:

add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;

五、PHP 运行用户的配置与常见权限报错

前面提到,Nginx 和 PHP-FPM 是不同用户,实践中经常出现"Nginx 能读到文件、PHP 写不进去"或反过来的情况。解决思路是让两方在一个组里,或者干脆统一用户。

以 RHEL 系为例,让 PHP-FPM 用 nginx 用户:

; /etc/php-fpm.d/www.conf
user = nginx
group = nginx

Debian 系对应的文件是 /etc/php/*/fpm/pool.d/www.conf:

user = www-data
group = www-data

修改用户后要重启 php-fpm,并且确认新用户对代码目录和 session 目录有正确的读/写权限,否则网站会白屏或报 500。

# RHEL 系
systemctl restart php-fpm

# Debian 系(版本号按实际填)
systemctl restart php8.1-fpm

# 单独给 session 目录授权(PHP session 默认存这里)
chown -R www-data:www-data /var/lib/php/sessions     # Debian
chown -R nginx:nginx /var/lib/php/session             # RHEL

下面这张表列出常见权限报错和对应的修法,排查时对照很快:

现象 常见原因 修法
上传失败、写入被拒 上传目录 Web 用户无写权限 目录 775、属组设为 Web 用户
访问页面 403 目录缺 x 位或文件缺读权限 目录 755、文件 644
页面空白/500 PHP 无法读脚本或 session 目录不可写 检查 php-fpm 用户与文件属组
静态图片不显示 Nginx 用户对文件无读权限 文件给 644、属组含 Nginx 组
新建文件属组不对 未设 setgid 或属组配置错 目录加 setgid(2775)
缓存目录写满报错 缓存目录权限或磁盘满 检查权限 + df -h

最后强调一个容易忽略的点:代码目录绝对不要归 Web 用户所有。正确的模型是"部署用户拥有代码,Web 用户只能读,少数目录 Web 用户可写"。如果整个站点都归 www-data 所有且组可写,那么一旦有任何脚本执行漏洞,攻击者就能直接改写代码、植入后门。

常见问题

Q:为什么不能用 chmod 777 一劳永逸?
因为 777 给"其他"用户也开了写和执行权限。系统上任何进程(包括被攻陷的、以其他用户运行的服务)都能读写执行这个目录里的文件。这等于把 Web 目录变成了公共可写区,是 WebShell 和挂马的温床。绝大多数看似"需要 777"的场景,都能用正确的属主 + 755/775 解决。

Q:网站因为权限问题 500,临时改成 777 先顶着可以吗?
不建议,风险没有临时一说——攻击者不会等你改回来。正确做法是定位到底哪个进程、对哪个目录需要什么权限,然后精确授予。可以用 namei -l /path/to/file 逐级检查路径上每一层的权限,快速找到断点。

Q:Nginx 和 PHP-FPM 用不同用户时,最省心的做法是什么?
把 PHP-FPM 的运行用户设为 Nginx 的用户(或让 PHP-FPM 用户加入 Nginx 的组),代码目录属主归部署用户、属组归 Web 组。这样两方都能读,只有明确的可写目录给组写权限。

Q:上传目录加了"禁止执行 PHP"后,上传的图片打不开怎么办?
说明你的规则误伤了静态文件。检查 location 的正则,确保只匹配脚本后缀(\.php$),不要把图片后缀也拦了。~~* 是大小写不敏感匹配,注意别把 uploads 目录下的所有请求都 deny 掉。

Q:改了权限之后网站正常了,还需要做什么?
把当前权限导出备份(getfacl -R),记录在文档里,并纳入变更管理。下次再遇到权限问题,有基线可对比,也能避免有人again用 chmod 777 覆盖掉你的配置。同时监控代码目录的文件变更,及时发现被植入的 WebShell。

总结

  • 权限模型的核心是"三方协同":文件属主、权限位、Web/PHP 运行用户,先确认运行用户再动 chmod。
  • 基准权限用目录 755、文件 644,属主给部署用户、属组给 Web 组;可写目录收紧到 775/664,绝不使用 777。
  • 可写目录要"少而准",并利用 setgid(2775)让新建文件继承属组,避免权限漂移。
  • 防 WebShell 的关键是在上传/可写目录禁止执行脚本:Nginx 用 location + deny、Apache 用 FilesMatch,这是性价比最高的一招。
  • 代码目录不要归 Web 用户所有,正确模型是"部署用户拥有、Web 用户只读、少数目录可写"。
  • 所有 chmod/chown -R 操作前先 getfacl 备份权限、确认路径,出错可回滚;遇到 500 不要用 777 顶,用 namei -l 定位真正的权限断点。