docker-composeでNginx(リバースプロキシ)を立てて、ローカルでHTTPS通信をやってみた
はじめに
こんにちは、SHIFTの開発部門に所属している Katayama です。
AWS の「ID プロバイダーとフェデレーション」の仕組みを利用して、Google アカウントで AWS を利用・操作してみたでは、HTTPS 化するにあるようなhttpsを用いた実装をしていた。
ただ、実際の本番環境では ALB などの上段のプロキシに SSL 証明書を置いて…というパターンが多いと思うので、ローカルでのHTTPS通信環境の整備として、同じような構成を取る Nginx をプロキシとして置いたパターンで HTTPS 通信をやってみたいと思う。
Web(ブラウザ) +---< HTTPS >---+ Nginx +---< HTTP >---+ Express
※今回、Windows上での環境構築だったため、DNSはなんちゃってDNS(hostsファイルを利用したもの)でやっている。
Web(ブラウザ)と Nginx(プロキシ)間で HTTPS で通信する
後々アプリケーション(サーバー)もコンテナで起動して Nginx 経由で疎通確認する、などの時に柔軟に対応しやすいので、今回は docker-compose で Nginx を構築する。yaml としては以下のようになる。
# docker-compose.yaml
version: "3.9"
services:
nginx:
image: nginx:latest
container_name: nginx
ports:
- 443:443
volumes:
- ./config/nginx:/etc/nginx/conf.d
- ./data/nginx:/var/log/nginx
- ./nginx:/var/www/html
Nginx の設定ファイルをマウントするための volume と、ログファイルをマウントするための volume、あと Nginx にアクセスした際に表示する HTML をマウントするための volume の 3 つを定義している。
続いてプロジェクトルートに Nginx の設定ファイル nginx.conf と、SSL 証明書(オレオレだが)を配置する(証明書の作成方法は実際に HTTPS 化するなどを参照ください)。
$ tree ./config/
./config/
└── nginx
├── nginx.conf
└── ssl
├── server.crt
└── server.key
nginx.conf の中身は以下の通り。
# nginx.conf
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/conf.d/ssl/server.crt;
ssl_certificate_key /etc/nginx/conf.d/ssl/server.key;
root /var/www/html;
location / {
index index.html;
}
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
}
後は docker-compose コマンドで起動してブラウザからアクセスすれば、以下のように NginxHTTPS でアクセスでき、index.html が返ってくる事が確認できる。
study@localhost:~/workspace/openid-connect (aws-iam-federation *)
$ docker-compose up -d
study@localhost:~/workspace/openid-connect (aws-iam-federation *)
$ curl https://192.168.56.2 --insecure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
以下で nginx.conf の設定についていくつか補足をする。
listen ... ssl;
ssl ディレクティブは廃止され、代わりにlisten ディレクティブにて設定する事になった。
docker の Nginx のバージョンは 1.23.1。
study@localhost:~/workspace/openid-connect (aws-iam-federation *)
$ docker exec -it nginx bash
root@1d6f4c80f8a9:/# nginx -v
nginx version: nginx/1.23.1
ssl_certificate, ssl_certificate_key
これはConfiguring HTTPS serversに書かれている HTTPS server を立ち上げるための設定で、ディレクティブ自体はssl_certificateとssl_certificate_keyになる。
今回はオレオレ証明書なのでその証明書があるパスを記載している。
※オレオレの SSL 証明書を作る手順はここを参照ください。
root /var/www/html, index index.html
一旦 HTTPS でアクセスした際に表示するページを表示したいので設定している。root ディレクティブはルートディレクトリを設定するディレクティブで、index ディレクティブはインデックスとして使用されるファイルを定義するディレクティブ。
今回は docker-compose でマウントしている index.html を設定している。
※location ディレクティブはリクエスト URI に応じた設定を行うディレクティブで、今回の設定ではリクエスト URI のパスの先頭が"/"に一致した場合に適応=全ての URI パスで、index index.html;の設定が適応される。
Nginx(プロキシ)と Express(サーバー)間で HTTP 通信する
今度は、Nginx へのリクエストをプロキシで Express のサーバーに飛ばすというのを設定していく。docker-compose.yaml の方は volumes の"- ./nginx:/var/www/html"を削除するのみでいい。
nginx.conf は以下のように設定を変える(変更点は location 内がproxy_pass ディレクティブになっただけ)。
# nginx.conf
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/conf.d/ssl/server.crt;
ssl_certificate_key /etc/nginx/conf.d/ssl/server.key;
location / {
proxy_pass http://192.168.56.2:3000;
}
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
}
上記のように設定する事で、Niginx への HTTPS のリクエストを HTTP にして Express に振り向ける事ができる。ブラウザからアクセスしてみると、以下のようにAWS の「ID プロバイダーとフェデレーション」の仕組みを利用して、Google アカウントで AWS を利用・操作してみたで開いた画面と同じ画面が Nginx 経由で開けることが確認できる(windows の hosts を設定すれば、ドメインでのアクセスも可能)。
hosts の中身は以下の時、example.com でアクセスできるようになる。
# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
(省略)
192.168.56.2 example.com
まとめとして
今回はAWS の「ID プロバイダーとフェデレーション」の仕組みを利用して、Google アカウントで AWS を利用・操作してみたで行っていた Express サーバー自体を HTTPS 化するのではなく、プロキシを挟み、プロキシに SSL 証明書を配置してクライアントとの通信を HTTPS 通信にする方法を試してみた。
少し自分で設定すれば docker の Nginx を利用して簡単に HTTPS 通信を実現でき、それにより本番を想定したアプリケーションの領域とインフラの領域を区別した構成にする事ができるので良いと思った。
おまけ
Cookie を本番環境の設定で検証したい!
本番環境のように Cookie のSecureを設定してアプリケーションを動かすというのをやりたい場合、少し追加の設定が必要になる。
理由としては、Cookie の Secure は HTTPS の通信の場合のみ設定できるものだが、プロキシを経由している関係上、Express と Nginx は HTTP 通信になり、Cookie の Secure 属性を設定できないため。
Nginx +--- < HTTP > ---+ Express
この問題を解決するには、まず、Nginx から Express に対してのリクエストヘッダーにX-Forwarded-Protoを追加し、クライアントとのやり取りのプロトコルを Express が知る事ができるようにする。nginx.conf の設定としては以下。
server {
# 省略
location / {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://192.168.56.2:3000;
}
# 省略
}
proxy_set_header ディレクティブはリクエストヘッダのフィールドを再定義または追加できるようにするディレクティブで、新しく X-Forwarded-Proto ヘッダーを追加している($schemeは公式にあるように、Nginx の conf 内で使用できる組み込み変数で、リクエストスキーマ(プロトコル)である http or https が入る)。ちなみに、AWS の ALB などでも X-Forwarded-Proto はそれを経由する際に付与される(HTTP ヘッダーと Application Load Balancerを参照)。
これで Nginx から Express のリクエストには X-Forwarded-Proto が追加されるようになる。ただ、このままで設定は不十分で、今度は Express 側に追加で以下を追記する必要がある。
// 省略
const app = express();
app.set("trust proxy", 1);
// 省略
これはExpress behind proxiesに書かれているように、1(true と同じ意味)にする事で、上段のプロキシを信頼し、X-Forwarded-*の内容に基づいて Express が動作するように設定できる(本番ではほとんどロードバランサーなどのプロキシがいると思うので、この設定は必須になるだろう)。
ここまで設定すると、以下のようにレスポンスヘッダーの Set-Cookie に Secure が設定されている事が確認できる。
user@DESKTOP-PS112J6 MINGW64 ~
$ curl https://example.com --insecure --head
HTTP/1.1 200 OK
Server: nginx/1.23.1
Date: Mon, 05 Sep 2022 07:40:36 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 3010
Connection: keep-alive
X-XSS-Protection: 0
ETag: W/"bc2-MvNPMpsWfxBGdKEiti6FNmr1b2U"
Set-Cookie: op.sid=s%3AkHEMEj2EL7LYv3l6XDVFK3CiqP0uoHkM.vpqpWZdjDWDhKdnDrNl%2FKzkxz1Tt3VRfX7xyZM%2F%2FERU; Path=/; HttpOnly; Secure
※X-Forwarded-*は他にもX-Forwarded-ForやX-Forwarded-Hostなどがある。
レスポンスヘッダーの Server を消したい!
「Cookie を本番環境の設定で検証したい!」で取り上げた設定では、curl コマンドを実行した時にそのレスポンスヘッダーに"Server"というのが設定されていた。これは基本的にセキュリティ的にはよろしくない(サーバーの種類が分かるとそこから攻撃を受けるリスクが高まる)ので、隠蔽したいという事になると思う。そんな時は以下のようにproxy_pass_headerというディレクティブを利用して、プロキシ自体の Server ではなく、プロキシの背後にあるサーバーの"Server"をレスポンスヘッダーにする事で、サーバー側で特にレスポンスヘッダーに Server を設定していない場合には、クライアントから見たレスポンスヘッダーに Server がない状態を実現できる。
server {
# 省略
location / {
proxy_pass_header Server;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://192.168.56.2:3000;
}
# 省略
}
この設定をした上で curl で確認してみると、以下の通り、Server の情報がない事が確認できる。
user@DESKTOP-PS112J6 MINGW64 ~
$ curl https://example.com --insecure --head
HTTP/1.1 200 OK
Date: Mon, 05 Sep 2022 07:51:09 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 3010
Connection: keep-alive
X-XSS-Protection: 0
ETag: W/"bc2-MvNPMpsWfxBGdKEiti6FNmr1b2U"
Set-Cookie: op.sid=s%3AIYM1zb2ZwgbsnUFgodeO2szcYH3FUwOB.onOrgTurgXo0pVfMIpsKQ9XLywUmcUTtWH7CryniIwQ; Path=/; HttpOnly; Secure
※デフォルトの設定では公式に書かれている通り、プロキシの背後にあるサーバーのレスポンスヘッダーの内、Server などの情報は返さないようになっているので、プロキシの背後のサーバーのレスポンスヘッダーの情報を返したい場合は proxy_pass_header の設定が必要になる。
《この公式ブロガーの記事一覧》
お問合せはお気軽に
https://service.shiftinc.jp/contact/
SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/
SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/
SHIFTの導入事例
https://service.shiftinc.jp/case/
お役立ち資料はこちら
https://service.shiftinc.jp/resources/
SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/