HTTPS通信のリバースプロキシ

フロントのApacheやNginxでHTTPS通信を受けてSSL終端し、バックエンドのアプリケーションへプロキシするとする。アプリケーションでリダイレクトをしようとすると、HTTPS通信をしてほしいのにLocationヘッダにHTTP通信が指定されてしまうことがある。
Jenkinsをバックエンドのアプリケーションにおいた場合を想定し、ApacheとNginxでの解決方法をそれぞれ記載する。

Apache

HTTPS通信を正しくリバースプロキシするための方法は三つある。

<VirtualHost *:443>
 ServerName ci.example.com
 SSLEngine on
 (略)

### 方法1
#  <Location /jenkins>
#    ProxyPass http://localhost:8080/jenkins nocanon
#    ProxyPassReverse http://localhost:8080/jenkins
#    ProxyPassReverse http://ci.example.com/jenkins
#  </Location>

### 方法2
#  <Location /jenkins>
#    ProxyPass http://localhost:8080/jenkins nocanon
#    ProxyPassReverse http://localhost:8080/jenkins
#    Header edit Location ^http:// https://
#  </Location>

### 方法3
#  <Location /jenkins>
#    ProxyPass http://localhost:8080/jenkins nocanon
#    ProxyPassReverse http://localhost:8080/jenkins
#    RequestHeader set X-Forwarded-Proto https
#    RequestHeader set X-Forwarded-Port 443
#  </Location>
</VirtualHost>

リバースプロキシの仕方

三つの方法について詳細を書く前に、Apacheでのリバースプロキシの仕方について簡単に書く。

リバースプロキシする場合に一般的な記述は次の二行だ。

ProxyPass http://localhost:8080/jenkins nocanon
ProxyPassReverse http://localhost:8080/jenkins

ProxyPassディレクティブでリバースプロキシを行う。バックエンドアプリケーションを同じサーバにおいているためlocalhost:8080としているが、別のリモートサーバでも大丈夫だ。
ProxyPassReverseディレクティブでバックエンドからのリダイレクトがあった場合を対処する。このディレクティブはApacheにHTTPリダイレクト応答の Location, Content-Location, URI ヘッダの調整をさせる。これがないとアプリケーションがリダイレクトするときにバックエンドのURI(この場合は http://localhost:8080/jenkins )がLocationヘッダに設定されてしまう。

方法1: ProxyPassReverseを使う

それではHTTPS通信を扱う場合の方法について見ていく。
ひとつ目の方法はProxyPassReverseを注意深く使う。

ProxyPass http://localhost:8080/jenkins nocanon
ProxyPassReverse http://localhost:8080/jenkins
ProxyPassReverse http://ci.example.com/jenkins

バックエンドアプリケーションからhttp://ci.example.com/jenkinsにリダイレクトするように指定された場合に元々のHTTPSプロトコルとFQDNのhttps://ci.example.com/jenkinsが設定されるようなProxyPassReverseを設定している。

Jenkinsはリダイレクト時のLocationでアクセス時のFQDNで返してくるようだった。
試しにProxyPassだけ記載してcurlでアクセスすると以下のLocationが返ってきた。

$ curl -sI -X GET -u usr:pass --insecure https://ci.example.com/jenkins | awk '/Location/ || NR==1'
HTTP/1.1 302 Found
Location: http://ci.example.com/jenkins/

このLocationにマッチするようにProxyPassReverseを書くために、ProxyPassReverse http://localhost:8080/jenkins以外にProxyPassReverse http://ci.example.com/jenkinsを書いている。

Jenkinsがlocalhost:8080でLocationヘッダを書き戻さないのは、Apacheがリバースプロキシする際に以下のヘッダを追加するが、この中のX-Forwarded-Hostをうまく扱ってくれるからか?

ヘッダ 説明
X-Forwarded-For クライアントの IP アドレス
X-Forwarded-Host オリジナルのホスト名。クライアントが Host リクエストヘッダで渡す
X-Forwarded-Server プロキシサーバのホスト名

アプリケーションのリダイレクト時のLocationヘッダが何になるかはアプリケーション依存のところが強く、実際の挙動を確かめて注意深く設定することになる。一般的にはProxyPassと同じURIを持つProxyPassReverseをいつでも書いておくのがいいと思う。

ちなみにpythonで簡易HTTPサーバを立ち上げてApacheのProxyPassディレクティブでプロキシしてみると、FQDNもつけずに相対パスで返ってきた。

$ mkdir python
$ python -m SimpleHTTPServer &
$ curl -sI --insecure https://ci.example.com/python | awk '/Location/ || NR==1'
HTTP/1.1 301 Moved Permanently
Location: /python/

HTTP/HTTPSのプロトコルがついていない//FQDN/PATHのようなスキーマレスURLや相対パスの場合はProxyPassReverseをつけなくても上手くHTTPSでアクセスして入ればHTTPSにリダイレクトしてくれるだろう。

方法2: Header editを使う

荒技だが方法1よりも簡単に使える。Header editでHTTPヘッダをhttpからhttpsに書き換えてしまえばいい。

Header edit Location ^http:// https://

Locationのhttp://https://に置換されているのがわかる。

$ curl -sI -X GET -u usr:pass --insecure https://ci.example.com/jenkins | awk '/Location/ || NR==1'
HTTP/1.1 302 Found
Location: https://ci.example.com/jenkins/

方法3: X-Forwarded-Protoを使う

バックエンドのアプリケーションの実装次第となるが、X-Forwarded-Protoヘッダを加えると、そこで指定されたプロトコルでアクセスされていると判断してリダイレクトのLocation等を生成してくれる。
Ruby on RailsやJenkinsは対応しているが、対応していないアプリケーションも多い。

X-Forwarded-Proto以外にX-Forwarded-Portもあり、なくても動くが念のためHTTPSのポート443を指定しておくのがいい。

RequestHeader set X-Forwarded-Proto https
RequestHeader set X-Forwarded-Port 443

Apacheにおけるベストな方法

個人的に思うApacheにおけるベストは方法2だ。

方法1は素直にApacheのリバースプロキシ関連のディレクティブを使っているが具体的にバックエンドのアプリケーションがどのようなURLをLocationにセットするのか確認する必要がある。方法3はデファクトスタンダードといってもいいX-Forwarded系のヘッダを使っているものの、RFC標準ではないため全てのアプリケーション/フレームワークが対応しているとも限らない。

方法2のLocationヘッダを書き換えてしまうという方法が強引に見えても一番確実かつ汎用的に思う。

Nginx

NginxはApacheより簡単だ。方法は二つある。

server {
   listen       443;
   server_name  ci.example.com ;
(略)
   location / {
   (略)

### 方法1
#        proxy_pass http://127.0.0.1:8080;
#        proxy_set_header Host $http_host;
#        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#        proxy_redirect http:// https://;

### 方法2
#        proxy_pass http://127.0.0.1:8080;
#        proxy_set_header Host $http_host;
#        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#        proxy_set_header X-Forwarded-Proto https;
#        proxy_set_header X-Forwarded-Port 443;
   }
}

方法1: proxy_redirectを使う

proxy_redirectでhttp://https://に変えることができる。ApacheのHeader edit Locationと内容は同じだが、標準でNginxが用意しているものなのでApacheの時のような強引さがない。

proxy_redirect http:// https://;

これがNginxにおいて個人的にベストな方法であるのはApacheからの流れで言うまでもない。

方法2: X-Forwarded-Protoを使う

Apacheの方法3と同様のことがNginxでもできる。

proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Port 443;