はじめに
Among Us のゲームの進行に応じて Discord 上の各参加者のミュート・アンミュートを自動制御してくれるとても便利なボット、AutoMuteUs を初めて触りました。
動作にはボットのインスタンスを立てる必要があり、公開インスタンスが満員だったため、その場で自前サーバでのセルフホスト構成を作ったのですが、Docker でホストできることもあって、動かすだけならとても簡単です。Discord でのボット操作を一手に引き受けられるのであれば、インタネット上で公開する必要すらなく、LAN 内に配置するだけで済みます。
が、自分が居ないときでも仲間内で共同利用できるようにしたい場合、どうしてもボットのインスタンスはインタネット上でホストする必要があり、であれば少しでもセキュアにしたい感触があります。
そんなわけで、平文で行われている一部の HTTP や WebSocket の通信を、Let’s Encrypt の SSL 証明書とリバースプロキシ(Nginx)で暗号化したので、実装例の紹介です。
追記: Caddy でも実装 しました。エントリ末尾の追記部分 で紹介しています。
通信と登場人物
通信が少し複雑なので、登場人物と併せて簡単に整理します。デフォルト状態で普通にホストすると、こんな感じになります。
- AmongUsCapture
- 実行中の Steam 版 Among Us のメモリ(たぶん)を横から覗いて、ゲームの状態を監視し続けるアプリケーション
- 監視結果(ゲームの状態)は AutoMuteUs の galactus に随時投げる
- ボット経由での Discord の操作も担うことがある(たぶん)
- AutoMuteUs
- Docker ホスト上で動作するボットのインスタンス
- ゲームの状態を AmongUsCapture から受け取り、 Discord の操作(参加者のミュートやアンミュート)を担う(galactus)
- Discord でのユーザインタラクションやメッセージ操作を担う(amongusdiscord)
- ほか、galactus と amongusdiscord の連携用に Redis、統計情報の保存などのために PostgreSQL も動作する
課題
AmongUsCapture と galactus 間の通信が、平文の HTTP や WebSocket で行われています。
このため、AutoMuteUs を下図のようにインタネット上でホストした場合、インタネット上に非暗号化トラフィックが流れることになり、経路上での盗聴に対して脆弱になります。誤情報を流し込むこともできてしまいそうですね。
実際にパケットを覗くと、WebSocket のペイロードが露出していることがわかります。
なお、AutoMuteUs を LAN 内でホストする(ゲームの実行端末で Docker を動かすか、または仮想マシンなどで Docker ホストを用意する)場合は、この通信も LAN 内で閉じることになるので、ほとんどの場合は気にしなくても大丈夫でしょう。
対策
galactus の手前に Nginx でリバースプロキシを構成して、経路を HTTPS/WSS 化させます。
この時の SSL 証明書には、自己署名証明書ではなく、Let’s Encrypt から発行された正規の証明書を利用します。
実装
実装していきます。
方針
大げさな実装にはしたくなかったので、
- AutoMuteUs 用の Compose ファイルのカスタマイズ
- AutoMuteUs 用の .env ファイルのカスタマイズ
だけで完結するようにしました。
つまり、Nginx 用の設定ファイルの用意や ACME クライアントの手動構成などは不要です。この目的で、既成の nginx-proxy と letsencrypt-nginx-proxy-companion を組み込みます。
できたもの
最新のものは Gist に載せました。
前提
- AutoMuteUs 用のサブドメインを保有していること
- そのサブドメインが、AutoMuteUs ホストのグローバル IP アドレスに解決できること
- AutoMuteUs ホストで必要なポートがブロックされていないこと
- Let’s Encrypt の HTTP-01 チャレンジで利用するポート(
80/tcp
) - リバースプロキシの待ち受けに利用するポート(後述の環境変数
NGINXPROXY_EXTERNAL_PORT
で指定)
- Let’s Encrypt の HTTP-01 チャレンジで利用するポート(
.env ファイル
ファイルは Gist に載せています。環境変数を 5 つ追加しています。
NGINXPROXY_EXTERNAL_PORT
- リバースプロキシが外部で待ち受けるポートを指定します。例では
8443
です。このポートが AmongUsCapture の通信先になります。
- リバースプロキシが外部で待ち受けるポートを指定します。例では
NGINXPROXY_TAG
- nginx-proxy のコンテナイメージ のタグです。GitHub のリポジトリ のタグと一緒です。
NGINXPROXY_COMPANION_TAG
- letsencrypt-nginx-proxy-companion のコンテナイメージ のタグです。GitHub のリポジトリ のタグと一緒です。
LETSENCRYPT_HOST
、LETSENCRYPT_EMAIL
- Let’s Encrypt で証明書を発行するドメイン名と、更新通知を受け取るメールアドレスです。
また、既存の環境変数群は次のように工夫します。
GALACTUS_HOST
- HTTPS で記述し、ポート番号は前述の
NGINXPROXY_EXTERNAL_PORT
と一致させます。
- HTTPS で記述し、ポート番号は前述の
GALACTUS_EXTERNAL_PORT
- galactus の待ち受けポート番号なので、つまり、リバースプロキシが転送する先のポート番号でもあります。例ではデフォルトの
8123
としています。
- galactus の待ち受けポート番号なので、つまり、リバースプロキシが転送する先のポート番号でもあります。例ではデフォルトの
docker-compose.yml ファイル
ファイルは Gist に載せています。
nginx-proxy
と nginx-proxy-letsencrypt
を追加し、その動作に必要なもろもろを修正しています。設定は環境変数ファイルから読み込まれるため、修正は不要です。
効果
.au new
したあとにボットから届く DM のリンクを踏むと、AmongUsCapture が galactus に HTTPS と WSS でつなぐようになります。
パケットの中も暗号化されました。
補足
今回は、Google Cloud Platform(GCP)の Google Computing Engine(GCE)で、f1-micro な Container-Optimized OS インスタンスを作成しました。
- 静的外部 IP アドレスをひとつ付与
- SSH のポート番号をデフォルトから変更
- Docker Compose のインストール
- ファイアウォールルールの作成と割り当て
をしています。ドメインは手持ちのもので、DNS サーバには Azure DNS を使っています。
一点だけ、この nginx-proxy を使う方式の気に入らないところは、Let’s Encrypt の HTTP-01 チャレンジのため 80 番ポートを閉じられないところです。Let’s Encrypt を使う場合、個人的に普段は DNS-01 チャレンジを好んでいますが、letsencrypt-nginx-proxy-companion は DNS-01 チャレンジに対応していないので、今回は妥協しています(もちろん、Certbot などを別に構成すれば DNS-01 チャレンジも利用可能です)。
HTTP-01 チャレンジの完了後は、nginx-proxy は 80 番へのアクセスには 301 を返して HTTPS に誘導するようになりはするので、NGINXPROXY_EXTERNAL_PORT
を 443 以外にしておけば AutoMuteUs の存在が過度に露出することにはなりませんが、それでもポートスキャンなどで Nginx まではたどり着けてしまうのがちょっと微妙ですね。
なお、80 番ポートは HTTP-01 チャレンジの発生時(≒リバースプロキシの起動直後と期限が近付いてからの更新時)だけ開放されていればよいので、不測の再起動や更新忘れなどへの備えを犠牲にして(つまりサービスレベルを下げて)よければ、普段は閉じておくのも手です(閉じています)。
追記: Caddy での実装
ACME クライアントがバンドルされた HTTP サーバの実装、ともいえる Caddy でのリバースプロキシの実装例を先の Gist に追記しました。nginx-proxy と違って DNS-01 チャレンジにも(そこそこ気軽に)対応できるので、80 番ポートを完全に閉塞したままでも(またはそもそもインタネットからの到達性がないサーバでも)証明書の発行・更新ができるのが利点です。
この Gist では、Caddy での Let’s Encrypt の証明書発行に nginx-proxy と同様 HTTP-01 チャレンジを利用する場合の例と、併せて Azure DNS を題材に DNS-01 チャレンジを利用する場合の例も紹介しています。
Caddy は DNS-01 チャレンジ用のプラグイン があまり豊富でない(lego のラッパ は Deprecated)ので、今回はおもしろがって Azure DNS 用のプラグインを自製して使っています。