
foxcage — 已更新!
在无根 Podman 容器中运行 Firefox,通过丢弃 capabilities、隔离网络和临时存储来遏制沙箱逃逸并防止主机受损。
foxcage
在无根 Podman 容器中运行 Firefox,以实现安全隔离。你的浏览器几乎没有任何 Linux 能力(capabilities),运行在独立的用户和网络命名空间中,与宿主机隔离——同时仍拥有完整的 GPU 加速、音频和 DRM 支持。
为什么选择 foxcage?
Firefox 已经拥有一个多进程沙箱,利用 Linux 命名空间和 seccomp-bpf 隔离网页内容渲染进程。对于大多数威胁来说,这已经足够有效。foxcage 增加了第二道防线:如果攻击者利用漏洞逃逸出 Firefox 的沙箱(这种情况确实会发生——存在相关的 CVE),他们只会落入一个锁定严密的容器中,而不是你的完整用户会话。
foxcage 能防御什么
- 利用后的文件访问。 在裸 Firefox 上逃逸沙箱,攻击者可以访问你的用户能读取的一切:
~/.ssh、~/.gnupg、其他浏览器的配置文件、密码管理器数据库、文档、源代码。而在 foxcage 中,攻击者只能看到你显式挂载进去的内容。 - 磁盘上的跟踪残留。
@tmp临时笼子在窗口关闭后在磁盘上不留任何痕迹——包括扩展、HSTS 状态、TLS 会话缓存以及 Firefox 隐私浏览模式仍会持久化的 DNS 缓存。多个@tmp笼子可以并发运行,互不干扰。 - 持久化。 在裸 Firefox 上,恶意软件可以写入
~/.config/autostart、~/.bashrc、cron 或任何其他位置以在重启后存活。foxcage 的临时容器(--rm)意味着除非你进行了绑定挂载,否则任何内容都不会持久化。 - 横向网络移动。 默认情况下,容器无法探测
localhost上的服务。在裸 Firefox 上,沙箱逃逸后拥有完整的网络访问权限。(如果某个笼子需要访问 localhost,例如用于本地开发,可使用[network] mode = "host"——但请参阅“网络”部分下的注意事项:host 模式也会暴露宿主的抽象 Unix 套接字。) - 权限提升。 容器丢弃了除
CAP_SYS_CHROOT之外的所有 Linux 能力,并阻止获取新权限。Setuid 二进制文件、通过晦涩系统调用利用内核漏洞等提权路径均被切断。
foxcage 不能防御什么
- 浏览器层面的攻击。 钓鱼、恶意扩展以及任何在 Firefox 正常功能范围内运作的攻击不受影响——foxcage 隔离的是容器与宿主,而不是用户与浏览器。
- 绑定挂载的目录。 你挂载进去的任何内容(
profile、downloads_dir、额外的绑定挂载)对受感染的浏览器都是完全可访问的。如果你挂载了宿主配置文件目录,攻击者可以像在裸 Firefox 上一样篡改它。 - 通过 PulseAudio 进行音频捕获。 PulseAudio 套接字被绑定挂载进容器。虽然在文件系统层面它是只读挂载的,但 Unix 域套接字是双向的——受感染的进程仍然可以通过该套接字发送录音请求。浏览器沙箱逃逸后可能录制宿主麦克风的音频。
- Wayland 合成器漏洞。 Wayland 套接字被透传。Wayland 合成器在设计上会隔离客户端之间的通信,但合成器本身的漏洞仍然可以被触达。
安全配置
容器运行时的配置包括:
- 丢弃所有 Linux 能力(仅添加回
CAP_SYS_CHROOT以支持 Firefox 的内容沙箱;当配置了init.root时,会临时添加CAP_SETUID/CAP_SETGID) - 启用
no-new-privileges以防止权限提升 - 无根用户命名空间(
--userns keep-id) - 私有的
/dev/shm(不与宿主共享)——可通过shm_size配置大小 - 通过 pasta 实现隔离网络,默认阻止宿主回环(loopback)
- DNS 默认使用宿主 DNS(可通过
network.dns配置) - 仅将
XDG_RUNTIME_DIR中的特定套接字绑定挂载进容器(Wayland、PulseAudio、PipeWire 以及经过过滤的 D-Bus 代理)——宿主的完整运行时目录永远不会被暴露 - 对宿主 D-Bus 会话总线的访问始终由运行在宿主上的经过过滤的
xdg-dbus-proxy进行中介。只有org.freedesktop.Notifications、org.freedesktop.portal.Desktop、org.mozilla.*以及(对于分支浏览器)分支自身的命名空间(例如org.librewolf.*)是可访问的——像钥匙环和 SSH/GPG 代理这样的会话服务会被阻止 - Portal 访问范围较广。
org.freedesktop.portal.Desktop整体被允许,因为文件选择器、“在其他应用中打开链接”和屏幕共享都依赖它。它还暴露了RemoteDesktop(针对整个会话的合成键盘/鼠标)、Camera和Location。这些由你桌面环境自身的批准对话框来把关,而非 foxcage——而且RemoteDesktop的提示与屏幕共享提示相似,因此在接受前请仔细阅读批准对话框。xdg-dbus-proxy没有“拒绝单个接口”的规则,因此要缩小范围意味着需要枚举 Firefox 所需的每一个接口;关于为何默认不这样做,请参阅docs/DESIGN.md - 所有绑定挂载(
profile、downloads_dir、额外的[mounts] bind)均使用nosuid,noexec - 浏览器下载会对照 GPG 签名进行验证:Firefox 对照 Mozilla 签名的 SHA-512 校验和,LibreWolf 对照 LibreWolf 维护者的分离签名以及配套的 SHA-256。验证比
gpg --verify更严格,后者对于由已撤销密钥以及钥匙环中任何密钥所做的签名都会以退出码 0 结束。foxcage 额外要求签名必须链到固定的主密钥,并拒绝任何由其所有者撤销为已泄露的子密钥所签署的版本——请参阅已撤销的签名密钥 - 临时容器(
--rm)——文件系统写入在退出时丢失 - 不传递任何宿主设备(摄像头、安全密钥、打印机),除非显式启用
你启用的每个 [network] 和 [mounts] 选项,都是用一些隔离性换取便利性。默认配置是在仍能提供可用浏览器的前提下最严格的配置。
系统要求
- Python 3.11+
- Podman(无根模式)
- Wayland 合成器(不支持 X11)
- pasta(
sudo apt install passt)——除非network.mode = "host" - xdg-dbus-proxy(
sudo apt install xdg-dbus-proxy) - PulseAudio 或带 PulseAudio 兼容性的 PipeWire(用于音频)
- 支持 DRI 的 GPU——可选;如果没有
/dev/dri,foxcage 会发出警告,Firefox 将以软件方式渲染。镜像中已安装 Intel、AMD 和 nouveau 的 VA-API 驱动,因此硬件视频解码无需宿主驱动包即可工作——请参阅硬件视频解码
请以你正常的桌面用户身份运行 foxcage,而不是 root 或通过 sudo——沙箱会将你的用户映射进容器,而以 root 运行会消除 foxcage 存在的意义——隔离。它会拒绝以 root 身份启动。
已测试环境: Debian 13(Trixie)搭配 GNOME 3。其他 Linux 发行版和 Wayland 合成器可能可以工作,但尚未经过测试。
安装
foxcage 是一个单一的 Python 脚本,除 Python 标准库外没有其他依赖。将其复制到你的 PATH 中的某个目录即可:```sh
sudo cp foxcage /usr/local/bin/foxcage
或者对于用户本地安装:```sh
cp foxcage ~/.local/bin/foxcage
确保脚本具有可执行权限(chmod +x foxcage)。
使用 foxcage --version 检查你所拥有的版本——这在报告问题时很有用,因为 foxcage 是通过复制单个文件来安装的。
使用方法```sh
./foxcage
首次运行时,脚本会构建容器镜像(从 Mozilla 下载 Firefox,安装最简 Debian 依赖),然后启动 Firefox。后续运行时,foxcage 会检查 Firefox 更新,并在有新版本可用时自动重建镜像。镜像还会定期重建(默认每 7 天一次),以获取系统软件包更新。如果更新检查失败(网络错误、超时),会记录一条警告并使用现有镜像——启动永远不会被阻塞。
将参数传递给 Firefox:```sh
./foxcage https://example.com
将命名笼与Firefox标志结合:```sh ./foxcage @work --kiosk https://example.com
如果某个 cage 已在运行,URL 会在现有浏览器的新的标签页中打开,而不会启动第二个容器。对正在运行的 cage 执行 `foxcage`(或 `foxcage @cage`)且不带 URL 时,会以“cage is already running”消息干净退出——foxcage 无法从容器外部唤起现有的 Wayland 窗口,因此它不会尝试这样做。
每次启动时的标志在 cage 已在运行时**不**适用。`--dns`、`--ipv4-only`、`--lifetime`、`--color` 和 `--fork` 会在容器启动时被消费,而正在运行的容器的设置无法从外部更改,因此它们会被忽略并附带警告。关闭 cage 并重新运行以应用这些设置。
> 使用 `private_browsing` 配置键来启用隐私模式会话——*不要*使用 Firefox 原始的 `--private-window` CLI 标志。该配置键会设置会话级隐私模式(`browser.privatebrowsing.autostart`),因此后续的 `foxcage @cage URL` 调用可以在标签页中重新打开。`--private-window` 作为 Firefox 透传参数只会让第一个窗口处于隐私模式,并破坏上述在标签页中重新打开的行为。
>
> **注意:**通过 `private_browsing = true` 启用的会话不会显示 Firefox 通常的隐私窗口 UI 提示(紫色强调栏、面具图标、标题中的“(Private Browsing)”)。这是因为会话中的每个窗口都是隐私的,所以 Firefox 没有非隐私窗口可供视觉对比——它会抑制该指示器。该会话*确实*是真正隐私的;如需验证,可在 cage 中访问 `about:privatebrowsing`(显示标准的隐私浏览信息页)或 `about:config` 并检查 `browser.privatebrowsing.autostart = true`。
### 使用 `@tmp` 进行临时浏览
对于应不留痕迹的一次性链接,请使用保留的 `tmp` cage:```sh
./foxcage @tmp https://somewhere-suspicious.example
每个 @tmp 启动都是一个全新的、一次性的 Firefox,没有持久配置文件。当窗口关闭时,一切都会消失——Cookie、缓存、历史记录、扩展、HSTS 状态、TLS 会话缓存、DNS 缓存、已保存的标签页状态。这比 Firefox 隐私浏览更进一步,后者仍会保留扩展和相当多的磁盘状态。
多个 @tmp 隔离区可并发运行,彼此相互隔离。菜单栏会显示 FoxCage - tmp (<短 ID>),以便你区分并发的临时窗口。
临时隔离区在启动时会打开一个空白页面和空白的新标签页——默认的 Firefox 主页和新标签页内容(热门站点、Pocket 推荐、活动流)对于一个即将被丢弃的全新配置文件来说纯粹是噪音,因此它们会被抑制。持久隔离区则保留 Firefox 的默认设置。
命名临时隔离区
如果你希望为一次性会话指定一个有意义的名称(例如,一个你稍后想在新标签页中重新打开的研究深坑),请使用 @tmp-<名称>:```sh
./foxcage @tmp-research https://example.com # first call → new window
./foxcage @tmp-research https://another.example # second call → new tab in the existing window
`@tmp-<name>` 仍然是临时的——当你关闭窗口时,一切都会消失。与裸 `@tmp` 的区别在于,使用相同名称的第二次启动**会复用现有窗口**(与持久化 cage 相同),因此你可以在之后添加更多标签页,而无需启动一个并行副本。裸 `@tmp` 则保持其“每次启动都是全新的一次性窗口”行为。
菜单栏标签会显示你选择的名称(`FoxCage - tmp-research`),因此窗口会被有意义地标记。
#### 自定义临时默认值
创建 `~/.config/foxcage/tmp.toml` 来为所有临时 cage(包括裸 `@tmp` 和每个 `@tmp-<name>`)设置默认值。例如:```toml
private_browsing = true
lifetime = "30m"
extensions = ["ublock-origin"]
[network]
dns = "cloudflare"
每次临时启动现在都会获得一个私有窗口、uBlock Origin、Cloudflare DoH,并在 30 分钟后自动关闭——同时保持完整的临时性。命名的临时配置默认继承 tmp.toml;如果你想按名称覆盖,请创建 ~/.config/foxcage/tmp-<name>.toml。该文件随后会替代 tmp.toml 生效——不进行合并,更具体的文件直接胜出。如果你需要共享默认值,请将其复制到该文件中。
常规 cage 配置中可设置的任何内容在这里都适用,除了一个会破坏临时性本身的键:
profile— 硬错误。
它指向主机上持久化的配置文件目录,这与 @tmp 的用途直接矛盾。如果你想要一个带持久化配置文件的沙盒 cage,请使用不以 tmp- 开头的常规命名 cage(如 @work、@research 等)。
预装扩展
cage 可以将扩展内置到其镜像中,并在每次启动时强制安装:```toml extensions = ["ublock-origin"]
将内容放入 `~/.config/foxcage/tmp.toml`,这样**每个临时 cage 启动时就已经运行着 uBlock Origin**——这一点很重要,因为临时 cage 本来就是你拥有的保护最弱的浏览器,却恰恰用于你最不信任的链接。全新的 `@tmp` 配置文件没有任何扩展,而在一个关闭即自毁的会话中手动安装扩展毫无意义。
每个条目可以是附加组件在 addons.mozilla.org URL 中的短名称、该列表 URL 本身,或附加组件 ID:```toml
extensions = [
"ublock-origin",
"https://addons.mozilla.org/firefox/addon/noscript/",
"[email protected]",
]
插件通过 addons.mozilla.org API 在启动时解析,在镜像构建期间下载,并对照 AMO 发布的 SHA-256 进行验证。解析出的版本是 Containerfile 的一部分,因此扩展的新版本会改变镜像哈希并触发重建——扩展的更新方式与 Firefox 相同,原因也一样:运行时不会安装任何东西,所以全新的临时配置文件永远不会重新下载任何内容。
由于它们是通过企业策略而非手动安装的:
- 无法在沙盒内(
about:addons会显示它们是由你的组织安装的)移除或禁用它们。 - 它们在隐私窗口中处于启用状态,因此在运行
private_browsing = true的沙盒中仍然有效。这需要 Firefox 136 或 ESR 128.8;旧版本会忽略该设置,并让附加组件在隐私窗口中保持无效状态。 - 它们的图标会放置在工具栏中,因此你可以看到拦截器确实存在。
- 浏览器内的扩展更新已关闭。更新随镜像重建一起到达。
限制:
- 仅限 AMO。 扩展必须列在 addons.mozilla.org 上——该查询提供策略条目所需的附加组件 ID,以及构建所验证的摘要。裸
.xpiURL 会被拒绝。 - 不支持版本固定。 重建会采用 AMO 当前列出的附加组件最新版本。
- 构建时必须能访问 AMO。 如果查询失败且沙盒已有镜像,foxcage 会发出警告并使用现有镜像启动;如果沙盒尚无镜像,它会退出,而不是构建一个静默缺少你所请求附加组件的沙盒。
- 所有临时沙盒共享一个镜像。
@tmp和每个@tmp-<name>都会构建一个foxcage-tmp镜像,因此给某个命名临时沙盒设置与tmp.toml不同的extensions列表,会导致两者在交替启动时互相重建。请将临时扩展列表保留在tmp.toml中。