エンドユーザーの実際のIPアドレスの「信頼の連鎖」の処理

背景

Discourse は、エンドユーザーの実際の IP アドレスを把握する必要があります。

ただし、Discourse コンテナ内で動作する nginx などの 1 つ以上の上流 Web サーバーが常にあるため、エンドユーザーは Discourse に直接接続することはありません。したがって、この情報を信頼できる方法で Discourse に渡す仕組みが必要です。

x-forwarded-for ヘッダーがその解決策です。このトピックでは、この情報を適切に処理するための具体的なメカニズムと、その伝播方法について説明します。

テンプレート

上流のプロキシを信頼するための各種テンプレート(例:cloudflare.template.ymlfastly.template.yml)は、テキスト置換(これは脆弱です)に依存するのではなく、アウトレットで予測可能なファイル名を使用するように更新されています。

ファイル名

server/real-ip-header.conf

このファイルには、コンテナ内で動作する nginx が信頼の基準として使用するヘッダーが含まれています。例えば:

real_ip_header x-forwarded-for;

または Cloudflare テンプレートで設定されている通り:

real_ip_header cf-connecting-ip;

server/real-ip-recursive.conf

このファイルが存在する場合、“実際の IP” ヘッダーの処理における再帰を制御します。Discourse コンテナ の前に複数のプロキシがある場合は、これを有効にする必要があります。

real_ip_recursive on;

Cloudflare → ロードバランサー → Discourse コンテナ(nginx と Discourse 本体を含む)

この構成では、ロードバランサーの IP からの接続において、nginx は以下のような x-forwarded-for を受け取ります:

x-forwarded-for: real_end_user_ip, cloudflare_ip

これを処理するには、まず nginx が接続の送信元アドレス(ロードバランサーの IP)が信頼できるか(set_real_ip_from を参照)を判定し、信頼できる場合は x-forwarded-for ヘッダーの最後の IP を処理します。

その IP アドレスが cloudflare_ip であるため、nginx は次に cloudflare_ip が信頼できるかを確認し、次の IP アドレスである real_end_user_ip を使用するために、この処理を再度実行する必要があります。

server/set-real-ip-from-ENVIRONMENT.conf

このファイルには、nginx がどの IP アドレスを信頼するかを指示するディレクティブが含まれており、必要に応じて複数のファイルやディレクティブを持つことができます。

discourse_docker のテンプレートは必要に応じてこれらのファイルを作成します(例:set-real-ip-from-cloudflare.conf)。追加のニーズがある場合は、独自にファイルを追加できます。

AWS 環境で Discourse コンテナの前に ALB(Application Load Balancer)を配置している場合、以下をコンテナ定義に追加することで(環境に合わせて調整)、追加のファイルを作成できます:

run:
  - file:
      path: /etc/nginx/conf.d/outlets/server/set-real-ip-from-aws.conf
      chmod: 644
      # AWS VPC は 10.42.0.0/16、ALB ネットワークからの接続をすべて信頼
      contents: |
        set_real_ip_from 10.42.66.0/24;
        set_real_ip_from 10.42.67.0/24;
  - file:
      path: /etc/nginx/conf.d/outlets/server/real-ip-header.conf
      chmod: 644
      contents: |
        real_ip_header x-forwarded-for;
「いいね!」 5

現在、/data/lc-manager-playbook/discourse/docker-templates/allow-local-proxy.template.yml というファイルを持っています。

after_bundle_exec:
  - replace:
    filename: /etc/nginx/conf.d/discourse.conf
    from: "types {"
    to: |
      set_real_ip_from 192.168.1.0/24;
      set_real_ip_from 192.168.11.0/24;
      set_real_ip_from 172.16.0.0/12;
      set_real_ip_from 10.0.0.0/8;
      real_ip_recursive on;
      real_ip_header X-Forwarded-For;
      types {

この方法は今でも問題なく動作しているようです。

このようなテンプレートを配布しない理由は何でしょうか?

「いいね!」 1

このテンプレートは完全に問題ありません。今日も、見通しの範囲内の未来も動作します。

基本的には、将来の互換性(フューチャープローフィング)と堅牢性です。

もしファイルが変更された場合、つまりあなたが検索している types { という文字列を削除したり変更したりした場合、単に動作しなくなります。

before-server または server アウトレットを使用するように変更すれば、その場合でも動作し続けます。

「いいね!」 2

X-Forwarded-For を設定する意味は一体何でしょうか?本来、このヘッダーにはクライアントIP(既知の範囲で)だけでなく、プロキシチェーン全体も含まれるのが一般的です。

コミットメッセージには以下のように記載されています:

and might end using `client_ip` or `proxyA_ip` depending on codepath.

これはつまり、Discourse側でそのヘッダーの一般的な値を一貫して(または各処理に適した方法で)正しく解析・使用できていないというバグではないでしょうか?ヘッダーの値自体(Nginxがプロキシとして付加する値)に問題があるのではなく。

Discourseがプロキシチェーンの情報を必要とせず、本来の構成が「既知の範囲で真のクライアントIPのみを渡す」ものであるなら、X-Forwarded-For の存在意義は失われます。X-Real-IP ヘッダーは既に設定されており、その目的に合致しているため、X-Forwarded-For は冗長なものとなっています。

その指摘はもっともです。単に削除しても同じ結果になります(ただし…以下参照)。

アプリケーションの境界は実際には Discourse や Rails ではなく、nginx 自体です。したがって、どのリモートプロキシを信頼するかという判断は、アプリケーションのエントリポイントである nginx で行われます。そして、nginx がその判断を Discourse に渡すことができます。

デフォルトでは、Rails は x-f-f の処理時にローカルアドレスのみを信頼するため、私たちが制御しやすい別の場所でその設定を行っています。

実際には、Rails は x-real-ip ヘッダーを見ようともしないことが判明しました… Rails が参照するヘッダーは以下の通りです。

  • forwarded
  • client-ip
  • x-forwarded-for

これが何らかの理由でここから

commit 21b562852885f883be43032e03c709241e8e6d4f (tag: v0.8.0)
Author: Robin Ward
Date:   Tue Feb 5 14:16:51 2013 -0500

    Discourse の初期リリース

diff --git a/config/nginx.sample.conf b/config/nginx.sample.conf
new file mode 100644
index 00000000..62fabf4a
--- /dev/null
+++ b/config/nginx.sample.conf
…
+    proxy_set_header  X-Real-IP  $remote_addr;

もう少し調査が必要ですが、現時点での答えは「動いているから」です。これが当初この状態になった理由かもしれません。

おそらく、何らかの gem がそれを使用しているのでしょうか?

「いいね!」 1

了解しました。つまり、これは Rails や他の gem がヘッダーをどのように処理するかという話であり、Discourse のコードがどう動くかという話ではありませんね。

Rails が X-Real-IP を使用していないのは興味深いです。X-Forwarded-For よりも使用頻度は低いかもしれませんが、ForwardedClient-IP よりも確実に知名度が高いはずです :thinking:

おそらく Nginx の設定では X-Real-IP は時代遅れなのでしょう。Discourse はログで X-Forwarded-For と共に X-Real-IP を拡張して使用しているのでしょうか?私の解釈が正しいとすれば、コード内の他の明示的な使用箇所や言及は見つかりませんでした:

以下は、共有レート制限のデバッグ中に無効な「unix:」クライアントIPに関するエラーをログで確認した際、2点において不自然に見えました(Discourse アップグレード後、コンテナの前にUNIXソケットプロキシを使用しており、X-Forwarded-For に依存しています)。

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;

しかし、「これで動く」というのは、$remote_addr を唯一の信頼できる情報源とし、real_ip_header を Discourse/Rails が受け取る単一のIPを管理者が制御するための標準的な方法とする、という理解で正しいと思います。これが Serve Discourse from a subfolder (path prefix) instead of a subdomain にすでに追加されていたことを確認しました :+1: