返回更新列表
已更新Aug 6, 2026

foxcage — 已更新!

在 rootless Podman 容器中运行 Firefox,通过丢弃 capabilities、隔离网络和临时存储来遏制沙箱逃逸并防止宿主机被入侵。

分享

foxcage 图标 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 隔离的是容器与宿主,而不是用户与浏览器。
  • 绑定挂载的目录。 你挂载进去的任何内容(profiledownloads_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.Notificationsorg.freedesktop.portal.Desktoporg.mozilla.* 以及(对于分支浏览器)分支自身的命名空间(例如 org.librewolf.*)是可访问的——像钥匙环和 SSH/GPG 代理这样的会话服务会被阻止
  • Portal 访问范围较广。 org.freedesktop.portal.Desktop 整体被允许,因为文件选择器、“在其他应用中打开链接”和屏幕共享都依赖它。它还暴露了 RemoteDesktop(针对整个会话的合成键盘/鼠标)、CameraLocation。这些由你桌面环境自身的批准对话框来把关,而非 foxcage——而且 RemoteDesktop 的提示与屏幕共享提示相似,因此在接受前请仔细阅读批准对话框。xdg-dbus-proxy 没有“拒绝单个接口”的规则,因此要缩小范围意味着需要枚举 Firefox 所需的每一个接口;关于为何默认不这样做,请参阅 docs/DESIGN.md
  • 所有绑定挂载(profiledownloads_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,以及构建所验证的摘要。裸 .xpi URL 会被拒绝。
  • 不支持版本固定。 重建会采用 AMO 当前列出的附加组件最新版本。
  • 构建时必须能访问 AMO。 如果查询失败且沙盒已有镜像,foxcage 会发出警告并使用现有镜像启动;如果沙盒尚无镜像,它会退出,而不是构建一个静默缺少你所请求附加组件的沙盒。
  • 所有临时沙盒共享一个镜像。 @tmp 和每个 @tmp-<name> 都会构建一个 foxcage-tmp 镜像,因此给某个命名临时沙盒设置与 tmp.toml 不同的 extensions 列表,会导致两者在交替启动时互相重建。请将临时扩展列表保留在 tmp.toml 中。

每次启动时覆盖 DNS

--dns 标志(以及等效的 network.dns 配置键)接受三种形式:```sh ./foxcage @tmp --dns 1.1.1.1 https://example.com # IP ./foxcage @tmp --dns cloudflare https://example.com # alias ./foxcage @tmp --dns https://dns.nextdns.io/ # custom DoH URI

**当值匹配到已知提供商(通过别名或 IP)时,foxcage 会自动对该提供商启用强制 DNS over HTTPS。** Firefox 的 TRR 会被设置为模式 3(严格模式,无明文回退),并填入引导地址,这样启动时就不会有未加密的解析泄漏。你会在 stderr 上看到一行提示,例如 `Enabling DNS over HTTPS via Cloudflare`。

内置别名:

| 别名 | IP | 过滤 |
|-------|------|-----------|
| `cloudflare` | 1.1.1.1 | 无 |
| `cloudflare-security` | 1.1.1.2 | 拦截恶意软件 |
| `cloudflare-family` | 1.1.1.3 | 拦截恶意软件 + 成人内容 |
| `google` | 8.8.8.8 | 无 |
| `quad9` | 9.9.9.9 | 拦截恶意软件(Quad9 默认) |
| `quad9-unfiltered` | 9.9.9.10 | 无 |
| `adguard` | 94.140.14.14 | 拦截广告 + 跟踪器 |
| `adguard-family` | 94.140.14.15 | 广告 + 跟踪器 + 成人内容 |
| `opendns` | 208.67.222.222 | 部分 |

表中未列出的 IP(例如你局域网中的 Pi-hole)则保持纯明文——不会启用 DoH,因为 foxcage 不知道对应的 DoH 端点。这种情况下请使用 URI 形式:`--dns https://pi.hole/dns-query`(需有效证书)可启用 DoH,并保持容器 DNS 不变。

URI 形式会跳过设置容器的明文 DNS,因此容器内除 Firefox 以外的任何程序仍会使用宿主机的 DNS。这是有意为之——`--dns URI` 的意思是“让 Firefox 使用这个 DoH 解析器”,仅此而已。

`--dns` 与 `network.mode = "host"` 不兼容,后者已经拥有完整的宿主机网络访问权限。

### 可视化笼子标识

每个命名笼子都会在菜单栏中获得一个强调色,方便你一眼区分不同窗口。**你无需配置任何东西**——颜色是根据笼子名称确定性推导出来的(通过 SHA256 哈希生成色相,并固定饱和度和明度)。`@banking`、`@work`、`@personal`、`@tmp-research` 都会自动获得各自不同且稳定的颜色,无需你动手。

默认(匿名)笼子保持内置的橙色。

如果你想覆盖自动推导的颜色,可以显式设置:```toml
# ~/.config/foxcage/banking.toml
color = "#dc2626"   # red — overrides the auto-derived colour

好的,我将按照要求翻译这段内容。由于您没有提供具体的输入文本,我无法进行翻译。请提供需要翻译的英文Markdown内容,我将为您翻译成中文。```sh ./foxcage @experiment --color "#10b981" https://example.com # teal, one-off

接受标准 CSS 十六进制颜色:`#rgb`、`#rrggbb` 或 `#rrggbbaa`(带 alpha 通道)。自动派生的颜色经过调校,可在浅色和深色菜单栏上均清晰可见(亮度固定为 55%,饱和度固定为 75%),因此通常无需因主题原因而覆盖这些颜色。

### 限时沙箱

`--lifetime` 标志(以及等效的 `lifetime` 配置键)会在设定时长后自动关闭沙箱。格式为 `<数字><单位>`,单位可为 `s`、`m` 或 `h`:```sh
./foxcage @tmp --lifetime 10m https://example.com
./foxcage @work --lifetime 2h

倒计时从 Firefox 真正在笼内启动时开始——容器启动和镜像构建时间不会占用你的预算。笼子的菜单栏标签会同时显示倒计时和笼子身份,例如 FoxCage - tmp (a3f2b1) | 9m——当剩余时间超过一分钟时每分钟更新一次,最后一分钟内则每秒更新一次。当倒计时归零时,Firefox 会自行关闭,容器随之退出。如果你在生命周期结束前自行关闭 Firefox,不会发生任何异常情况。

在每个笼子的配置中为其设置默认生命周期:```toml

~/.config/foxcage/tmp.toml — every @tmp launch auto-closes after 15 minutes

lifetime = "15m" private_browsing = true

`--lifetime` 命令行参数优先于任何配置文件中的值。

强制完全重建镜像(重新下载 Firefox 及所有系统软件包):```sh
./foxcage --rebuild

一个正在运行的容器会保留其启动时所用的镜像,即使 foxcage 之后重建了该镜像标签也是如此。如果你尝试在某个笼子中打开标签页,而该笼子的镜像自那以后已被更新(通过 --rebuild、Firefox 更新或计划重建),foxcage 会拒绝操作并报错(同时以桌面通知的形式弹出),要求你退出 Firefox 并重新启动——这样会在当前镜像上启动一个全新的容器。在 --rebuild 且笼子处于活动状态时,foxcage 会事先发出警告,执行构建,然后应用同样的检查。

更新

foxcage 会在每次启动时检查新的浏览器版本——Firefox 使用 Mozilla 的发布 API,LibreWolf 使用 GitLab 的 releases 端点。如果有可用更新,容器镜像会自动重建。镜像还会定期重建(默认每 7 天一次),以获取 Debian 安全更新。浏览器的内置自动更新功能已被禁用,因为更新是在镜像层面处理的。

如果更新检查失败(无网络、API 超时),会打印一条警告并使用现有镜像——你始终可以正常浏览。

更新频率位于配置的顶层;版本和频道固定则位于各个 fork 的分节中:```toml rebuild_days = 14 # rebuild for base-image updates every 14 days (0 to disable)

[firefox] channel = "beta" # track the beta channel instead of stable (firefox only) version = "149" # pin to Firefox 149.x (latest patch release)

**固定 ESR 版本时也需要指定渠道。** Mozilla 的版本索引列出的 ESR 版本不带其下载所携带的 `esr` 后缀,因此在默认渠道上仅设置 `version = "140"` 会解析到一个不存在的版本。请同时设置两者:```toml
[firefox]
channel = "esr"
version = "140"        # → 140.13.0esr

与任何发布版本都不匹配的固定版本现在会报错并指明该固定版本,而不是静默回退到最新版本。暂时无法访问 Mozilla 的 API 时仍会发出警告并继续使用现有镜像,因此网络不稳定绝不会阻塞启动。

带后缀的固定版本必须完全限定——"140.13.0esr""150.0b9" 可用,而 "140esr""150b9" 会在配置加载时被拒绝,因为没有任何发布版本能够匹配它们。LibreWolf 修订版本同样如此:"146.0.1-1" 可用,"146-1" 则不行。

要强制立即进行完整重建:./foxcage --rebuild

已撤销的签名密钥

foxcage 拒绝安装其签名由上游项目标记为已泄露(RFC 4880 撤销原因 0x02)的签名子密钥所生成的浏览器构建。gpg --verify 本身不会执行此检查:它只会打印警告并以退出码 0 结束,因此如果没有额外的检查,泄露的签名密钥仍可能验证通过被篡改的下载。

拒绝情况如下所示,并且会使构建失败而不是安装:``` foxcage: REFUSING /tmp/SHA512SUMS - signed by 09BEED63F3462A2DFFAB3B875ECB6497C1A20256, which its owner revoked as compromised. This build cannot be trusted; wait for upstream to re-sign this release with a current key.

无需任何配置,也没有可覆盖的设置。若遇到此问题,修复方案在上游:要么固定使用以当前密钥签名的版本,要么等待受影响的版本被重新签名。

常规密钥轮换的处理方式不同。若子密钥因被取代、退役或无明确原因而被吊销,则不会使吊销*之前*所做的签名失效,因此这些版本安装时仅会给出警告。而任何吊销*之后*日期的签名,无论所述原因如何,都会被拒绝。

**Mozilla 2026 年 8 月密钥轮换。** Mozilla 于 2026-08-06 吊销了签名子密钥 `09BEED63…C1A20256`,原因是其未加密副本被提交至一个私有 GitHub 仓库,并以 `827E6586…76767AA3` 取代。使用旧子密钥签名的 Firefox 版本——即 2025-03-13 至 2026-08-06 之间的所有版本,截至撰写本文时仍包含当前 ESR(`140.13.0esr`)以及任何指向该时间段的 `version` 固定值——会被上述检查拒绝。Release 和 beta 频道不受影响。foxcage 从 `keys.openpgp.org` 而非 `keyserver.ubuntu.com` 获取密钥,因为后者在轮换后数天内既未提供替代子密钥,也未提供吊销信息;过时的密钥服务器既会直接导致构建失败,也会悄然将吊销检查降级为无效操作。

### Firefox 分支(LibreWolf)

foxcage 可以运行注重隐私的 Firefox 分支以替代上游 Firefox:```toml
fork = "librewolf"         # default is "firefox"

[librewolf]
version = "146.0.1-1"      # optional pin; partial pins ("146", "146.0.1") also work

或者通过CLI按启动时指定:```sh foxcage @tmp --fork librewolf https://example.com

**LibreWolf**:注重隐私的 Firefox 分支——默认启用严格跟踪保护、DoH、RFP,遥测功能被锁定关闭。来自 GitLab(`librewolf-community/browser/bsys6`)的签名 Linux tarball,使用 LibreWolf 维护者密钥 `662E 3CDD 6FE3 2900 2D0C A5BB 4033 9DD8 2B12 EF16` 进行 GPG 验证,并附带 `.sha256sum` 交叉校验,遵循与 Firefox 相同的[吊销规则](#revoked-signing-keys)。LibreWolf 自带的 `librewolf.cfg` 会被保留;foxcage 会在其之上追加自己的偏好设置,而非覆盖。维护者于 2026-04-25 轮换了签名子密钥,但未说明原因;当前 tarball 在该日期之前签名,因此安装时会附带警告而非被拒绝。

**频道仅限 Firefox**:当 `fork` 不是 `"firefox"` 时,`firefox.channel = "beta" | "esr"` 会被拒绝。LibreWolf 只有单一发布轨道。

切换 `fork`(通过配置或 `--fork`)会改变 Containerfile 哈希,从而在下次启动时触发重建——无需手动执行 `--rebuild`。

#### 配置文件兼容性

> **每个 fork 使用专用配置文件。** 最安全的默认做法是让 foxcage 自行配置其配置文件(从配置中省略 `profile`),或将 `profile` 指向一个你不会同时在主机上打开的目录。

- **LibreWolf**:*通常*可以安全地与你的主机 Firefox 配置文件共享——LibreWolf 会在几天内跟进 Firefox 版本,因此 `compatibility.ini` 架构冲突很少见。风险:(1) 仅顺序使用是安全的(Firefox 的锁文件会阻止并发打开);(2) 在 Firefox 稳定版发布后的短暂窗口期内,先运行 Firefox 再运行 LibreWolf 可能触发“已被更新版本使用”的迁移对话框;(3) LibreWolf 移除的功能(Sync、Pocket、Mozilla 账户)会静默失效,但不会损坏数据。

### 命名沙箱

使用各自的配置和 Firefox 配置文件运行独立的沙箱实例:```sh
./foxcage @work

这会加载 ~/.config/foxcage/work.toml,并使用独立的镜像(foxcage-work)、容器(foxcage-work)和卷(foxcage-work-profile)。命名笼必须存在配置文件。笼名称只能包含字母、数字、连字符和下划线。

配置

配置文件位于 $XDG_CONFIG_HOME/foxcage/(默认为 ~/.config/foxcage/)。

  • config.toml — 默认笼(可选,没有它时使用合理的默认值)
  • <name>.toml — 命名笼,通过 @<name> 加载(必需)

未知的配置键会被拒绝并报错。有关所有可用选项及默认值,请参阅 config.toml.example

config.toml 示例```toml

Bind-mount a host Firefox profile directory into the cage

profile = "~/.mozilla/firefox/xxxxxxxx.default-release"

Allow downloading files to ~/Downloads

downloads_dir = "~/Downloads"

Extensions pre-installed into the cage (addons.mozilla.org short name,

listing URL, or add-on ID). Most useful in tmp.toml.

extensions = ["ublock-origin"]

Shared memory size for Firefox IPC (default: 256m)

shm_size = "256m"

Pass through webcam devices (/dev/video*)

webcam = true

Pass through host CUPS socket for locally-connected printers (e.g. USB)

local_printers = true

Pass through FIDO2/U2F security key devices (/dev/hidraw*)

security_keys = true

Always open Firefox in private browsing mode

private_browsing = true

Auto-close the cage after a duration ( with unit s, m, or h)

lifetime = "30m"

Accent colour for the menu-bar label. Named cages get a colour derived

from the name automatically; set this to override it.

color = "#4a90e2"

Browser fork: "firefox" (default) or "librewolf"

fork = "librewolf"

Full image rebuild interval in days for base-image updates (default: 7, 0 to disable)

rebuild_days = 7

[firefox]

Firefox release channel: "release" (default), "beta", "esr".

Only valid when fork = "firefox".

channel = "release"

Pin to a specific Firefox version (overrides channel).

Partial versions like "149" or "149.0" resolve to the latest patch release.

Suffixed versions must be fully qualified ("140.13.0esr", "150.0b9"); to

follow the ESR line by major version, pair a numeric pin with

channel = "esr" above.

version = "149.0.2"

[librewolf]

Pin to a specific LibreWolf version. Tags are "-",

e.g. "146.0.1-1". Partial pins like "146" or "146.0.1" also work.

version = "146.0.1-1"

[network]

"host" for full host networking (needed if the cage has to reach services

on the host's localhost), or omit for isolated pasta (default)

mode = "host"

DNS server (isolated mode only, default: host DNS)

dns = "1.1.1.1"

Disable IPv6 in the cage (isolated mode only)

ipv4_only = true

[mounts]

Additional bind mounts into the container. Supported forms:

"~/Documents" — same path in container

"/Documents:/Documents" — ~ expanded on both sides

"~/Documents:/home/user/Documents" — explicit container path

Append :ro for read-only, e.g. "~/Documents:ro"

nosuid,noexec are always enforced on bind mounts; an explicit "exec" or

"suid" is rejected rather than silently dropped.

Host paths must be absolute or start with "~/".

bind = [ "~/Documents:ro", ]

[init]

Commands to run at image build time (as root). Changes trigger a rebuild.

build = ["apt-get update && apt-get install -y --no-install-recommends vim"]

Commands to run at container startup as root, before Firefox.

root = ["chown user:user /some/path"]

Commands to run at container startup as your user, before Firefox.

user = ["mkdir -p ~/custom-dir"]

### 主机 Firefox 配置文件

若要与沙箱共享主机 Firefox 配置文件,请将 `profile` 设置为配置文件目录。您可以在主机上的 Firefox 中访问 `about:profiles` 来查找配置文件路径——或者,如果您希望沙箱以干净且持久保存在主机上的配置文件启动,也可以直接指向一个新的空目录。```toml
profile = "~/.mozilla/firefox/xxxxxxxx.default-release"

只有这一个目录被绑定挂载到沙箱中。~/.mozilla/firefox/ 下的同级配置文件和 profiles.ini 注册表并未暴露——被攻破的沙箱无法篡改它们。

如果未设置 profile,则使用一个命名的 Podman 卷来存储 Firefox 配置文件(参见下文“哪些内容会持久化”)。如果同一配置文件已在主机上的 Firefox 中打开,Firefox 的按配置文件锁文件将导致冲突——请为每个沙箱使用专用的配置文件。

网络

默认情况下,容器使用 pasta,并阻止主机回环且使用主机 DNS。pasta 需要 podman 4.4 或更高版本(自 podman 5.0 起,它已成为 rootless 的默认选项)。

主机网络完全移除网络隔离。当沙箱需要访问主机 localhost 上的服务(例如本地开发服务器、127.0.0.1 上的数据库)时,请使用此选项:```toml [network] mode = "host"

`dns` 不能与 `mode = "host"` 组合使用——主机网络模式已经使用了主机的解析器。

> **主机模式放弃的远不止 localhost。** 它将 cage 置于主机的网络命名空间中,而抽象 Unix 套接字的作用域是该命名空间而非文件系统。因此,处于主机模式的 cage 可以直接访问主机上的抽象地址套接字——包括 Xwayland 的 `@/tmp/.X11-unix/X0`(如果你运行 X11 或 Xwayland,尽管 foxcage 仅支持 Wayland,仍可进行输入日志记录),以及配置了 `unix:abstract=…` 的会话总线,这将绕过经过过滤的 D-Bus 代理。这是共享网络协议栈的固有特性,并非 foxcage 所能过滤的。在需要时使用主机模式,并优先使用仅为该目的而启动的命名 cage。

**仅 IPv4 的 cage** 会完全禁用 IPv6:```toml
[network]
ipv4_only = true

每次启动时也可使用 --ipv4-only 标志(短格式为 -4,与 ssh/curl/pasta 相同):```sh ./foxcage @tmp -4 https://example.com

此命令以仅 IPv4 模式(`-4`)运行 pasta,因此容器完全没有 IPv6 协议栈,同时还会在 Firefox 中设置 `network.dns.disableIPv6`,使其不解析 AAAA 记录——这在启用 DoH 时很重要,因为 DoH 应答会绕过容器的解析器。`ipv4_only` 不能与 `mode = "host"` 组合使用——主机网络直接使用主机的网络协议栈,因此请改为在主机上禁用 IPv6。

### 初始化命令

通过 `[init]` 在构建时或容器启动时运行自定义命令:

- **`build`** — 在镜像构建时以 root 身份运行。用于安装软件包或其他较慢的设置。对构建命令的更改会自动触发镜像重建。
- **`root`** — 在容器启动时、Firefox 启动前以 root 身份运行。用于快速的运行时 root 任务(调整权限、写入配置文件)。
- **`user`** — 在容器启动时、Firefox 启动前以你的用户身份运行。用于创建目录、设置用户级状态。```toml
[init]
build = [
    "apt-get update && apt-get install -y --no-install-recommends fonts-noto-cjk",
    "rm -rf /var/lib/apt/lists/*",
]
root = ["chmod 777 /tmp/shared"]
user = ["mkdir -p ~/workspace"]

所有三个键都是 shell 命令字符串的列表。如果任何命令失败,容器将退出而不会启动 Firefox。

安全说明: 当设置了 init.root 时,容器以 root 身份启动,并添加了 CAP_SETUIDCAP_SETGID(在默认的 CAP_SYS_CHROOT 之上),以便它可以降回普通用户。这些能力仅在 root 初始化阶段持有——在权限降级后,普通用户进程不再拥有额外能力。如果没有 init.root,容器将使用默认的最小能力集运行。

持久化内容

在没有配置的情况下,一个命名的 Podman 卷会存储 Firefox 配置文件(书签、设置、扩展、Widevine DRM 插件)。其他所有内容都是临时的。

  • 默认 cage:foxcage-profile
  • 命名 cage:foxcage-<name>-profile

要重新开始,请删除该卷:```sh podman volume rm foxcage-profile

如果设置了 `profile`,则主机目录会被直接绑定挂载,不会创建卷。

`extensions` 中列出的扩展不属于该状态的一部分:它们存在于镜像中,并在每次启动时重新安装,因此删除卷(或使用没有卷的临时 cage)不会丢失它们。

### 磁盘使用

每个 cage 镜像约为 1 GB。重新构建会重新打标签镜像,并将之前的镜像保留为未标记的 `<none>` 条目,因此 foxcage 会在每次成功构建后移除刚被替换的镜像。它只移除该特定镜像,绝不会移除正在运行的 cage 仍在使用的镜像。

在此行为出现之前遗留的孤立镜像不会被追溯清理。要回收它们:```sh
podman images --filter dangling=true    # review first
podman image prune                      # then remove

Firefox 更新会在每次启动时自动检测。若要强制完整重建(例如,立即获取系统安全更新):```sh ./foxcage --rebuild

## 主题定制

foxcage 会自动透传宿主机的以下配置,使容器中的 Firefox 看起来和用起来都像原生应用:

- **字体。** 系统字体(`/usr/share/fonts`)和用户字体(`~/.local/share/fonts`)以只读方式绑定挂载。来自 `~/.config/fontconfig` 的字体配置也会透传。
- **GTK 主题和深色模式。** 通过 `GTK_THEME` 或 `gsettings` 检测,并传入容器。来自 `~/.config/gtk-3.0` 和 `~/.config/gtk-4.0` 的 GTK 配置以只读方式绑定挂载。
- **时区。** 宿主机的时区名称(从 `TZ`、`/etc/localtime` 符号链接或 `/etc/timezone` 检测)以 `TZ` 的形式传入容器,同时 `/etc/localtime` 以只读方式绑定挂载。两者缺一不可:Firefox 从时区*名称*(而非文件内容)推导 JavaScript 时区——如果没有 `TZ`,网站会显示 UTC 时间。
- **区域设置。** `LANG` 会被透传。宿主机的区域设置会在构建镜像时生成到容器中。

**Cage 标签。** Firefox 菜单栏会显示“FoxCage”(命名 cage 则显示“FoxCage - 名称”),让你一眼就能看出自己处于容器化会话中。菜单栏通过企业策略始终保持可见。

容器仅包含 Adwaita GTK 主题。在 GNOME 桌面上开箱即用。在 KDE 或其他桌面上,如果你的 GTK 主题(如 Breeze)未安装在容器中,Firefox 会回退到 Adwaita。只要通过 `gsettings` 或 `GTK_THEME` 设置了偏好,深色模式检测仍然有效。

## 硬件视频解码(VA-API)

容器拥有自己的用户态环境,因此宿主机上安装的 VA-API 驱动无关紧要——镜像自带驱动。`va-driver-all` 会拉取 `i965-va-driver`(旧款 Intel)和 `mesa-va-drivers`(AMD、nouveau),同时包含 `intel-media-va-driver-non-free`(Intel Gen8+,即 iHD 驱动)以及 `libva2`/`libva-drm2`,Firefox 会在运行时加载这些驱动。

硬件解码需要透传 `/dev/dri`,foxcage 会在宿主机具备该设备时自动完成。无需任何配置。

Intel 驱动是**非自由**版本,因此镜像启用了 Debian 的 `non-free` 组件。Debian 的自由版 `intel-media-va-driver` 是经过 `+dfsg` 重新打包的版本,移除了不可再分发的编解码器内核,而它丢失的正是 AV1 解码——这是 YouTube 现在默认提供的格式。自由版会导致所有 Intel 机器上的 AV1 回退到软件解码。

要检查是否真正生效,请在活动 cage 中运行 `vainfo`:```bash
podman exec foxcage-<name> vainfo

它应列出正在使用的驱动程序(Intel 上为 iHD,AMD 上为 radeonsi)以及受支持的配置——VAProfileH264*VAProfileVP9Profile0VAProfileAV1Profile0 等。从浏览器内部进行的等效检查是 about:support → 媒体,其中硬件解码列对 H264、VP8、VP9、HEVC 和 AV1 应显示为 Supported。播放视频时,主机上的 intel_gpu_top 会显示 Video 引擎的活动。

硬件编码是独立的,在 Intel 上它来自同一驱动程序:H264 和 HEVC 在该列中应显示为 Supported,这正是 WebRTC 在视频通话中用于传出摄像头流的内容,也是 MediaRecorder 所使用的内容。VP8、VP9 和 AV1 编码保持 Unsupported——无论 GPU 具备何种能力,Firefox 仅接入 H264 和 HEVC 的 VA-API 编码器。

音频编解码器(AAC、MP3、Opus、Vorbis、FLAC、Wave)在每台机器上的硬件解码下均显示为 Unsupported——没有消费级 GPU 带有音频解码模块。该行并非配置错误。

如果 vainfo 报告 failed to initialize display,则容器无法打开 /dev/dri/renderD128。在普通的 systemd 桌面上,logind 会为你的用户授予该设备的 ACL,因此这通常意味着 foxcage 是从不拥有该会话(SSH、不同的 TTY)的会话中运行的。

DRM(Netflix、Disney+ 等)

Widevine DRM 开箱即用。首次访问受 DRM 保护的网站时,Firefox 会自动下载 Widevine CDM。这可能需要一点时间。

主机集成(始终开启)

foxcage 使用经过过滤的 D-Bus 代理,为 Firefox 提供对主机 XDG Desktop Portal 和通知守护进程的访问。这些功能是安全的,因为所有访问都需用户介入——主机会显示你必须与之交互的原生对话框。被攻破的浏览器无法静默访问主机资源。

  • 文件上传 — 主机的原生文件选择器(由你选择要共享哪些文件)
  • 外部链接mailto:、磁力链接等通过主机应用选择器打开
  • 桌面通知 — 转发至主机通知守护进程
  • 屏幕共享 — portal 屏幕选择器 + PipeWire 视频流(需要主机上安装 PipeWire)

设备直通(可选启用)

这些功能将主机设备直接传入容器,且默认关闭——与上述 portal 功能不同,这里没有主机端的确认环节。被攻破的浏览器可以静默使用该硬件。```toml webcam = true # /dev/video* — webcam for video calls local_printers = true # CUPS socket — USB printers (network printers work by default) security_keys = true # /dev/hidraw* — FIDO2/U2F hardware keys

## 尚不支持

由于缺少主机集成,某些 Web 平台功能在容器中无法使用。此处列出这些功能,以供透明说明。

**蓝牙、USB、串口和 NFC。** Web Bluetooth、WebUSB、Web Serial 和 WebNFC API 需要设备访问权限以及容器中不可用的系统服务(BlueZ、udev)。

**游戏手柄和 MIDI。** Gamepad API 需要访问 `/dev/input/`。Web MIDI 需要 ALSA 音序器访问权限。两者均未透传。

**PWA 安装。** 无法从容器内将渐进式 Web 应用安装到主机桌面。

**辅助功能。** 通过 AT-SPI 提供的屏幕阅读器支持已被禁用(`NO_AT_BRIDGE=1`)——容器与主机的辅助功能总线没有连接。Web Speech API 语音合成功能可用:`speech-dispatcher` 及 `espeak-ng` 引擎已安装在 cage 中,并在首次使用时自动启动,音频通过共享的 PulseAudio 套接字路由。

## 主机配置

### 推荐:overlay 存储

Rootless Podman 可能会回退到 `vfs` 存储驱动,该驱动会复制整个镜像层,而不是使用 overlay 挂载。这会使构建后的容器启动速度慢得多。检查你当前使用的驱动:```sh
podman info --format '{{.Store.GraphDriverName}} {{.Store.GraphStatus}}'

overlayNative Overlay Diff:true 是快速路径,无需任何配置——在内核 5.13 或更新版本且使用 ext4/xfs 后端文件系统时,Podman 会直接使用非特权 overlayfs。如果你看到的是这种情况,就无需做任何操作,安装 fuse-overlayfs 也不会有帮助。

只有当你使用的是 vfs(旧内核,或无法支持非特权 overlay 的后端文件系统)时,才需要安装 fuse-overlayfs 并添加到 ~/.config/containers/storage.conf:```toml [storage] driver = "overlay"

[storage.options.overlay] mount_program = "/usr/bin/fuse-overlayfs"

这是回退方案,而非升级:FUSE 会将每次文件系统操作都经由用户空间处理,因此比原生 overlay 更慢。在支持原生 overlay 的系统上设置 `mount_program` 只会让情况更糟,而非更好。

### 将 foxcage 设为默认浏览器

首先,确保 `foxcage` 脚本位于其永久位置(例如 `~/bin/foxcage` 或 `/usr/local/bin/foxcage`)。安装命令会将脚本的当前路径记录在 `.desktop` 文件中,因此之后移动脚本会导致启动器失效。

然后运行:```sh
foxcage --install

这会创建一个指向脚本当前位置的 .desktop 文件,安装 foxcage 图标,并刷新桌面和图标数据库。之后 FoxCage 应会出现在你的应用程序菜单中。

要将 foxcage 设置为默认 Web 浏览器,以便在其他应用程序中点击的链接能在 foxcage 中打开:```sh xdg-settings set default-web-browser foxcage.desktop

如果 cage 已在运行,URL 会在现有浏览器中作为新标签页打开。

要撤销:```sh
foxcage --uninstall

StartupNotify=true 已设置在 .desktop 文件中,这会告诉合成器在 foxcage 启动时显示一个旋转光标。当需要构建镜像(可能需要几分钟)时,foxcage 会发送桌面通知,让你知道 Firefox 正在启动中。任何提前退出的错误(配置拼写错误、缺少依赖、cage 名称格式错误)也会以桌面通知的形式呈现,这样通过桌面启动的用户在 foxcage 因没有终端附加而失败时,不会对着空白屏幕发呆。两者都需要 notify-send(在 Debian/Ubuntu 上来自 libnotify-bin)——如果未安装,通知会被静默跳过,错误仍会输出到 stderr。

手动设置

如果你更倾向于手动创建 .desktop 文件,请创建 ~/.local/share/applications/foxcage.desktop:```ini [Desktop Entry] Type=Application Name=FoxCage Comment=Firefox in a rootless Podman container Exec=/path/to/foxcage %u Icon=foxcage MimeType=text/html;x-scheme-handler/http;x-scheme-handler/https; Terminal=false Categories=Network;WebBrowser; StartupNotify=true StartupWMClass=foxcage

将 `/path/to/foxcage` 替换为脚本的实际路径。注册它:```sh
update-desktop-database ~/.local/share/applications

运行测试

测试套件使用 pytest + pytest-cov,这些在 requirements-dev.txt 中声明为仅开发依赖。``` pip install -r requirements-dev.txt pytest

测试完全封闭——不使用 podman、不联网、不访问真实文件系统,仅依赖 pytest 的 `tmp_path`。测试套件以 **100% 行覆盖率和分支覆盖率** 为门槛(在 `pytest.ini` 和 `.coveragerc` 中配置);任何未覆盖的行或条件语句中未执行的分支都会导致运行失败。CI 通过 `.gitlab-ci.yml` 在每次推送时运行该测试套件。

## 致谢

本项目由 Mike Cardwell 开发,并得到了 [Claude Code](https://claude.ai/claude-code)(Anthropic 的 AI 编码工具)的协助。

## 支持/感谢我的工作

- [Bitcoin](bitcoin:1PQLtWnjUi1itHLG6QCQeHM3Nxua8pRsq1): 1PQLtWnjUi1itHLG6QCQeHM3Nxua8pRsq1
- [Paypal](https://www.paypal.me/grepular)

分类