しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

ASP.NET / IIS で X-Forwarded-Proto の値を正しく扱いたい

リバースプロキシやロードバランサーで SSL offload を行った場合には、後ろにいる Web サーバーにクライアントの IP などを伝えるために X-Forwarded-* を付けますが、ASP.NET は上手く扱ってくれません。

X-Forwarded-For に関しては ARR Helper を入れることで対応できますが、X-Forwarded-Proto は完全に未対応なので自力で何とかする必要があります。

ARR Helper がインストールされている場合には X-ARR-SSL というヘッダーを渡してあげれば HTTPS = on としてくれますが、非標準なヘッダーなので ARR と組み合わせた場合しか上手く動作しません。

HTTP モジュールで上書きする

ASP.NET の世界だけで扱えれば良いのであれば、専用のマネージ HTTP モジュールを用意して HTTPS / SERVER_PORT サーバー変数を書き換えてあげれば解決します。

適当に書いたコードですが、大体こんな感じになるのではないかと思います。

public class HttpForwardedModule : IHttpModule
{
    public void Init(HttpApplication app)
    {
        app.BeginRequest += OnBeginRequest;
    }

    public void Dispose()
    {
    }

    private void OnBeginRequest(object sender, EventArgs e)
    {
        var app = (HttpApplication)sender;

        if (app.Context.Request.Headers["X-Forwarded-Proto"] == "https")
        {
            app.Context.Request.ServerVariables["HTTPS"] = "on";
            app.Context.Request.ServerVariables["SERVER_PORT"] = "443";
        }
    }
}

このモジュールを PreApplicationStartMethod を使って IIS に登録してあげれば有効になります。

[assembly: PreApplicationStartMethod(typeof(WebApplication17.ModuleInitializer), "Start")]

public class ModuleInitializer
{
    public static void Start()
    {
        HttpApplication.RegisterModule(typeof(HttpForwardedModule));
    }
}

実際のところは BeginRequest のタイミングで上書きの処理が出来ればよいので、Global.asax でも同様に動作します。これで実行して X-Forwarded-Proto 付きのリクエストを投げてあげれば、ASP.NET は HTTPS なリクエストだと判断します。

URL Rewrite で上書きする

ASP.NET の世界で閉じていればマネージ HTTP モジュールで上手くいきますが、ASP.NET 側で変更したサーバー変数は IIS 側には反映されないので、後ろに PHP や HttpPlatformHandler が居る場合には使えません。

要はサーバー変数を書き換えてあげれば良いので、URL Rewrite でも同じことが実現できます。

<system.webServer>
  <rewrite>
    <!-- unlock が必要なので少し面倒 -->
    <allowedServerVariables>
      <add name="HTTPS" />
      <add name="SERVER_PORT" />
    </allowedServerVariables>
    <rules>
      <rule name="HttpForwarded">
        <match url=".*" />
        <conditions>
          <add input="{HTTP_X_FORWARDED_PROTO}" pattern="https" />
        </conditions>
        <serverVariables>
          <set name="HTTPS" value="on" />
          <set name="SERVER_PORT" value="443" />
        </serverVariables>
      </rule>
    </rules>
  </rewrite>
</system.webServer>

やっていることは HTTP モジュールと同じです。allowedServerVariables はデフォルトではロックされているので、書き換えるには applicationHost.config を直接弄るか unlock してから追加する必要があります。

上の例ではアプリケーションの Web.config で書くことを想定してますが、globalRules に入れてマシン全体で使うことも出来るので、考えることが減って少し楽になるかも知れません。

リダイレクト時に https にならない問題

ARR の時には発生しないのですが、それ以外のリバースプロキシや LB を使っている場合には、URL Rewrite でリダイレクトを行うと http として返される問題があります。

何故 ARR の時には発生しないかというと、ARR にはレスポンスヘッダーを自動で書き換える機能があるからです。デフォルトで有効になっているので、ARR / ARR Helper / ASP.NET の場合は何も考えずに済みます。

f:id:shiba-yan:20180616005307p:plain

しかし、それ以外の組み合わせになると一気に破綻するので、リダイレクトする際には https 付きの絶対 URL として URL Rewrite を書く必要があります。地味に面倒です。

Outbound Rule を使って一括で Location ヘッダーを書き換える方法も使えると思いますが、事故りやすいのであまりお勧めは出来ないと思ってます。

App Service を使う

例によって App Service を使う場合には ARR と ARR Helper という組み合わせになるので、いい感じに HTTPS = on としてくれます。リダイレクト時の書き換え処理も走るので、ちゃんと https としてクライアントには返ります。

f:id:shiba-yan:20180606002253p:plain

App Service で実行する場合には、特に必要な作業はありません。お手軽ですね。ちなみに App Service 以外でも ARR と ARR Helper の組み合わせであれば、同じように動作します。

根本的な対応は IIS が X-Forwarded-Proto に対応するか、専用のネイティブ HTTP モジュールを書くしかないと考えてます。ARR Helper がもっと汎用的になって Proxy Helper とでもなれば最高なのですが。