站点图标 茶栗栗屋

Vaultwarden/Bitwarden 密码管理服务器,与鉴权相关的讨论评述【自建折腾记3/3】

关于本系列

本文是笔者计划的系列《自建折腾记》的第三篇。此系列记录了笔者在自己刚租的服务器上折腾三个自搭建(self-hosting)服务的过程。但是,如果看列表会发现这个系列从 1/3 直接跳到了 3/3, 这是因为作为 2/3 的 Matrix 服务器已经驾鹤西去了(既因为一些“大人的原因”,又因为 Dendrite 服务器实在是太难用了)。剩下的服务分别是分布式短文社交平台 Misskey 与密码管理器 Bitwarden(实际上用的 Vaultwarden 作为服务端)。

背景

笔者曾经的密码管理可谓是把所有不能犯的错误都犯了——弱密码、所有网站/服务用同一个密码、密码与生日等相关等等。最后,在 000webhost 爆出明文密码泄露后,笔者终于神功大成,实现了裸奔上网——这是笔者第一次被迫改几十个网站的密码。

然而,显然那次并没有吃够教训,只是改了一个稍强的密码,但仍然所有网站共用。因此在某年(忘了哪年),笔者再次神功大成,实现又一次网络裸奔,区别是这次要改的密码数量跑上了100。

在第二次泄露之后,笔者决定彻底改变现状。显然,每个网站/服务需要设置不同的密码,且最好是越随机越好(或者说,熵越大越好)。一个一个记忆如此多的密码是不现实的,而以前杂志上看到的一个方法是通过一个根密码+各个网站的名字,在脑子里计算出一个密码(如根密码是abcdefg,网站域名是 google,则得一个密码 agbocodgelfeg)。然而这样仍有被社工的风险,同时每次输密码来这么算一下也太花脑力了。

而另一条路径是每个网站使用随机生成的密码(足够的熵),并使用密码管理器记忆、自动输入。原先一直不这样做是因为没有找到合适的密码管理器方案:“古时候”浏览器自带的密码管理器直接存在本地,在手机/其它人电脑上没法访问,而 Yubikey 等纯硬件方案有丢失风险;“新时代”浏览器的密码管理器使用 Google 账户甚至国内服务商的账户同步,我十分不放心。即使是 LastPass、1Password 等声称使用端到端加密的方案,也会有数据存在云厂商管理的数据库上,且部分公司曾爆出来泄漏事故。

最后,笔者先后找到了两个可完全自己掌握数据的密码管理方案:KeePass 与 BitWarden。其主要原理就是首先设置一个足够长的“主密码”,且用户需要记住这个主密码。通过这个主密码加密整个密码库其它部分(事实上是由主密码派生出一系列对称和非对称密钥),实现在服务器不知道明文密码的情况下,用户可取出明文密码输入到网站。

KeePass:纯本地方案的尝试

笔者在前些年使用的方案是 KeePass 2。KeePass 是一个 Windows-only 的本地开源密码管理器,且基本没有自动填充功能(提供了一个全局快捷键通过键盘模拟的方式输入密码)。同步使用 WebDAV 进行,而自动填充一般使用非官方浏览器插件 Kee 进行,手机端提供了 Keepass2android 等应用实现同步与自动填充。

KeePass 大概用了 2~3 年,过程中笔者遇到了以下几个问题:

因此,笔者在友人的推荐下了解了 Bitwarden。

Bitwarden 与 Vaultwarden

Bitwarden 为一个开源/SaaS 并行的密码管理器项目。其主数据库存在云端,各客户端通过 HTTP REST API 从服务器上同步库,而不是像 KeePass 那样以一台电脑上的为主数据库,这台电脑把主库通过 WebDAV 同步到云存储后其它端再去同步。另外其还提供了一个网页界面,这样在其它人的完全没有安装客户端的电脑上也能临时取出一个密码进行登录。

Bitwarden 在其安全白皮书中描述了其如何保证用户的密码库安全。主要是通过主密码与邮箱派生出一个 Master Key,利用这一个 Master Key 的 Hash 与服务器进行鉴权(相当于事实上对于服务器上的这个账户而言,你的密码是这个 Master Key 的 Hash,而非主密码或 Master Key 本身——后两者从来不会发到服务器),同时利用这一个 Master Key 保护一个对称密钥。加密后的对称密钥与利用此密钥加密过的密码库存放在服务器上。由于服务器没有 Master Key,无法拿到这个对称密钥,也就无法看到明文密码库。

然而我并不想用 Bitwarden 的官方服务器,也不想自建 Bitwarden 实例,因为官方实现比较吃资源,且笔者也用不到如此丰富的功能,如 OIDC 等。但是,社区有人维护了一个非官方 Bitwarden 服务器项目——Vaultwarden。其使用 Rust 编写,比较节省资源。

服务器安装

本体安装

这次笔者同样使用了 Docker Compose 进行容器的编排。这部分的官方教程:https://github.com/dani-garcia/vaultwarden/wiki/Using-Docker-Compose

直接上 docker-compose.yml 文件内容:

version: "3.6"
services:
  vaultwarden:
    image: vaultwarden/server:latest
    restart: always
    volumes:
      - "./data:/data/"
    ports:
      - "127.0.0.1:9001:80"
      - "127.0.0.1:9002:3012"
    environment:
      - SIGNUPS_ALLOWED=true
      - WEBSOCKET_ENABLED=true
      - DOMAIN=https://分配给vaultwarden的域名

这里有几点注意:

本体没有其它需要配置的(如 .env),也不需要 build,直接 docker compose up 看到没有问题,即可 Ctrl+C 后 docker compose start

反向代理配置

在 Vaultwarden 这种安全至关重要的应用上,笔者强烈建议使用 HTTPS。官网的配置也全部包括 HTTPS 的配置。笔者仍然是使用宿主机上的 Caddy 提供反向代理与 SSL 功能,往 Caddyfile 中追加如下内容:

分配给vaultwarden的域名 {
        handle {
                reverse_proxy 127.0.0.1:9001
        }

        handle /notifications/hub {
                reverse_proxy 127.0.0.1:9002
        }
}

注意域名要和上面 docker-compose.yml 中的一致。追加后重载 Caddy 配置,即可通过浏览器访问你的 Vaultwarden 实例地址。

用户注册与数据导入

进入实例后,点击“创建用户”,创建自己的用户,并进入密码库。

由于笔者已经导入了数据,所以密码库里有密码。如果你在此之前也用过其它密码管理工具(如 KeePass、Chrome 自带管理器等),可以点击上方的工具,并在左侧切换到“导入数据”。选择导入格式后会弹出获得此格式文件的教程,照做即可。

同时笔者在全部密码导入配置好后,也导出了一份加密后的密码库文件,以便不时之需。

在用户注册配置好后,由于此实例为单人实例,我们需要关闭新用户的注册。将 docker-compose.ymlSIGNUP_ALLOWED 后的 true 改为 false,再 docker compose down && docker compose up 即可。

客户端配置与使用

Vaultwarden 作为非官方服务端,近乎完全兼容 Bitwarden 的客户端协议,因此直接使用 Bitwarden 的客户端就可以了,笔者安装的是 Windows 与 Android 平台的客户端程序,以及 Chrome 里的插件。安装后三者提供了十分相似的界面:选择自建实例,输入服务器的地址,再使用邮箱和主密码登录即可。

Windows 和 Android 上的应用程序可以配置为使用指纹快速解锁密码库,同时 Chrome 插件可以连接到 Windows 客户端,利用 Windows 客户端为中转实现指纹解锁,但考虑到安全性笔者基本没有打开这些功能,仅仅保留了 Android 上的指纹解锁(因为手机上每次输快 30 位的密码实在太难受了,PC 上还行)。同时还可以设置超时自动锁定密码库、自动同步等。

Android 上为了使用自动填充,我们还需要打开 Bitwarden 客户端的自动填充权限——在“设置”中选择最上面的“自动填充服务”,按照指引配置即可。

配置好客户端后,我们自然要讲讲密码管理器的两大关键功能——自动创建条目与自动填充。

Bitwarden 的自动条目创建只能用灾难来形容。笔者前面提到了 Kee 插件在这功能上做得有些糟糕,一般是在检查到网页提交密码后,在 Chrome 插件栏上高亮自身,用户点击后再提示是否新建或更新条目。而 Bitwarden 插件正常情况下会在提交了密码后,在网页上方弹出一个通知条,问你是否新建更新条目。问题就在于,这个条大部分需要它的时候它并弹不出来,倒是有的时候不需要(比如已经创建过了,直接按原密码登录时)会弹出来。另外这个条是直接通过 DOM 渲染在网页上的,因此遇到部分全屏的 Web APP 会把画面顶到下面去。笔者在这几个月的使用中,几乎没有成功用过这个自动条目创建功能。

而 Chrome 上的自动填充就要比 Kee 好一些,Kee 很多时候即使检测到网页上有可以填充的框,也不会填充进去,而需要用户在框上右键,选择查找匹配的条目才行。Bitwarden 的话,只要处于解锁状态,一般都会检测到密码框并自动填充进去,但如果进入网页时处于锁定状态,则解锁后(此时会立刻匹配网站并显示出对应的密码条目)需要手动点一下对应出来的密码条目,就自动填充了。Bitwarden 的密码匹配的灵活性上和 Kee 基本一致,但在浏览器扩展端的支持更强(而不是所有高级设置都得去 KeePass 桌面程序里调)。可以配置单条目多 URL,也可以配置 URL 上不同的匹配策略,总体上令人满意。

同时,在密码搜索/匹配窗口,事实上匹配(即搜索框留空,与当前 URL 匹配)与搜索(忽略当前 URL,按搜索框内找条目)两种视图下,显示的内容与点击的逻辑是不同的。

在新建、编辑条目时,可以使用插件提供的较方便的密码生成功能,点击密码框里的箭头图标即可。在框里已经有密码时,点生成器会警告你是否覆盖旧密码。

在此界面里,点密码后面的箭头会重新生成一个密码,而重叠正方形自然是复制。笔者一般会先在此界面复制好密码,再点上方的“选择”,这样生成的密码会进到上一个画面(即条目创建/编辑画面)的密码框中,填好其它条目,保存即可,接下来在注册界面自动填充,即可把新密码填进去(但修改密码时不可以这样做,如果修改界面要填旧密码,那么建议操作是 进入界面自动填充旧密码->生成新密码并复制->覆盖密码管理器的旧密码,注意一定不要填充->保存条目->在网页新密码框里粘贴新密码并提交)。如果填错了,再粘贴剪贴板里的来修正。

同时,在可以开 TOTP 二因素验证的网站上,也可以粘贴 TOTP 密钥到 Bitwarden 条目中,这样在打开条目详细信息时,可以看到当前的 TOTP 一次性密码。

在 Android 端,笔者认为 Bitwarden 的使用体验好于 Keepass2Android,主要是自动填充成功率高得多,大部分应用和网站都能正确弹出自动填充下拉菜单。

讨论

在笔者搭建、迁移密码管理器方案的过程中,已经注意到了“密码”或者说口令这一悠久的鉴权条件,正在失去其地位。一方面,TOTP、邮箱验证等二因素/多因素辅助鉴权得到发展,而另一方面,国内的短信鉴权、国外的 FIDO2/WebAuthn 等无密码登录方案也开始走入主流,单纯使用密码的鉴权方案必将被废弃。同时,OpenID Connect 等 SSO 方案的发展让整个互联网的鉴权趋向集中化。在大家用的的鉴权服务(Google、Facebook 等)值得信任的前提下,这可以被认为是良好的趋势——越小的公司,越容易出现不良的信息安全实践,而大厂的服务至少技术上是过关的。

同时,笔者也打算从用户的角度写一写对各种鉴权方法的评价。

首先是关于鉴权因素的分类,常见的鉴权主要从三个角度入手:

上面这是用户端的“鉴权因素”之分。同时,针对不同的验证环境,也开发出来了不同的认证“体系”(注意这些“鉴权体系”和上面的“鉴权因素”很大程度上是正交的):

列出来各种已有的手段后,笔者分别进行评价。拨开各种鉴权体系带来的层级,用户最终是要在某个地方使用鉴权因子证明自己的身份的,这里事实上干了两件事:

在使用不同的鉴权因素时,上述两阶段的组合关系也可以不同,如用 FIDO2 的时候两者是合在一起做的;但使用密码的时候就需要分开,第一个步骤通过用户提供用户名/邮箱等完成。下方主要集中讨论鉴权这一阶段,除非讨论的手段同时进行认证与鉴权。

同时各种“鉴权因素”并不一定单个使用,而是常常组合使用,先有第一因素,再辅助第二因素。甚至会引入 IP 地址(虽然个人极端反对用 IP 地址作为鉴权因素……说你呢 NFS)、地理位置、设备信息等更辅助性质的因素综合进行鉴权决策。

知道什么

这部分最主要的就是密码,而众所周知密码有多方面的不足:复杂的密码容易遗忘,而简单的密码容易被暴力破解/窥视/碰撞,同时密码本身就容易被冒用,一旦有同密码跨站使用的情况可能被撞库。但这都顶不上密码最大的优点——简单。无论是对用户还是对提供方,使用密码鉴权都是最简单的手段(只有 SMS/E-Mail OTP 能一战)。

当然,还是会有一些实现者、用户错误实现/使用密码鉴权,这里说说笔者遇到过的两种:

因此一个比较平衡的方案是使用本文所述的密码管理器,每个站使用不同的、熵够大的密码,再由管理器自动记忆填充。用户只需要记忆一个主密码,并保管好自己机器上的密码库即可。另一个方案是使用 Yubikey 等,用静态长密码模式设置一个超长的密码,按一下按钮自动填写。这样虽然各站用了同一密码,只要不是后台直接存明文,因为密码够长,不太容易被彩虹表爆破。但笔者认为终究是这种静态长密码是不够强的。

另一种基于“知道什么”的鉴权因素是 PIN (Person Identification Number),其定义比较众说纷纭(银行卡密码也是 PIN,但显然和这里定义不同),笔者这里采微软那边看到的定义——即在设备本地用的叫 PIN;可以上云,或设备间迁移的叫密码。PIN 整个校验放在设备本地,且一个设备的 PIN 不能用来校验另一个设备、PIN 也完全不向外传输,因此安全性上可以放松些(如一般 4~6 位纯数字或数字字母组合就可以当 PIN 了)。

从上面的定义,也可以看到 PIN 只能用作设备开机解锁等本地应用,同时 PIN 也可以用来解锁第二把钥匙,作为“拥有什么”的鉴权因素上的一环,如用 PIN 解开一个长的私钥,系统再用这个私钥去登录服务器。读者可以把 SSH 私钥的 Passphrase 与此类比。

拥有什么

这部分鉴权手段一般基于用户持有某个物品,借助这个物品进行鉴权。这个“物品”的两个通常特性:携带信息量较大(如一个 4096 位长的私钥 vs 一个普通的密码)、可以执行较复杂计算(可以进行私钥计算与应答质询),使得“拥有什么”的鉴权方式更加牢固、灵活。缺点则是用户必须好好保管这个“物品”,否则账户的恢复会十分困难,因为并不能轻易允许用户取消/绕过这些基于“拥有什么”的鉴权,否则就失去了其价值。

具体地讲手段,这里开始就百花齐放了,我们尽量把相同使用场景的放在一起说:

总的说来,笔者在 Web/App 的环境下喜欢的鉴权手段是 FIDO2/WebAuthn 与 TOTP,且最好能用 FIDO2 做无密码鉴权。然而,FIDO2/WebAuthn 有各种模式,不是所有网站都支持所有模式,如很多网站是不能用“platform”模式的 WebAuthn 的,这样就仅支持使用 Yubikey 等专门的硬件进行登录,而不能用手机指纹、Windows Hello 面容验证了。另一方面 TOTP 只能做第二因素,但其十分简单,甚至可以内置在 Bitwarden 里。可惜国内实现 FIDO2/WebAuthn 的应用并不多,更多用的是下面说的变种。

还有几个“变种”(并不知道把这两个放在这是否恰当):

这两个变种一般都拥有较强的鉴权力,且常常被用作单因素鉴权(国内特别多的手机号登录,甚至密码都省了)。同时也多见登录环境异常时,用这两种因素来进行辅助验证,或第一因素不可用(密码遗忘时)的救援措施。

邮箱暂且不说,但笔者事实上对短信一次性密码认证是深恶痛绝的:

是什么

这里主要是生物特征手段,即“一个人本身具有的区别于其它人的、最好终生不变的特征”。常用的有指纹、面容、掌纹、虹膜等。成熟的方案也有很多,这里不赘述。Windows Hello 和苹果的 Touch ID、Face ID 均划进此列。

笔者比较不能忍的是很多国内厂商丧心病狂地滥用生物特征手段,特别是人脸,进行鉴权,甚至在公共区域进行这样的操作(如微信/支付宝刷脸支付)。这样既是安全的漏洞,也是隐私泄露的危机。笔者认为生物特征鉴权的一个红线是,对普通应用而言(即除公安/海关/边检/银行这样的特殊业务外,另外公安是否应该对无案底的人留存高精度的特征数据都值得商榷),任何生物特征数据不应进云端。手机上进行指纹面容识别,一般没有这个问题(指纹的话甚至不会进手机 CPU,会在独立的安全芯片上完全处理),但大街上的自动贩卖机显然是会把你的脸传到云上的。

鉴权体系

锐评完了各个鉴权因素,再来看看各个鉴权体系:

写在后面

一篇讲 Vaultwarden 的文章,花了近一半的篇幅在评述各种鉴权因素与方案,倒也合理。由于我的评述带有较强的主观色彩,且比较站在上帝视角(如站在 2023 年看 1990s 发明的协议),大家看个乐就好。但作为用户(偶然当实现者),还是希望这些评述能反映一些痛点,让更多实现者能支持更合理的鉴权方案、更多用户能选择适合的鉴权因子。

退出移动版