我之前一直是在一台机器上使用 Docker compose 安装了 Bitwarden,但是这样存在一个隐患,那就是如果这一台机器宕机了,或者发生任何意外,那么我可能有一段时间无法访问我的所有密码仓库,所以为了避免这样的问题,尤其是在我已经稳定运行 K3s 一段时间之后,我就想着将 Bitwarden 迁移到 K3s 上,并且我希望直接使用 Bitwarden 历史的数据,并且也直接使用原来的域名,密码等等,这样就不需要让我所有的设备重新再登录一遍。那么本文就记录一下我在 K3s 上搭建 Bitwarden ,以及将历史数据迁移到 K3s 中的过程。

Bitwarden 我就不多说明了,是一个开源的密码管理器,而我使用的 vaultwarden/server 则是一个开源的 Rust 编写的 Bitwarden 兼容的后端,Bitwarden 提供了跨平台的客户端,以及浏览器插件,所以可以在任何的平台上非常方便地使用。

环境配置

前提条件

  • 确保有一个已经在运行 K3s 的集群
  • 使用 Helm 安装好 Longhorn,Traefik,cert-manager
  • 确保本地安装好 kubectl 命令,可以管理 K3s
  • 准备一个域名,并且可以配置 DNS A 记录

部署 Longhorn 持久化存储

Longhorn 更详细的安装步骤可以参考这篇文章,本文简略地再提一下。

节点准备 (如果需要): 根据 Longhorn 的要求,可能需要在每个节点上安装 nfs-common、open-iscsi,并准备用于存储的磁盘或目录。

安装 Longhorn,使用 Helm 安装到 longhorn-system 命名空间。

helm repo add longhorn https://charts.longhorn.io
helm repo update
ubectl create namespace longhorn-system
helm upgrade -i longhorn longhorn/longhorn --namespace longhorn-system

验证安装: 检查 longhorn-system 命名空间中的 Pod 是否都正常运行,并确认 longhorn StorageClass 是否已创建

kubectl -n longhorn-system get pod
kubectl get storageclass

安装 Traefik(Ingress 控制器)

K3s 默认已经安装了 Traefik 作为反向代理和负载均衡器,可以将外部流量路由到集群内部的服务中。

配置 DNS

在域名服务提供商那边,为域名 bitwarden.einverne.info 创建一个 A 记录,指向 Traefik 的外部 IP 地址,或者指向集群中任意一台机器 IP。

部署 cert-manager (证书管理)

可以使用 Helm 来安装 cert-manager ,cert-manager 会自动从证书发行商,比如 Let’s Encrypt 等获取以及续订 TLS 证书。

helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --set installCRDs=true

部署 Bitwarden 应用

  • 配置中,会创建一个 vaultwarden 命名空间,也可以直接使用命令创建 kubectl create namespace vaultwarden
  • 创建一个 Deployment 来运行 vaultwarden/server 容器,并挂载 PVC vaultwarden-data
  • 创建一个 ClusterIP 类型的 Service,将流量引导到 Vaultwarden 的 Pod
  • 创建一个 Certificate 资源,让 cert-manager 为域名创建证书,并将证书存放在名为 vaultwarden-tls-secret 的 Secret 中。
  • 创建一个 Traefik IngressRoute 资源,将来自域名的 HTTPS 流量路由到 Vaultwarden Service,并使用 cert-manager 生成的 TLS 证书
apiVersion: v1
kind: Namespace
metadata:
  name: vaultwarden
---
apiVersion: v1
kind: Secret
metadata:
  name: vaultwarden-secrets
  namespace: vaultwarden
stringData: # 使用 stringData 更易读,Kubernetes 会自动 base64 编码
  ADMIN_TOKEN: ""  # 替换为你生成的 Admin Token openssl
  SMTP_HOST: "smtp.gmail.com"             # 替换为你的 SMTP 服务器地址
  SMTP_FROM: "@gmail.com" # 替换为发件人邮箱
  SMTP_PORT: "587"                        # 替换为 SMTP 端口 (e.g., 587 for TLS)
  SMTP_SSL: "true"                        # 或 "false", 根据你的 SMTP 服务器设置 TLS/SSL
  SMTP_USERNAME: ""     # 替换为 SMTP 用户名
  SMTP_PASSWORD: ""     # 替换为 SMTP 密码
  # 注意:DOMAIN, WEBSOCKET_ENABLED, SIGNUPS_ALLOWED 将直接在 Deployment 中设置,因为它们不敏感或依赖于部署本身
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: vaultwarden-data
  namespace: vaultwarden # 假设部署在 vaultwarden 命名空间
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: longhorn
  resources:
    requests:
      storage: 512Mi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vaultwarden
  namespace: vaultwarden
  labels:
    app: vaultwarden
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vaultwarden
  template:
    metadata:
      labels:
        app: vaultwarden
    spec:
      containers:
      - name: vaultwarden
        image: vaultwarden/server:1.32.7 # 使用官方镜像
        ports:
        - name: http
          containerPort: 80
        - name: websocket
          containerPort: 3012
        env:
        - name: DOMAIN # 可选,配置域名
          value: "https://bitwarden.einverne.info"
        - name: WEBSOCKET_ENABLED
          value: "true"
        - name: SIGNUPS_ALLOWED
          value: "false"
        envFrom: # 从 Secret 加载敏感环境变量
        - secretRef:
            name: vaultwarden-secrets
        volumeMounts:
        - name: data
          mountPath: /data # 挂载持久化存储
        - name: localtime
          mountPath: /etc/localtime
          readOnly: true # 挂载宿主机时区文件
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: vaultwarden-data # 引用之前创建的 PVC
      - name: localtime # 定义宿主机路径卷
        hostPath:
          path: /etc/localtime
          type: File # 确保挂载的是文件
---
apiVersion: v1
kind: Service
metadata:
  name: vaultwarden-service
  namespace: vaultwarden
spec:
  selector:
    app: vaultwarden
  ports:
  - name: http # 命名端口以便 IngressRoute 引用
    protocol: TCP
    port: 80
    targetPort: http # 对应 Deployment 中的 containerPort name: http
  - name: websocket # 暴露 websocket 端口
    protocol: TCP
    port: 3012
    targetPort: websocket # 对应 Deployment 中的 containerPort name: websocket
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: vaultwarden-certificate
  namespace: vaultwarden
spec:
  secretName: vaultwarden-tls-secret # K8s Secret 名称,用于存储证书
  dnsNames:
  - bitwarden.einverne.info # 需要签发证书的域名
  issuerRef:
    name: letsencrypt-prod # 引用之前创建的 ClusterIssuer
    kind: ClusterIssuer
---
apiVersion: traefik.io/v1alpha1 # 使用 Traefik CRD
kind: IngressRoute
metadata:
  name: vaultwarden-ingressroute
  namespace: vaultwarden
spec:
  entryPoints:
    - websecure # 指定入口点,通常是 443/HTTPS
  routes:
  - match: Host(`bitwarden.einverne.info`) && PathPrefix(`/`) # 匹配域名
    kind: Rule
    services:
    - name: vaultwarden-service # 指向 Vaultwarden Service
      port: http
    # Websocket 流量路由规则
  - match: Host(`bitwarden.einverne.info`) && PathPrefix(`/notifications/hub`)
    kind: Rule
    services:
    - name: vaultwarden-service # 指向 Vaultwarden Service
      port: websocket # 指向 Service 的 websocket 端口 (3012)
  tls:
    secretName: vaultwarden-tls-secret # 引用包含证书的 Secret

可以将 YAML 配置文件保存成 vaultwarden-deployment.yaml 文件,然后应用

kubectl apply -f vaultwarden-deployment.yaml

K3s 会根据定义自动创建命名空间,资源等等。

运行如下的命令检查 vaultwarden 命名空间中的 Pod

kubectl get pods -n vaultwarden -w

检查其他资源

kubectl get pods,svc,certificate,ingressroute -n vaultwarden

当 Pod 在运行状态的时候,就可以在浏览器访问定义好的域名。完成这些步骤之后,Bitwarden 就已经在 K3s 集群中搭建好了,通过 Longhorn 进行持久化存储,通过 Traefik 和 cert-manager 来实现了自动化的 HTTPS 访问,如果某个节点发生故障,K3s 会尝试在其他可用节点上重新调度 Vaultwarden Pod,Longhorn 确保数据在节点之间共享。

迁移历史的 Bitwarden 数据

因为我之前已经安装好了 Bitwarden,所以下面我会讲述一下如何将历史数据通过临时 Pod 恢复到 K3s 集群中由 Longhorn 管理的 vaultwarden-data PersistentVolumeClaim(PVC)中。

核心思路就是先停止 K3s 中的 vaultwarden 应用,然后通过一个临时的 Pod 访问 Longhorn 卷,将本地数据复制进去,然后重启 Vaultwarden 应用。

前提确定

  • 已经按照文本上述的方法部署了 Vaultwarden,并且创建了名为 vaultwarden-data 的 PVC
  • 本地历史的 Vaultwarden 的数据目录 bw-data 已经备份好
  • 通过 kubectl 访问 K3s 集群管理

备份 K3s 中的数据(可选但是强烈推荐)

虽然目标是为了恢复旧的数据,但是最好还是先用 Longhorn UI 或者 kubectl 创建当前 vaultwarden-data PV 的快照或者备份,防止恢复过程中的问题。

停止 K3s 中的 Vaultwarden 应用

为了安全地修改 PV 内容,停止当前正在使用 PV 的 Pod,将 Vaultwarden Deployment 缩容到 0 个副本。

kubectl scale deployment vaultwarden --replicas=0 -n vaultwarden

确认 Pod 已经被终止

kubectl get pods -n vaultwarden

此时应该没有 Pod 在运行。

创建一个临时的 Pod 访问 PVC

创建一个简单的 Pod,例如使用 busybox 或者 alpine 镜像,并挂载 vaultwarden-data PVC,将这个 Pod 作为数据传输的中转站。

apiVersion: v1
kind: Pod
metadata:
  name: restore-helper
  namespace: vaultwarden
spec:
  volumes:
    - name: vaultwarden-storage
      persistentVolumeClaim:
        claimName: vaultwarden-data # 确保这个名称与你的 PVC 名称一致
  containers:
    - name: helper
      image: busybox:latest # 使用一个包含基本工具的轻量级镜像
      command: ["/bin/sh", "-c", "sleep 3600"] # 让 Pod 持续运行一段时间
      volumeMounts:
        - name: vaultwarden-storage
          mountPath: /restore-data # 将 PVC 挂载到 Pod 内的 /restore-data 目录
  # 可选:如果 Vaultwarden 需要特定用户 ID 运行,可以在这里设置 securityContext
  # securityContext:
  #   runAsUser: 1000 # 示例 User ID,根据 Vaultwarden 容器实际情况调整
  #   runAsGroup: 1000 # 示例 Group ID
  #   fsGroup: 1000 # 确保挂载卷的文件系统组权限正确

将上述配置保存成 restore-helper-pod.yaml ,然后应用这个 YAML 创建 Pod

kubectl apply -f restore-helper-pod.yaml -n vaultwarden

等待 Pod 编程 Running 状态

kubectl get pods -n vaultwarden

将本地数据复制到临时的 Pod 挂载点

首先找到本地通过 Docker Compose 部署的 Bitwarden 挂在的路径,我一般会放在 HOME 目录下,假设是 ~/bitwarden 目录

sudo tar czvf - /home/einverne/bitwarden | ssh einverne@your.server.ip "cat > /home/einverne/bitwarden-2025.tgz"

然后解压文件,假设 Bitwarden 的数据在 ~/bitwarden/bw-data 下。

# 首先检查一下当前数据
kubectl exec -n vaultwarden restore-helper -- ls -al /restore-data/

# 清空目标目录
kubectl exec -n vaultwarden restore-helper -- rm -rf /restore-data/*

# 复制本地数据到 Pod 内的卷挂载点
# 注意:源路径末尾的 "." 表示复制目录内容,而不是目录本身
kubectl cp ~/bitwarden/bw-data/. restore-helper:/restore-data/ -n vaultwarden

最后再运行验证一下数据是否已经复制,应该可以看到 db.sqlite3config.jsonattachmentssendsrsa_key.*  等文件。

移除旧的 RSA 密钥

根据迁移指南,恢复数据后需要移除旧的 RSA 密钥对,让 Vaultwarden 在启动时重新生成。在临时 Pod 中执行此操作:

kubectl exec -n vaultwarden restore-helper -- rm -f /restore-data/rsa_key.pem /restore-data/rsa_key.pub.pem

清理临时 Pod

数据复制和清理完成后,删除  restore-helper Pod:

kubectl delete pod restore-helper -n vaultwarden

重新启动 Vaultwarden 应用

将 Vaultwarden Deployment 扩容到 1 个副本

kubectl scale deployment vaultwarden --replicas=1 -n vaultwarden

观察 Pod 启动情况

kubectl get pods -n vaultwarden -w
kubectl logs -n vaultwarden -l app=vaultwarden -f # 检查日志是否有错误

最后,验证数据恢复,通过域名访问实例,使用之前的用户名和密码访问,检查密码库,设置,组织等等是否恢复。

如果访问没有问题,可以在 Cloudflare 上(如果使用的话),配置多个 A 记录分别指向集群中的公网 IP,完成 Bitwarden 高可用的配置。