リバースプロキシHowTo

はじめに

Apache HTTP Serverモジュールmod_jkおよびMicrosoft IIS用のISAPIリダイレクターは、AJPプロトコルを使用してウェブサーバーをバックエンド(通常はTomcat)に接続します。ウェブサーバーはHTTP(S)リクエストを受信し、モジュールはそのリクエストをバックエンドに転送します。この機能は通常、ゲートウェイまたはプロキシと呼ばれ、HTTPのコンテキストではリバースプロキシと呼ばれます。

典型的な問題

リバースプロキシは、バックエンドのアプリケーションに対して完全に透過的ではありません。例えば、元のクライアント(ブラウザなど)が通信する必要があるホスト名とポートは、バックエンドではなくウェブサーバーに属するため、リバースプロキシは異なるホスト名とポートと通信します。バックエンドのアプリケーションが自身のバックエンドアドレスとポートを使用した自己参照URLを含むコンテンツを返した場合、クライアントは通常これらのURLを使用できません。

別の例はクライアントIPアドレスです。ウェブサーバーにとってのクライアントIPは、受信接続の送信元IPですが、バックエンドにとっては接続は常にウェブサーバーから来ます。これは、バックエンドアプリケーションが例えばセキュリティ上の理由でクライアントIPを使用する場合に問題となることがあります。

ソリューションとしてのAJP

これらの問題のほとんどは、AJPプロトコルとバックエンドのAJPコネクタによって自動的に処理されます。AJPプロトコルはこの通信メタデータを転送し、バックエンドコネクタはアプリケーションがServlet APIメソッドを使用して要求するたびにこのメタデータを提供します。

以下は、AJPによって処理される通信メタデータと、それらを取得するために使用できるServletRequest/HttpServletRequest API呼び出しのリストです。

  • ローカル名: getLocalName()。これは、リクエストにHostヘッダーが含まれていない限り、getServerName()と同じです。この場合、サーバー名は当該ヘッダーから取得されます。
  • ローカルIPアドレス: getLocalAddr()。ローカルIPアドレスは当初サポートされていませんでした。ApacheまたはIISでバージョン1.2.41を使用し、Tomcatバージョンが少なくとも6.0.42、7.0.55、または8.0.11の場合に利用可能です。古いバージョンやNSAPIリダイレクターを使用する場合、getLocalAddr()getLocalName()と同じ結果を誤って返します。回避策として、JkEnvVar SERVER_ADDRを設定することでローカルIPアドレスを転送し、getLocalAddr()の代わりにrequest.getAttribute("SERVER_ADDR")を使用するか、フィルターを使用してリクエストをラップし、getLocalAddr()request.getAttribute("SERVER_ADDR")でオーバーライドすることができます。
  • ローカルポート: getLocalPort()。これは、リクエストにHostヘッダーが含まれていない限り、getServerPort()と同じです。この場合、サーバーポートは、明示的なポートが含まれていればそのヘッダーから取得されるか、使用されるスキームのデフォルトポートと同じになります。
  • クライアントアドレス: getRemoteAddr()
  • クライアントポート: getRemotePort()。リモートポートは当初サポートされていませんでした。ApacheまたはIISでバージョン1.2.32を使用し、Tomcatバージョンが少なくとも5.5.28、6.0.20、または7.0.0の場合に利用可能です。古いバージョンやNSAPIリダイレクターを使用する場合、getRemotePort()は誤って0または-1を返します。回避策として、JkEnvVar REMOTE_PORTを設定することでリモートポートを転送し、getRemotePort()の代わりにrequest.getAttribute("REMOTE_PORT")を使用するか、フィルターを使用してリクエストをラップし、getRemotePort()request.getAttribute("REMOTE_PORT")でオーバーライドすることができます。
  • クライアントホスト: getRemoteHost()
  • 認証タイプ: getAuthType()
  • リモートユーザー: getRemoteUser()tomcatAuthentication="false"の場合
  • プロトコル: getProtocol()
  • HTTPメソッド: getMethod()
  • URI: getRequestURI()
  • HTTPS使用: isSecure(), getScheme()
  • クエリ文字列: getQueryString()
以下の追加のSSL関連データは、SSLOptions +StdEnvVarsを設定した場合にのみ、Apache HTTP Serverによって利用可能になり、mod_jkによって転送されます。証明書情報については、SSLOptions +ExportCertDataも設定する必要があります。
  • SSL暗号: getAttribute(javax.servlet.request.cipher_suite)
  • SSL鍵サイズ: getAttribute(javax.servlet.request.key_size)JkOptions -ForwardKeySizeを使用することで無効にできます。
  • SSLクライアント証明書: getAttribute(javax.servlet.request.X509Certificate)。証明書チェーン全体が必要な場合は、JkOptions ForwardSSLCertChainも設定する必要があります。この場合、ワーカー属性max_packet_sizeを使用してAJPパケットの最大サイズを調整する必要がある可能性が高いです。
  • SSLセッションID: getAttribute(javax.servlet.request.ssl_session)。これはTomcat用であり、まだ標準化されていません。

詳細設定

しかし、状況によってはこれだけでは不十分な場合があります。例えば、ウェブサーバーの前に、HTTPロードバランサーやSSLアクセラレーターとしても機能する、あまり賢くない別のリバースプロキシがある場合を想定します。

その場合、すべてのクライアントがHTTPSを使用していることは確実でも、ウェブサーバーはそのことを知りません。ウェブサーバーが見ることができるのは、アクセラレーターからプレーンなHTTPで来るリクエストだけです。

もう一つの例は、ウェブサーバーの前に単純なリバースプロキシがある場合です。その場合、ウェブサーバーが認識するクライアントIPアドレスは常にこのリバースプロキシのIPアドレスであり、元のクライアントのものではありません。多くの場合、このようなリバースプロキシは、元のクライアントIPアドレス(または、複数のリバースプロキシが連鎖している場合はIPアドレスのリスト)を含むX-Forwareded-forのような追加のHTTPヘッダーを生成します。このようなヘッダーの内容をバックエンドに渡すクライアントIPアドレスとして使用できれば素晴らしいでしょう。

したがって、AJPがバックエンドに送信するデータの一部を操作する必要があるかもしれません。Apache HTTP Server内でmod_jkを使用する場合、いくつかのApache環境変数を使用して、mod_jkにどのデータを転送すべきかを知らせることができます。これらの環境変数は、構成ディレクティブSetEnvまたはSetEnvIfによって設定できますが、mod_rewrite(Apache 2.x以降では環境変数に対してテストできるだけでなく、設定もできます)を使用すると非常に柔軟な方法で設定することもできます。

以下は、mod_jkがデータをバックエンドに送信する前にチェックするすべての環境変数です。

  • JK_LOCAL_NAME: ローカル名
  • JK_LOCAL_PORT: ローカルポート
  • JK_REMOTE_HOST: クライアントホスト
  • JK_REMOTE_ADDR: クライアントアドレス
  • JK_AUTH_TYPE: 認証タイプ
  • JK_REMOTE_USER: リモートユーザー
  • HTTPS: HTTPSが使用されていることを示すため、On(大文字小文字を区別しない)
  • SSL_CIPHER: SSL暗号
  • SSL_CIPHER_USEKEYSIZE: SSL鍵サイズ
  • SSL_CLIENT_CERT: SSLクライアント証明書
  • SSL_CLIENT_CERT_CHAIN_: クライアント証明書チェーンを含む変数名のプレフィックス
  • SSL_SESSION_ID: SSLセッションID

注意: 通常、これらを設定する必要はありません。モジュールはウェブサーバーからデータを自動的に取得します。このデータを変更したい場合にのみ、これらの変数を使用して上書きできます。

これらの変数の一部は、他のウェブサーバーモジュールでも使用される可能性があります。「JK」で始まらないすべての変数は、Apache HTTP Serverによって直接設定されます。データを変更したいが、他のモジュールの動作に悪影響を与えたくない場合は、mod_jkが使用するすべての変数の名前をプライベートなものに変更できます。詳細については、Apacheリファレンスページを参照してください。

SSL関連ではないすべての変数は、バージョン1.2.27で初めて導入されました。

さらに、転送されるクライアントIPアドレスに影響を与える2つの特別なショートカットがあります。JkOptions ForwardLocalAddressを使用すると、ウェブサーバーのローカルIPアドレスをクライアントIPアドレスとして転送できます。これは、例えば、登録されたApache HTTP Serverからの接続のみを許可するためにTomcatのリモートアドレスバルブを使用する場合に役立ちます。JkOptions ForwardPhysicalAddressを使用すると、物理的なピアIPアドレスを常にクライアントアドレスとして転送します。デフォルトでは、mod_jkはウェブサーバーによって提供される論理アドレスを使用します。例えば、mod_remoteipモジュールは、プロキシによってX-Forwarded-Forヘッダーで転送されたクライアントIPを論理IPアドレスに設定します。

Tomcat AJPコネクタ設定

前のセクションで説明した環境変数(Apache使用時にのみ存在)を使用する代わりに、Tomcatを設定してmod_jkによって転送される通信データの一部を上書きすることもできます。Tomcatのserver.xmlにあるAJPコネクタでは、以下のプロパティを設定できます。

  • proxyName: getServerName()によって返されるサーバー名
  • proxyPort: getServerPort()によって返されるサーバーポート
  • scheme: getScheme()によって返されるプロトコルスキーム
  • secure: isSecure()が「true」を返すことを希望する場合、「true」に設定します。
注意: 通常、これらを設定する必要はありません。mod_jkを実行しているウェブサーバーが正しいデータを認識しているすべてのケースでは、AJPが自動的に処理します。

URLの処理

URL書き換え

アプリケーションが利用可能になるURLのパスコンポーネントを変更したい場合があります。特に、ウェブアプリケーションが例えば/myappのようなコンテキストとしてデプロイされている場合、マーケティングは短いURLを好み、アプリケーションがhttp://www.mycompany.com/で直接利用できるようにしたいと考えることがあります。いわゆるROOTコンテキストとしてアプリケーションをデプロイし、「/」で直接利用できるようにすることもできますが、管理者は通常ROOTコンテキストの使用を避ける傾向があります。例えば、1つのホストにつき1つのアプリケーションしかルートコンテキストになれないためです。

リバースプロキシでURLを変更する手順は面倒です。なぜなら、アプリケーションは外部に隠そうとしたパスコンポーネントを含む自己参照URLを生成することが多いためです。それでも、どうしても行う必要がある場合は、以下の手順に従ってください。

ケースA: アプリケーションをシンプルなURLで利用可能にする必要がありますが、ユーザーがそれらを入力する必要がない限り、より複雑なURLを使用しても問題ありません。これは簡単なケースであり、これで十分であれば幸運です。Apache HTTP Serverに単純なRedirectMatchを使用してください。

RedirectMatch ^/$ http://www.mycompany.com/myapp/

その後、アプリケーションはhttp://www.mycompany.com/で利用可能になり、各訪問者はすぐに実際のURLhttp://www.mycompany.com/myapp/にリダイレクトされます。

ケースB: アプリケーションへのすべてのリクエストでパスコンポーネントを隠す必要があります。ここでは、最初のパスコンポーネント/myappを隠したい場合のレシピを紹介します。より複雑な操作は読者の演習とします。まず、Apache HTTP Serverの場合の解決策です。

1. mod_rewriteを使用して、バックエンドに転送する前にすべてのリクエストに/myappを追加します。

# Don't forget the PT flag! (pass through)
RewriteRule ^/(.*) http://www.mycompany.com/myapp/$1 [PT]

2. mod_headersを使用して、アプリケーションが返す可能性のあるHTTPリダイレクトを書き換えます。このようなリダイレクトには通常、隠したいパスコンポーネントが含まれています。これは、HTTP標準により、リダイレクトには常に完全なURLを含める必要があり、アプリケーションはクライアントが短縮されたURLを介して通信していることを認識していないためです。HTTPリダイレクトは、Locationという名前の特別なレスポンスヘッダーで行われます。ここでは、レスポンスのLocationヘッダーを書き換えます。

# Keep protocol, server and port if present,
# but insert our webapp name before the rest of the URL
Header edit Location ^([^/]*//[^/]*)?/(.*)$ $1/myapp/$2 

3. mod_headersを再度使用して、アプリケーションが設定する可能性のあるすべてのクッキーに含まれるパスを書き換えます。このようなクッキーパスにも、隠したいパスコンポーネントが含まれている場合があります。クッキーは、Set-CookieというHTTPレスポンスヘッダーで設定されます。ここでは、レスポンスのSet-Cookieヘッダーを書き換えます。

# Fix the cookie path
Header edit Set-Cookie "^(.*; Path=/)(.*)" $1/myapp/$2 

3. 一部のアプリケーションには、ハードコードされた絶対リンクが含まれている場合があります。この場合、ウェブフレームワークのベースURLを構成するための設定項目があるかどうかを確認してください。ない場合、唯一の解決策は、すべてのレスポンスコンテンツボディを解析し、検索と置換を行うことです。これは脆弱で、非常にリソースを消費します。本当にこれを行う必要がある場合は、このタスクのためにmod_proxy_htmlmod_substitute、またはmod_sedを使用できます。

Microsoft IISをウェブサーバーとして使用している場合、ISAPIリダイレクターには、組み込み機能で最初の手順を実行する方法が用意されています。以下のような簡単なプレフィックス変更用のマッピングファイルを定義します。

# Add a context prefix to all requests ...
/=/myapp/
# ... or change some prefix ...
/oldapp/=/myapp/

そして、そのファイル名をレジストリまたはisapi_redirect.propertiesファイルのrewrite_rule_fileエントリに記述します。uriworkermap.propertiesファイルでは、書き換え前のURLをそのままマップする必要があります!

より複雑な書き換えは、同じファイルを使用しても、正規表現を使って行うことができます。先頭のチルダ記号「~」は、正規表現を使用していることを示します。

# Use a regular expression rewrite
~/oldapps([0-9]*)/=/newapps$1/

ステップ2(リダイレクトレスポンスの書き換え)またはステップ3(クッキーパスの書き換え)のサポートはありません。

URLエンコーディング

エンコードされたURLの使用によって引き起こされる問題があります(パーセントエンコーディングを参照)。同じ場所に対して、多くの異なる同等のURLが存在します。リバースプロキシは、独自の認証ルールを適用し、リクエストをどのバックエンドに送信すべきか(または自身で処理すべきか)を決定するために、URLを検査する必要があります。そのため、まずリクエストURLは正規化されます。パーセントエンコードされた文字はデコードされ、/.//に、/XXX/..//に置き換えられるなど、URLの同様の操作が行われます。その後、ウェブサーバーは書き換えルールを適用して、URLをさらに目立たない方法で変更する可能性があります。最終的に、結果として生成されるURLを、元のURLで使用されたエンコーディングと「類似」したエンコーディングに戻す方法はありません。

歴史的な理由から、mod_jkとISAPIプラグインが、結果のURLをバックエンドに送信する前にどのようにエンコードするかについて、いくつかの代替手段がありました。これらはJkOptions (mod_jk) またはuri_select (ISAPI) を介して選択できました。これらの過去のエンコーディングは、機能的な悪影響を及ぼすか、セキュリティリスクをもたらすため、いずれも推奨されません。バージョン1.2.24以降のデフォルトエンコーディングはForwardURIProxy (mod_jk) またはproxy (ISAPI) であり、デフォルトを維持し、すべての古い明示的な設定を削除することを強く推奨します。

リクエスト属性

Apache HTTP Serverを使用している場合、転送する任意のリクエストにさらに属性を追加することもできます。これにはJkEnvVarディレクティブを使用します(詳細についてはApacheリファレンスページを参照)。このようなリクエスト属性は、Tomcat側でrequest.getAttribute(attributeName)を介して取得できます。mod_jkを介して設定された属性名はrequest.getAttributeNames()にはリストされないことに注意してください!