メインコンテンツへスキップ

パスワード管理ツールをセルフホストする

·1052 文字·5 分
inamuu
著者
inamuu

概要
#

最近数多のSaaSが値上げしており、パスワード管理ツールの大御所でもある1Passwordがまさかの年額47.88ドルまであがるとのこと。

1Password、3月27日から値上げ。個人プランは年額35.88ドル→47.88ドルに

円安もあいまって、4/30日時点で1ドル160円となったので、約7,660円/年となる。
ソースネクスト経由であれば3年で12800円なので、約4267円/年となる。
私自身は1Passwordは使っておらず、Bitwardenの無料版を使っているが無料版も添付ファイルは添付できなかったり、スマホは結局優良プランに入らないとダメそうだった。
ChromeでBitwardenで管理したり、Chromeで管理したりしていてそろそろ本腰あげてちゃんと管理することにした。
色々検討したが、自宅サーバー勢なのでセルフホストなパスワード管理ツールを検討することにした。

要件
#

要件としては、この辺りが抑えられていればある程度運用するメリットがありそうと考えた。

  • 無料であること(買い切りならヨシ)
  • Dockerイメージがあって、簡単に管理できること
  • ブラウザの拡張があること(これはMUSTじゃない、最悪自分で作れば良い)
  • スマホアプリが使えること(これもMUSTじゃないけど、あったほうが嬉しい)
  • インターネット経由でアクセスできること(場合によっては無しでもOK)
  • できればMFAも管理できること

Bitwarden, Vaultwarden, Keepass
#

ChatGPTに聞いたらこの辺りが出てきたので、比較表を作成してもらった。

項目BitwardenVaultwardenKeePass
種別OSS + 公式サービスOSS(Bitwarden互換サーバ)OSS(ローカルDB型)
セルフホスト可能(公式サーバあり)可能(軽量)不要(ファイル管理)
スマホアプリ公式あり(iOS / Android)Bitwardenアプリを使用サードパーティ(KeePassium等)
クライアントUX非常に良い非常に良い(Bitwarden同等)やや弱い
セットアップ難易度中(公式はやや重い)低(Dockerですぐ)低(ファイルだけ)
リソース消費高め低い(軽量)ほぼゼロ
同期方式サーバ経由サーバ経由自分で同期設計(S3, Nextcloud等)
オートフィル対応(ブラウザ/スマホ)対応限定的(環境依存)
チーム利用強い(共有/権限あり)制限あり(基本個人向け)弱い
セキュリティ高(ゼロ知識暗号)高(Bitwarden互換)高(ローカル管理)
監査/信頼性高(企業利用多数)中(非公式実装)高(実績長い)
運用コスト低(ただし同期設計必要)
拡張性/APIありあり(Bitwarden互換)ほぼ無し
バックアップサーバ側で管理サーバ側で管理ファイルコピーのみ
向いている用途個人〜チーム個人・小規模完全ローカル主義

セルフホストをしないというKeePassも検討にあがったが、GoogleDriveとかでの同期はなにかと面倒なのでセルフホストできたほうが逆にいいかなと考えた。
あと、今はどうかわからないけど昔KeePassXを一時的に使っていたのもあり、便利なのはわかっていたけどUIがあまりカッコよくない印象があった(今はもしかしたら違うかもしれないが)

機能差
#

特に要件を満たしているかはこの辺りの機能差を重視することとした。
トータルではBitwardenをセルフホストできたらいいかと考えたが、OSSでないと無料版に制限がかかったりしそうなので食指が動かなかった。
また、無料版だとBitwardenのソフトから見るとTOTP保存が出来なかったので、この辺りがわからなかった。

なんとなくできなそうには見える。
Any way to selfhost bitwarden with tOTP support?

機能BitwardenVaultwardenKeePass
パスワード保存/検索〇(完全互換)
自動入力(ブラウザ/スマホ)◎(標準機能)◎(Bitwardenアプリ経由)△(環境依存)
クロスデバイス同期◎(公式サーバ)◎(自前サーバ)△(手動 or 外部同期)
オフライン利用〇(キャッシュあり)◎(完全ローカル)
パスワード生成
2FA(TOTP保存)△(プラグイン等)
生体認証(FaceID/指紋)△(アプリ依存)
セキュアノート
添付ファイル保存◎(公式対応)△(制限あり)△(DB肥大化)
パスワード共有◎(細かい権限管理)△(簡易的)✕(基本不可)
組織/グループ管理△(制限あり)
アクセス権限(RBAC)
監査ログ◎(企業向け機能)
API/CLI◎(互換API)
Web UI△(専用ツール必要)
ブラウザ拡張
パスワード漏洩チェック◎(HaveIBeenPwned連携)△(制限あり)
Emergency Access
Send機能(安全な共有リンク)△(制限あり)
SSO連携◎(企業向け)
カスタムフィールド

Vaultwarden
#

結果、Vaultwardenを検証することにした

  • 個人利用のみであること
  • RUSTで軽量であること
  • Dockerコンテナで管理が容易であること
  • TOTP管理ができること(Bitwardenだと有償ライセンスぽい)
  • SQLiteなのでバックアップが容易であること

Vaultwarden構築環境
#

我が家にあるNASではDockerを動かすための専用のアプリケーションがあり、compose.ymlを設定するとそれを常時動かすことが可能である。

左が昨年導入したUGREENのNAS。
OSがDebianでrootまで取れるので、なんでも出来ちゃうやつ。
8TB2本のRAID1+256GBのM.2 SSDのキャッシュ付き。
右が10年くらい動かしていた旧NAS。2TBx2 RAID1で運用していた。

UGREENのNASはいろいろなアプリケーションがあり、その中にDockerアプリがある。
そこで画像のように時前でアプリケーションをDockerで動かしたりが簡単にできるので、ローカルだけで参照できる簡易なサイトを動かたりしている。

alt text

なお、Vaultwardenをただ動かすなら下記だけでOKである。

docker pull vaultwarden/server:latest
docker run --detach --name vaultwarden \
  --env DOMAIN="https://vw.domain.tld" \
  --volume /vw-data/:/data/ \
  --restart unless-stopped \
  --publish 127.0.0.1:8000:80 \
  vaultwarden/server:latest

ローカル内サイトと同じようにやったらいけるかなと思って手元でDockerを動かすのは秒でできたものの、アカウントを作成しようとしたところ、HTTPSでないとErrorになってしまって作成ができなかった。
アカウント作成だけならやりようがあるかもしれないが、どちらにしろスマホ対応するならインターネット越しにアクセスしないといけないのでHTTPSは必須と考えた。

そこで、このブログが動いているk8sでHTTPSプロキシすればよいと考えた。
構成としてはこんな感じ。
コンテナで動かすのにステートフルになるのは微妙なため、データ領域としてNFSマウントを有効化することとした。

自宅サーバーでk8sクラスターをホストしており、そこですでに数サイト動いている。
ASROCK Deskmini X300というベアボーンキットで動き、余ったVESAマウントでマウントして空中に浮かせている。

こんな感じのシンプルな構成。
このクラスターでは、幸いすでにcert-managerコンテナが動いているので、証明書の更新実績もあり利用は簡単に行える。

NASの設定変更
#

ControlPlaneの証明書切れ対応
#

自宅は固定IPアドレスではないので、定期的に今のIPアドレスをRoute53にレコードを上書きするcronjobを動かしているのだけど、そこで今回アクセスするためのレコードを更新しようとしたらk8sの証明書周りがみな期限切れていた。
ちょうど1年前に構築したのでその時から1年経過したらしい。
以下はVaultwardenは無関係だけど備忘録。

kubectl apply -n update-ddns -f update-dns.yaml
error: error validating "update-dns.yaml": error validating data: failed to download openapi: Get "https://192.168.x.x:6443/openapi/v2?timeout=32s": tls: failed to verify certificate: x509: certificate has expired or is not yet valid: current time 2026-04-29T12:25:57+09:00 is after 2026-04-11T03:30:39Z; if you choose to ignore these errors, turn validation off with --validate=false

ControlPlaneにアクセスして証明書を確認。

❯ ssh 192.168.x.x

root@k8s-control-vm01:~# kubeadm certs check-expiration
[check-expiration] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
[check-expiration] Use 'kubeadm init phase upload-config --config your-config.yaml' to re-upload it.
[check-expiration] Error reading configuration from the Cluster. Falling back to default configuration

CERTIFICATE                EXPIRES                  RESIDUAL TIME   CERTIFICATE AUTHORITY   EXTERNALLY MANAGED
admin.conf                 Apr 11, 2026 03:30 UTC   <invalid>       ca                      no
apiserver                  Apr 11, 2026 03:30 UTC   <invalid>       ca                      no
apiserver-etcd-client      Apr 11, 2026 03:30 UTC   <invalid>       etcd-ca                 no
apiserver-kubelet-client   Apr 11, 2026 03:30 UTC   <invalid>       ca                      no
controller-manager.conf    Apr 11, 2026 03:30 UTC   <invalid>       ca                      no
etcd-healthcheck-client    Apr 11, 2026 03:30 UTC   <invalid>       etcd-ca                 no
etcd-peer                  Apr 11, 2026 03:30 UTC   <invalid>       etcd-ca                 no
etcd-server                Apr 11, 2026 03:30 UTC   <invalid>       etcd-ca                 no
front-proxy-client         Apr 11, 2026 03:30 UTC   <invalid>       front-proxy-ca          no
scheduler.conf             Apr 11, 2026 03:30 UTC   <invalid>       ca                      no
super-admin.conf           Apr 11, 2026 03:30 UTC   <invalid>       ca                      no

CERTIFICATE AUTHORITY   EXPIRES                  RESIDUAL TIME   EXTERNALLY MANAGED
ca                      Apr 09, 2035 03:30 UTC   8y              no
etcd-ca                 Apr 09, 2035 03:30 UTC   8y              no
front-proxy-ca          Apr 09, 2035 03:30 UTC   8y              no

証明書を更新とチェック。

root@k8s-control-vm01:~# kubeadm certs renew all
[renew] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
[renew] Use 'kubeadm init phase upload-config --config your-config.yaml' to re-upload it.
[renew] Error reading configuration from the Cluster. Falling back to default configuration

root@k8s-control-vm01:~# kubeadm certs check-expiration
[check-expiration] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
[check-expiration] Use 'kubeadm init phase upload-config --config your-config.yaml' to re-upload it.

CERTIFICATE                EXPIRES                  RESIDUAL TIME   CERTIFICATE AUTHORITY   EXTERNALLY MANAGED
admin.conf                 Apr 29, 2027 03:28 UTC   364d            ca                      no
apiserver                  Apr 29, 2027 03:28 UTC   364d            ca                      no
apiserver-etcd-client      Apr 29, 2027 03:28 UTC   364d            etcd-ca                 no
apiserver-kubelet-client   Apr 29, 2027 03:28 UTC   364d            ca                      no
controller-manager.conf    Apr 29, 2027 03:28 UTC   364d            ca                      no
etcd-healthcheck-client    Apr 29, 2027 03:28 UTC   364d            etcd-ca                 no
etcd-peer                  Apr 29, 2027 03:28 UTC   364d            etcd-ca                 no
etcd-server                Apr 29, 2027 03:28 UTC   364d            etcd-ca                 no
front-proxy-client         Apr 29, 2027 03:28 UTC   364d            front-proxy-ca          no
scheduler.conf             Apr 29, 2027 03:28 UTC   364d            ca                      no
super-admin.conf           Apr 29, 2027 03:28 UTC   364d            ca                      no

systemctl restart kubelet は実行したはず

下記コピーしてMacの ~/.kube/configに上書き

root@k8s-control-vm01:~/.kube# cat /etc/kubernetes/admin.conf

これでkubectlが動くようになった。

vaultwarden用のserviceとdeployment等を用意して適用。
ingressで証明書を参照できるようにした。この時点では共通のingressのまま。

❯ kc apply -f vaultwarden.yaml
persistentvolume/vaultwarden-data-pv unchanged
persistentvolumeclaim/vaultwarden-data created
deployment.apps/vaultwarden created
service/vaultwarden created

❯ kc apply -f ingress.yaml
ingress.networking.k8s.io/web-ingress unchanged
ingress.networking.k8s.io/redirect-www-inamuu-com created
ingress.networking.k8s.io/redirect-kazuma-tokyo created

NASをNFSマウントするのでNFS Clientをインストール
#

sudo apt update
sudo apt install -y nfs-common
which mount.nfs

NAS側でNFSサービスを有効化。

最初はPodがNFSマウントできずにErrorになっていたが、上記サービスを有効化したあとマウントされるようになった。
MacのFinderでも確認できるようになった。

alt text

この時点でVaultwardenへインターネット越しにアクセスできるようになった。

alt text


クライアント証明書の導入
#

ここまでは良いものの、セキュリティのことを考えることに。 VPNも検討したが、

  • IP制限ではスマホからアクセスできないのと外出先からも使用できるようにしたい
  • VPNだと毎回繋がないといけない

ということを鑑みてクライアント証明書を導入することにした。
クライアント証明書を作成すれば、接続元を制限できるので証明書があるクライアントだけが接続できるようになる。

導入手順
#

# 自前CA
openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes \
    -key ca.key \
    -sha256 \
    -days 3650 \
    -out ca.crt \
    -subj "/CN=inamuu vaultwarden client CA"

❯ openssl x509 -in ca.crt -noout -subject -issuer -dates
subject=CN=inamuu vaultwarden client CA
issuer=CN=inamuu vaultwarden client CA
notBefore=Apr 29 05:39:37 2026 GMT
notAfter=Apr 26 05:39:37 2036 GMT

---
# クライアント証明書

openssl genrsa -out vaultwarden_client.key 4096
openssl req -new \
    -key vaultwarden_client.key \
    -out vaultwarden_client.csr \
    -subj "/CN=vaultwarden_client"

❯ openssl x509 -req \
    -in vaultwarden_client.csr \
    -CA ca.crt \
    -CAkey ca.key \
    -CAcreateserial \
    -out vaultwarden_client.crt \
    -days 3650 \
    -sha256
Certificate request self-signature ok
subject=CN=vaultwarden_client

❯ openssl pkcs12 -export \
    -out vaultwarden_client.p12 \
    -inkey vaultwarden_client.key \
    -in vaultwarden_client.crt \
    -certfile ca.crt \
    -name "Vaultwarden Client"
Enter Export Password:
Verifying - Enter Export Password:

Ingressを分ける
#

共有Ingressになっていたので、別々に分割。
vaultwardenのときのingressだけクライアント証明書を必須とする。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: vaultwarden
  namespace: apps
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
    nginx.ingress.kubernetes.io/auth-tls-secret: apps/vaultwarden-client-ca
    nginx.ingress.kubernetes.io/auth-tls-verify-client: "on"
    nginx.ingress.kubernetes.io/auth-tls-verify-depth: "1"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - "vaults.inamuu.com"
      secretName: tls
  rules:
    - host: vaults.inamuu.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: vaultwarden
                port:
                  number: 80
❯ kc apply -f vaultwarden.yaml
persistentvolume/vaultwarden-data-pv unchanged
persistentvolumeclaim/vaultwarden-data unchanged
deployment.apps/vaultwarden unchanged
service/vaultwarden unchanged
ingress.networking.k8s.io/vaultwarden created
❯ kc -n apps get ingress vaultwarden
NAME          CLASS   HOSTS               ADDRESS        PORTS     AGE
vaultwarden   nginx   vaults.inamuu.com   192.168.x.x   80, 443   26s

❯ kubectl -n apps describe ingress vaultwarden
Name:             vaultwarden
Labels:           <none>
Namespace:        apps
Address:          192.168.x.x
Ingress Class:    nginx
Default backend:  <default>
TLS:
  tls terminates xxx.xxx.xxx
Rules:
  Host               Path  Backends
  ----               ----  --------
  vaults.inamuu.com
                     /   vaultwarden:80 (172.16.x.x:80)
Annotations:         cert-manager.io/cluster-issuer: letsencrypt
                     nginx.ingress.kubernetes.io/auth-tls-secret: apps/vaultwarden-client-ca
                     nginx.ingress.kubernetes.io/auth-tls-verify-client: on
                     nginx.ingress.kubernetes.io/auth-tls-verify-depth: 1
Events:
  Type    Reason  Age                From                      Message
  ----    ------  ----               ----                      -------
  Normal  Sync    32s (x2 over 37s)  nginx-ingress-controller  Scheduled for sync

証明書のインポート
#

MacOSについては上記で作成したp12ファイルをキーチェーンアクセスで開けばOK.

alt text

iPhone にも同じp12ファイルを送ればOK。

alt text

プロファイルとしてダウンロードされるので、それをインストールする。

クライアント証明書が無い場合は、nginxから下記のようなErrorが返される。

alt text

しかし…
#

Safariではうまくいったものの、ChromeやBitwardenアプリでは何度やっても接続できず。

あとで分かったのだが、BitwardenアプリがmTLSに対応していなかったのだった。
仕方が無いのでブラウザだけで使うかと思ったら、なんとタイムリーにも3週間前にiPhoneアプリでmTLSサポートのPRがマージされていたのだった。

[PM-23409] feat: Add client certificate authentication (mTLS) support for self-hosted environments #1720

まだリリースはされていないようだが、これがリリースされればそのままスマホでも使えそう。

構成図
#

出来上がりはこんな感じになった。

k8sが含まれているのでなんとなく複雑なように見えるが、構成としてはシンプルである。
HTTPSプロキシとしてnginxで証明書を管理して、バックエンドのvaultwardenへアクセスしているだけである。
またデータは喪失しないように、NASにマウントされている。
RAID1ではデータの損失を防げないのでバックアップジョブを別途定期的に走らせようと考えている。

作ってみて
#

Vaultwardenの構築とは関係ないk8s周りでErrorになった所とか、NASでNFSマウントのサービス有効化がどこか迷ったりしたものの、比較的スムーズに構築することができた。
TLS関連がハマらなければ、結構すぐに作れてしまうのではという気がしている。

ログイン画面もインターネットフェイシングではあっても、クライアント証明書が無いと駄目なようになっている上にMFAも有効化できているのがとても安全でありがたい。

あと、何が嬉しいってTOTPの管理もできるのが良い。
もちろんスマホでもできるんだけど、バックアップとして登録しておけば安心である。

※下記はテスト

数日かかるかなと思ったけどそんなことはなく、k8s側の調整とか証明書対応も含めて数時間でできたのでわりかしコスト少なくできたと思う。
パスワード管理ツールは沢山あるが脆弱性もよくあるので自前運用はリスクも抱えることになる。
そういう意味では割とセキュアにできたのは良かったと思う。
Cloudflareを使うとプライベートなサブネットのみで運用できるらしいが、Cloudflareに依存するのは避けたくて今回はこのような構成にした。

NASのDocker起動が思った以上に便利なので、これ以外にも色々導入していこうと思う。