Nginxのリバースプロキシ設定、転送先がCloudFrontの場合

isdはじめに

Nginxのリバースプロキシ設定で、さらに、転送先がAWS CloudFrontという場合の設定方法がすごく難しかったので、ここにまとめておきます。

isd要件

Nginx で、https://www.example.com/foo/~ へのリクエストを、
AWS側のCloudFront https://example8.jp/~ に転送するものとします。

構成図は以下のとおり。

CloudFrontディストリビューションには、CNAMEsとして example8.jp を指定し、CommonName: example8.jp の証明書を適用済み。
CloudFrontのバックエンドはEC2です。

Apacheであれば、以下の設定でOKなんですが、Nginxはけっこう複雑でした。

SSLProxyEngine On
ProxyPass /foo https://example8.jp
ProxyPassReverse /foo https://example8.jp

 

isdNginxのリバースプロキシ設定

以下の設定で、うまくいきました。

location ~ /foo/(.*) {
    resolver 8.8.8.8 8.8.4.4;
    set $backend_server example8.jp;
    proxy_pass https://$backend_server/$1$is_args$args;
    proxy_ssl_protocols TLSv1.2;
    proxy_ssl_server_name on;
    proxy_ssl_name $backend_server;

    proxy_set_header  Host              $backend_server;
    proxy_set_header  X-Forwarded-Host  $host;
    proxy_set_header  X-Real-IP         $remote_addr;
    proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header  X-Forwarded-Proto $scheme;
    proxy_pass_header Authorization;
}

 

ポイントとなる箇所についてコメントします。

IPアドレスのキャッシュ、階層、クエリストリングの問題への対策

location ~ /foo/(.*) {
    resolver 8.8.8.8 8.8.4.4;
    set $backend_server example8.jp;
    proxy_pass https://$backend_server/$1$is_args$args;

 

Nginxは起動時に、転送先のホスト名(ここでは example8.jp)の名前解決を実施して、そのIPアドレスをキャッシュして、終了するまで使い続けます。
そうすると、転送先のIPアドレスが変更となった場合に転送エラーとなってしまいます。
CloudFrontやELBは、わりと頻繁にIPアドレスが変わりますから、キケンですね。

この場合、resolver でDNSサーバーのIPアドレスを指定しておくと、転送時にDNSのTTLを超えていれば、DNSサーバーに問い合わせてくれます。
DNSサーバーは、ここでは、GoogleDNSの 8.8.8.8 と 8.8.4.4 を指定しましたが、/etc/resolv.conf の nameserver で設定しているIPアドレスを指定するとよいでしょう。

ただし、変数を使用しないと resolver でセットした値が使われないので、転送先のホスト名 example8.jp を、変数 $backend_server にセットします。

set $backend_server example8.jp;

 

proxy_pass については、例えば、以下の設定でもよさそうです。

location /foo/ {
    set $backend_server example8.jp;
    proxy_pass https://$backend_server/;

 

ですが、変数を使うと、/foo/ の階層を転送先まで引継ぎ、https://example8.jp/foo/~ としてアクセスしてしまいます。
さらに、Nginxの仕様で、このままでは、転送先にクエリストリングが引き継がれません。

このため、
location ~ /foo/(.*) で正規表現を使い、proxy_pass行では、
proxy_pass https://$backend_server/$1$is_args$args;
として、後方参照の $1 で location の (.*) の部分を代入し、さらに $is_args$args を追加し、?から始まるクエリストリングが抜けないようにします。

これらの問題の対策をまとめた設定が、以下となります。

location ~ /foo/(.*) {
    resolver 8.8.8.8 8.8.4.4;
    set $backend_server example8.jp;
    proxy_pass https://$backend_server/$1$is_args$args;

 

これらの問題については、以下の記事や、記事内の参照記事が参考になります。

(参考)
・NginxでIPアドレスをキャッシュしないようにするために – Qiita
https://qiita.com/kawakawaryuryu/items/af5dcb59aea1a10e4939

・nginx proxy の名前解決問題、ファイナルアンサー? – 1Q77
http://blog.1q77.com/2016/03/nginx-resolving-proxy-upstream/

・nginx の proxy_pass の闇 – サブスレッド技術ブログ
https://www.subthread.co.jp/blog/20190424/

SNI、証明書問題の対策

まず、proxy_ssl_xxx の設定。

    proxy_ssl_protocols TLSv1.2;
    proxy_ssl_server_name on;
    proxy_ssl_name $backend_server;

 

proxy_ssl_protocols TLSv1.2 は不要かもしれませんが、TLSバージョンを明に指定しておきます。
CloudFrontはTLS SNIに対応しているWebサーバーですので、
proxy_ssl_server_name on; で、転送先へのHTTP(S)接続時にTLS SNIを有効とし、
proxy_ssl_name $backend_server; で、SNIで使用するホスト名を指定します。

これらを設定しないと、Nginxが「502 Bad Gateway」でエラーします。
エラーログには以下のように記録されます。

2021/04/06 17:00:05 [error] 55067#55067: *59166 SSL_do_handshake() failed (SSL: error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol) while SSL handshaking to upstream, client: <クライアントのIPアドレス>, server: www.example.com, request: "GET /foo/ HTTP/2.0", upstream: "https://<CloudFrontのIPアドレス>:443/", host: "www.example.com"

 

続いて、HTTPリクエストヘッダー情報の設定。

proxy_set_header  Host              $backend_server;

 

転送時、CloudFrontにリクエストする際のHostヘッダーは、デフォルトだと、もともとの www.example.com となります。
この場合、CloudFront側で「421 Misdirected Request」のエラーが返されます。
おそらく、CloudFrontディストリビューションのCNAMEsで指定した example8.jp と異なるホスト名なので、エラーとなるのでしょう。

このため、ホストヘッダーには、
proxy_set_header Host $backend_server;
として、転送先には、HTTPリクエストのホスト名は example8.jp としてアクセスするようにします。

proxy_set_header Host example8.jp;
でもよいです。

他のヘッダー指定はお好みで。

また、
proxy_set_header X-Forwarded-Host $host;
を指定すると、転送先のサーバー側で、X-Forwarded-Hostを参照することで、リバースプロキシ前のホスト名がわかります。

例えば、転送先のバックエンドのWebサーバーで、
「www.example.com を経由したアクセス以外は拒否する」
という設定が可能となります。

Apache の .htaccess なら、以下のように設定できるでしょう。

SetEnvIf X-Forwarded-Host "www\.example\.com" via_proxy
Require env via_proxy

 

これで、example8.jp への直アクセスや、他のドメインからのリバースプロキシアクセスを拒否することができますね。
CloudFrontにAWS WAFをアタッチして、X-Forwarded-Hostで制御することもできそうです。
(試してはいませんが。)

これらの問題については、以下の記事が参考になります。

(参考)
・Proxying nginx to AWS – Longwave Consulting
http://longwaveconsulting.com/2017/06/proxying-nginx-to-aws

・nginx で TLS SNI 利用サーバーへの reverse proxy – Qiita
https://qiita.com/tak-onda/items/68f9584c8d4624144df0

isd(参考)転送先がWordPressサイトの場合

リバースプロキシの転送先がWordPressサイトの場合は、WordPressの wp-config.php で、以下のようにHTTPヘッダの変数を変更する必要がありました。

$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_X_FORWARDED_HOST'];
$_SERVER['REQUEST_URI'] = '/foo' . $_SERVER['REQUEST_URI'];
$_SERVER['SCRIPT_NAME'] = '/foo' . $_SERVER['SCRIPT_NAME'];
$_SERVER['PHP_SELF'] = '/foo' . $_SERVER['PHP_SELF'];
$_SERVER['SERVER_NAME'] = $_SERVER['HTTP_X_FORWARDED_HOST'];
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];

 

また、WordPress管理画面で「jQuery is not defined」のエラーが発生し、表示崩れが発生したので、以下のように、JavaScript連結を無効化する設定も追加しました。

define('CONCATENATE_SCRIPTS', false);

 

これにより、管理画面のパフォーマンスが少し落ちるそうですが、公開ページのパフォーマンスには影響がなければ問題ありませんね。

ご参考まで。

isdまとめ

転送先がCloudFrontの場合の、Nginxのリバースプロキシ設定についてまとめました。

ApacheやNginxの、リバースプロキシやURLリライトの設定は非常に難しく、いろいろなパターンでテストしないと、思わぬ落とし穴があります。
クエリストリングを含む場合など、いろいろなURLパターンでしっかりテストして、転送先側のCloudFrontやWebサーバーのアクセスログをきちんと確認することも重要なポイントかと思います。

 

Follow me!