SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor

Vaultwarden VPS 自托管密码管理器部署教程

在 Ubuntu 24.04 VPS 上用 Docker 部署 Vaultwarden,兼容官方 Bitwarden 客户端。了解 HTTPS 前置要求、关闭公开注册、管理令牌、Fail2ban,以及经过恢复测试的备份方案。

构建内容

一个完全由您掌控的密码管理器:在一个小型容器中运行 Vaultwarden,由负责终止 HTTPS 的反向代理放在前端,并让手机、笔记本电脑和浏览器上的官方 Bitwarden 应用连接到它。Vaultwarden 使用 Rust 重新实现了 Bitwarden 服务器 API,并使用与 bitwarden.com 相同的协议,因此只要保持服务器为最新版本,官方客户端无需修改即可连接使用(新客户端版本有时需要匹配的 Vaultwarden 版本:客户端版本为 2026.8.0 及更高版本时,需要 1.37.2),同时内存占用约为 100 MB,低于官方多容器部署。

安装本身只需要十几行 Compose 配置。真正重要、也最容易出问题的是以下三点:必须先配置 TLS,再打开 Web 密码库;创建您自己的账户后,必须立即关闭公开注册;必须备份数据卷并测试恢复,因为该目录保存着您的全部密码。

前置条件和必须了解的实际限制

  • 一台 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 Express、Identity、Api、Admin 等,至少需要 2 GB 内存。Bitwarden 还提供 Bitwarden lite,这是一个单独的 Docker 镜像,至少需要 200 MB 内存,但它面向个人用户和家庭实验室,不适用于企业;数据库及其备份也完全由您负责。Vaultwarden 是一个单独的二进制文件,默认将所有数据存储在 SQLite 数据库中,空闲时只占用几十 MB 内存。对于个人、家庭或小型团队,这是显而易见的选择。由于它忠实实现了 Bitwarden API,您的数据可以在 Vaultwarden 与 bitwarden.com 之间迁移。

您放弃的是大部分企业级功能:不支持 SCIM 配置(但 1.35.0 已加入 OpenID Connect SSO 支持)。此外,您是运营者,因此补丁更新、HTTPS 和备份都由您负责。本指南介绍如何完成这三项工作。

为什么 HTTPS 不是可选项

Bitwarden Web Vault 和浏览器扩展会在浏览器中使用 Web Crypto API(window.crypto.subtle)派生加密密钥。浏览器只会在安全上下文中开放 crypto.subtle,包括 HTTPS,以及 http://localhost 和 http://127.0.0.1 等回环地址这一特殊情况。在普通 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$... 开头的字符串。这里有一个容易浪费 1 小时的陷阱:Docker Compose 会将 $ 视为变量插值。因此,如果您直接将哈希值粘贴到 Compose 文件的 environment: 下方,就必须将每个 $ 都加倍为 $$。在该位置为值加引号没有问题,关键是完成转义:

    environment:
      ADMIN_TOKEN: $$argon2id$$v=19$$m=19456,t=2,p=1$$c29tZXNhbHQ$$RdescudvJCsgt3ub+b+dWRWJTmaaJObG

如果您希望将哈希值保存在 .env 中,规则则相反:保留单个 $ 符号,将整个值放在单引号(ADMIN_TOKEN='$argon2id$v=19$...')中,Compose 文件中现有的 ${ADMIN_TOKEN} 行会读取该值。绝不要在 .env 中将 $ 加倍。如果 Compose 文件本身保留单个 $ 符号,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,输入明文管理员令牌(随机字符串,或您进行哈希处理的密码,而不是哈希值本身)。进入后,您可以列出用户、调整设置、发送测试邮件并创建数据库快照。

如果页面显示 The admin panel is disabled, please configure the 'ADMIN_TOKEN' variable to enable it,则表示 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 推送导致,下面会介绍相关内容。

第 7 步:为登录端点配置 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。从 1.37.4 开始,启用 X-Forwarded-For 后,Vaultwarden 会选择不属于 IP_HEADER_TRUSTED_PROXIES 的最右侧地址(默认值为 local,可覆盖同一主机或 Docker 网络中的代理)。因此,如果代理前面还有 CDN,也要将 CDN 的地址列入其中。

其次,正确的 iptables 链取决于代理类型:如果 Traefik 作为容器运行并发布端口,流量会经过 Docker 的 FORWARD 路径,因此必须像上面一样将封禁放在 DOCKER-USER 中;但如果在第 1 步选择了 host-nginx 方案,连接会在主机的 nginx 上终止,并经过主机的 INPUT 链,DOCKER-USER 封禁无法拦截这些连接。此时删除 chain = DOCKER-USER 行,让 Fail2ban 使用默认的 INPUT 链。

第三,使用 banaction = iptables-allports,不要使用基于端口的默认方式。此 jail 不定义端口,而在 DOCKER-USER 中执行全端口封禁,可以阻止攻击者访问该主机上发布的所有服务。

第 8 步:备份密码库,然后实际执行一次恢复

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(或 http://127.0.0.1:8888)。由于回环地址属于安全上下文,crypto.subtle 可用;此处密码库会通过普通 http 解密,这类地址允许这样做。使用主密码登录,并确认条目存在:如果条目存在,说明数据库、RSA 密钥和主密码都已成功往返恢复,您可以在几分钟内于新的 VPS 上重建环境。使用 Ctrl-C 停止容器,并删除 /tmp/vw-restore。对于服务器上其他不应暴露到互联网的管理界面,也应保持这种隧道习惯;例如,您也可以通过这种方式访问自托管的 open-kritt 安全扫描器的 5173 端口。

故障模式及其对应提示

浏览器控制台中出现 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 显示有效的挂锁图标,然后在客户端的 self-hosted 设置中重新输入该地址。

/admin 拒绝正确的密码。 Argon2 哈希的转义字符丢失。Compose 文件中的每个 $ 都必须写成 $$;但在单引号包围的 .env 中,单个 $ 除外。也可能是你输入了哈希,而不是哈希对应的明文密码。

跨设备同步缓慢;控制台显示 WebSocket connection to 'wss://vault.example.com/notifications/hub' failed。 代理未转发 Upgrade/Connection 标头;Traefik 会自动处理,nginx 需要添加 Step 1 中的两行 upgrade 配置。Vault 仍可正常工作,但只会在打开时同步。自 v1.31.0 起,旧的专用端口 3012 已移除,因此不需要单独的 WebSocket 路由。

Fail2ban 报告已封禁,但攻击者仍在连接。 它封禁的是 127.0.0.1,因为 IP_HEADER 配置错误;或者封禁规则位于错误的 iptables 链中。请设置 chain = DOCKER-USER 和 banaction = iptables-allports。

升级

拉取新镜像并重新创建容器;命名卷和所有数据都会保留:

docker compose pull
docker compose up -d

Vaultwarden 会频繁发布新版本。请关注项目的发行说明,不要固定到某个补丁版本,因为部分版本包含迁移说明。执行任何重大升级前,都应先创建新的备份;如需回滚,可将 tarball 恢复到新卷中。

FAQ

Vaultwarden 和 Bitwarden 是同一个项目吗?

Vaultwarden 是兼容的独立服务器,并非官方服务器。Vaultwarden 使用 Rust 重新实现了 Bitwarden 服务器 API,因此官方桌面端、移动端、浏览器和 CLI 客户端都可以连接到它,同时所需资源仅为官方技术栈的一小部分。保管库格式相同,因此可以通过导出和导入双向迁移。

我确实需要 HTTPS 吗?能否在局域网中通过 http 运行?

除回环测试外,其他情况都需要 HTTPS。Bitwarden Web 保管库和扩展使用浏览器的 Web Crypto API,该 API 仅在安全上下文中可用。因此,通过普通 http 访问时,客户端会抛出 Cannot read properties of undefined,并且始终无法登录。只有 http://localhost 或 http://127.0.0.1 这类回环地址支持 http,这也是第 8 步的恢复测试使用 SSH 隧道的原因。

如何阻止陌生人在我的服务器上注册?

在 Compose 文件中设置 SIGNUPS_ALLOWED: "false",然后在创建自己的账户后立即运行 docker compose up -d。之后,通过 /admin 中的 邀请用户 按钮添加新用户。此操作需要配置 SMTP,以便用户收到邀请链接。定期检查管理员用户列表,确认没有出现意外账户。

如何备份我的 Vaultwarden 保管库?

短暂停止容器,然后归档整个 vw-data 卷、db.sqlite3、attachments/、sends/、config.json 以及 rsa_key.* 文件,再将归档文件复制到服务器之外,最好通过 nightly cron 执行。服务器运行时复制正在使用的 SQLite 文件可能导致快照损坏,因此应在服务停止时执行复制。最重要的是,先将备份恢复到一次性容器中并登录,确认备份确实可用后再依赖它。

自托管密码实际上安全吗?

如果完成本指南涵盖的 3 项措施,就可以安全使用:使用真正的 HTTPS、关闭公开注册并设置强管理员令牌,以及测试备份。保管库使用主密码在客户端加密,因此即使服务器也无法看到明文密码;即使 db.sqlite3 被窃取,没有主密码也无法读取其中的条目。不过,服务器仍会保存账户电子邮件地址,以及由各个主密码派生的加盐哈希值。攻击者可以离线尝试破解这些哈希值,因此应保护数据库文件及其所有副本。代价是补丁更新和备份现在由您负责,这也是本指南将 Fail2ban 和恢复演练视为必需步骤的原因。完成这些措施后,进一步了解自托管保管库实际可能遭受攻击的位置 是下一步,因为条目本身已在客户端加密,剩下需要防护的就是管理员令牌和备份归档文件。