リバースプロキシ ハウツー

はじめに

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

一般的な問題

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

別の例として、クライアントIPアドレスがあります。Webサーバーの場合、これは着信接続の送信元IPアドレスですが、バックエンドの場合、接続は常にWebサーバーから発生します。これは、クライアント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()`
`SSLOptions +StdEnvVars`を設定した場合のみ、Apache HTTP Serverによって提供され、mod_jkによって転送される追加のSSL関連データが利用可能になります。証明書情報については、`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ロードバランサーや同様のデバイスなど、Webサーバーの前に別の、あまり賢くないリバースプロキシがある場合を考えてみましょう。このデバイスはSSLアクセラレータとしても機能します。

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

別の例としては、Webサーバーの前に単純なリバースプロキシがあるため、Webサーバーに見えるクライアントIPアドレスが常にこのリバースプロキシの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

覚えておいてください。一般的に、これらを設定する必要はありません。モジュールはWebサーバーからデータを自動的に取得します。このデータを変更する場合にのみ、これらの変数を使用してオーバーライドできます。

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

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

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

Tomcat AJP コネクタ設定

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

  • proxyName: `getServerName()`によって返されるサーバー名
  • proxyPort: `getServerPort()`によって返されるサーバーポート
  • scheme: `getScheme()`によって返されるプロトコルスキーム
  • secure: `isSecure()`が「true」を返すようにしたい場合に「true」に設定します。
覚えておいてください。一般的に、これらを設定する必要はありません。AJPは、mod_jkを実行しているWebサーバーが正しいデータを知っているすべてのケースを自動的に処理します。

URL処理

URL書き換え

アプリケーションが利用可能なURLのパスコンポーネントを変更したい場合があります。特に、Webアプリケーションが` /myapp`などのコンテキストとしてデプロイされている場合、マーケティング担当者は短いURLを好むため、アプリケーションが`http://www.mycompany.com/`で直接利用可能になるようにしたいと考えています。ルートコンテキストとしてアプリケーションをデプロイすることもできますが、これは「/」で直接利用可能になります。管理者は、ホストごとに1つのアプリケーションしかルートコンテキストにできないなど、ルートコンテキストを使用しないことを好むことがよくあります。

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

ケースA:アプリケーションを単純なURLで利用可能にする必要がありますが、ユーザーが複雑なURLを使い続けることは問題ありません(ユーザーが自分で入力する必要がない限り)。これは簡単なケースであり、これで十分であれば幸運です。Apache HTTP Serverで単純な`RedirectMatch`を使用します。

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

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

ケースB:アプリケーションに送信されるすべてのリクエストに対してパスコンポーネントを非表示にする必要があります。最初のパスコンポーネント` /myapp`を非表示にしたい場合のレシピを次に示します。より複雑な操作は、読者への練習問題として残されています。まず、Apache HTTP Serverの場合の解決策を示します。

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

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

2. アプリケーションが返すHTTPリダイレクトを書き換えるには、mod_headers を使用します。このようなリダイレクトには通常、非表示にしたいパスコンポーネントが含まれています。これは、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. アプリケーションが設定する可能性のあるCookieに含まれるパスを書き換えるためにも、mod_headersを再度使用します。このようなCookieパスにも、非表示にしたいパスコンポーネントが含まれている場合があります。Cookieは、Set-CookieというHTTPレスポンスヘッダーを使用して設定されます。レスポンスのSet-Cookieヘッダーを書き換えます。

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

3. 一部のアプリケーションには、ハードコードされた絶対リンクが含まれている場合があります。この場合、Webフレームワークの基本URLを設定するための構成項目があるかどうかを確認します。ない場合は、すべてのレスポンス本文を解析して置換を行うしかありません。これは脆弱で、リソースを大量に消費します。どうしてもこれを行う必要がある場合は、mod_proxy_htmlmod_substitute、またはmod_sed を使用できます。

Microsoft IISをWebサーバーとして使用している場合、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(Cookieパスの書き換え)はサポートされていません。

URLエンコーディング

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

歴史的な理由から、mod_jkとISAPIプラグインは、バックエンドに送信する前に結果のURLをエンコードする方法にいくつかの選択肢がありました。これらはJkOptions(mod_jk)またはuri_select(ISAPI)を使用して選択できます。これらの履歴エンコーディングはどれも推奨されません。機能的な問題があるか、セキュリティリスクをもたらすためです。バージョン1.2.24以降のデフォルトエンコーディングはForwardURIProxy(mod_jk)またはproxy(ISAPI)であり、デフォルトを維持し、古い明示的な設定をすべて削除することを強くお勧めします。

リクエスト属性

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