在近期折腾一文写作的时候,其实我已经在尝试使用firejail了。然而当时仅仅是在用它已经提供好的Profile,以及为支持fcitx进行了细微修改,并没有深入去了解其Profile如何写就,以及它究竟能限制什么。
而我由于在使用pass进行密码管理,故而在firefox上使用passff插件以便自动填写密码。然而在默认的firejail profile下,passff无法正常工作。于是我起了去折腾的心,而直到今日终于解决问题。鉴于网上对于firejail的文章很少,中文尤其少,故而觉得可以留篇介绍文章。
其实我好几年前就开始用pass了,但直到最近几个月才发现passff这个插件。
基本firejail知识
Firejail是一个沙盒软件,所以可以期待它的功能也就是沙盒软件的各项功能——文件系统隔离,进程隔离,访问控制等等。它用到了Linux Namespace等机制以便实现沙盒,而且它本身需要SUID(显然)。
我本身也在简单的使用apparmor(最早是为了限制ruijieclient的权限),那么它们两个是什么关系呢?本来它们两个是没太大关系的,毕竟apparmor是直接工作在内核里的,目的是控制进程的访问权限以及其能力(比如可以/不可以修改某文件等);而firejail是沙盒,出了沙盒啥都没了。然而firejail毕竟可以将外部信息暴露给沙盒内的程序,而且也可以进行访问控制,于是这时候它就和apparmor的部分功能有所重叠。而firejail更是提供了一个apparmor整合模式,使得整个事情更复杂了。
所以,其实我并没有弄清楚到底它们俩(当交织在一起时)关系是什么。Arch wiki的相关章节也没有解释得十分清楚——大约是说firejail的apparmor整合会允许apparmor继续在沙盒中工作?
至于「为什么要使用沙盒」……好吧,如果你有这个疑问,那么你可能不适合看本文,建议先去看看沙盒的意义或者多好好用用计算机。
firejail的使用比较简单,只要在想执行的软件前加上firejail
就行,例如firejail firefox
。如果有相应的Profile,那么firejail就会去按照相应的Profile来限制和授予访问权。
而如果没有定义相应的Profile,似乎会按默认Profile来授权?
Firejail的Profile在好几个地方存放,在arch wiki中说得很清楚。许多情况下,它都会载入相应的local Proflie,以便于用户自定义(这其实是需要显式写明的,不过默认的所有Profile都有这句)。而firejail的命令行参数本身也可以对权限进行控制,可以用来调试。
Firejail的Profile还是比较可读的,看看就大概理解什么意思了,然后简单对应一下manpage就能完全理解。当然,书写还是需要对照manpage的。而且它也支持嵌套(include),以便重用。
理所当然地,firejail也支持通过--profile=XXX
在命令行参数中指定要使用的Profile。这样就有一个很方便的调试方案:通过指定想要调试的Profile,打开一个shell,然后在其中进行调试。
对firefox的控制
在firejail的网站上专门有一节讲它是如何控制firefox的权限的。有兴趣理解细节的可以看一看,但核心就是下面的这些内容。
- 文件系统仅暴露必要部分
- /home下仅有当前用户,当前用户下仅暴露
- firefox本身的配置和缓存目录
- Downloads目录
- 其余部分能不暴露就不暴露,能少给权限就少给权限
- /home下仅有当前用户,当前用户下仅暴露
- 对系统工具进行限制,尤其是禁止启动各语言的解释器
- 禁止dbus等的访问
做这一切,其实就是为了最大可能地防止恶意网站通过浏览器漏洞对系统进行恶意访问乃至控制。它默认的Profile安全是安全了,但一些正常的功能也被限制了。如果你打开默认的firefox Profile,你就会发现其中许多注释都是在说「本行目的是xxx,但会导致xxx无法使用」。
中文输入法
那么知道了默认的Profile都做了什么限制,我们就可以着手修改它了。即使对其本身不完全理解,我们也可以自己打开一个shell来检查需要的功能是否存在。
既然我是中文用户嘛,自然也需要中文输入法的。然而在沙盒中,默认配置下是没有办法使用中文输入法的(我的是fcitx)。翻阅了一些资料(不记得了……),查到我需要打开dbus和协议的支持。换句话说,我需要禁止对它们的禁用。
推荐的做法是在用户配置目录下,新建一个相应的local配置文件(比如firefox-common.local
),在其中写上相关内容:
ignore nodbus ignore protocol
再次打开firefox的时候就会发现fcitx已经可以正常工作了。注意该做法是允许外界的fcitx和沙盒内部通信,而不是在沙盒内再开一个fcitx。
我其实没完全弄清楚到底哪些protocol需要被允许。这句ignore protocol
直接允许了所有的,按其man page说就是「unix, inet, inet6, netlink and packet」。哪位有fcitx开发经验的还望不吝赐教。
支持passff
相比起fcitx能找到资料,passff就稍微麻烦一些了——没有资料,只能自己去摸。我在检查后发现,其问题的核心是passff的程序本身(/usr/lib/passff/passff.py
)无法被启动。而passff程序无法被启动,则是因为python解释器没有可执行权限。
于是解法有两个:
- 打开python解释器的可执行权限;
- 想办法将两者打包在一起(类似静态链接)。
理论上来说,后者会更安全一些,毕竟打包在一起可以减少python解释器被非法调用所产生的风险。然而操作上实在比较麻烦,所以我还是选择了方案一。毕竟我并不打算给firefox以其他额外权限,尤其是没给它对我的HOME的任意读取,所以就算python解释器被恶意调用,存在的唯一风险仅取决于firejail的漏洞。
很显然,解法肯定不是chmod +x
,不然也太坑爹了。在调试前,首先去看一眼man firejail-profile
,确定firejail的配置中都能干什么。于是会发现,firejail没有「赋予可执行权限」这么一个配置选项,所以需要从「取消不可执行」这方面着手。
打开firefox的Profile(/etc/firejail/firefox.profile
),会看到其本体并没有什么相关内容,但它包含了另一个文件——firefox-common.profile
。这个文件又继续包含了disable-interpreters.inc
,而它就是我要找的核心部分。
打开disable-interpreters.inc
文件会发现,其按种类禁止了各种解释器的可执行权限。所以解法嘛,自然就是将其反过来。例如:
noblacklist ${PATH}/python3*
显然,这里的${PATH}
代指实际的PATH
环境变量。然而要注意的是,不知是否是firejail机制的问题,如果在这里写/usr/bin/python3*
,则不会被认为和${PATH}/python3*
匹配,因而最终它还是会上黑名单,于是只读。
上面的做法是「打补丁」的形式,所以需要严格匹配原来的内容,以便「补丁」生效。另一种方案是直接完全重写一个Profile,也完全可行。然而我觉得这样写起来比较麻烦,而且更新的时候还要再次去检查,维护起来也比较麻烦。
为了方便的话,也可以直接去包含软件包已经提供好的Profile,而免得自己写漏或是其他:
include allow-python3.inc
解决了python解释器的问题,打开firefox就能发现passff启动成功,但会抱怨没有密码存储。这是因为~/.password-store/
目录不在沙盒中,程序找不到相关文件。解决方案也很理所当然——将它加进去就是了:
whitelist ${HOME}/.password-store/
另外,仔细翻看firefox的Profile会发现,其实firejail已经提供了一些额外配置,尤其是firefox-common-addons.inc
。有需要的可以直接包含或参考它们,免去每次都自己翻找的麻烦。
按说到这里,passff的问题就解决了。然而不要忘了,passff还需要对密码进行解密啊!所以我们还需要进行额外的一步,那就是允许对GPG密钥进行访问:
noblacklist ${HOME}/.gnupg whitelist ${HOME}/.gnupg
白名单和黑名单
眼尖的人会发现,我既写过noblacklist
,又写过whitelist
,那么它们俩(blacklist和whitelist)是什么关系呢?
此事我还真的没有查到简明且完善的解释,唯一的相对靠谱的是arch wiki的内容,但还是没有解释清楚。当然,arch wiki里面倒是提到了它们都是成对出现的,这对于理解稍微有所帮助。在翻阅man page和进行试验之后,我似乎弄明白了一些。
从man page来看,似乎这两个东西只是凑巧叫了个相关的名称,但其工作方法并无关系。首先看到,blacklist是阻止对某目录/文件的访问,彻底的阻止,但不移除;没有被blacklist的文件则默认会维持原状。然后看到,whitelist会自动做两件事:1.将顶层目录作为tmpfs挂载;2.将whitelist的目录和文件bind-mount进去。
注意,whitelist的顶层目录有限,是「home, /dev, /etc, /media, /mnt, /opt, /srv, /sys/module, /usr/share, /var, and /tmp」。出了这些目录,就不能用whitelist了,而得考虑其他方案了(比如直接上bind)。
因而,这两者单独使用的时候都很符合直觉:在没有whitelist但有blacklist的时候,但凡没有blacklist掉的就可以访问,但凡blacklist掉的就没法访问(但文件存在);而没有blacklist但有whitelist的时候,但凡whitelist掉的都可以访问,但凡没有whitelist掉的都无法访问(文件不存在,但可以新建,只不过会退出沙盒会丢失)。于是当两者同时出现时,事情就是两者的交织。
就我个人的实践来看,对一个blacklist掉的东西再写whitelist并不会使得其可以访问,而是需要既noblacklist又whitelist。如果只进行whitelist,那么会得到一个属于nobody的无权访问的目录/文件;如果只进行noblacklist,在沙盒中该目录/文件根本不会存在。所以,它们应是两种不同机制,而且whitelist先考虑,然后再考虑blacklist。
而且额外地,(no)blacklist可以进行更多展开(比如前面用到的${PATH}
),而(no)whitelist则更少(因为其顶层目录有限)。
附——我的配置文件
整理一下前面提到的所有补丁,最终形成的就是这么一个内容:
ignore nodbus ignore protocol noblacklist ${HOME}/.gnupg whitelist ${HOME}/.gnupg whitelist ${HOME}/.password-store/ include allow-python3.inc noblacklist /usr/lib/passff/passff.py
我在这里额外写了一句noblacklist /usr/lib/passff/passff.py
,但这句目前没有任何意义——该文件本身并没有上黑名单。但我还是写了这句,防患于未然嘛。
您可以在Hypothesis上的該群組內進行評論,或使用下面的Disqus評論。