Vaultwarden のデータを定期バックアップしてクラウドストレージにアップロードする

Bitwarden API 互換のサーバサイド実装として Vaultwarden が有名だが、そのデータをバックアップするツールを Golang で書いた。

github.com

Vaultwarden の 3rd party バックアップツールは公式 Wiki の Backup に関するページ の Examples にも複数挙げられているし、ここに掲載されている以外にも Google 等で "Vaultwarden backup" と検索するといくつか見つかる。

それらを利用してもよかったのだが、個人的には以下の要件を満たしたかったので自力で書いた。

(おそらくそれらのツールをうまいことセットアップして設定すると要件を満たすことも可能だったとは思うが)

  • バックアップファイル自体を暗号化できること
    • そもそも Vaultwarden (Bitwarden) 的に Vault が流出したとしても、パスフレーズがわからないと復号できないしくみにはなっているので、そこまで必須な要件でもないが、念のために……
  • クラウドストレージにバックアップファイルをアップロードできること
  • Docker コンテナとして簡単に動かせること
    • つまり K8s クラスタで CronJob 等により簡単に定期実行できること
      • さらに Helm Chart で簡単にインストールできること
  • できれば (依存のない) シングルバイナリであること
    • 単純にイメージつくるのが楽だから
    • このため Golang を採用した。 Rust という手もなくはなかったが、(後述するように) 内部で利用している RcloneGolang で書かれているため結果的に最善手となったと思う。

ちなみにバックストアストレージとして SQLite を利用している場合しか対応していない。

以下、利用方法 (については上記 GitHub レポジトリを参照してください) ではなく、おもに実装にまつわる話を書く。

バックアップのための方針

どのファイル (ディレクトリ) をバックアップすればいいかについては、公式 Wiki の Backup your vault に詳細に書いてある。

SQLite をバックアップするためには、単純にファイルをコピーするのではなくスナップショットをとる必要がある。

Vaultwarden 1.32.1 以降の場合、そもそも vaultwarden コマンドに (DB の) バックアップ機能が搭載されているのだが、 Docker image としてシングルバイナリにしたかったのもあり、 Golangsqlite 実装を組み込むことにした。

利用したモジュールは github.com/ncruces/go-sqlite3。なんとなく CGO を利用したくなかったというのがある。(深い理由はない)

これで VACUUM INTO を利用してバックアップをファイルとして保存している。

バックアップファイルの暗号化

OpenSSL で復号できる形式で暗号化しており、 実装としては github.com/Luzifer/go-openssl/v4 を利用している。

これは OpenSSL の実装モジュールというわけではなく、下部では標準の crypto/aes を利用して OpenSSL と互換性のある出力をすることができるモジュールである。

クラウドストレージへのアップロード

各種クラウドストレージに対応している Rclone を利用している。

さいわいにも Golang で書かれており、またライブラリとして利用することもできる。このため、 rclone コマンドを Docker image に同梱しているわけではなく、内部に組み込むことができた。(このためシングルバイナリにできた)

ライブラリとして利用するためのドキュメントが充実しているわけではないので、 rclone CLI の実装を読みながら雰囲気で実装することになってしまったが……

どのクラウドストレージにアップロードするか

利用方法ではなく実装について書くといったわりにここだけ運用について書くことになるが……

Rclone はもともと各種クラウドストレージ向けの OAuth の認証コード (Client ID / Key) がデフォルトとして入っている。 しかし Rclone を利用するすべての人で共有されることになり、 rate limit をオーバーすることも多い。 そのような場合は自前で App を用意して Client ID と Key を発行すればよい。(Microsoft OneDrive での例)

最初 Microsoft OneDrive を利用していたのだが、 refresh token がそのうち expire してしまい、定期実行するうえでの障害となってしまっていた。

このため Google Drive を利用することにした。 しかし当初こちらも refresh token が expire してしまっていた。 原因は単純に Google で発行した App が開発中モードになっていたことだった。 いまはちゃんと公開モードに変更した結果、継続してバックアップできている。

おわりに

自分がほしかった要件を満たすために自分のために書いたツールだが、満足いくものができた。 流行りの vive coding ではなく *1、 (GitHub Copilot は使っているものの) ほぼ自力で書いている。ただ README についてはほぼ完全に Gemini に書かせた。

これで毎日自動的にバックアップしているが、実はまだバックアップファイルから復旧する素振りをしていない。

*1:単純にこれを書き始めたころまだ流行っていなかっただけともいえる