
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临时笼子在窗口关闭后不会在磁盘上留下任何痕迹——包括 Firefox 的隐私浏览仍然会持久化的扩展、HSTS 状态、TLS 会话缓存和 DNS 缓存。多个@tmp笼子可以并发运行而互不干扰。 - 持久化。 在裸 Firefox 上,恶意软件可以写入
~/.config/autostart、~/.bashrc、cron 或任何其他位置,从而在重启后继续存活。foxcage 的临时容器(--rm)意味着除非你将其绑定挂载进来,否则什么都不会持久化。 - 横向网络移动。 默认情况下,容器无法探测
localhost上的服务。在裸 Firefox 上,沙箱逃逸后拥有完整的网络访问权限。(如果你的某个笼子需要访问 localhost,例如用于本地开发,可以使用[network] mode = "host"——但请参阅“网络”部分下的注意事项:host 模式还会暴露主机的抽象 Unix 套接字。) - 权限提升。 容器丢弃了除
CAP_SYS_CHROOT之外的所有 Linux capabilities,并阻止获取新的权限。Setuid 二进制、通过生僻系统调用利用内核漏洞以及类似的提权路径都被切断。
foxcage 无法防御的内容
- 浏览器层面的攻击。 钓鱼、恶意扩展以及任何在 Firefox 正常功能范围内运作的攻击都不受影响——foxcage 隔离的是容器与主机,而不是用户与浏览器。
- 绑定挂载的目录。 你挂载进去的任何内容(
profile、downloads_dir、额外的绑定挂载)对已被攻破的浏览器都是完全可访问的。如果你挂载了主机上的配置文件目录,攻击者可以像在裸 Firefox 上一样篡改它。 - 通过 PulseAudio 进行音频采集。 PulseAudio 套接字被绑定挂载进容器。虽然它在文件系统层面以只读方式挂载,但 Unix 域套接字是双向的——被攻破的进程仍然可以通过该套接字发送录音请求。浏览器沙箱逃逸后可能录制主机麦克风的音频。
- Wayland 合成器漏洞。 Wayland 套接字被传递进去。Wayland 合成器按设计将客户端彼此隔离,但合成器本身的漏洞仍可能被触达。
安全配置
容器运行时具有:
- 丢弃所有 Linux capabilities(仅因 Firefox 的内容沙箱而加回
CAP_SYS_CHROOT;在配置了init.root时会临时添加CAP_SETUID/CAP_SETGID) no-new-privileges以防止权限提升- 无根用户命名空间(
--userns keep-id) - 私有的
/dev/shm(不与主机共享)——可通过shm_size配置大小 - 通过 pasta 进行隔离的网络,默认阻止主机环回
- 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 将以软件方式渲染
请以你普通的桌面用户身份运行 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
将命名的 cage 与 Firefox 标志结合:```sh ./foxcage @work --kiosk https://example.com
如果 cage 已在运行,URL 会在现有浏览器的新标签页中打开,而不会启动第二个容器。在没有 URL 的情况下对正在运行的 cage 运行 `foxcage`(或 `foxcage @cage`)将干净地退出,并显示“cage 已在运行”消息——foxcage 无法从容器外部将现有的 Wayland 窗口提升到前台,所以它也不会尝试这样做。
当 cage 已在运行时,每次启动的标记(per-launch flags)**不**适用。`--dns`、`--ipv4-only`、`--lifetime`、`--color` 和 `--fork` 在容器启动时被消耗,而正在运行的容器的设置无法从外部更改,因此它们会被忽略并发出警告。关闭 cage 并重新运行即可应用这些设置。
> 使用 `private_browsing` 配置键进行隐私模式会话 — *而不是* Firefox 原始的 `--private-window` CLI 标志。该配置键会设置会话级别的隐私模式(`browser.privatebrowsing.autostart`),因此后续的 `foxcage @cage URL` 调用可以在标签页中重新打开。作为 Firefox 直通参数的 `--private-window` 只会让第一个窗口处于隐私模式,并破坏上述在标签页中重新打开的行为。
>
> **注意:** 通过 `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 (<short id>),以便你区分并发的临时窗口。
临时隔离区在启动时打开空白页,新标签页也是空白——默认的 Firefox 主页和新标签页内容(热门站点、Pocket 推荐、活动流)在一个即将被丢弃的全新配置文件上纯属噪音,因此会被抑制。持久化隔离区则保留 Firefox 的默认设置。
命名临时隔离区
如果你希望在一次性会话上使用一个有意义的名称(比如,你想在新标签页中重新打开的研究深坑),请使用 @tmp-<name>:```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"
[network]
dns = "cloudflare"
现在,每次临时启动都会获得一个隐私窗口、Cloudflare DoH,并在 30 分钟后自动关闭——同时保持完整的临时性。命名临时实例默认继承 tmp.toml;如果你想按名称覆盖,可创建 ~/.config/foxcage/tmp-<name>.toml。该文件随后会替代 tmp.toml 生效——不进行合并,更具体的文件直接胜出。如果你需要共享默认设置,请将其复制到该文件中。
常规 cage 的配置中可设置的任何内容在此处同样有效,除了那个会破坏临时性本身的键:
profile— 硬错误。
它指向一个宿主机上的持久配置文件目录,这与 @tmp 的用途直接矛盾。如果你想要一个带持久配置文件的沙箱 cage,请使用一个不以 tmp- 开头的常规命名 cage(如 @work、@research 等)。
每次启动时覆盖 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"` 不兼容,后者已经拥有完整的主机网络访问权限。
### 可视化 cage 标识
每个命名 cage 都会在菜单栏中获得一个强调色,让你一眼就能区分不同窗口。**你无需配置任何东西**——该颜色由 cage 名称确定性地派生(通过 SHA256 哈希到色相,并采用固定饱和度和亮度)。`@banking`、`@work`、`@personal`、`@tmp-research` 都会获得不同且稳定的颜色,而你无需动手。
默认(匿名)cage 保持内置的橙色。
如果你想覆盖自动派生的颜色,可显式设置:```toml
# ~/.config/foxcage/banking.toml
color = "#dc2626" # red — overrides the auto-derived colour
未收到任何需要翻译的内容。请提供 chunk 21/75 的实际 Markdown 文本后,我将按规则执行翻译。```sh ./foxcage @experiment --color "#10b981" https://example.com # teal, one-off
接受标准 CSS 十六进制色值:`#rgb`、`#rrggbb` 或 `#rrggbbaa`(含 alpha 通道)。自动推导出的颜色经过调校,可在浅色和深色菜单栏上均清晰可见(亮度固定为 55%,饱和度为 75%),因此通常无需因主题原因进行覆盖。
### 限时 cage
`--lifetime` 标志(以及等价的 `lifetime` 配置键)会在设定时长后自动关闭 cage。格式为 `<数字><单位>`,单位可为 `s`、`m` 或 `h`:```sh
./foxcage @tmp --lifetime 10m https://example.com
./foxcage @work --lifetime 2h
The countdown starts when Firefox actually launches inside the cage — container startup and image-build time don't eat into your budget. The cage's menu-bar label shows the countdown alongside the cage identity — e.g. FoxCage - tmp (a3f2b1) | 9m — updated once per minute while there's more than a minute left, and once per second in the final minute. When the countdown hits zero Firefox closes itself and the container exits. If you close Firefox yourself before the lifetime is up, nothing unusual happens.
Set a default lifetime per cage in its config:```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 更新或计划重建)的 cage 中打开标签页,foxcage 会报错拒绝(同时通过桌面通知提示),并要求你退出 Firefox 后重新启动——这将在当前镜像上启动一个全新的容器。在 --rebuild 且 cage 处于运行状态时,foxcage 会预先发出警告,执行构建,然后执行同样的检查。
更新
foxcage 会在每次启动时检查新的浏览器版本——Firefox 使用 Mozilla 的 release API,LibreWolf 使用 GitLab 的 releases endpoint。如果发现有可用更新,容器镜像会自动重建。镜像还会定期重建(默认每 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
一个与任何发行版都不匹配的固定版本(pin)现在会作为错误被报告,并指明该固定版本,而不是静默回退到最新发行版。如果暂时无法访问 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 是在该日期之前签名的,因此安装时会出现警告,而不是被拒绝。
**Channel 仅限 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 账户)会静默失效,但不会损坏数据。
### 命名 cage
运行独立的沙盒实例,每个实例都有自己的配置和 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"
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 配置文件
若要与 cage 共享主机的 Firefox 配置文件,请将 `profile` 设置为配置文件目录。您可以在主机上的 Firefox 中访问 `about:profiles` 来找到配置文件路径——或者直接指向一个全新的空目录,如果您希望 cage 以持久保存在主机上的干净配置文件启动的话。```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 起它已成为无根模式默认设置)。
宿主机网络 完全移除网络隔离。当沙盒需要访问宿主机 localhost 上的服务时(例如本地开发服务器、127.0.0.1 上的数据库),请使用此模式:```toml
[network]
mode = "host"
`dns` 不能与 `mode = "host"` 组合使用——主机网络已经使用了主机的解析器。
> **主机模式放弃的不只是 localhost。**它将 cage 置于主机的网络命名空间中,而抽象 Unix 套接字的作用域是该命名空间而非文件系统。因此,处于主机模式的 cage 可以直接访问主机上的抽象地址套接字——包括如果你运行 X11 或 Xwayland 时的 Xwayland `@/tmp/.X11-unix/X0`(尽管 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-only 模式(`-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_SETUID 和 CAP_SETGID(在默认的 CAP_SYS_CHROOT 之上),以便它可以降级回普通用户。这些 capabilities 仅在 root 初始化阶段持有——在权限降级之后,普通用户进程没有额外的 capabilities。如果没有 init.root,容器将使用默认的最小 capability 集合运行。
持久化内容
在没有配置的情况下,一个命名的 Podman 卷会存储 Firefox 配置文件(书签、设置、扩展、Widevine DRM 插件)。其他一切都是临时的。
- 默认 cage:
foxcage-profile - 命名 cage:
foxcage-<name>-profile
要重新开始,请删除该卷:```sh podman volume rm foxcage-profile
If `profile` is set, the host directory is bind-mounted directly and no volume is created.
### 磁盘占用
每个 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 - name"),以便您一眼看出自己处于容器化会话中。菜单栏通过企业策略始终保持可见。
容器仅包含 Adwaita GTK 主题。在 GNOME 桌面上开箱即用。在 KDE 或其他桌面上,如果您的 GTK 主题(例如 Breeze)未安装在容器中,Firefox 将回退到 Adwaita。只要通过 `gsettings` 或 `GTK_THEME` 设置了偏好设置,深色模式检测仍然有效。
## DRM(Netflix、Disney+ 等)
Widevine DRM 开箱即用。首次访问受 DRM 保护的网站时,Firefox 会自动下载 Widevine CDM。这可能需要稍等片刻。
## 宿主机集成(始终开启)
foxcage 使用经过过滤的 D-Bus 代理,使 Firefox 能够访问宿主机的 XDG Desktop Portal 和通知守护进程。这些功能是安全的,因为所有访问都需用户介入——宿主机显示必须由您进行交互的原生对话框。被攻破的浏览器无法静默访问宿主机资源。
- **文件上传** — 宿主机的原生文件选择器(由您选择要共享哪些文件)
- **外部链接** — `mailto:`、磁力链接等通过宿主机应用选择器打开
- **桌面通知** — 转发到宿主机通知守护进程
- **屏幕共享** — 门户屏幕选择器 + PipeWire 视频流(需要在宿主机上安装 PipeWire)
## 设备透传(可选开启)
这些功能会将宿主机设备直接传入容器,并且**默认关闭**——与上述门户功能不同,这里没有宿主机侧的确认。被攻破的浏览器可能会静默使用硬件。```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 语音合成可以正常工作:安装了使用 espeak-ng 引擎的 speech-dispatcher,并在首次使用时自动启动,音频通过共享的 PulseAudio 套接字路由。
主机配置
推荐:使用 fuse-overlayfs 的 overlay 存储
无根 Podman 可能默认使用 vfs 存储驱动,它会复制整个镜像层,而不是使用 overlay 挂载。这会使构建后的容器启动慢得多。要解决此问题,请安装 fuse-overlayfs,并将以下内容添加到 ~/.config/containers/storage.conf:```toml
[storage]
driver = "overlay"
[storage.options.overlay] mount_program = "/usr/bin/fuse-overlayfs"
### 将 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)
- [Patreon](https://patreon.com/grepular)