
2025 - 12 - 05
使用 LXC 增强 X11 应用程序的安全性
给浏览器或基于 Electron 的即时通讯应用程序增加一层额外的安全防护,是不是很棒?毕竟,如果浏览器被攻破,用户的整个主目录可能都会面临风险。
如何降低这种风险呢?可以通过使用 LXC 将应用程序与主机系统隔离。
本示例使用的系统是 Arch Linux,但该过程应该可以轻松适配其他发行版。
网络功能
首先,需要安装并预配置 LXC。安装以下软件包:
# pacman -S lxc lxcfs
接下来,要为 LXC 容器赋予网络功能。为此,编辑 `/etc/default/lxc` 文件,并在文件底部添加以下行:
USE_LXC_BRIDGE="true"
现在,可以启动 LXC 桥接接口了。启用并启动相应的 systemd 单元:
# systemctl enable lxc - net.service --now
现在应该会有一个名为 `lxcbr0` 的新接口。使用以下命令进行验证:
# ip a show dev lxcbr0
创建容器
完成上述步骤后,就可以创建第一个应用程序容器了。首先,来创建一个初始配置。
导航到 `/etc/lxc` 目录,并创建一个新的配置文件。由于要创建一个浏览器容器,所以将其命名为 `www.conf`。
使用喜欢的编辑器打开该文件,并添加以下行:
lxc.net.0.type = veth
lxc.net.0.link = lxcbr0
lxc.net.0.flags = up
lxc.net.0.hwaddr = 10:66:6a:xx:xx:xx
lxc.idmap = u 0 100000 65536
lxc.idmap = g 0 100000 65536
前四行指定容器应使用之前创建的网络桥接。
接下来,定义容器的 UID 和 GID 应如何映射到主机上的 UID 和 GID。由于目标是实现最高级别的安全,所以将使用非特权容器。为此,将容器的 ID 映射到主机上不存在的 ID 范围。这样可以确保即使恶意进程逃出容器,它在主机系统上也不会拥有任何有意义的权限。
理解 `idmap`
以下是 `idmap` 配置工作原理的详细解释:
lxc.idmap = [type] [container_id] [host_id] [range]
- `[type]`:指定要映射的 ID 类型。选项包括:
- `u` 表示 UID
- `g` 表示 GID
- `[container_id]`:容器内要映射的第一个 UID/GID。在示例中,它是 0(容器的根用户)。
- `[host_id]`:容器 ID 映射到的主机上的起始 UID/GID。在这里,容器中的 0 映射到主机上的 100000。ID 按顺序递增:
- `container_id = 1` → `host_id = 100001`
- `container_id = 1000` → `host_id = 101000`
- `[range]`:要映射的 UID/GID 块的大小。使用 65536 来提供完整的标准 Linux ID 范围。
示例映射表:
| 容器 ID | 主机 ID |
|---|---|
| 0 | 100000 |
| 1 | 100001 |
| 1000 | 101000 |
| 65535 | 165535 |
接下来,需要告诉 LXC 在新容器中使用哪些 UID/GID 映射。通过在 `/etc/subuid` 和 `/etc/subgid` 文件中添加以下行来实现这一点:
root:100000:65536
这行代码意味着主机用户 `root` 可以为 LXC 容器创建 100000 - 165535 范围内的 UID 映射。这与之前创建的非特权容器配置直接对应。
如果想创建另一个具有不同映射的容器,例如将 `container_id = 0` 映射到 `host_id = 200000`,只需在 `subuid`/`subgid` 文件中添加另一行:
root:100000:65536
root:200000:65536
启动容器
现在,终于可以使用准备好的配置文件来创建容器了。在这个示例中,将使用 `debian:trixie` 镜像作为基础:
# lxc - create --config /etc/lxc/www.conf --name www -t download --- -d debian -r trixie -a amd64
验证容器是否正在运行:
# lxc - ls -f
设置完成后,登录到容器:
# lxc - attach www /bin/bash
在容器内部,可以像在常规 Linux 系统上一样安装所需的软件。例如,要运行 Firefox:
# apt update
# apt - get -y install firefox - esr
许多 X11 应用程序以 root 用户身份运行效果不佳,因此最好创建一个专用用户在容器内部运行应用程序:
# /sbin/useradd -m -s /bin/bash www
设置 X11
基本的容器配置已完成,现在直接开始配置容器以运行 X11 应用程序。首先,必须映射 X11 套接字。如果当前正在运行 X 服务器,可以在 `/tmp/.X11 - unix` 下找到它。
还需要为容器提供一个 `.Xauthority` 文件,以便它可以向 X 服务器进行身份验证。简单地挂载主机的文件是行不通的:每个 cookie 条目都由主机名作为键,而客户端只使用其主机名与运行它的机器匹配的条目 —— 在容器内部,这是不可能的。解决方法是将条目的族字段替换为 `FamilyWild` 通配符 (`ffff`),这样它就可以匹配任何主机,然后将其合并到一个新的 `.Xauthority` 文件中供容器使用:
: > /tmp/lxc.Xauthority && xauth nlist :0 |
sed 's/^..../ffff/' | xauth -f /tmp/lxc.Xauthority nmerge -
# 将 :0 替换为你的 $DISPLAY 值
最后,必须确保设置了以下环境变量:
- `DISPLAY`:将其设置为主机的 `DISPLAY` 值。
- `XAUTHORITY`:将其设置为 `lxc.Xauthority` 文件将在容器内部挂载的路径。
为了实现这些步骤,编辑容器的配置文件,并添加以下行。请注意,这是 `lxc - create` 在 `/var/lib/lxc/www/config` 下生成的每个容器的配置文件,与创建时传递的 `/etc/lxc/www.conf` 模板不同:
lxc.environment = XAUTHORITY=/tmp/lxc.Xauthority
lxc.environment = DISPLAY=:0
lxc.mount.entry = /tmp/.X11 - unix tmp/.X11 - unix none bind,optional,create=dir,ro
lxc.mount.entry = /tmp/lxc.Xauthority tmp/lxc.Xauthority none bind,optional,create=file,ro
还有一个细节:由于构建的是非特权容器,其 ID 会根据 `idmap` 进行偏移(容器 UID 0 映射到主机 UID 100000)。然而,`.Xauthority` 文件是由 `xauth` 创建的,权限为 0600,并且由主机用户拥有,其 UID 不在该映射范围内。因此,在容器内部,该文件显示为由 `nobody` 拥有,容器用户无法读取它 —— 所以 X 服务器仍然会以 `Authorization required` 拒绝连接。
最简单的解决方法是在主机上使 cookie 文件对所有用户可读:
chmod 644 /tmp/lxc.Xauthority
如果更愿意将其权限保持为 0600,可以将其所有权更改为容器用户映射后的主机 UID(对于容器 UID 为 1000 的 `www` 用户,对应的主机 UID 为 101000)。
如果一切顺利,现在应该能够运行之前安装的浏览器,并在主机的桌面上看到其窗口:
# lxc - start www
# lxc - attach www -- su www -c firefox
可选地,如果希望在容器内部实现硬件加速渲染和视频解码,可以通过挂载 `/dev/dri` 来传递 GPU:
lxc.mount.entry = /dev/dri dev/dri none bind,optional,create=dir
设置声音
音频设置与显示设置的模式相同。PipeWire 提供了一个与 PulseAudio 兼容的服务器,因此可以在主机上公开一个 PulseAudio 套接字,并将其绑定挂载到容器中,就像对 X11 套接字所做的那样。
首先,在容器内部安装音频软件包,以便应用程序可以使用该套接字:
# apt - get -y install pulseaudio pipewire - pulse
接下来,通过在 `~/.config/pipewire/pipewire - pulse.conf` 文件的 `server.address` 中添加它,告诉 PipeWire 打开一个专用的 PulseAudio 套接字:
pulse.properties = {
# ... 保留现有的默认设置 ...
server.address = [
"unix:native"
"unix:/tmp/pulse - socket - 0"
]
}
重新加载 PulseAudio 服务器,使套接字出现:
systemctl --user restart pipewire - pulse
现在 `/tmp/pulse - socket - 0` 应该已经存在。接下来,将其挂载到容器中,并将 `PULSE_SERVER` 指向它。在容器配置中添加以下内容:
lxc.environment = PULSE_SERVER=unix:/tmp/pulse - socket - 0
lxc.mount.entry = /tmp/pulse - socket - 0 tmp/pulse - socket - 0 none bind,optional,create=file,ro
与 cookie 文件不同,该套接字对所有用户都可访问,因此之前非特权容器的所有权问题在这里不适用 —— 不需要进行 `chmod` 操作。重新启动容器,它应该就可以正常播放音频了。
结论
现在,已经在非特权 LXC 容器中运行了一个 GUI 应用程序,其显示和声音都从主机转发过来。如果浏览器被攻破,影响范围仅限于一个容器,其 UID 映射到主机上未使用的范围 —— 主目录和系统的其他部分将不会受到影响。
不过,这种隔离并非没有代价,也不是绝对的。打开的每个通道 —— X 套接字、PulseAudio 套接字,以及可选的 GPU —— 都是隔离墙上的一个洞,每一个都会扩大被攻破的进程可以触及的范围。例如,音频套接字既允许捕获音频,也允许播放音频。只转发应用程序实际需要的内容,其余的则不要转发;这种权衡就是整个操作的关键。
同样的设置适用于任何不可信的 GUI 应用程序,而不仅仅是浏览器。可以使用 seccomp 或 AppArmor 配置文件进一步加强安全性,但即使保持现状,被攻破的应用程序也已经被限制在容器内,而不是直接影响主目录。

380

被折叠的 条评论
为什么被折叠?



