第 16 章 Jails

This translation may be out of date. To help with the translations please access the FreeBSD translations instance.

16.1. 概述

这一章将为您介绍 FreeBSD jail 是什么, 以及如何使用它们。 Jail, 有时也被认为是对 chroot 环境 的一种增强型替代品, 对于管理员而言是非常强大的工具, 同时, 它的一些基本用法, 对高级用户而言也相当有用。

读完这章, 您将了解:

  • jail 是什么, 以及它在您安装的 FreeBSD 中所能发挥的作用。

  • 如何联编、 启动和停止 jail。

  • 如何从 jail 内部或主机上进行管理的一些基础知识。

其他一些能够为您提供关于 jail 的有用信息的地方还有:

  • jail(8) 联机手册。 这是关于 jail - 用于在 FreeBSD 中启动、 停止和控制 FreeBSD jails - 工具的完整说明书。

  • 邮件列表及其存档。 由 FreeBSD 邮件列表服务器 提供的 FreeBSD 一般问题邮件列表 和其他邮件列表的存档, 已经包含了一系列关于 jails 的有价值的信息。 通常搜索存档或询问 freebsd-questions 邮件列表能够给您带来很多有用的信息。

16.2. 与 Jail 相关的一些术语

为了帮助您更好地理解与 jail 有关的 FreeBSD 系统知识, 以及它们如何与 FreeBSD 的其它部分相互作用, 您应理解下列术语:

chroot(8) (命令)

这个工具使用 FreeBSD 的系统调用 chroot(2) FreeBSD 来改变进程, 以及进程的所有衍生进程所能看到的根目录。

chroot(2) (环境)

在 "chroot" 中运行的进程环境。 这包括类似文件系统中的可见部分、 可用的用户及用户组 ID、 网络接口以及其他 IPC 机制等资源。

jail(8) (命令)

用以在 jail 环境中运行进程的系统管理工具。

宿主 (系统、 进程、 用户等等)

能够控制 jail 环境的系统。 宿主系统能够访问全部可用的硬件资源, 并能够控制 jail 环境内外的进程。 宿主系统与 jail 的一项重要区别是, 在宿主系统中的超级用户进程, 并不像在 jail 中那样受到一系列限制。

hosted (系统、 进程、 用户等等)

可访问资源受 FreeBSD jail 限制的进程、 用户或其他实体。

16.3. 介绍

由于系统管理是一项困难而又令人费解的任务, 因此人们开发了一系列强大的工具, 来让管理员的工作变得更加简单。 这些改进通常是让系统能够以更简单的方式安装、 配置, 并毫无问题地持续运转。 这其中, 许多管理员希望能够为系统正确地进行安全方面的配置, 使其能够用于真正的用途, 而阻止安全方面的风险。

FreeBSD 系统提供的一项用于改善安全的工具就是 jail。 jail 是在 FreeBSD 4.X 中由 Poul-Henning Kamp <phk@FreeBSD.org> 引入的, 它在 FreeBSD 5.X 中又进行了一系列改进, 使得它成为了一个强大而灵活的系统。 目前仍然在对其进行持续的开发, 以提高其可用性、 性能和安全性。

16.3.1. Jail 是什么

BSD-类的操作系统从 4.2BSD 开始即提供了 chroot(8)chroot(2) 工具能够改变一组进程的根目录的位置, 从而建立一个与系统中其他部分相隔离的安全环境: 在 chroot 环境中的进程, 将无法访问其外的文件或其他资源。 正是由于这种能力, 即使攻击者攻破了某一个运行于 chroot 环境的服务, 也不能攻破整个系统。 chroot(8) 对于那些不需要很多灵活性或复杂的高级功能的简单应用而言相当好用。 另外, 在引入 chroot 概念的过程中, 曾经发现过许多跳出 chroot 环境的方法, 尽管这些问题在较新的 FreeBSD 版本中已经修正, 但很明显地, chroot(8) 并不是一项用于加固服务安全的理想解决方案。 因此, 必须实现一个新的子系统来解决这些问题。

这就是为什么要开发 jail 最主要的原因。

Jail 以多种方式改进了传统的 chroot(2) 环境概念。 在传统的 chroot(2) 环境中, 只限制了进程能够访问文件系统的哪些部分。 其他部分的系统资源 (例如系统用户、 正在运行的进程, 以及网络子系统) 是由 chroot 进程与宿主系统中的其他进程共享的。 jail 扩展了这个模型, 它不仅将文件系统的访问虚拟化, 而且还将用户、 FreeBSD 的网络子系统, 以及一些其他系统资源虚拟化。 关于这些精细控制以及调整 jail 环境访问能力的更具体的介绍, 可参见 微调和管理

jail 具有以下四项特点:

  • 目录子树 - 进入 jail 的起点。 一旦进入了 jail, 进程就不再被允许访问这棵子树以外的对象。 传统上影响到最初 chroot(2) 设计的安全问题不会影响 FreeBSD jail。

  • 主机名 - 将用于 jail 的主机名。 jail 主要用于存放网络服务, 因此在每个 mail 上能够标注一个有意义的主机名, 能够在很大程度上简化系统管理员的工作。

  • IP 地址 - 这个地址是指定给 jail 的, 在 jail 的生命周期内都无法改变。 通常 jail 的 IP 地址是某一个网络接口上的别名地址, 但这并不是必需的。

  • 命令 - 准备在 jail 中执行的可执行文件的完整路径名。 这个命令是相对于 jail 环境的根目录的, 随 jail 环境的类型不同, 可能会有很多不同之处。

除了这些之外, jail 也可以拥有自己的用户和自己的 root 用户。 自然, 这里的 root 用户的权力会受限于 jail 环境, 并且, 从宿主系统的观点看来, jail root 用户并不是一个无所不能的用户。 此外, jail 中的 root 用户不能执行除了其对应 jail(8) 环境之外的系统中的一些关键操作。 关于 root 用户的能力和限制, 在后面的 微调和管理 中将加以介绍。

16.4. 建立和控制 jail

一些系统管理员喜欢将 jail 分为两类: "完整的" jail, 通常包含真正的 FreeBSD 系统, 以及 "服务" jail, 专用于执行一个可能使用特权的应用或服务。 这只是一种概念上的区分, 并不影响如何建立 jail 的过程。 在联机手册 jail(8) 中对如何创建 jail 进行了清晰的阐述:

# setenv D /here/is/the/jail
# mkdir -p $D (1)
# cd /usr/src
# make buildworld (2)
# make installworld DESTDIR=$D (3)
# make distribution DESTDIR=$D (4)
# mount -t devfs devfs $D/dev (5)
1第一步就是为 jail 选择一个位置。 这个路径是在宿主系统中 jail 的物理位置。 一种常用的选择是 /usr/jail/jailname, 此处 jailname 是 jail 的主机名。 /usr/ 文件系统通常会有足够的空间来保存 jail 文件系统, 对于 "完整" 的 jail 而言, 它通常包含了 FreeBSD 默认安装的基本系统中每个文件的副本。
2如果你已经通过使用 make world 或者 make buildworld 重新编译过了你的 userland, 则可以跳过这一步骤并把现有的 userland 安装进新的 jail。
3这个命令将在 jail 目录中安装所需的可执行文件、 函数库以及联机手册等。
4distribution 这个 make target 将安装全部配置文件, 或者换句话说, 就是将 /usr/src/etc/ 复制到 jail 环境中的 /etc$D/etc/
5在 jail 中不是必须要挂接 devfs(8) 文件系统。 而另一方面, 几乎所有的应用程序都会需要访问至少一个设备, 这主要取决于应用程序的性质和目的。 控制 jail 中能够访问的设备非常重要, 因为不正确的配置, 很可能允许攻击者在 jail 中进行一些恶意的操作。 通过 devfs(8) 实施的控制, 可以通过由联机手册 devfs(8)devfs.conf(5) 介绍的规则集配置来实现。

一旦装好了 jail, 就可以使用 jail(8) 工具来安装它了。 jail(8) 工具需要四个必填参数, 这些参数在 Jail 是什么 中进行了介绍。 除了这四个参数之外, 您还可以指定一些其他参数, 例如, 以特定用户身份来在 jail 中运行程序等等。 这里, command 参数取决于您希望建立的 jail 的类型; 对于 虚拟系统, 可以选择 /etc/rc, 因为它会完成真正的 FreeBSD 系统启动所需的操作。 对于 服务 jail, 执行的命令取决于将在 jail 中运行的应用程序。

Jail 通常应在系统启动时启动, 因此, FreeBSD rc 机制提供了一些很方便的机制来简化这些工作。

  1. 在引导时需要启动的 jail 列表应写入 rc.conf(5) 文件:

    jail_enable="YES"   # 如果设为 NO 则表示不自动启动 jail
    jail_list="www"     # 以空格分隔的 jail 名字列表

    jail_list 中的名字中, 可以使用字母和数字, 而不应使用其他字符。

  2. 对于 jail_list 中列出的 jail, 还应指定一系列对应的 rc.conf(5) 设置, 用以描述具体的 jail:

    jail_www_rootdir="/usr/jail/www"     # jail 的根目录
    jail_www_hostname="www.example.org"   # jail 的主机名
    jail_www_ip="192.168.0.10"          # jail 的 IP 地址
    jail_www_devfs_enable="YES"          # 在 jail 中挂接 devfs
    jail_www_devfs_ruleset="www_ruleset" # 在 jail 中应用的devfs 规则集

    默认情况下, 在 rc.conf(5) 中配置启动的 jail 会执行其中的 /etc/rc 脚本, 也就是说, 默认情况下将 jail 作为虚拟系统方式来启动。 对于服务 jail, 您应另外指定启动命令, 方法是设置对应的 jailjailnameexec_start 配置。

    如欲了解全部可用的选项, 请参阅联机手册 rc.conf(5)

/etc/rc.d/jail 脚本也可以用于手工启动或停止 rc.conf 中配置的 jail:

# /etc/rc.d/jail start www
# /etc/rc.d/jail stop www

目前, 尚没有一种方法来很干净地关闭 jail(8)。 这是因为通常用于正常关闭系统的命令, 目前尚不能在 jail 中使用。 目前, 关闭 jail 最好的方式, 是在 jail 外通过 jexec(8) 工具, 在 jail 中执行下列命令:

# sh /etc/rc.shutdown

更进一步的详细说明, 请参见联机手册 jail(8)

16.5. 微调和管理

您可以为 jail 设置许多不同的选项, 并让 FreeBSD 宿主系统以不同的方式与 jail 交互, 以支持更高级别的应用。 这一节将介绍:

  • 一些用于微调 jail 行为和安全限制的选项。

  • 一些可以通过 FreeBSD Ports 套件安装的高级 jail 管理应用程序, 这些程序可以用于实现一般的基于 jail 的解决方案。

16.5.1. FreeBSD 提供的用于微调 jail 的系统工具

对于 jail 的配置微调, 基本上都是通过设置 sysctl(8) 变量来完成的。 系统提供了一个特殊的 sysctl 子树, 全部相关的选项均在这棵子树中; 这就是 FreeBSD 内核的 security.jail.* 选项子树。 下面是与 jail 有关的主要 sysctl, 以及这些变量的默认值。 这些名字都比较容易理解, 如欲了解进一步的详情, 请参阅联机手册 jail(8)sysctl(8)

  • security.jail.set_hostname_allowed: 1

  • security.jail.socket_unixiproute_only: 1

  • security.jail.sysvipc_allowed: 0

  • security.jail.enforce_statfs: 2

  • security.jail.allow_raw_sockets: 0

  • security.jail.chflags_allowed: 0

  • security.jail.jailed: 0

系统管理员可以在 宿主系统 中, 透过设置这些变量的值来默认为 root 用户增加或取消限制。 需要注意的是, 某些限制是不能够取消的。 在 jail(8) 中的 root 用户, 无法挂载或卸下文件系统, 此外在 jail 中的 root 用户也不能加载或卸载 devfs(8) 规则集、 配置防火墙规则, 或执行其他需要修改内核数据的管理操作, 例如设置内核的 securelevel 等等。

FreeBSD 的基本系统包含一系列用于查看目前在使用的 jail 信息, 以及接入 jail 并执行管理命令所需的基本工具。 jls(8)jexec(8) 命令都是 FreeBSD 基本系统的一部分, 并可用于执行简单的任务:

  • 列出在用的 jail 以及对应的 jail 标识 (JID)、 IP 地址、 主机名和路径。

  • 从宿主系统中接入正在运行的 jail, 并在其中执行命令, 以完成一系列 jail 管理任务。 这在 root 希望干净地关闭 jail 时非常有用。 jexec(8) 工具也可以用于在 jail 中启动 shell 以便对其进行管理; 例如:

    # jexec 1 tcsh

16.5.2. 由 FreeBSD Ports 套件提供的高级管理工具

在众多第三方 jail 管理工具中, sysutils/jailutils 是最完整和好用的。 它是一系列方便 jail(8) 管理的小工具。 请参见其网站以了解进一步的详情。

16.6. Jail 的应用

16.6.1. 服务 Jail

这一节主要基于 Simon L. B. Nielsen <simon@FreeBSD.org> 的 http://simon.nitro.dk/service-jails.html 中的思路, 以及由 Ken Tom locals@gmail.com 更新的文档。 这一节中描述了如何配置 FreeBSD 系统的 jail(8) 功能为其增加一个安全层次。 这部分假定您运行 RELENG_6_0 或更新版本, 并理解本章之前部分的内容。

16.6.1.1. 设计

jail 的一个主要问题是如何对它们进行升级和管理。 由于每个 jail 都是从头联编的, 对于单个 jail 而言升级也许还不是个很严重的问题, 因为升级不会太过麻烦, 而对于多个 jail 而言, 升级不仅会耗费大量时间, 并且是十分乏味的过程。

这个配置过程需要您对 FreeBSD 有较多的配置和使用经验。 如果这些过程显得太过复杂, 您应考虑使用较简单的系统, 例如 sysutils/ezjail, 它提供了更简单的管理 FreeBSD jail 的方法。

基本的想法是, 在不同的 jail 中尽可能多地以安全的方式使用共享的资源 - 使用只读的 mount_nullfs(8) 挂接, 这会让升级简单许多, 从而使为每个服务建立不同的 jail 这种方案变得更加可行。 另外, 它也为增加、删除以及升级 jail 提供了更为便捷的方法。

在这里服务的常见例子包括: HTTP 服务、 DNS 服务、 SMTP 服务等等, 诸如此类。

这节介绍的配置的目的包括:

  • 建立简单并易于理解的 jail 结构。 也就是说 不必 为每个 jail 执行完整的 installworld 操作。

  • 使增删 jail 更容易。

  • 使更新或升级 jail 更容易。

  • 使运行自订的 FreeBSD 分支成为可能。

  • 对安全的更偏执的追求, 尽可能减少被攻陷的可能。

  • 尽可能节省空间和 inode。

如前面提到的那样, 这个设计极大程度上依赖于将一份只读的主模板 (known as nullfs) 挂接到每一个 jail 中, 并为每个 jail 配置一个可读写的设备。 这种设备可以是物理磁盘、 分区, 或以 vnode 为后端的 md(4) 设备。 在这个例子中, 我们将使用可读写的 nullfs 挂接。

下面的表中描述了文件系统格局:

  • 每个 jail 挂接到 /home/j 目录下的一个目录。

  • /home/j/mroot 是每个 jail 共用的模板, 对于所有的 jail 而言都是只读的。

  • /home/j 目录中, 每个 jail 有一个对应的空目录。

  • 每个 jail 中都有一个 /s 目录, 这个目录将连接到系统中的可读写部分。

  • 每个 jail 应基于 /home/j/skel 建立其可读写空间。

  • 每个 jailspace (jail 中的可读写部分) 应创建到 /home/js

这假定所有的 jail 都放置于 /home 分区中。 当然, 您可以根据需要将这个配置改为需要的任何样子, 但在接下来的例子中, 也应相应地加以变动。

16.6.1.2. 建立模板

这一节将介绍创建 jail 所需的只读主模板所需的步骤。

一般来说, 您应将系统升级到最新的 FreeBSD -RELEASE 分支, 具体做法请参见本手册的相关 章节。 当更新不可行时, 则需要完成 buildworld 过程, 另外, 您还需要 sysutils/cpdup 软件包。 我们将使用 portsnap(8) 工具来下载 FreeBSD Ports 套件。 在使用手册的 Portsnap 章节 中, 提供了针对初学者的介绍。

  1. 首先, 需要为将要存放只读的 FreeBSD 执行文件的文件系统建立一个目录, 接着进入 FreeBSD 源代码的目录, 并在其中安装 jail 模板:

    # mkdir /home/j /home/j/mroot
    # cd /usr/src
    # make installworld DESTDIR=/home/j/mroot
  2. 接着, 准备一份 FreeBSD Ports 套件, 以及用于执行 mergemaster 的 FreeBSD 源代码:

    # cd /home/j/mroot
    # mkdir usr/ports
    # portsnap -p /home/j/mroot/usr/ports fetch extract
    # cpdup /usr/src /home/j/mroot/usr/src
  3. 创建系统中可读写部分的骨架:

    # mkdir /home/j/skel /home/j/skel/home /home/j/skel/usr-X11R6 /home/j/skel/distfiles
    # mv etc /home/j/skel
    # mv usr/local /home/j/skel/usr-local
    # mv tmp /home/j/skel
    # mv var /home/j/skel
    # mv root /home/j/skel
  4. 使用 mergemaster 安装缺失的配置文件。 接下来, 删除 mergemaster 创建的多余目录:

    # mergemaster -t /home/j/skel/var/tmp/temproot -D /home/j/skel -i
    # cd /home/j/skel
    # rm -R bin boot lib libexec mnt proc rescue sbin sys usr dev
  5. 现在, 将可读写文件系统连接到只读文件系统中。 请确保您在 s/ 目录中建立了适当的符号连接。 如果没有建立目录或建立的位置不正确, 可能会导致安装失败。

    # cd /home/j/mroot
    # mkdir s
    # ln -s s/etc etc
    # ln -s s/home home
    # ln -s s/root root
    # ln -s ../s/usr-local usr/local
    # ln -s ../s/usr-X11R6 usr/X11R6
    # ln -s ../../s/distfiles usr/ports/distfiles
    # ln -s s/tmp tmp
    # ln -s s/var var
  6. 最后, 创建一个默认的包含下列配置的 /home/j/skel/etc/make.conf

    WRKDIRPREFIX?=  /s/portbuild

    配置 WRKDIRPREFIX 使得在每个 jail 中分别编译 FreeBSD 成为可能。 请注意 ports 目录是只读系统的一部分。 而自订的 WRKDIRPREFIX 则使得联编过程得以在 jail 中的可读写部分完成。

16.6.1.3. 建立 Jail

现在我们已经有了完整的 FreeBSD jail 模板, 可以在 /etc/rc.conf 中安装并配置它们了。 这个例子中演示了建立 3 个 jail: "NS"、 "MAIL" 和 "WWW"。

  1. /etc/fstab 文件中加入下列配置, 以便让系统自动挂接 jail 的只读模板和读写空间:

    /home/j/mroot   /home/j/ns     nullfs  ro  0   0
    /home/j/mroot   /home/j/mail   nullfs  ro  0   0
    /home/j/mroot   /home/j/www    nullfs  ro  0   0
    /home/js/ns     /home/j/ns/s   nullfs  rw  0   0
    /home/js/mail   /home/j/mail/s nullfs  rw  0   0
    /home/js/www    /home/j/www/s  nullfs  rw  0   0

    扫描批次号 (pass number) 为 0 的分区不会在启动时使用 fsck(8) 进行检查, 而转存批次号 (dump number) 为 0 的分区则不会在 dump(8) 时备份。 我们不希望 fsck 检查 nullfs 挂接, 或让 dump 备份 jail 中的只读 nullfs 挂接。 这就是为什么在每个 fstab 条目的最后两列是 "0 0" 的原因。

  2. /etc/rc.conf 中配置 jail:

    jail_enable="YES"
    jail_set_hostname_allow="NO"
    jail_list="ns mail www"
    jail_ns_hostname="ns.example.org"
    jail_ns_ip="192.168.3.17"
    jail_ns_rootdir="/usr/home/j/ns"
    jail_ns_devfs_enable="YES"
    jail_mail_hostname="mail.example.org"
    jail_mail_ip="192.168.3.18"
    jail_mail_rootdir="/usr/home/j/mail"
    jail_mail_devfs_enable="YES"
    jail_www_hostname="www.example.org"
    jail_www_ip="62.123.43.14"
    jail_www_rootdir="/usr/home/j/www"
    jail_www_devfs_enable="YES"

    应把 jailnamerootdir 变量设置成 /usr/home 而不是 /home 的原因是 /home 目录在默认安装的 FreeBSD 上是指向 /usr/home 的一个符号连接。 而 jailnamerootdir 变量必须是一个 包含符号连接的路径, 否则 jail 将拒绝启动。 可以使用 realpath(1) 工具来决定这一变量应被赋予一个什么样的值。 更详细的信息请参阅安全公告 FreeBSD-SA-07:01.jail

  3. 为每个 jail 创建所需的只读文件系统挂接点:

    # mkdir /home/j/ns /home/j/mail /home/j/www
  4. 在 jail 中安装可读写的模板。 注意您需要使用 sysutils/cpdup, 它能够帮助您确保每个目录都是正确地复制的:

    # mkdir /home/js
    # cpdup /home/j/skel /home/js/ns
    # cpdup /home/j/skel /home/js/mail
    # cpdup /home/j/skel /home/js/www
  5. 这样, 就完成了 jail 的制作, 可以运行了。 首先为 jail 挂接文件系统, 然后使用 /etc/rc.d/jail 脚本来启动它们:

    # mount -a
    # /etc/rc.d/jail start

现在 jail 应该就启动起来了。 要检查它们是否运行正常, 可以使用 jls(8) 命令。 它的输出应该类似这样:

# jls
   JID  IP Address      Hostname                      Path
     3  192.168.3.17    ns.example.org                /home/j/ns
     2  192.168.3.18    mail.example.org              /home/j/mail
     1  62.123.43.14    www.example.org               /home/j/www

这时, 就可以登入 jail 并增加用户和配置服务了。 JID 列给出了正在运行的 jail 的标识编号。 您可以使用下面的命令来在 JID 编号为 3 的 jail 中执行管理任务:

# jexec 3 tcsh

16.6.1.4. 升级

有时, 由于安全问题, 或新增功能有用, 会希望将系统升级到一个新版本的 FreeBSD。 这种安装方式的设计使得升级现有 jail 变得很容易。 另外, 它也能最大限度地减小停机时间, 因为 jail 只在最后时刻才需要关闭。 另外, 它也提供了简单的回退到先前版本的方法。

  1. 第一步是按通常的方法升级主机的系统。 接着, 在 /home/j/mroot2 中建立一个新的临时模板:

    # mkdir /home/j/mroot2
    # cd /usr/src
    # make installworld DESTDIR=/home/j/mroot2
    # cd /home/j/mroot2
    # cpdup /usr/src usr/src
    # mkdir s

    在运行 installworld 时会创建一些不需要的目录, 应将它们删除:

    # chflags -R 0 var
    # rm -R etc var root usr/local tmp
  2. 重建到主系统中的可读写符号连接:

    # ln -s s/etc etc
    # ln -s s/root root
    # ln -s s/home home
    # ln -s ../s/usr-local usr/local
    # ln -s ../s/usr-X11R6 usr/X11R6
    # ln -s s/tmp tmp
    # ln -s s/var var
  3. 现在是时候关闭 jail 了:

    # /etc/rc.d/jail stop
  4. 卸下原先的文件系统:

    # umount /home/j/ns/s
    # umount /home/j/ns
    # umount /home/j/mail/s
    # umount /home/j/mail
    # umount /home/j/www/s
    # umount /home/j/www

    可读写的文件系统 (/s) 会在只读系统之后挂接, 因此应首先卸载。

  5. 将先前的只读文件系统挪走, 换成新的系统。 这样做也同时保留了先前系统的备份, 从而可以在出现问题时从中恢复。 这里我们根据新系统的创建时间来命名。 此外我们把先前的 FreeBSD Ports 套件直接移动到新的文件系统中, 以节省磁盘空间和 inode:

    # cd /home/j
    # mv mroot mroot.20060601
    # mv mroot2 mroot
    # mv mroot.20060601/usr/ports mroot/usr
  6. 现在新的只读模板就可以用了, 剩下的事情是重新挂接文件系统并启动 jails:

    # mount -a
    # /etc/rc.d/jail start

最后用 jls(8) 检查 jail 启动是否正常。 不要忘记在 jail 中运行 mergemaster。 配置文件和 rc.d 脚本在升级时应进行更新。


Last modified on: December 11, 2021 by Sergio Carlavilla Delgado