Tailnet内にBitwardenサーバを立ち上げる

2023年4月25日

注意:商用のBitWardenもオープンソースのVaultWardenも同じだが、マスターパスワードを復旧することはできない。サーバ内部のデータは、このマスターパスワードで暗号化されており、どこにも格納されていないからである。マスターパスワードを忘れてしまうと復旧はできない。

Tailnet内でのBitWardenサーバの立ち上げは簡単なことかと思いきや、意外に面倒なことがわかった。httpでのアクセスはできず、必ずhttpsが要求されるからである。

最初にやろうとしたこと

実際には、BitWardenではなく、VaultWardenを使う。BitWardenは商用サービスだが、オープンソースなので、VaultWardenのような互換性のあるオープンソースソフトが存在している。

このBitWardenサーバはTailnet内からしかアクセスできないようにする。

DNSエントリを追加

実際には、TailScaleではなく、HeadScaleを使っており、MagicDNSをONにしているため、Tailnet内からアクセスさせるには、DNSエントリを追加する。

Headscaleのconfig/config.yamlに以下を追加する。ドメインは、example.comで、ユーザ名はmachine、BitWardenサーバの名称がbitwardenだ。

 domains: []

  # Extra DNS records
  # so far only A-records are supported (on the tailscale side)
  # See https://github.com/juanfont/headscale/blob/main/docs/dns-records.md#Limitations
  # extra_records:
  #   - name: "grafana.myvpn.example.com"
  #     type: "A"
  #     value: "100.64.0.3"
  #
  #   # you can also put it in one line
  #   - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.3" }
  extra_records:
    - { name: "bitwarden.machine.example.com", type: "A", value: "100.64.0.3" }

Vaultwardenの起動

単純に、httpsを使う必要がないので、Traefikにはhttpを指定する。

version: '3'

services:
  vaultwarden:
    image: vaultwarden/server:1.25.2
    container_name: vaultwarden
    environment:
      # 初回起動時はtrueにしておかないと、誰も登録できなくなるので注意。
      - SIGNUPS_ALLOWED=false
    volumes:
      - ./data:/data
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.vaultwarden.rule=Host(`bitwarden`)"
      - "traefik.http.routers.vaultwarden.entrypoints=web"
networks:
  default:
    external:
      name: traefik-network

失敗

これを起動して、「http://bitwarden」にアクセスし、初期アカウントを作ろうとすると、以下のエラーだ。

※同じユーザ(machine)に属しているので、「http://bitwarden.machine.example.com」とする必要は無い。

This browser requires HTTPS to use the web vault
Check the Vaultwarden wiki for details on how to enable it

エラーの原因

これは、VaultWardenが、Web Crypto APIというものを使っているのが原因らしい。つまり、VaultWardenのWeb GUIは必ずhttpsである必要がある。httpでは、このAPIが使えなくなり、先のようなエラーが表示されるというわけだ。このエラーは、VaultWardenではなく、ブラウザが出していると言う話だ(未確認)。

VaultWarden自体は、リバースプロキシであるTraefikとhttpで通信しており、httpsには無関係なのだが、最終的なUIはhttpsでなければならないらしい。つまり、どうしてもTraefik側でSSL化する必要がある。

しかし、一般的に、TraefikでSSL化するには、外部からアクセス可能なドメインをつけ、一般公開することになってしまう(そして、Let’s Encryptの証明書をTraefikに自動取得させる)。これでは、セキュリティがかえって低くなるではないか。物理的にTailnet内からのみアクセス可能にしようと思ったのだが、それができないらしい。

また、内部用のbitwardenあるいは、bitwarden.machine.example.comというドメインは、外部的には存在しないドメインなので、Let’s Encryptの証明書を取得できない。したがって、普通に解決する方法としては、bitwarden.example.comなどというサブドメインを外のDNSサーバに登録し、Traefikに対して普通にLet’s Encrypt証明書をとらせることになってしまう。

代替案

その1

VaultWardenにSSLを前提としないよう設定する。

しかし、このような設定は見当たらない。

その2

  • bitwardenというドメインのオレオレ証明書(self-signed certificate)を作成し、それをTraefikで扱わせる
  • それ以外のドメインについては、通常通りLet’s Encryptで取得させる。
  • これによって、「https://bitwarden」というhttpsでのアクセスをTailnet内部で行う。
  • ブラウザが警告を出すが構わない。

しかし、これはできないようだ。SSL certificate location command line optionを見てみると、他はLet’s Encryptのままにし、一部ドメインをこちらで用意したオレオレ証明書にすることはできない。

その3

では、Let’s Encryptで普通にSSL証明書を取得し、しかし、外部からのアクセスを禁止して、内部アクセスのみを受け入れることはできないだろうか?

  • 外部DNSにて、bitwarden.example.comを定義
  • Tailscale(headscale)にも同じドメインを定義
  • traefikには、普通にbitwarden.example.comのSSLを取得させる。
  • ただし、実際にアクセス可能なIPの制限をつける。

これは、IPWhiteListの機能を使えばできそうだ。

実際に行った方法

基本的にはその3のアイデアなのだが、実際にはそううまくはいかなかった。最大の問題は以下であった。

  • 外部DNSでのみドメイン定義し、ごく普通にサーバにアクセスすると、VaultWardenサーバには、こちらのグローバルIPが伝えられる。
  • ところが、TailscaleのMagicDNSにてドメインを定義して、サーバにアクセスすると、こちらのTailnet上のIPアドレスではなく、DockerネットワークのIPがVaultWardenサーバに伝えられてしまう。

前者の場合、サーバ上の物理NICであるeth0からDockerにアクセスすることになり、後者では、tailscale0という仮想NICからDockerにアクセスすることになり、条件は同じはずなのだが、なぜか、VaultWardenサーバに伝わるこちら側のIPが異なってしまう。ここに問題があることがわかるまで多大な時間を費やしたので、とりあえずこれ以上は追求しないことにする。

ともあれ、headscale(Tailscale)には、以下の新たなDNSエントリを追加する。100.64.0.3とは、このサーバ自身のTailnet上のIPアドレスである。

  extra_records:
    - { name: "bw.example.com", type: "A", value: "100.64.0.3" }

VaultWardenのdocker-compose.ymlは以下のようにする。

version: '3'

services:
  bw:
    image: vaultwarden/server:1.25.2
    container_name: bw
    environment:
      # 組織内での表示だけなので、誰でも登録できて構わない。
      - SIGNUPS_ALLOWED=true
    volumes:
      - ./data:/data
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.bw-ssl.rule=Host(`bitwarden.example.com`)"
      - "traefik.http.routers.bw-ssl.entrypoints=websecure"
      - "traefik.http.routers.bw-ssl.tls.certresolver=myresolver"

      - "traefik.http.routers.bw-ssl.middlewares=bw-ssl-middle"
      - "traefik.http.middlewares.bw-ssl-middle.ipwhitelist.sourcerange=192.168.0.0/16"

      - "traefik.http.routers.bw-http.entrypoints=web"
      - "traefik.http.routers.bw-http.rule=Host(`bitwarden.example.com`)"

      - "traefik.http.routers.bw-http.middlewares=bw-http-middle"
      - "traefik.http.middlewares.bw-http-middle.redirectscheme.scheme=https"
networks:
  default:
    external:
      name: traefik-network

このVaultWardenサーバにアクセスできるのは、192.168..というアドレス範囲のみになる。Dockerネットワークはこのアドレス範囲になるし、これはプライベートアドレスなので、Tailscaleを経由しない外部からのアクセスはできない。不本意なのだが、とりあえずこれで良しとする。

※Tailnet上の端末は、100.64..というアドレスになるはずなのだが、どういうわけか192.168..というアドレスとしして見える。この理由はつかめていない。

SMTPの設定

SMTPの設定方法は、SMTP Configurationに説明があり、環境変数として指定すれば良いようだ。

SMTPの用途としては、今のところ、「パスワードのヒント」を送信する目的しか見当たらない。おそらくは、パスワードを忘れてしまった場合の復旧方法は無いのでは?(現在調査中)

結局、このようになった。

version: '3'

services:
  bw:
    image: vaultwarden/server:1.28.1
    container_name: bw
    environment:
      - SIGNUPS_ALLOWED=true
    volumes:
      - ./data:/data
    restart: always
    environment:
      - SMTP_HOST=mail.example.com
      - SMTP_FROM=admin@example.com
      - SMTP_PORT=587
      - SMTP_SECURITY=starttls
      - SMTP_USERNAME=admin@example.com
      - SMTP_PASSWORD=****
      - DOMAIN=https://bitwarden.example.com
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.bw-ssl.rule=Host(`bitwarden.example.com`)"
      - "traefik.http.routers.bw-ssl.entrypoints=websecure"
      - "traefik.http.routers.bw-ssl.tls.certresolver=myresolver"

      - "traefik.http.routers.bw-ssl.middlewares=bw-ssl-middle"
      - "traefik.http.middlewares.bw-ssl-middle.ipwhitelist.sourcerange=192.168.0.0/16"

      - "traefik.http.routers.bw-http.entrypoints=web"
      - "traefik.http.routers.bw-http.rule=Host(`bitwarden.example.com`)"

      - "traefik.http.routers.bw-http.middlewares=bw-http-middle"
      - "traefik.http.middlewares.bw-http-middle.redirectscheme.scheme=https"
networks:
  default:
    external:
      name: traefik-network

※DOMAINの指定も必要。さもないと、メールアドレス確認の際のリンクが、「http://localhost」になってしまう。

パスワードのヒント送信をさせてみると、次のようなメールが来る。ヒントは特に入れていなかった。

やはり、パスワードを忘れた場合の復旧方法は存在しないようだ。絶対に忘れない、ヒントをきちんと入れておくなどの措置が必要だ。

総評

内部的にどうなっているのか知らないのだが、パスワード復旧方法が存在しないということは、完全に暗号化されているのだろう。ウェブGUIについてもWeb Crypto APIを使い(これがどういうものなのか知らないが)、強固なセキュリティを目指しているらしい。

※パスワード復旧方法が無いものは他にもたくさんある。そもそも復旧できてしまっては困るのだから。

その一方で、個人でLANを使う場合や、小さな組織でVPNを組んだ場合には使いにくいことになってしまっているのは否めないのだが、しかし、そもそも商用サービスのソースコードを元にしているので仕方の無い話かもしれない。