SSLアクセラレータ配下のapacheで、アクセスがhttpかhttpsかを判別する方法

少し前に試行錯誤して現在はひとまず解決したのですが、同じようにはてなで悩んでた人がいるみたいなので、自分の設定例を軽くまとめてみる。

SSLアクセラレータ配下にあるapache上でクライアントからのアクセスがhttpsかhttpであるかの判別をする方法はありますか?
mod_rewriteを利用しhttpのアクセスをhttpsにリダイレクトする設定を考えていますが、SSLアクセラレータを経由してのアクセスとなるため、apacheへの接続は全てhttpとなります。

回答にもあるように、恐らくapache単独では解決できませんが、SSLアクセラレータの設定と組み合わせれば可能です。それも拡張ヘッダやmod_rewriteなどの特別な仕組みは使用せずに。ただし、ここで書いてるのはSSLアクセラレータ付きのロードバランサ(以下、LB)を使用してる例なので、他の全てのSSLアクセラレータには当てはまらないかもしれません。でも似たようなことはできるかと。


あと、記事を書きながらhttps時のリダイレクト(自己参照URL)についても思い出して、それについても考慮して書きました。単にportを判別するだけならもっと簡単で良さげな気もしますが、長々と書いちゃっています。

設定

とりあえず先に結論を。

LB,SSLアクセラレータの設定

実サーバにhttp/httpsのリクエストを送信する際に、転送先のポート番号をそれぞれ個別に設定する。例えば

言い換えると、http/httpsともに実サーバの80番にしか送れない場合は判別できません。たぶん大抵の場合は大丈夫だと思います。

### 2016 追記
L7まで見れる最近のロードバランサ(ADC)の場合、http headerをいじれるので
その場合はhttp/httpsともに実サーバが80番でも大丈夫ですね。
port:80のvirtualserverの場合はheaderをいじらず実サーバに渡す。
port:443のvirtualserverの場合はこのエントリ末尾に書いているように、"X_HTTPS: ON"のようなheaderをinsertする。
アプリケーション側でX_HTTPS headerがあるかどうかで判別する。
### 追記終わり

実サーバのapacheの設定

NameVirtualHostでポート番号を変えて、http用とhttps用の2つの設定を作ります。
後で詳しく書きますが自己参照URLとServerNameの設定が大事。apacheのServerNameのドキュメントにもこう書いています。

リバースプロキシやロードバランサやSSL負荷軽減装置のような、 SSLを処理するマシンの後ろでサーバを動かす場合は、 サーバが正しい自己参照 URLを確実に生成するように、 https:// スキームとクライアントが接続するポート番号を、 ServerName ディレクティブに指定してください。

Listen 80
Listen 8443

NameVirtualHost *:80
NameVirtualHost *:8443

# http用の設定
<VirtualHost *:80>
  ServerName www.hoge
  ## その他色々
</VirtualHost>

# https用の設定
<VirtualHost *:8443>
  ServerName https://www.hoge
  ## その他色々
</VirtualHost>

schemeとport番号を明示的に書くならこう。

# http用の設定
<VirtualHost *:80>
  ServerName http://www.hoge:80
  ## その他色々
</VirtualHost>

# https用の設定
<VirtualHost *:8443>
  ServerName https://www.hoge:443
  ## その他色々
</VirtualHost>

判別方法

こう設定しておけば、httpとhttps環境変数SERVER_PORTの値が変わります。cgi,phpなどで適当に出力すればわかります。

ここでhttps環境変数が8443ではなくて、443になっているのがポイントです。

  1. clientからのhttpsのアクセスはLB,SSLアクセラレータを介して実サーバの8443に転送
  2. 実サーバのapacheはVirtualHostの8443番で待ち受けて応答

しているので、8443になりそうですが。。。
以下、それの補足です。

ServerNameと自己参照URL

これを理解するまで色々試行錯誤しました。詳細はapacheの以下ドキュメントを参照。

なぜhttps用のVirtualHostのServerNameにhttps://のschemaを指定しているのか。

話がややこしくならないように、ここでは以下のapacheのデフォルト設定が入ってるとします。

UseCanonicalName Off
UseCanonicalPhysicalPort Off

そして、ServerNameに他の設定をした例を紹介します。

ServerNameにschemaを指定せず、かつポート番号を書かない場合

通常のVirtualHostのように設定すると、以下のようになると思います。

# https
<VirtualHost *:8443>
  ServerName www.hoge
  ## 残りhttps用の設定
</VirtualHost>

このとき、clientからhttps://www.hoge/にアクセスされた場合の、apacheの自己参照URLはhttp://www.hoge/になっていると思われます。すると以下の問題が起こります。

  1. 環境変数で取得できるSERVER_PORTが80番になる
  2. 自己参照URLがhttp://のため、"/"なしのdirectoryにアクセスした場合などのリダイレクトがhttpになる

1番はそのままですね。本エントリの条件を満たしてないので問題外。2番は例を見てもらったほうがわかりやすいと思います。

"/"なしのディレクトリにアクセスした場合、

apacheデフォルトではmod_dirのDirectorySlashが働いて"/"が補完されます。このときにclientに返されるURLは、自己参照URLに/が補完された形です。こんな感じです。

はてなグループSSLアクセラレータを使っているかは知りませんが、httpにリダイレクトするという例で紹介します。

opensslで確認するとよくわかります。

$ openssl s_client -connect fragments.g.hatena.ne.jp:443 -state
(中略)
HEAD /hogem HTTP/1.0
host: fragments.g.hatena.ne.jp

HTTP/1.1 302 Found
Date: Fri, 14 Nov 2008 12:22:30 GMT
Server: Apache/2.2.3 (CentOS)
Location: http://fragments.g.hatena.ne.jp/hogem/
X-Framework: Hatena/2.0
Content-Type: text/html; charset=iso-8859-1
Vary: Accept-Encoding
Connection: close

SSL3 alert read:warning:close notify
closed
SSL3 alert write:warning:close notify

302でhttpのURLが返ってきています。

ServerNameに8443のポート番号を明示的に指定する場合

最初はこの設定にしていました。この設定だとclientがhttpsに来た場合にSERVER_PORTを8443で取れるから。ただ、ServerNameの説明を読み直して、"これはひどい"状態になっていることが確認できたので慌てて元に戻しました。
同じようにclientからhttps://www.hoge/にアクセスされた場合を考えます。この設定だとapacheの自己参照URLがhttp://www.hoge:8443 になります。そのため

  1. 環境変数で取得できるSERVER_PORTが8443になってhttpとhttpsは判別できる、、、が!
  2. リダイレクトするとhttp://www.hoge:8443/directory/のようなURLになる

opensslで確認すると302でLocatin: http://www.hoge:8443/directory/ が返ってくるのが確認できます。8443番ポートなんてのは通常FWで閉じるのでアクセスすら出来ません。おまけに内部の仕組みが出てカッコ悪いですね。あわてて修正した苦い記憶が。。。

まとめ

httpかhttpsかを判別する方法。

  1. SSLアクセラレータ: httpとhttpsのポート番号を変える
  2. apache: httpとhttpsでポート番号を変えたNameVirtualHostを設定する
  3. httpsのVirtualHostのServerNameはhttps://のschemeを明示的に指定する

20090323追記

よくよく考えたらこんな面倒なことしてポート番号を見なくてもいいのではないかということに気が付いた。mod_envで適当な環境変数作るだけでいい?
https用のVirtualHost内で

Setenv X_HTTPS ON

とか

Setenv HTTPS ON

とかを設定して、後はプログラム側で$ENV{X_HTTPS}を判定するだけ。(phpは$_SERVER["HTTPS"] でHTTPSを判別してると思うので後者のほうがいいかも?本当のSSLじゃないので個人的には環境変数 HTTPS を設定するのは気持ち悪いですが)

この場合でもやはりhttpとhttps用のvirtualhostをわけることは必須ですね。というか、わけなかった場合、DocumentRootやCustomLogの設定まで全て同じになるので、ポート番号を判別したくなくても個別に設定したほうが良いのか。

*1:実サーバにhttps用のバーチャルホストを設定しますが、プロトコルはhttpなので443は使わず8443にしています。サーバでmod_sslなどを使ってないなら443でも問題ないです