アジョブジ星通信

進捗が出た頃に更新されるブログ。

比較的安全に Docker で Pleroma サーバーを建てる

みんな大好き「やってみた」、今回は Pleroma サーバー構築をやってみました!

以前 Kubernetes で隔離 Mastodon 環境を作ったりして、 ActivityPub のデバッグ環境を整えたりしていましたが、 ActivityPub をしゃべるサーバーを自作するという夢に挑む時間とやる気は残っていないということで、 Pleroma で妥協しようということにしました。みんな大好きやってみたはいくらでもやってる人がいると思うので、ドキュメントにあんまり書いてないようなことを重点的に書いていきます。

お品書き

  • Docker を使って構築する
  • SSRF 対策をする
  • フロントエンドを改造する
  • reCAPTCHA を導入する

Docker を使って構築する

とりあえず起動できるところまで

While we don’t provide docker files とか書いてありますが、 v1.1.4 現在、リポジトリには Dockerfile が入っています(「it's kind of WIP」だからまだ README を書き換えていないという発言が IRC でちょうどありました)。

簡単に言うと、この Dockerfile から Docker イメージを作成して、適切に環境変数を設定すれば、それでサーバーが建ちます。便利だ。

適切な環境変数というのは、 config/docker.exs を読むとわかるようになっています。表にしておくと便利なので表にしておきます。

環境変数 必須 デフォルト値 説明
DOMAIN N localhost 構築するサーバーのドメイン名。 https://DOMAIN/ で動いているということになる。スキーマやポート番号を変更したい場合は、環境変数だけではできないので、後で紹介する設定ファイルを使って上書きする。
INSTANCE_NAME N Pleroma サーバーの名前。フロントエンドのナビバーの左上に表示されるやつ。
ADMIN_EMAIL N 管理者のメールアドレス。 /api/v1/instance で公開される。
NOTIFY_EMAIL N 通知メールを送信するときに FROM として指定するメールアドレス。
DB_USER N pleroma PostgreSQL のユーザー名
DB_PASS Y PostgreSQL のパスワード
DB_NAME N pleroma PostgreSQL のデータベース名
DB_HOST N db PostgreSQL のホスト名。ポート番号の指定はできないので、指定したかったら設定ファイルを作って上書きする必要がある。

あとは、 /var/lib/pleroma/uploads ディレクトリを永続化するようにコンテナを作れば完成です! 便利! 簡単!

初回起動をすると、コンテナ内に /var/lib/pleroma/secret.exs というファイルが作成されます。このファイルには、 Cookie 暗号化のための適当な乱数や、ブラウザのプッシュ通知に使用するための鍵が生成され、このファイルに書き込まれています。必要であれば、このファイルをバックアップして、また別のコンテナを作成するときに再利用することができます。

さらに設定ファイルを書く

環境変数だけで指定できない設定項目は大量にあるので、他に書くべきことは設定ファイルに書いておきましょう。コンテナ内の /var/lib/pleroma/config.exs に書いたスクリプトは、上記の環境変数が評価された後に読み込まれます。したがって、このファイルに書いた設定が最も強い設定になります。

書き方は import Config に続いて、設定を書き込んでいきます。例えば、 127.0.0.1:4000 をリッスンして、それをそのままのアドレスで公開するならばこんな感じ。

import Config

config :pleroma, Pleroma.Web.Endpoint,
   url: [host: "localhost", scheme: "http", port: 4000],
   http: [ip: {127, 0, 0, 1}, port: 4000]

設定項目については、 Configuration Cheat Sheet に詳しく書いてあります。

注意: 一部設定項目は、この Docker イメージでは正しく動作しないかもしれません。例えば、使用できる HTML タグの制限を指定する :markup に関する設定は、 /var/lib/pleroma/config.exs に書いても動作しないことを確認しています(報告しましたが、サニタイズ処理の根本から書き換えないといけない雰囲気で厳しそうです)。動作しない設定項目があった場合、 config/config.exs を書き換えてから、 Docker イメージをビルドすることで回避することができます。

実際にどのように運用するか

(11/12 22:36 追記セクション)

この Dockerfile では mix release が実行され、その結果がイメージとなって出力されます。このとき Elixir のコンパイルが行われるわけですから、実運用サーバー(メモリ 512MB しかない貧弱)で行うべき処理ではありません。そこで、 Azure Pipelines で docker build を行い、その結果を Docker Hub にアップロードする戦略を取っています。

ビルドしたイメージが azyobuzin/pleroma という名前だとしましょう。実運用サーバーでは、このイメージをベースに、設定を上書きする Dockerfile を用意しておき、 docker-compose build でビルドされるようにしておきます。例えばこんな感じ。

# ベースイメージ(適当な名前に置き換えてください)
FROM azyobuzin/pleroma

# 設定ファイル config.exs, secret.exs をコピー
COPY --chown=pleroma:0 *.exs /var/lib/pleroma/

# フロントエンドや背景画像などに手を加えているなら、それをコピー
COPY --chown=pleroma:0 static /var/lib/pleroma/static

# HACK: 実際の運用では host ネットワークを使用していますが、 Erlang VM の名前が被ると同時起動できません。
# テスト環境と本番環境を同じマシンで動かすために、別の名前を設定します。
USER root
RUN echo 'export RELEASE_NODE="MyPleroma-${PORT}@127.0.0.1"' >> $(ls /opt/pleroma/releases/*/env.sh)
USER pleroma

ここで設定ファイルを書き換えても、再度コンパイルする必要はありません。

これで、実運用サーバーに負荷をかけずに、公開 Docker イメージレジストリに入れられないような設定も含めることができます。

SSRF 対策をする

Server Side Request Forgery は、ユーザー投稿型サービスで、外部リソースへアクセスする必要があるならば、割と狙いやすい攻撃です。例えば、同一 Docker ネットワーク内に「secretweb」という名前のコンテナが建っていたら http://secretweb でアクセスできてしまうわけですが、ユーザーが「http://secretweb」と投稿して、このコンテナの Web サーバーのレスポンスがプレビューされてしまうのは意図していないはずです。このような、ユーザーの投稿や、フェデレーション先サーバーからの入力によって、本来ならアクセスできてはいけないサーバー、ポートへのアクセスから守る手段を施しておかないと、安全とは言えません。

Pleroma では、プレビューの取得には有効な TLD かの検証をしていますが、これは簡易的なものです。また ActivityPub のオブジェクト名については、そのまま HTTP リクエストを送信しています。このような状況で危険なリクエストからサーバーを守るには、接続先を検証して、必要があればブロックするような処理を挿入する必要があります。

そこで、汎用的なブラックリスト形式でリクエストをブロックするフォワードプロキシサーバーを作成しました。

Coroxy は Go で作成したプロキシサーバーで、 goproxy をベースに、設定ファイルのブラックリストにある IP アドレス、ポートに対するリクエストをブロックします。要するに、名前解決を行って、その結果の IP アドレスがまずそうならブロックという戦略です。

例として、実際に使用している設定ファイルを紹介します。

# ループバック、マルチキャストアドレスへの通信をブロック
allowGlobalUnicastOnly: true

# IPv4 のプライベートアドレスをブロック
# 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
blockPrivateAddressV4: true

# 自分自身へのリクエストは 80, 443 番ポートのみ許可
blacklist:
  - addr: "133.130.114.138"
    portWhitelist: [80, 443]
  - addr: "2400:8500:1301:747:133:130:114:138"
    portWhitelist: [80, 443]
  - addr: "2400:8500:1301:747:a133:130:114:1380/124"
    portWhitelist: [80, 443]

これを config.yaml として保存したならば、次のコマンドでプロキシが起動します。実際の運用では、 IPv6 ネットワークの設定をするのが面倒だったので、 host ネットワークを使っています。

docker run -p 127.0.0.1:8080:8080 -v $(pwd):/etc/coroxy azyobuzin/coroxy -config /etc/coroxy/config.yaml

Pleroma からこのプロキシサーバーを使用するには、次のように設定します。

config :pleroma, :http,
  proxy_url: "プロキシのホスト名:8080"

これで、 SSRF への備えはできたはずです。できてるよね? セキュリティ詳しくないからわからない……。

11/28 追記: HTTP ライブラリのバグにより、接続先サーバーがおかしくなることがあるようです。詳しくは、次の Issue とワークアラウンドを参照してください。

フロントエンドを改造する

Pleroma バックエンドのリポジトリには、最初からコンパイル済みのフロントエンドが入っていますが、フロントエンドを改造して、それを Docker イメージに含めるにはどうしたら良いでしょうか?

フロントエンドの開発環境を整える

フロントエンドは pleroma-fe というリポジトリで開発されています。開発環境は、 Node.js (v8 以上が必要、 v10 で動作を確認)と Yarn が入っていれば大丈夫です。 Windows では Babel まわりの依存解決がうまくいきませんでした。 Node.js に疎く、深追いする元気はなかったので Debian でやっていきます。

やるべきことは HACKING.md に書いてある通りですが、かいつまむと次の通りです。

  1. バックエンドの URL を設定ファイルに書きます。 config/local.example.jsonconfig/local.json にコピーして、 target をテストしたいバックエンドの URL に書き換えます。
  2. yarn dev を実行して、ブラウザから http://localhost:8080 にアクセスすれば、デバッグできます(コンパイルが終わるまではレスポンスが返ってきません)。ホットリロードはしてくれないようなので、コードを変更したら、 yarn dev を実行し直してください。

こんな感じで、フロントエンドをいじりまわすことができます。おそらくこれを読んでいるみなさんがフロントエンドを改造して最初にやりたいことは、やさしい日本語の撲滅でしょう。 src/i18n/messages.jsjaja: require('./ja_pedantic.json') に書き換えれば終わりです。よかったですね。

11/28 追記: v1.1.6 でやさしくない日本語がデフォルトになりました。

バックエンドに組み込む

yarn build を実行すると、 dist ディレクトリに webpack されたファイルたちが出力されます。

直接バックエンドに組み込むならば、このファイルたちをバックエンドの priv/static にコピーします。

先ほどの Docker イメージがすでにできているならば、コンテナ内の /var/lib/pleroma/static にコピーします。

私の環境では、バックエンドの Docker イメージ作成と、フロントエンドのビルドを別々に行ったあと、 Azure Pipelines でフロントエンドのビルド結果を /var/lib/pleroma/static にコピーした Docker イメージを作成しています。

reCAPTCHA を導入する

ユーザー登録を開放するにあたって、 bot のような悪意のあるユーザーの登録は避けたいものです。そこで CAPTCHA が登場するわけです。 Pleroma 標準では、簡易的な CAPTCHA 実装である Kocaptcha を使った CAPTCHA に対応しています。このプログラムは、ランダムな文字列を描画し、それをゆがませたり、ごま塩ノイズを乗せたりした画像を出力します。

という実装を見て、あんまり強くなさそうだなぁと感じたので、巨人の肩に乗って reCAPTCHA を導入したいなと思いました。 Pleroma の中の人たちはきっと GAFA が嫌いなので、公式に導入されることはないでしょう。ということで、自分で作っていきます。

今回は、 reCAPTCHA v2 のチェックボックス形式を使っていきます。見栄えが良くて、タイミングも扱いやすいので。

バックエンド

Pleroma に新たな CAPTCHA の実装を追加するには、 Pleroma.Captcha.Service ビヘイビアを実装します。 reCAPTCHA では、サーバーサイドでやることは検証だけで、前準備は必要ないので、 new は適当な値を返して、 validateGoogle のサーバーに検証してもらいます。

次にフロントエンド……の前にバックエンドでまだやることがあり、 Content-Security-Policy で reCAPTCHA のリソースへのアクセスを許可しておく必要があります。許可すべき項目は、 reCAPTCHA の FAQ に書いてあります。

v1.1.4 において、バックエンドへの変更はこんな感じにすれば動くと思います(ブログ用に変更を抽出してテストしてないので、間違ってるかもしれません)。

フロントエンド

フロントエンドでは、 Kocaptcha を表示するスペースに、 reCAPTCHA のチェックボックスを表示させてしまえば良さそうです。 Vue で reCAPTCHA を使うには、ちょうど vue-recaptcha というドンピシャなパッケージがあったので、これを使っていきたいと思います。

vue-recaptcha では、ユーザーがチェックボックスをクリックし、 Google からレスポンスを受け取ると、 verify イベントが発生します。このイベントの引数に、そのレスポンス文字列が入っています。 Kocaptcha の実装では、ユーザーの入力した文字列が captcha.solution にバインドされているので、同じように、 reCAPTCHA からレスポンスを受け取ったら captcha.solution にセットすれば良さそうです。

やることはこれだけなので、コード量もほとんどありません。差分はこんな感じです。

終わりに

本気で Fediverse に参戦しようとすると、これくらいの準備が必要になります。大変ですね。それでも自由は勝ち取らなきゃいけない。勝ち取りましょう、インターネットで発言する自由を。