我在 LEMP VPS 上托管了几个 WordPress 网站。
- 每个网站都有自己的 Wordpress 安装(位于 下
/srv/http/domain/wordpress
),自己的用户帐户(网站用户)及其自身php-fpm池正在运行网站用户。 - Nginx 运行于http帐户及其下的所有文件
/srv/http/domain/wordpress
均站点用户:http具有 0640(文件)和 2750(目录)权限的所有权。
因此,每个用户都有完整的RW可以访问自己的文件/目录,但不能访问其他网站的文件/目录。Nginx 具有仅限 R访问每个站点的文件/目录。
我正在使用设置组ID强制在其下创建的新目录/srv/http/domain/wordpress
继承http组,因此 Nginx 对这些文件具有读取权限。当通过 SFTP 创建文件时,此方法可按预期工作。
这对我来说似乎是合理的,我可以通过wp-admin界面。但是,每当我执行需要 WordPress 创建新文件/目录的任务时(基本上每次我安装或更新插件/主题或更新 Wordpress 本身时),生成的文件所有权都是站点用户:站点用户。我希望新创建的文件/目录尊重设置组ID使用以下命令创建文件/目录站点用户:http所有权,因此 Nginx 可以在没有我干预的情况下读取这些文件。
我在每个站点的wp-config.php文件:
define( 'FS_CHMOD_DIR', ( 02750 & ~ umask() ) );
define( 'FS_CHMOD_FILE', ( 0640 & ~ umask() ) );
这会强制执行我想要的权限,但对所有权没有任何影响。
我读了这和这但“解决方案”要求修改 PHP 代码以避免使用,move_uploaded_file()
如果您编写自己的 PHP 代码,这没问题,但我不会开始修改 WordPress PHP 文件。我甚至不确定这是我的问题的根源。
此外,我知道我可以放宽权限并将其设置为 755(目录)和 644(文件),以便 Nginx 可以读取文件,无论它是否拥有这些文件的组所有权,但出于安全原因,我试图避免在共享服务器上使用全世界可读的文件。
如何控制 WordPress/PHP 创建的文件的所有权?
答案1
我相信我对共享网站托管有相同的设置:
- 该文件夹
/var/www
包含带有项目的子文件夹 - 每个项目文件夹包含各种不同的文件夹,其中一个文件夹是保存在线发布的内容的文件夹(即,可由 Apache 访问并在项目域下路由) - 该
/var/www/<project>/htdocs
文件夹 - 每个 PHP 站点都以 PHP-FPM 的形式运行,使用自己的用户 ID 和组 ID(用户 ID = 组 ID,介于 10000 和 65535 之间的数字);这些用户 ID 和组 ID 不存在,因为
/etc/passwd
内核不关心它,这简化了设置 - PHP 进程使用 umask 运行
0027
- htdocs 文件夹设置了权限
02750
,由 PHP 用户和 Apache 组拥有;里面的所有文件夹和所有文件都是htdocs
(也就是说,没有可执行文件和 setgid 位)02750
0640
02750
- 除了 PHP 进程之外,
htdocs
只有在与 PHP 用户具有相同权限下运行的 SFTP 进程才能写入该文件夹
除非有人手动更改权限,否则这可确保正确设置权限,无论是从 PHP 创建还是通过 SFTP 上传时。
为了解决 Web 文件上传问题,我的解决方案是这样的:
- 项目文件夹中还有另一个文件夹
/var/www/<project>/tmp
用于保存站点的临时文件;该文件夹归 PHP 用户和 PHP 组所有,因为 Apache 永远无权访问该文件夹 - 在这个文件夹中,我创建了一个
/var/www/<project>/tmp/upload
文件夹,由 PHP 用户拥有,并且阿帕奇组和02750
权限;这不是为了授予 Apache 访问权限,而是为了确保上传文件的组正确;但由于它也授予 Apache 访问权限,我决定为此创建一个单独的文件夹,而不是用于文件/var/www/<project>/tmp
上传;这不会降低安全性,因为上传的文件在调用组后将归move_uploaded_file()
Apache 所有 - PHP 已进行相应配置,用于
/var/www/<project>/tmp/upload
上传和/var/www/<project>/tmp
所有其他临时文件
当文件上传时,这些是临时文件上传和目标文件(使用 创建move_uploaded_file()
)的权限:
Filename: /var/www/<project>/tmp/upload/phphEfMeB
User: 10000
Group: 33
Permissions: 0100600
Umask: 0027
Target file:
Filename: test/test-upload-1666022534
User: 10000
Group: 33
Permissions: 0100640
Umask: 0027
0600
如您所见,PHP 首先创建具有权限但正确(Apache)组的文件,然后move_uploaded_file()
保留该组但将权限更改为0640
。
我使用这种方法是因为上述两种解决方案都不符合我的需求,因为:
- 与上面所述相同,我也想避免 PHP 进程访问共享网站托管中的所有文件
- 将 Apache 添加到所有 PHP 组需要我在 中“发布”它们
/etc/passwd
,更不用说对于 Apache 用户来说这将是一个很长的组列表。但更重要的是,这意味着 Apache 将可以访问 PHP 组中的所有内容,这也会降低安全性。有些由 PHP 创建的文件(例如在“home”文件夹或“tmp”文件中)不必由 Apache 访问。但是,使用这种方法,它们就可以访问(除非应用程序从文件中删除组权限)。
现在,我的上述设置仍然需要文件中上述设置wp-config.php
:
define('FS_CHMOD_DIR', 02750 & ~umask());
define('FS_CHMOD_FILE', 0640 & ~umask());
Wordpress 中的默认设置如下:
# ABSPATH is the root of the Wordpress installation
define( 'FS_CHMOD_DIR', ( fileperms( ABSPATH ) & 0777 | 0755 ) );
define( 'FS_CHMOD_FILE', ( fileperms( ABSPATH . 'index.php' ) & 0777 | 0644 ) );
这实际上意味着在 Wordpress 根文件夹上设置“最小 0755”和“最大 0777”权限,从而从新文件夹中删除 setgid 并使所有内容均可读取。
因此,如果不明确设置常量的值,Wordpress 中就会发生以下情况:
- Wordpress 第一次创建文件夹时,它会更改权限,删除 setgid 并使其可供所有人读取
- 下次运行并写入受影响的文件夹时,它们不再设置 setgid 位,因此 Apache 无法访问其中创建的文件(并且是世界可读的)
如果有人感兴趣的话,这里有一些使用上述方法的 Docker 镜像:
https://gitlab.com/craynic.com/craynic.net/mvh/
它旨在与 MongoDB 一起作为信息源,支持 XFS 配额并在 Kubernetes 上运行,因此被分成几个容器。
容器initializer
在 K8s pod 的“初始化”期间准备目录结构(包括 XFS 配额)以及 Apache 和 PHP 配置文件,然后监视 MongoDB 中的更改以更新配置。所有其他容器在配置更改时重新加载。
答案2
我找到了几个解决方案:
Wordpress(实际上是 PHP)创建的新文件由用户在php-fpmPHP 执行时使用的池配置文件。因此,要强制新创建的文件站点用户:http所有权,因为我原本希望你可以设置用户=站点用户和组=http在您的站点特定php-fpm池配置文件。这样做的原因是新创建的文件具有我所寻找的组所有权。但是,从安全角度来看,这在一定程度上违背了创建单独文件的目的php-fpm每个站点的池网站用户可能会创建具有对其他站点的文件/目录的读取权限的 PHP 文件。
比上面的第 1 点更简单、更安全的方法是让网站的所有文件/目录归站点用户:站点用户(代替站点用户:http正如我最初计划的那样),然后添加http 用户到网站用户 团体. 由于所有站点均具有 640(文件)和 750(文件夹)权限,因此实际上nginx根据需要对每个站点的文件进行只读访问,同时仍然不允许任何用户读取除他们自己的文件之外的任何其他站点的文件。
上面的选项 2 不需要使用设置组ID这可以简化事情,但它需要在你的wp-config.php文件:
define( 'FS_CHMOD_DIR', ( 0750 & ~ umask() ) );
define( 'FS_CHMOD_FILE', ( 0640 & ~ umask() ) );
如果没有上述代码行,WordPress/PHP 将创建全世界可读的文件 (644) 和文件夹 (755)。
答案3
我继续研究如何最好地解决权限问题,并相信我找到了比我上面建议的更好的方法。
我将其添加为第二个答案,以便您可以阅读我之前调查的详细信息。
以下所有解决方案的共同定义
首先,让我从几个常见的定义开始:
- 网络服务器- Apache 或 nginx;除非其中部分另有说明,否则此答案同样适用于 LAMP 和 LEMP 堆栈
- 地点- 一个在其自己的用户下运行/可访问的项目;在 Apache 中表示为一个虚拟主机;一个 Web 服务器运行多个站点
- 网站用户- 用户有权访问地点;PHP 和 SFTP 进程均以站点用户身份运行
- 网站用户,站点组- 系统用户的名称和站点用户的组
- ws 组- 网络服务器运行所在的组
- 公共文件- 旨在供网络服务器访问的文件,可能已发布到互联网
- 私人文件- 不得发布到互联网且不应被网络服务器访问的文件
问题陈述,经过细化
当 Web 服务器用于托管多个站点时如何设置权限,以便:
- 每个站点都使用 PHP-FPM 作为单独的用户运行
- 每个站点用户只能看到自己的文件
- 站点文件可通过 SFTP 访问
- 网络服务器可以看到所有文件
- 所有适用于文件上传的功能
- 所有这些都适用于 Wordpress - 但是,这个问题并不是 Wordpress 独有的,正如上面的一条评论中提到的那样,它涉及任何操作文件系统权限的应用程序
为了方便我的使用,我扩展了问题陈述并添加了以下附加内容:
- 每个站点都可以有公共文件和私有文件;网络服务器只能看到公共文件(即,并非全部)。PHP 和 SFTP 应该对公共文件和私有文件具有完全访问权限。
- 有一个用于存放私有文件的目录;只需将文件放在此目录中即可将其设为私有。这是为了尽量减少对网站管理员及其应用程序如何将文件设为私有的要求。具体来说,
chmod()
应避免使用具有具体权限常量来将文件设为私有的需要。 chmod()
在 PHP 中,不应限制和其他权限相关操作的使用,例如,用存根函数替换它们。
站点的目录结构
本答案中使用的示例站点的目录结构如下:
drwxr-x---+ root site-group / # site root
drwxr-x--- site-user site-group /home # private files
drwxr-x---+ site-user site-group /htdocs # public files
drwxr-x--- root site-group /logs # logs root
drwxr-S--- root site-group /logs/ws # webserver logs
drwxr-x--- site-user site-group /logs/php # PHP logs
drwxr-x--- root site-group /tmp # tmp files root
drwxr-x--- site-user site-group /tmp/php # PHP tmp files
drwxr-x---+ site-user site-group /tmp/upload # uploaded files
笔记:
- 根目录拥有的目录(站点根目录、日志根目录、tmp 文件根目录)是为了确保没有人可以创建或删除子目录或弄乱它们的权限;具体来说,这可以确保例如网站用户将无法删除该
/htdocs
目录,或者他将无法授予所有用户访问权限 - 该
+
符号表示文件系统 ACL;具体使用的 ACL 在下面的“ACL”解决方案中讨论,与其他解决方案无关 - 该
/logs/ws
目录具有专为 Apache 设计的权限;Apache 在启动时打开其日志,然后删除权限,因此该目录可以由 root 拥有;设置组ID位被设置在那里以确保站点组棍棒,以便网站用户具有对日志的读取(而不是写入!)权限 - PHP 和 SFTP 进程均以 umask 运行
0027
这种结构与原始问题中的结构不同,但它与下面描述的解决方案是等效的 - 它们将同样适用于原始问题。
解决方案
该setgid
方法
正如问题的作者发现的那样,解决这个问题的一种方法是让整个网站归网站用户,除非该/htdocs
目录属于ws 组。
这样,站点用户(PHP、SFTP)可以访问任何地方,而网络服务器只能访问/htdocs
目录。
然而,由于网站用户不希望属于ws 组(如下面“共享组”解决方案中所述),由网站用户目录中的/htdocs
内容将无法被ws 组,网络服务器无法访问。
确保新文件的组所有权的解决方案是设置设置组ID在整个/htdocs
目录上递归:
chmod -R g+s -- ./htdocs
这带来了一些挑战:
- 正如原始问题所问,这对于文件上传不太有效。
- 它非常脆弱 - Wordpress(或任何其他应用程序)可以轻易摧毁设置组ID因错误配置或事故而标记。
- 这ws 组将被设置为所有上传的文件,即使它们不是要放在公共文件目录中。理想情况下,该组应仅在移动到目录时应用
/htdocs
。
1. 文件上传
当 PHP 上传文件时,它会将文件放入由上传临时目录配置指令。由于此目录几乎肯定位于 之外/htdocs
,因此设置组ID不会生效。该文件不会获得ws 组并且move_uploaded_file()
PHP 中的函数不会改变这一事实。TLDR:网络服务器将无法访问文件。
解决这个问题的方法很简单:创建一个专门的“上传”目录(如上例目录结构所示,并使用前面提到的 PHP 配置指令)上传临时目录),使其成为ws 组并应用设置组ID那里也一样。这足够安全 - 该目录将仅用于 PHP 上传,其他临时文件将放在其他地方。并且它将使文件由ws 组。
2. 脆弱性
设置组ID极其脆弱——任何mkdir()
或chmod()
操作都可能轻易摧毁它,而Wordpress中充满了这样的调用。
即使你使用FS_CHMOD_DIR
和FS_CHMOD_FILE
配置常量更改 Wordpress 的行为,一些插件仍然会忽略这一点,并将模式更改为“更安全”0400
或类似——有效地关闭设置组ID。
虽然我相信 Wordpress 插件应该不是假设如何保护服务器上的文件,它就是这样,并且这种方法无法稳定地使用。
我也见证过一些其他框架的类似行为。
所有 PHP 进程的共享组
上面建议的另一种解决方案是将所有 PHP 进程放入ws 组。
这会起作用,但正如作者所说,这是一个安全问题 —— 所有 PHP 进程都能够读取所有站点的数据,而且没有太多方法可以阻止这种情况:它open_basedir
不是万无一失的,而且chroot
设置起来很棘手。
因此,出于安全考虑,该解决方案是不可接受的。
将网络服务器添加到所有站点组
另一种方法是将 Web 服务器添加到所有站点组。这样,PHP 进程只能看到自己的文件,而 Web 服务器可以看到所有内容。
这不符合我的扩展要求,因为所有 PHP 文件(包括私有文件)对网络服务器都是可见的,除非它们的访问模式仅限于所有者。不可能指望网站管理员足够自律和/或深入了解 Linux 文件系统权限的工作原理。
但是,此解决方案的一个更大问题是,我无法使 Apache 成为超过 30 个组的成员。至少在 Alpine 发行版中,一旦将 Apache 添加到超过 30 个组,就会出现以下错误:
initgroups: unable to set groups for User apache...
因此,Apache 没有被添加到任何组。
即使我能解决这个问题,另一个问题是 Web 服务器必须监视站点的变化并动态地将自身添加到站点组或从站点组中删除。至少在 Apache 中,这还需要 Apache 重新启动(而不仅仅是重新加载)。
太笨拙了,所以我决定进一步观察。
使用mpm-itk
Apache 的模块
出现了一个有趣的仅限 Apache 的解决方案:mpm-itk模块。
该模块允许每个 Apache 的虚拟主机作为单独的用户运行。
更多信息可以在模块主页上找到,也可以在这个 StackOverflow 答案。
该解决方案的缺点是,自 Apache 2.4 版以来,它不再捆绑在 Alpine 发行版中,因此我必须切换到 Ubuntu(它仍然存在),或者自己编译它。
另一个缺点是它让 Apache 可以访问 PHP 组可以访问的所有内容。与之前的解决方案类似,网站管理员必须确保私有文件不能被组访问。
使用文件系统 ACL
最后,我找到了一个使用文件系统 ACL 的解决方案。
本质上,它与设置组ID方法:它允许更细粒度的访问控制,并允许授予选定的系统组对目录/文件的访问权限,即使他们不拥有或不属于这些目录/文件。
我的解决方案是按如下方式使用:
setfacl -m "g:ws-group:x" -- "./"
setfacl -d -R -m "g:ws-group:rX" -- "./htdocs"
setfacl -R -m "g:ws-group:rX" -- "./htdocs"
第一个命令将“搜索”权限授予 Web 服务器到站点根目录。如果没有该权限,Web 服务器将无法访问任何内容。
第二条命令将默认 ACL(应用于所有新目录或文件)递归地设置为/htdocs
目录,第三条命令将 ACL 添加到目录中已经存在的内容。
类似于设置组ID方法,还需要在上传文件的临时目录中设置:
setfacl -d -m "g:ws-group:r" -- "./tmp/upload"
虽然它实际上和设置组ID解决方案,这种方法的优点是 ACL 不像设置组ID和chmod()
或mkdir()
操作不会影响它们。
服务器管理员可以轻松控制哪些目录默认是公开的,哪些是私有的。
网站管理员可以选择是否要让某些文件不发布,是否要使用私有文件目录并跳过理解 Linux 文件系统权限模型的麻烦,或者是否要通过删除对选定文件的组访问权限来手动控制权限。与设置组ID,如果您删除目录/文件夹的组访问权限,ACL 仍会保留。这意味着,通过重新授予组访问权限,ACL 将再次生效。
这种方法满足我的所有需求并且是我选择的解决方案。
结束语
文件系统权限似乎是应用程序控制文件访问的当前接口。
我深信这是错误的,因为这导致应用程序作者对托管做出各种假设,例如:
- “如果我从群组和其他人中删除某个文件的访问权限,那么该文件将无法公开访问。”
- “为了使文件可公开访问,我需要赋予它访问模式
0666
(或0444
)。”
这两个假设可能都是错的。但即使它们是真的,这个超级用户问题的存在也表明事情很容易出错:
- 应用程序创建一个目录并通过分配访问模式使其成为私有的
0700
。 - 应用程序在新目录中创建文件。
- 稍后,应用程序决定通过授予组访问权限 (
chmod -R g+rX dir
) 将访问权限公开。 - 不知道被移除的设置组ID是需要的,但文件不在正确的组中,因此仍然无法访问。
- 然后应用程序将决定让所有用户都可以读取该文件(
chmod -R go+rX dir
)。 - 现在,敏感数据可能会暴露给系统的其他用户。
当然,解决方案是托管提供商声明保证和要求,应用程序也必须遵守。虽然我基本上同意这一点,但我认为,当前的保证和要求太模糊或太复杂,无法可靠且安全地使用。
我认为,托管应用程序根本不应该干扰底层文件系统权限。这是基础设施的工作。这就是为什么我建议托管提供“公共”和“私有”目录,并让底层基础设施负责授予和撤销对它们的公共访问权限。
这当然也要求网站管理员对基础设施有一定的了解,但这是一个非常简单的概念,很容易达成一致。它也更容易实现,我认为它比围绕文件系统权限制定半复杂的逻辑更容易,正如 Wordpress 示例中所见。
我知道这不适用于大型应用程序,因为大型应用程序的基础设施将根据其需求进行调整。但是,全球仍有许多小型应用程序应该遵守大规模虚拟托管的要求,因为托管提供商在单独的容器中运行每个容器并不划算。