我正在用 C 语言编写一个 HTTP 服务器守护进程(这是有原因的),并使用 systemd 单元文件对其进行管理。
我正在重写一个 20 年前设计的应用程序,大约在 1995 年。他们使用的系统是先 chroot,然后 setuid,以及标准程序。
在我之前的工作中,通常的政策是永远不要以 root 身份运行任何进程。您为其创建一个用户/组并从那里运行。当然,系统确实以root身份运行了一些东西,但是我们可以在不root的情况下实现所有业务逻辑处理。
现在对于 HTTP 守护进程,如果我不在应用程序内 chroot,我可以在没有 root 的情况下运行它。那么应用程序永远不要以 root 身份运行不是更安全吗?
从一开始就以 mydaemon-user 身份运行不是更安全吗?而不是使用 root 启动它,chrooting,然后 setuid 到 mydaemon-user?
答案1
似乎其他人没有理解你的观点,这不是为什么要使用改变的根的原因,这当然你已经清楚地知道了,也不是你还能做些什么来限制守护进程,当你也清楚地知道在守护进程的帮助下运行时非特权用户帐户;但为什么要做这些事在应用程序内部。实际上有一个相当恰当的例子来说明原因。
httpd
考虑Daniel J. Bernstein 的 publicfile 包中守护程序的设计。它所做的第一件事是将 root 更改为被告知与命令参数一起使用的根目录,然后将特权授予在两个环境变量中传递的非特权用户 ID 和组 ID。
守护进程管理工具集具有专用工具,用于更改根目录以及删除非特权用户和组 ID 等操作。 Gerrit Pape 的 runit 有chpst
。我的 nosh 工具集有chroot
和setuidgid-fromenv
。 Laurent Bercot 的 s6 有s6-chroot
和s6-setuidgid
。韦恩·马歇尔的 Perp 有runtool
和runuid
。等等。事实上,他们都有 M. Bernstein 自己的 daemontools 工具集setuidgid
作为前因。
人们可能会认为可以从此类专用工具中提取功能httpd
并使用这些专用工具。然后,正如你想象的那样,不服务器程序的一部分曾经以超级用户权限运行。
问题是,直接后果是必须做更多的工作来建立更改后的根,这暴露了新的问题。
就伯恩斯坦httpd
而言,仅有的根目录树中的文件和目录是要发布到全世界的文件和目录。有没有其他的根本就在树上。而且,没有理由任何可执行程序映像文件存在于该树中。
但是将根目录更改为链式加载程序(或 systemd),突然出现 的程序映像文件httpd
、它加载的任何共享库以及 、 、 和/etc
程序/run
加载/dev
器或 C 运行时库访问的任何特殊文件在程序初始化期间(如果您truss
/ strace
C 或 C++ 程序,您可能会感到非常惊讶),还必须存在于更改的根中。否则httpd
无法链接并且不会加载/运行。
请记住,这是一个 HTTP(S) 内容服务器。它有可能在更改后的根目录中提供任何(世界可读的)文件。现在,这包括共享库、程序加载器以及操作系统的各种加载器/CRTL 配置文件的副本。如果通过某种方式(偶然)意味着内容服务器可以访问写东西,受感染的服务器可能会获得对httpd
自身程序映像的写入权限,甚至是您系统的程序加载器的写入权限。 (请记住,您现在有两组并行的/usr
、/lib
、/etc
、/run
和/dev
目录来保证安全。)
httpd
这一切都不是更改 root 并删除权限本身的情况。
因此,您已经拥有少量特权代码,这些代码相当容易审计,并且在程序启动时httpd
以超级用户权限运行;在更改后的根目录中大大扩展了文件和目录的攻击面。
这就是为什么它不像在服务程序外部执行所有操作那么简单。
请注意,这仍然是其本身的最低限度的功能httpd
。所有执行诸如在操作系统的帐户数据库中查找用户 ID 和组 ID 并首先放入这些环境变量等操作的代码是在程序外部httpd
,在简单的独立可审核命令中,例如envuidgid
. (当然,它是一个 UCSPI 工具,因此它不包含侦听相关 TCP 端口或接受连接的代码,这些代码属于命令域,例如tcpserver
,tcp-socket-listen
,tcp-socket-accept
,s6-tcpserver4-socketbinder
,s6-tcpserver4d
, 等等。)
进一步阅读
- 丹尼尔·伯恩斯坦 (1996)。
httpd
。公共文件。 cr.yp.to。 httpd
。Daniel J. Bernstein 的软件合一。软件。乔纳森·德博因·波拉德。 2016年。gopherd
。Daniel J. Bernstein 的软件合一。软件。乔纳森·德博因·波拉德。 2017年。- https://unix.stackexchange.com/a/353698/5132
- https://github.com/janmojzis/httpfile/blob/master/droproot.c
答案2
我认为你的问题的许多细节同样适用于avahi-daemon
我最近看过的。 (不过我可能错过了另一个不同的细节)。在 chroot 中运行 avahi-daemon 有很多优点,以防 avahi-daemon 受到损害。这些包括:
- 它无法读取任何用户的主目录并窃取私人信息。
- 它不能通过写入 /tmp 来利用其他程序中的错误。此类错误至少有一整类。例如https://www.google.co.uk/search?q=tmp+race+security+bug
- 它无法打开 chroot 之外的任何 unix 套接字文件,其他守护进程可能正在侦听和读取该文件上的消息。
当你不是使用 dbus 或类似的...我认为 avahi-daemon 使用 dbus,因此它确保即使从 chroot 内部也能访问系统 dbus。如果您不需要在系统 dbus 上发送消息的能力,则拒绝该能力可能是一个非常好的安全功能。
使用 systemd 单元文件管理它
请注意,如果重写 avahi-daemon,它可能会选择依赖 systemd 来确保安全,并使用例如ProtectHome
.我建议对 avahi-daemon 进行更改,将这些保护添加为额外层,以及 chroot 无法保证的一些附加保护。您可以在此处查看我提出的选项的完整列表:
https://github.com/lathiat/avahi/pull/181/commits/67a7b10049c58d6afeebdc64ffd2023c5a93d49a
看起来如果 avahi-daemon 这样做的话我可以使用更多限制不是使用 chroot 本身,其中一些在提交消息中提到。我不确定这在多大程度上适用。
请注意,我使用的保护不会限制守护进程打开 unix 套接字文件(上面的第 3 点)。
另一种方法是使用 SELinux。然而,您可能会将您的应用程序与 Linux 发行版的子集联系起来。我在这里积极看待 SELinux 的原因是 SELinux 以细粒度的方式限制进程对 dbus 的访问。例如,我认为您通常会期望它systemd
不会出现在您需要能够向其发送消息的总线名称列表中:-)。
“我想知道,使用 systemd 沙箱是否比 chroot/setuid/umask/... 更安全”
摘要:为什么不能两者兼而有之呢?让我们稍微解码一下上面的内容:-)。
如果您考虑第 3 点,那么使用 chroot 会提供更多限制。 ProtectHome= 及其朋友甚至不尝试像 chroot 那样进行限制。 (例如,没有指定的 systemd 选项 blacklists /run
,我们通常将 unix 套接字文件放在黑名单中)。
chroot 表明限制文件系统访问可以是非常强大的,但不是一切在 Linux 上是一个文件:-)。有一些 systemd 选项可以限制文件以外的其他内容。如果程序受到损害,这很有用,您可以减少可用的内核功能,它可能会尝试利用其中的漏洞。例如,avahi-daemon 不需要蓝牙套接字,我猜您的 Web 服务器也不需要:-)。因此,请勿授予其访问 AF_BLUETOOTH 地址系列的权限。只需使用该选项将 AF_INET、AF_INET6 甚至 AF_UNIX 列入白名单即可RestrictAddressFamilies=
。
请阅读您使用的每个选项的文档。有些选项与其他选项结合使用会更有效,而有些选项并非在所有 CPU 架构上都可用。 (不是因为 CPU 不好,而是因为该 CPU 的 Linux 端口设计得不够好。我认为)。
(这里有一个一般原则。如果您可以编写您想要允许的内容的列表,而不是您想要拒绝的内容,那么它会更安全。就像定义 chroot 为您提供允许访问的文件列表一样,这更可靠而不是说你想阻止/home
)。
原则上,您可以在 setuid() 之前自行应用所有相同的限制。这只是您可以从 systemd 复制的代码。然而,systemd 单元选项应该更容易编写,并且由于它们采用标准格式,因此应该更容易阅读和审查。
因此,我强烈建议您阅读man systemd.exec
目标平台上的沙箱部分。但如果您想要最安全的设计,我不会害怕在您的程序中尝试chroot
(然后放弃root
特权)还有。这里需要权衡。使用chroot
会对您的整体设计施加一些限制。如果您已经有一个使用 chroot 的设计,并且它似乎可以满足您的需要,那么听起来非常棒。
答案3
如果您可以依赖 systemd,那么将沙盒留给 systemd 确实更安全(也更简单!)。(当然,应用程序还可以检测它是否已由 systemd 沙盒启动,如果它仍是 root,则可以检测它本身是否是沙盒。)您描述的服务的等效项将是:
[Service]
ExecStart=/usr/local/bin/mydaemon
User=mydaemon-user
RootDirectory=...
但我们不必就此止步。 systemd 还可以为您执行许多其他沙箱操作 - 以下是一些示例:
[Service]
# allocate separate /tmp and /var/tmp for the service
PrivateTmp=yes
# mount / (except for some subdirectories) read-only
ProtectSystem=strict
# empty /home, /root
ProtectHome=yes
# disable setuid and other privilege escalation mechanisms
NoNewPrivileges=yes
# separate network namespace with only loopback device
PrivateNetwork=yes
# only unix domain sockets (no inet, inet6, netlink, …)
RestrictAddressFamilies=AF_UNIX
请参阅man 5 systemd.exec
参考资料 获取更多指令和更详细的描述。如果你让你的守护进程socket-activatable ( man 5 systemd.socket
),你甚至可以使用与网络相关的选项:该服务与外界的唯一链接将是它从systemd接收到的网络套接字,它将无法连接到其他任何东西。如果它是一个简单的服务器,仅侦听某些端口并且不需要连接到其他服务器,那么这可能很有用。 (在我看来,与文件系统相关的选项也可能会过时RootDirectory
,因此也许您不再需要费心设置包含所有必需的二进制文件和库的新根目录。)
较新的 systemd 版本(自 v232 起)还支持DynamicUser=yes
,其中 systemd 将仅在服务运行时自动为您分配服务用户。这意味着您不必为该服务注册永久用户,并且只要该服务不写入除StateDirectory
、LogsDirectory
和之外的任何文件系统位置CacheDirectory
(您也可以在单元文件中声明 –再次参见man 5 systemd.exec
- 然后哪个 systemd 将管理,注意将它们正确分配给动态用户)。