CloudFrontのオリジンをEC2にしてHTTPS通信を強制する構成について

2024.01.16

しばたです。

個人的にはあまりお勧めしないのですが、小規模な環境でコストを重視する場合にEC2インスタンス単体をCloudFrontのオリジンにすることがあると思います。
また、セキュリティ要件によりエンドツーエンドで通信の暗号化が必須という場合もあるでしょう。

この様な場合ざっくり下図の構成になると思います。

エンドツーエンドで暗号化通信を行うためEC2インスタンスにもSSL証明書を導入しCloudFront⇔EC2間の通信をHTTPSにしてやります。
環境によってはAWS WAFを導入したり静的コンテンツをS3バケットに配置することもあるでしょう。

本記事ではこの構成のうちCloudFrontとEC2を中心にした構築方法について解説していきます。

CloudFrontとカスタムオリジン間の通信でHTTPSを必須にする

CloudFrontからEC2オリジン(カスタムオリジン)の間でHTTPS通信を必須にする方法は公式ドキュメントに記載されています。

オリジンプロトコルポリシー

CloudFrontからオリジンに対するプロトコルはオリジンの設定で決定します。

この設定で「HTTPSのみ」を選んでやればCloudFrontからはHTTPS通信のみ行われます。
ポート番号やサポートするTLSのバージョンは環境に応じて設定してやります。

オリジンで必要な証明書

オリジンで必要な証明書については少し注意が必要です。
まず、

・ Elastic Load Balancing ロードバランサー以外のオリジンの場合、信頼されたサードパーティー認証機関 (CA) (Comodo、DigiCert、Symantec など) によって署名された証明書を使用する必要があります。

であり自己署名証明書(いわゆるオレオレ証明書)ではダメです。
そして、CloudFrontがオリジンの証明書を検証する際の条件として

オリジンから返される証明書には、次のいずれかのドメイン名が含まれている必要があります。
・ オリジンのオリジンドメインフィールド (CloudFront API の DomainName フィールド) のドメイン名。
・ キャッシュ動作が Host ヘッダーをオリジンに転送するように設定されている場合は、Host ヘッダーのドメイン名。

と記述されています。
文章だとすこし分かりにくいですが、図にするとそんなに難しいことは言っていないことが理解できます。

1. オリジンドメイン名を検証するパターン

CloudFrontに割り当てるドメイン名をwww.example.com、EC2オリジンをorigin.example.comとした場合、CloudFrontからはhttps:// origin.example.comへのアクセスが発生するためオリジンドメインのorigin.example.comに対する証明書が必要なのは直感的に理解できると思います。

オリジンにある証明書がCloudFrontのオリジンドメイン名と一致する場合はHOSTヘッダ転送の有効・無効に関わらずHTTPSアクセス可能でした。
また、この場合CloudFrontのデフォルトドメイン*.cloudfront.netをそのまま使っても特に問題はありませんでした。

ここまでをざっくりまとめると下表のとおりです。
SNIホスト名については後述します。

CloudFrontのドメイン名 オリジンドメイン名 オリジンの証明書 HOSTヘッダ SNIホスト名 ブラウザでの表示
www.example.com origin.example.com origin.example.com 転送する www.example.com OK : 200
www.example.com origin.example.com origin.example.com 転送しない origin.example.com OK : 200
cloudfront.net origin.example.com origin.example.com 転送する cloudfront.net OK : 200
cloudfront.net origin.example.com origin.example.com 転送しない origin.example.com OK : 200

2. HOSTヘッダのホスト名を検証するパターン

2番目の条件についてCloudFrontの設定でHOSTヘッダを転送するとヘッダにはwww.example.comが設定されオリジンアクセスすることになりますが、この場合はwww.example.comに対する証明書でもOKという扱いになっています。
そして、この条件でHOSTヘッダを転送しない場合は検証エラーとなり502を返し、レスポンスヘッダでX-Cache = Error from cloudfrontが設定されます。

加えてCloudFrontのデフォルトドメイン*.cloudfront.netをそのまま使う場合はHOSTヘッダを転送する設定にしても502エラーとなります。
この場合はオリジンにある証明書と名前が一致することが一切無いため当然といえば当然です。

ここまでをまとめると以下の通りです。

CloudFrontのドメイン名 オリジンドメイン名 オリジンの証明書 HOSTヘッダ SNIホスト名 ブラウザでの表示
www.example.com origin.example.com www.example.com 転送する www.example.com OK : 200
www.example.com origin.example.com www.example.com 転送しない origin.example.com NG : 502
cloudfront.net origin.example.com www.example.com 転送する cloudfront.net NG : 502
cloudfront.net origin.example.com www.example.com 転送しない origin.example.com NG : 502

余談 : HOSTヘッダ転送時のSNIホスト名について

HOSTヘッダを転送した場合はオリジンアクセス時のSNIホスト名(NGINXだと$ssl_server_name)もヘッダと同じ値に設定され、パケットキャプチャの結果も確かにそうなっていました。
(HOSTヘッダを転送しない場合はSNIホスト名はオリジンドメイン名となる)

本記事の内容にはあまり関係ないとは思いますが一応記録だけ残しておきます。

試してみた

ここからは簡単な検証環境を作って実際に試した結果を共有します。

検証環境は私の検証用AWSアカウントになります。

1. EC2単体でのサーバー公開

最初に東京リージョンにVPC環境とEIPを持つAmazon Linux 2023 EC2インスタンスを一台用意し、www.example.shibata.techというDNS名を与えておきます。

DNSはRoute 53を使っています。

このEC2インスタンスにLet's Encryptで発行した証明書を登録してWEBサーバーを公開できる様にします。
証明書登録の手順は本筋では無いので簡単に紹介するにとどめておきます。

# NGINXのインストール
sudo dnf install -y nginx
sudo systemctl start nginx
sudo systemctl enable nginx

# テスト用コンテンツ更新
cat << EOF | sudo tee /usr/share/nginx/html/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>テストページ</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>テストページ</h1>
<p>Hello Classmethod!</p>
</body>
</html>
EOF

/etc/nginx/nginx.confserver_nameディレクティブだけ変更。

/etc/nginx/nginx.conf

# server_name ディレクティブを変更
server_name  www.example.shibata.tech;

certbotをインストールして証明書の発行と設定を実施。
今回は一時的な検証用のため更新のことは一切考えずにHTTP-01チャレンジにしています。

# certbotのインストール
sudo dnf install -y certbot python3-certbot-nginx

# 証明書発行 ※今回はテスト用なのでメアド登録はせず
sudo certbot --nginx -d www.example.shibata.tech --register-unsafely-without-email
#
# あとは画面の指示に従いよしなに対応
#

結果、下図の様にhttps:// www.example.shibata.techでHTTPSアクセス可能になっています。

ここまでがEC2構築手順となります。

2. CloudFrontの追加 (HOSTヘッダのホスト名を検証するパターン)

次にこの環境に対してCloudFrontを追加します。

CloudFrontの代替ドメイン名をwww.example.shibata.techにしたいためEC2のDNS名をorigin.example.shibata.techに変更します。

その上でACMに前節で作成した証明書をインポートし *1、新たにCloudFrontディストリビューションを作成してやります。

ざっくり以下の設定をしてやります。
オリジンドメイン名をorigin.example.shibata.techにし、プロトコルをHTTPSのみとします。
これでオリジンに対してHTTPSアクセスが必須になります。

対象となるビヘイビア(今回はデフォルトビヘイビア)の設定はビューワープロトコルポリシーをHTTPS onlyRedirect HTTP to HTTPSにします。
今回はRedirect HTTP to HTTPSを選んでいます。

キャッシュポリシーは環境に応じて変えることになりますが、まずは「2. HOSTヘッダのホスト名を検証するパターン」を作りたいのでHOSTヘッダを転送する設定にしてやります。
下図ではキャッシュポリシーがCachingDisabledで一切キャッシュせず、オリジンリクエストポリシーをAllViewerにしてHOSTヘッダを含むすべてのヘッダを転送する様にしています。

全体設定はCNAMEをwww.example.shibata.techとし、ACMにインポート済みの証明書を使う様にします。

これでディストリビューションを作成してやり、最終的には以下の様にします。

EC2のセキュリティグループはこんな感じでCloudFrontからのHTTPS通信のみ許可する様にしています。
これで誤ってHTTP通信を受けることはありません。

この状態でhttps:// www.example.shibata.techにアクセスしてやるとエラー無くコンテンツを表示してくれます。

これで「2. HOSTヘッダのホスト名を検証するパターン」の環境が完成です。
はじめにEC2単体で公開しているサーバーがあり、そこにCloudFrontを追加する場合がこのパターンを選ぶユースケースとなるでしょう。

HOSTヘッダ転送を止めた場合

ここでオリジンリクエストポリシーをAllViewerExceptHostHeaderにしてHOSTヘッダの転送を止めてみます。

すると想定通りhttps:// www.example.shibata.techにアクセスすると502エラーとなりました。

この場合リクエストのレスポンスヘッダにX-Cache: Error from cloudfrontが返ってくるのに加え、

NGINXのデバッグログで以下の様な「CloudFront側(Peer)からのSSL接続切断」メッセージが記録されます。

# NGINXのデバッグログからSSLに関するものを抽出
$ tail -f /var/log/nginx/error.log | grep SSL
2024/01/16 03:41:18 [debug] 1626#1626: *11 SSL server name: "origin.example.shibata.tech"
2024/01/16 03:41:18 [debug] 1626#1626: *11 SSL_do_handshake: -1
2024/01/16 03:41:18 [debug] 1626#1626: *11 SSL_get_error: 2
2024/01/16 03:41:18 [debug] 1626#1626: *11 SSL handshake handler: 0
2024/01/16 03:41:18 [debug] 1626#1626: *11 SSL_do_handshake: -1
2024/01/16 03:41:18 [debug] 1626#1626: *11 SSL_get_error: 5
2024/01/16 03:41:18 [info] 1626#1626: *11 peer closed connection in SSL handshake (104: Connection reset by peer) while SSL handshaking, client: 130.176.135.149, server: 0.0.0.0:443

残念ながらCloudFrontのログに欲しい情報はありませんでした。
NGINXの方もデバッグログでないとダメだったのでトラブルシューティングは少し難しい感じです。

3. オリジンドメイン名を検証するパターンの環境構築

最後に「1. オリジンドメイン名を検証するパターン」の構成も紹介します。

こちらについては前節までの環境に対し、

  1. EC2の証明書をorigin.example.shibata.techで取得しなおす
  2. (Optional) CloudFrontの証明書をACMで取得しなおす

をしてやればOKです。
HOSTヘッダは転送してもしなくても構いません。

二種類の証明書が必要になるため、最初からCloudFront + EC2の環境を作る際にこのパターンを選ぶと良いでしょう。
CloudFrontの証明書をACMで発行する様にすれば運用負荷も減るはずです。
(SANやワイルドカード証明書を使う形でも構いません)

4. その他リソースについて

最初に紹介した図ではAWS WAFやS3も含めていますが、これらの設定については特筆する点が無いため割愛します。
強いて上げるなら「CloudFrontでS3オリジンにする際はHOSTヘッダを転送してはいけない」くらいですが、本日時点ではマネジメントコンソールでも警告してくれるので大丈夫かと思います。

(S3オリジンの場合AllViewerが設定できない等サポートが手厚くなっている)

終わりに

以上となります。

若干まとまりがない感じになってしまいましたが構成や構築方法の基本的なところは掴んでいただけると思っています。

あと、今回動作確認してみた結果かなり昔のブログ記事等にある挙動と異なるところがあり、この違いについて原因を突き止めることはできませんでした...
「何らかの前提条件が異なるせいなのか?」「CloudFrontの仕様が変わったのか?」など様々な要因があると思います。

このため環境によっては本記事の通りにならないかもしれませんのでご注意ください。

脚注

  1. ここはACMで証明書発行しても構わない