自建 Bitwarden Unified w/ 黑魔法 & 修 WebAuthn

自建 Bitwarden Unified w/ 黑魔法 & 修 WebAuthn

最近看到 Bitwarden 提供了新的自建镜像:Bitwarden Unified,非常感兴趣。

我是 Vaultwarden 的用户,好用那确实也挺好用的,而且能白嫖全功能它不香么?当然众所周知的是这东西没有安全审计,会比较灵;而 Bitwarden 自身毕竟要卖钱,所以相对来说比开源灵车更令人安心一点。

不过 Bitwarden 免费的版本也缺了一些比较重要的功能,比如说 Login Item 的 TOTP 验证码生成(免费版依然能保存 TOTP Token,但是生成六位数验证码是高级会员功能),比如说用户登录的 WebAuthn 2FA,等等。

之前 Bitwarden 官方服务端的自建看着就让人毫无欲望,而现在的单 Docker 让我觉得还行,于是就来试试吧。

途中还搜到了黑魔法,可以在官方服务端上解锁高级会员功能

Requirements

  • Docker Engine(& Docker Compose)
  • HTTPS(证书喂进 Docker 里的 nginx 或使用 HTTP ← 反向代理 ← HTTPS 流量)
  • 能用 SMTP 发邮件的用户(管理员登录和邮件地址验证)

Docker Compose

直接用官方给的 Compose Template 就行了,当然用于存储环境变量的 settings.env 也要记得配置;数据库密码不改也没问题吧

当然,如果你想用 docker run 也是可以的,就不在这里赘述了。所有有关内容都可以在官方文档查询到。

这里只提一些注意事项:

  • 目前不用 Volume 的话启动之后就会 Bad Gateway,所以涉及到几个 Volume 的部分就不要改动了。
    • 现在(2023/01/11)有人提了 issue,所以从日志来看的话应该是 nginx 服务遇到权限问题了。不知道会不会修让 bind mount 也可以正常使用。
  • 如果用反向代理的话(这 Docker 镜像里其实已经塞了一个 nginx 了),例如我用的 Cloudflare Tunnel,只需要映射 HTTP 端口并且在 settings.env 里禁用 SSL,但是反向代理那侧一定要加上 SSL,否则会炸锅。
  • 数据库可以用 sqlite,可以只指定类型,路径有默认值[1]
    • 其实在本文发出来的时候还不太行,但是本文更新时已经基本没什么问题了。yào使用 dev 分支的镜像,目前 SQLite 基本 patch 好了(不过 org 相关的功能有点问题,暂不确定是不是数据库的问题)。

如果你配置完没有遇到任何问题,那么 docker compose pull && docker compose up -d 之后,你就能在你指定的端口见到一个正常的响应。

黑魔法

然后我就开始思考,既然自建 Bitwarden 采取的是上传 license file 的方法验证,那么有没有办法动点手脚呢?稍微研究一下官方服务端的代码之后,可以发现验证 license file 其实就是验证数字签名。所以,如果能替换掉用于验证的证书,那么就有了自签 license 的操作可能。

一通 Google 之后很快找到了这个:jakeswenson/BitBetter: Modify bit warden to provide my own licensing for self hosting

再一点进 Issues 区发现了这个:It works fine with Bitwarden unified. · Issue #154

感谢互联网,又可以摸大鱼了。

修改 Compose 配置

首先把 Issue 里的 Dockerfile 存下来备用。

Dockerfile >folded
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# Build BitBetter for API
FROM mcr.microsoft.com/dotnet/sdk:6.0 as buildapi
RUN cd / && git clone https://github.com/jakeswenson/BitBetter.git && cd /BitBetter && git checkout b819fe0c7de96f6fec99db0524b66b2a5ab261e8
# Import cert
COPY certs/cert.cert /newLicensing.cer
# Change paths for Bitwarden Unified
RUN sed -i 's#/app/Core.dll#/app/Api/Core.dll#g' /BitBetter/src/bitBetter/Program.cs && \
cd /BitBetter/src/bitBetter && set -e ;set -x; \
dotnet restore && \
dotnet publish

# Build BitBetter for Identity
FROM mcr.microsoft.com/dotnet/sdk:6.0 as buildidentity
RUN cd / && git clone https://github.com/jakeswenson/BitBetter.git && cd /BitBetter && git checkout b819fe0c7de96f6fec99db0524b66b2a5ab261e8
# Import cert
COPY certs/cert.cert /newLicensing.cer
# Change paths for Bitwarden Unified
RUN sed -i 's#/app/Core.dll#/app/Identity/Core.dll#g' /BitBetter/src/bitBetter/Program.cs && \
cd /BitBetter/src/bitBetter && set -e ;set -x; \
dotnet restore && \
dotnet publish

# Build licenseGen, when built, run the following in the created container:
# dotnet /licensegen/licenseGen.dll --core /app/Api/Core.dll --cert /certs/cert.pfx interactive
FROM mcr.microsoft.com/dotnet/sdk:6.0 as licensegen
RUN cd / && git clone https://github.com/jakeswenson/BitBetter.git && cd /BitBetter && git checkout b819fe0c7de96f6fec99db0524b66b2a5ab261e8
RUN cd /BitBetter/src/licenseGen && set -e ;set -x; \
dotnet add package Newtonsoft.Json --version 13.0.1 && \
dotnet restore && \
dotnet publish

# Final Bitwarden image
FROM bitwarden/self-host:beta

# Import all certs for using with licensegen
COPY --chown=1000:1000 certs/* /certs/
# Import built licensegen tool
COPY --chown=1000:1000 --from=licensegen /BitBetter/src/licenseGen/bin/Debug/netcoreapp6.0/publish/* /licensegen/
# Import cert, needed for some reason
COPY --chown=1000:1000 certs/cert.cert /newLicensing.cer
# Import BitBetter (api)
COPY --chown=1000:1000 --from=buildapi /BitBetter/src/bitBetter/bin/Debug/netcoreapp6.0/publish/* /bitBetter/
# Run BitBetter to modify api Core.dll
RUN cd /tmp && dotnet /bitBetter/bitBetter.dll && \
mv /app/Api/Core.dll /app/Api/Core.orig.dll && \
mv /tmp/modified.dll /app/Api/Core.dll && \
rm -rf /BitBetter
# Import BitBetter (identity)
COPY --chown=1000:1000 --from=buildidentity /BitBetter/src/bitBetter/bin/Debug/netcoreapp6.0/publish/* /bitBetter/
# Run BitBetter to modify identity Core.dll
# Remove temp files after that
RUN cd /tmp && dotnet /bitBetter/bitBetter.dll && \
mv /app/Identity/Core.dll /app/Identity/Core.orig.dll && \
mv /tmp/modified.dll /app/Identity/Core.dll && \
rm -rf /BitBetter

Dockerfile 里固定了一个 commit hash,算是审计之后没有问题代码的版本。

这个时候文件目录结构大概这样子:

1
2
3
4
bitwarden-unified
├── docker-compose.yml
├── Dockerfile
└── settings.env

然后对 docker-compose.yml 做一点修改即可。

1
2
-     image: ${REGISTRY:-bitwarden}/self-host:${TAG:-beta}
+ build: .

生成证书

不过,在 docker compose up 之前,还需要生成一下证书。

新建一个 certs 文件夹,在里面跑一下 BitBetter 的 README 里的几条 openssl 命令即可。

1
2
3
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.cert -days 36500 -outform DER -passout pass:test
openssl x509 -inform DER -in cert.cert -out cert.pem
openssl pkcs12 -export -out cert.pfx -inkey key.pem -in cert.pem -passin pass:test -passout pass:test

第一条运行的时候会让填些信息,随意填写即可。

生成好之后目录结构大概长这样:

1
2
3
4
5
6
7
8
9
bitwarden-unified
├── certs
│ ├── cert.cert
│ ├── cert.pem
│ ├── cert.pfx
│ └── key.pem
├── docker-compose.yml
├── Dockerfile
└── settings.env

跑起来

生成好证书之后就可以开跑了。

首先确保没有正在运行的实例,有的话 docker compose down 关一下。

然后就可以老三样啦。

1
2
3
# docker compose down
docker compose pull
docker compose up -d

签 license

先看看容器名称

1
docker ps

然后 sh 进去

1
docker exec -it bitwarden-unified-bitwarden-1 sh

按照 Dockerfile 注释里的说明

1
dotnet /licensegen/licenseGen.dll --core /app/Api/Core.dll --cert /certs/cert.pfx interactive

填写一下信息即可。注意用户 GUID 或服务器安装 ID 可能需要从 Admin Portal 获取,而登录的前提是 SMTP 工作正常,所以在这之前要确保邮件功能是 OK 的。

当然麻烦一点的话,从数据库里提应该也不是不可能就是了。

生成的 license 会以文本显示,复制保存为 json 上传就好啦。

修 WebAuthn

Bitwarden 已经修复了这个问题

(我提的 issue,还有位老哥 SSO 出问题了也提了一个,也涉及到 connector 页面)

然后是老大难问题:客户端的 WebAuthn 不工作。Vaultwarden 之前会有这问题算是我预料之中,官方的也有就有点 unexpected 了。

首先来回味一下 Vaultwarden 那边是怎么挂的:Windows 10 Desktop App and WebAuthn not working (mobile, browser extensions work fine)

然后在 Windows 客户端实际测试一下,打开 Dev Tools。可以发现 Vaultwarden 的死因是 Content-Security-Policy 头,而 Bitwarden Unified 的死因变成了 X-Frame-Options 头。

Vaultwarden 的贡献者建议修改 ALLOWED_IFRAME_ANCESTORS 配置,添加 file://(客户端的网页地址),这控制的显然就是 CSP 头了。

但是我在读了一下源代码之后,发现™这里[2]又恰好在 *-connector.html 页面把 CSP 头给排除了…… 所以这改了到底有没有用啊(恼

后来发现,是我的 nginx 配置中包含了过于安全的自动添加安全相关 headers 内容。对不起 Vaultwarden,我错怪你了!理论上在请求 Vaultwarden 的 *-connector.html 路径时,上述两个 headers 应该都不会被自动附加,所以应当参考 Vaultwarden Wiki 中提供的配置进行配置,里面还包含了用于推送更新的 WebSocket 相关配置。

而 Bitwarden Unified 和完整版官方服务端表现不一致的原因也找到了,Unified 的 nginx 配置模板[3]和完整版的 nginx 配置模板[4]存在一定的差异。在完整版的 nginx 配置中包含了这样一段,也许就处理掉了 Content-Security-PolicyX-Frame-Options 这两个响应头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  location = /duo-connector.html {
proxy_pass http://web:5000/duo-connector.html;
}

location = /webauthn-connector.html {
proxy_pass http://web:5000/webauthn-connector.html;
}

location = /webauthn-fallback-connector.html {
proxy_pass http://web:5000/webauthn-fallback-connector.html;
}

location = /sso-connector.html {
proxy_pass http://web:5000/sso-connector.html;
}

{{#if Captcha}}
location = /captcha-connector.html {
proxy_pass http://web:5000/captcha-connector.html;
}

location = /captcha-mobile-connector.html {
proxy_pass http://web:5000/captcha-mobile-connector.html;
}
{{/if}}

Captcha 的部分好像和我关系不大,所以我略过了;其他的部分,我修改了一下移植到了 Unified 的 nginx 配置中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
   location = /app-id.json {
root /app/Web;
include /etc/nginx/security-headers.conf;
proxy_hide_header Content-Type;
add_header Content-Type $fido_content_type;
}

+ location = /duo-connector.html {
+ root /app/Web;
+ include /etc/nginx/security-headers.conf;
+ add_header X-Robots-Tag "noindex, nofollow";
+ }
+
+ location = /webauthn-connector.html {
+ root /app/Web;
+ include /etc/nginx/security-headers.conf;
+ add_header X-Robots-Tag "noindex, nofollow";
+ }
+
+ location = /webauthn-fallback-connector.html {
+ root /app/Web;
+ include /etc/nginx/security-headers.conf;
+ add_header X-Robots-Tag "noindex, nofollow";
+ }
+
+ location = /sso-connector.html {
+ root /app/Web;
+ include /etc/nginx/security-headers.conf;
+ add_header X-Robots-Tag "noindex, nofollow";
+ }
+
location /attachments {
alias /etc/bitwarden/attachments/;
}

那么我修的方式也比较灵车:docker exec -u root ... 进去,然后直接在里面修改 /etc/nginx/http.d/bitwarden.conf

我知道我在灵车漂移,这不是一个好的示例,但我懒了。好孩子请不要学我!

经过这么一番折腾,(Windows)客户端上的 WebAuthn 也能正常工作了。不过我在测试的时候也发现一个离奇的事情:Android 上的 WebAuthn 好像炸了,不知道是 Android 或者 MIUI 或者 Google 谁的问题,非常奇妙。

终于能在客户端里用的 WebAuthn

导入和导出

即使同是 Bitwarden-compatible,导出密码库的时候还是会丢失一些东西,比如回收站和密码历史记录。看起来,这似乎是 Bitwarden 官方的设计;而我觉得这不太好。

所以如果你对这些东西比较在乎的话,最好在一个 instance 里一直用下去,不要轻易跑路。(

理论上来说通过迁移数据库也存在完整转移账号的可能,不过我不太想再去读 Vaultwarden 和 Bitwarden 的数据库结构差异了,跑(

总结

最后来点不负责任的特性对比吧,XD

特性 Bitwarden 官服 自建 Unified + 黑魔法 自建 Vaultwarden
代码审计 只需审计黑魔法
云存储 按需加钱 取决于你的硬盘大小 取决于你的硬盘大小
付费功能 氪金 FREE FREE
功能更新 最新 Almost™ 最新[5] 部分特性未实现[6]
HIBP[7] FREE (?) 需要氪金购入 API Key 需要氪金购入 API Key
SLA 比自建高 取决于你的运维水平 取决于你的运维水平

  1. https://github.com/bitwarden/server/blob/aa1f443530fd2b2fcd9bc236eb813fd4006bbc97/docker-unified/Dockerfile#L188 ↩︎

  2. https://github.com/dani-garcia/vaultwarden/blob/10dadfca068ed449fcd4a74b70ae2cd83990d3d4/src/util.rs#L44 ↩︎

  3. https://github.com/bitwarden/server/blob/aa1f443530fd2b2fcd9bc236eb813fd4006bbc97/docker-unified/hbs/nginx-config.hbs ↩︎

  4. https://github.com/bitwarden/server/blob/aa1f443530fd2b2fcd9bc236eb813fd4006bbc97/util/Setup/Templates/NginxConfig.hbs ↩︎

  5. dev Docker 镜像就是 bitwarden/server 的 latest commit,一般会在提交一段时间后由 CI 编译并推送 ↩︎

  6. Home · dani-garcia/vaultwarden Wiki (github.com) ↩︎

  7. Have I Been Pwned: API key ↩︎

作者

星野 みなと

发布于

2023-01-05

更新于

2023-01-12

许可协议

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×