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;