Vaultwarden VPS 自托管密码管理器部署教程
在 Ubuntu 24.04 VPS 上用 Docker 部署 Vaultwarden,兼容官方 Bitwarden 客户端。了解 HTTPS 前置要求、关闭公开注册、管理令牌、Fail2ban,以及经过恢复测试的备份方案。
构建内容
这是一个完全由您掌控的密码管理器:在一个小型容器中运行 Vaultwarden,由反向代理终止 HTTPS;手机、笔记本电脑和浏览器中的官方 Bitwarden 应用都连接到该实例。Vaultwarden 使用 Rust 重新实现 Bitwarden 服务器 API,并采用与 bitwarden.com 相同的协议,因此所有官方客户端都无需修改即可使用;但它只需约 100 MB 内存,而官方多容器堆栈需要更多资源。
安装过程只需十几行 Compose 配置。真正重要、也最容易出问题的有三点:在首次加载 Web 密码库之前,必须先配置 TLS;创建您自己的账户后,必须立即关闭公开注册;必须备份数据卷并测试恢复,因为该目录存放着您拥有的所有密码。
前置条件和必须了解的实际限制
- 一台 VPS,安装 Docker Engine 和 Compose plugin,运行在全新的 Ubuntu 24.04 KVM 实例上,并拥有 root 或 sudo 权限。512 MB 内存确实足够;1 GB 使用起来更宽裕。这是可运行的最轻量服务之一,位于值得自行托管的服务清单前列。但仍应根据同机运行的其他服务确定实例规格:在同一 VPS 上部署PhotoPrism 或 Immich 等自行托管的照片库,内存最低需求会升至数 GB,而 Vaultwarden 几乎不会增加内存需求。后续添加媒体前端时也适用同样的计算方式,因为将 Jellyfin 媒体库改造成可步入的 90 年代录像带租赁店,意味着在同一资源预算中增加一个常驻容器和转码余量。
- 一个配置了 A 记录的域名;如果使用 IPv6,还应配置 AAAA 记录,并将其指向 VPS 上的
vault.example.com。TLS 证书会针对这个确切域名签发,因此在开始部署前,DNS 必须已能正确解析。 - 将端口 80 和 443 开放到互联网,并由反向代理终止连接,绝不能由 Vaultwarden 直接终止。端口 80 仅用于 ACME 证书质询和 HTTP 到 HTTPS 的重定向。
- 首要且最容易被忽略的限制是:Bitwarden 客户端拒绝连接到非 HTTPS 服务器。不存在“先通过 http 测试”的做法,因为该路径无法工作,具体原因见下一节。
为什么选择 Vaultwarden,而不是官方 Bitwarden 堆栈
客户端相同,但资源占用只有一小部分。官方自托管 Bitwarden 以多个容器组成一个套件,包括 MSSQL、Nginx、Identity、Api、Admin 等,通常需要约 2 GB 内存。Vaultwarden 是一个单独的二进制文件,默认将所有数据存储在 SQLite 数据库中,空闲时仅占用几十 MB。对于个人、家庭或小型团队,它显然更合适。由于它忠实实现了 Bitwarden API,您的数据可在 Vaultwarden 与 bitwarden.com 之间迁移。
您放弃的是大部分企业级功能:不支持 SCIM 预配(不过,实验性的 OpenID Connect SSO 已在 1.35.0 中加入)。此外,您需要自行负责运维,因此补丁更新、HTTPS 和备份都由您处理。本指南将介绍这三项工作。
为何 HTTPS 不是可选项
Bitwarden Web Vault 和浏览器扩展会在浏览器中使用 Web Crypto API(window.crypto.subtle)派生加密密钥。浏览器只会在安全上下文中提供 crypto.subtle,也就是 HTTPS 或特殊的 http://localhost。在普通的 http://vault.example.com 上,该功能为 undefined,因此应用一旦派生密钥就会抛出异常,控制台会显示:
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'importKey')页面会卡住或显示通用加密错误,用户无法登录。桌面端、移动端和浏览器客户端都会针对自托管 URL 执行自身检查;如果端点使用 http 或无法访问,它们会拒绝连接,并显示:
This is not a recognized Bitwarden server. You may need to check with your provider or update your server.两者的原因相同:没有有效的 HTTPS。因此,我们首先配置 TLS,绝不通过 http 打开 Vault,即使只是临时查看也不例外。
第 1 步:DNS 和反向代理(先配置 TLS)
将记录指向您的 VPS,并确认它解析到正确的地址:
dig +short vault.example.com输出的这一行必须是您的 VPS IP。如果为空或不正确,请修复 DNS 并等待 TTL 生效。对于无法解析的域名,证书签发会失败。
本指南使用 Traefik 作为 HTTPS 入口。它会自动签发和续期 Let's Encrypt 证书,并可直接集成到 Compose 中。如果您尚未运行 Traefik,请先按照配置 Traefik 反向代理和自动 TLS操作;该过程会创建一个外部 Docker 网络(下面的 proxy)和一个 ACME resolver(letsencrypt),Vaultwarden 服务会连接到它。对于 Vaultwarden,使用配置了手动签发证书的普通 nginx,效果完全相同。
更倾向于使用 nginx 和 Certbot,而不是 Traefik? 将 Vaultwarden 放到 127.0.0.1:8080 上(在服务中添加 ports: ["127.0.0.1:8080:80"],并删除 Traefik labels),然后签发证书并将请求代理到该服务。证书部分请参阅使用 Certbot 和 nginx 签发 Let's Encrypt 证书。需要额外配置的是 notifications 路径上的 WebSocket 升级:
server {
listen 443 ssl;
server_name vault.example.com;
client_max_body_size 525M;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}注意 X-Real-IP 这一行。它使 Fail2ban 稍后能够看到真实的攻击者,而不是 127.0.0.1。除此之外,无论前端使用 Traefik 还是 nginx,本指南的其他步骤完全相同。
第 2 步:Compose 文件
先创建项目目录。本指南使用 /opt/vaultwarden,以便让 Compose 项目名称以及数据卷的名称 vaultwarden_vw-data 保持可预测;下文的 Fail2ban 和备份步骤依赖这个确切名称。
sudo mkdir -p /opt/vaultwarden
cd /opt/vaultwarden在该目录中创建一个用于存放管理员密钥的 .env 和 Compose 文件。
# .env
ADMIN_TOKEN=paste-a-strong-token-here使用 openssl rand -base64 48 生成该令牌并粘贴到文件中。(下一节会介绍更强的哈希形式;开始时使用较长的随机字符串即可。)
# docker-compose.yml
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
DOMAIN: "https://vault.example.com"
SIGNUPS_ALLOWED: "true" # closed in Step 4, keep true just to register
ADMIN_TOKEN: "${ADMIN_TOKEN}"
IP_HEADER: "X-Forwarded-For" # X-Real-IP if your proxy sends that instead
LOG_FILE: "/data/vaultwarden.log"
LOG_LEVEL: "warn"
volumes:
- vw-data:/data
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.vw.rule=Host(`vault.example.com`)"
- "traefik.http.routers.vw.entrypoints=websecure"
- "traefik.http.routers.vw.tls.certresolver=letsencrypt"
- "traefik.http.services.vw.loadbalancer.server.port=80"
volumes:
vw-data:
networks:
proxy:
external: true该文件中有两项设置决定了整个设计。文件中没有 ports: 映射,因此 Vaultwarden 只能通过 Traefik 及其 TLS 访问;在主机上发布该端口会导致人们意外地通过 http 提供密码库。DOMAIN 必须是完整的公网 HTTPS URL:它会写入附件链接、WebAuthn 2FA 和通知端点,因此即使网站可以加载,URL 错误或使用 http 也会导致这些功能失效。latest 标签是对通常禁止使用 latest 规则的有意例外。Vaultwarden 将稳定版本作为单个滚动镜像发布,并使用 :testing 作为独立的预发布渠道,因此应有计划地更新,并在拉取镜像前快速查看发行说明。不过,这个例外范围有限:对于大多数长期运行的容器,最好固定到确切的标签,这样 在同一 VPS 上自托管的常驻代理 就能在重启和拉取镜像后保持可预测。
启动服务并监控日志:
docker compose up -d
docker compose logs -f vaultwarden正确启动后,末尾应出现类似 Rocket has launched from http://0.0.0.0:80 的行。等待 Traefik 用几秒钟获取证书,然后加载 https://vault.example.com;此时应看到带有效挂锁且没有证书警告的 Bitwarden Web 密码库。
第 3 步:强健的 ADMIN_TOKEN 以及 $$ 陷阱
ADMIN_TOKEN 用于保护 /admin。该面板可以读取您实例中的所有用户和设置,因此应将其视为 root 密码。支持以下两种形式。
简单形式是您已使用 openssl rand -base64 48 生成的随机字符串。由于 base64 从不包含 $,因此可以直接写入 .env,无需转义。
更安全的形式是 Argon2 PHC 哈希,因此磁盘上不会存储明文令牌。针对同一镜像生成哈希:
docker run --rm -it vaultwarden/server /vaultwarden hash --preset owasp该命令会提示您输入两次,并输出一个以 $argon2id$v=19$... 开头的字符串。这里有一个容易浪费一小时的陷阱:Docker Compose 会将 $ 视为变量插值,因此将哈希粘贴到 Compose 文件时,必须将每个 $ 加倍为 $$。将其直接放在 environment: 下方,不要通过 .env 写入,也不要用引号包裹:
environment:
ADMIN_TOKEN: $$argon2id$$v=19$$m=19456,t=2,p=1$$c29tZXNhbHQ$$RdescudvJCsgt3ub+b+dWRWJTmaaJObG如果保留单个 $ 符号,Compose 会发出 The "argon2id" variable is not set 警告并将令牌置空,随后 /admin 会拒绝您输入的正确密码。运行 docker compose up -d,并将您在提示符中输入的明文令牌保存在自己的密码管理器中。
第 4 步,注册账户,然后关闭注册
使用 SIGNUPS_ALLOWED: "true" 打开 https://vault.example.com,点击 Create account,然后使用您的电子邮件地址和强主密码注册。此主密码无法恢复,也没有重置功能,因此请先将其存放在可靠的位置。
现在关闭注册功能。编辑 Compose 文件,关闭账户注册:
SIGNUPS_ALLOWED: "false"使用 docker compose up -d 重新应用配置。此项加固不能延后。注册功能保持开放时,任何找到该 URL 的人都可以在您的服务器上创建账户,爬虫也会这样做。他们无法读取您的密码库,但会消耗资源,并将您的私有实例变成开放服务。确认注册功能仍处于开启状态的方法是:/admin 列出了您从未创建的账户。
以后如需添加家人或团队成员,而不重新开放公开注册,请在 /admin 中使用 Invite User 按钮;此流程需要配置 SMTP,以便受邀者收到邀请链接。
第 5 步:访问 /admin
访问 https://vault.example.com/admin,输入明文 admin 令牌(随机字符串,或您进行哈希处理的密码,而不是哈希值本身)。在管理面板中,您可以列出用户、调整设置、发送测试邮件,以及创建数据库快照。
如果页面返回 404 Not Found,说明 ADMIN_TOKEN 为空或未设置,这会完全禁用管理面板。如果您始终不需要该面板,这也是有效的配置选择。如果页面可以加载,但拒绝您的令牌,请参阅下方故障列表中的 $$ 转义问题。忘记令牌后无法通过提示恢复;请编辑 .env 或 Compose 文件,设置新的令牌,然后 docker compose up -d。
第 6 步:连接 Bitwarden 客户端
每个官方客户端都可以连接到自托管服务器。因此,请从常规应用商店安装 Bitwarden 桌面端、移动端或浏览器客户端,不需要使用专门的 Vaultwarden 构建版本。
登录前,打开登录界面上的设置齿轮图标(标记为 Self-hosted 或 Region → Self-hosted),将 Server URL 设置为 https://vault.example.com,然后保存。接着使用注册时填写的电子邮件地址和主密码登录。客户端应立即连接,并提供自动填充和保存凭据的功能。
如果客户端显示 This is not a recognized Bitwarden server. You may need to check with your provider or update your server.,则 URL 错误、使用了 http,或证书不受信任。请先在浏览器中重新确认 https://vault.example.com 是否能正常加载。其他设备上的更新延迟由 WebSocket 推送导致,下面会介绍相关内容。
登录端点的 Fail2ban jail
Vaultwarden 会将每次失败的登录记录到 LOG_FILE 指定的文件中,这正是防暴力破解机制所需的信息。如果尚未运行 Fail2ban,请参阅Fail2ban SSH 加固指南了解安装和基本配置;这里仅为 vault 添加一个 jail。
首先查找命名卷在主机上的位置,以便 Fail2ban 读取日志:
docker volume inspect vaultwarden_vw-data --format '{{ .Mountpoint }}'该命令会输出类似 /var/lib/docker/volumes/vaultwarden_vw-data/_data 的内容;日志位于其中的 vaultwarden.log。创建过滤器:
# /etc/fail2ban/filter.d/vaultwarden.conf
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =然后创建 jail:
# /etc/fail2ban/jail.d/vaultwarden.local
[vaultwarden]
enabled = true
filter = vaultwarden
logpath = /var/lib/docker/volumes/vaultwarden_vw-data/_data/vaultwarden.log
banaction = iptables-allports
chain = DOCKER-USER
maxretry = 5
findtime = 600
bantime = 3600使用 sudo systemctl restart fail2ban 重新加载,并使用 sudo fail2ban-client status vaultwarden 确认。
以下 3 个 Docker 细节决定了该配置是否真正有效。首先,如果日志在每次失败尝试中都显示 IP: 127.0.0.1 或代理服务器的地址,说明 Vaultwarden 封禁的是代理服务器。请将 IP_HEADER 设置为代理实际发送的请求头:Traefik 使用 X-Forwarded-For,上文的 nginx 配置块使用 X-Real-IP,Cloudflare 后使用 CF-Connecting-IP。其次,正确的 iptables 链取决于代理的运行方式:如果 Traefik 作为发布端口的容器运行,流量会经过 Docker 的 FORWARD 路径,因此必须像上面一样将封禁规则放入 DOCKER-USER;但如果在第 1 步选择了主机 nginx 方案,连接会在主机的 INPUT 链中由 nginx 终止,DOCKER-USER 封禁规则无法看到这些连接。此时请删除 chain = DOCKER-USER 行,让 Fail2ban 使用默认的 INPUT 链。第三,应使用 banaction = iptables-allports,而不是基于端口的默认值。此 jail 未定义端口,在 DOCKER-USER 中执行全端口封禁可以直接阻止攻击者访问该主机上发布的所有服务。
备份密码库,然后实际执行一次恢复
vw-data 卷就是您的密码管理器。它包含 db.sqlite3(每个条目)、attachments/ 和 sends/ 目录、用于签署登录会话的 rsa_key.* 文件,以及管理面板中的 config.json。备份遗漏其中任何一项,真正需要恢复时都可能失败。
Vaultwarden 写入 db.sqlite3 时复制该文件,可能会得到一个只写入了一半的损坏文件。因此请创建冷快照,停机时间只需几秒:
#!/usr/bin/env bash
set -euo pipefail
STAMP=$(date +%F)
DEST=/root/vw-backups
VOL=$(docker volume inspect vaultwarden_vw-data --format '{{ .Mountpoint }}')
mkdir -p "$DEST"
docker compose -f /opt/vaultwarden/docker-compose.yml stop vaultwarden
tar czf "$DEST/vw-$STAMP.tgz" -C "$VOL" .
docker compose -f /opt/vaultwarden/docker-compose.yml start vaultwarden将其配置为每晚通过 cron 执行,并将 .tgz 复制到服务器之外。只保存在受保护服务器上的备份不能算备份。可靠的传输方式是每晚使用 restic 备份到另一台服务器或对象存储;它会加密归档,并自动对重复快照进行去重。管理面板中的 Backup Database 按钮可以方便地对 SQLite 文件单独创建热备份,但不会包含附件和密钥。
下面这一步可以区分真正的备份和仅凭希望的备份:恢复一次,并验证它确实可用:
mkdir -p /tmp/vw-restore
tar xzf /root/vw-backups/vw-2026-07-15.tgz -C /tmp/vw-restore
docker run --rm -p 127.0.0.1:8888:80 -v /tmp/vw-restore:/data vaultwarden/server在您的笔记本电脑上,使用 ssh -L 8888:127.0.0.1:8888 you@your-vps 建立隧道,然后打开 http://localhost:8888。由于 localhost 是安全上下文,crypto.subtle 可用,因此密码库可以在这里通过纯 http 解密;这是唯一允许这样做的位置。使用主密码登录,并确认条目都存在:如果存在,说明数据库、RSA 密钥和主密码都能完整恢复,您可以在几分钟内于新的 VPS 上重建服务。使用 Ctrl-C 停止容器,然后删除 /tmp/vw-restore。对于服务器上其他不应直接暴露到互联网的管理界面,也应保持使用隧道的习惯;例如,您也可以通过这种方式访问运行在端口 5173 上的自托管 open-kritt 安全扫描器。
故障模式及其对应的提示字符串
浏览器控制台中出现 Cannot read properties of undefined (reading 'importKey')。 Vault 通过 http 加载,因此 crypto.subtle 未定义。只能通过 https:// 访问它,并在代理中添加 HTTP 到 HTTPS 的重定向。
客户端中出现 This is not a recognized Bitwarden server...。 Server URL 使用了 http、拼写错误,或证书不受信任。确认 https://vault.example.com 显示有效的挂锁图标,然后在客户端的自托管设置中重新输入该 URL。
/admin 拒绝正确的密码。 Argon2 哈希的转义字符丢失。Compose 中每个 $ 都必须写成 $$。也可能是输入了哈希,而不是哈希所表示的明文密码。
跨设备同步缓慢,控制台显示 WebSocket connection to 'wss://vault.example.com/notifications/hub' failed。 代理没有转发 Upgrade/Connection 标头。Traefik 会自动处理这些标头;nginx 需要添加步骤 1 中的两行升级配置。Vault 仍可正常工作,但只会在打开时同步。旧的专用端口 3012 从 v1.31.0 开始已移除,因此不需要单独配置 WebSocket 路由。
Fail2ban 报告已封禁,但攻击者仍在连接。 它封禁的是 127.0.0.1,原因是 IP_HEADER 配置错误,或者封禁规则位于错误的 iptables 链中。设置 chain = DOCKER-USER 和 banaction = iptables-allports。
升级
拉取新镜像并重新创建容器;命名卷和所有数据都会保留:
docker compose pull
docker compose up -dVaultwarden 会频繁发布新版本。请关注项目的发行说明,不要固定到某个补丁版本,因为部分版本包含迁移说明。执行任何重大升级前,都应先创建新的备份;如需回滚,可将 tarball 恢复到新卷中。
FAQ
Vaultwarden 与 Bitwarden 相同吗?
Vaultwarden 是兼容的独立服务器,并非官方服务器。Vaultwarden 使用 Rust 重新实现了 Bitwarden 服务器 API,因此官方桌面、移动端、浏览器和 CLI 客户端都可以连接到它运行,同时所需资源仅为官方技术栈的一小部分。密码库格式相同,因此可以通过导出和导入双向迁移。
我真的需要 HTTPS 吗?还是可以在 LAN 上通过 http 运行?
除 localhost 测试外,其他情况都需要 HTTPS。Bitwarden Web 密码库和扩展使用浏览器的 Web Crypto API,而该 API 只在安全上下文中可用。因此,通过普通 http 访问时,客户端会抛出 Cannot read properties of undefined,并且始终无法登录。唯一可用的 http 地址是 http://localhost,所以第 8 步中的恢复测试使用 SSH 隧道。
如何阻止陌生人在我的服务器上注册?
在 Compose 文件中设置 SIGNUPS_ALLOWED: "false",并在创建自己的帐户后立即运行 docker compose up -d。之后,通过 /admin 中的 Invite User 按钮添加新用户。该功能需要配置 SMTP,以便用户收到邀请链接。定期检查管理员用户列表,确认没有出现意外帐户。
如何备份我的 Vaultwarden 密码库?
短暂停止容器,然后归档整个 vw-data 卷、db.sqlite3、attachments/、sends/、config.json 以及 rsa_key.* 文件,再将归档文件复制到服务器之外,最好通过 nightly cron 定时执行。服务器运行时复制正在使用的 SQLite 文件可能生成损坏的快照,因此应停止服务后再备份。最重要的是,先将备份恢复到一次性容器中并登录,确认备份确实可用后再依赖它。
自托管密码真的安全吗?
安全,前提是完成本指南涵盖的 3 件事:使用真正的 HTTPS、关闭注册并设置强管理员令牌,以及测试备份。密码库使用主密码在客户端加密,因此即使服务器也无法看到明文密码;没有主密码,被窃取的 db.sqlite3 也没有用处。代价是补丁更新和备份现在由您负责,因此 Fail2ban 和恢复演练在这里都不是可选项。完成这些配置后,进一步了解自托管密码库实际可能遭受攻击的位置 是下一步,因为密码库条目本身已在客户端加密,剩下需要保护的就是管理员令牌和备份归档文件。