CloudFront-Viewer-TLSヘッダとCloudFront Functionsで一定のTLSバージョン未満の場合にコンテンツを非表示にする仕組みをつくってみた

先日のアップデートで追加されたCloudFront-Viewer-TLSヘッダをCloudFront Functions内で参照し、TLSバージョンが指定の値未満の場合にコンテンツを非表示とする仕組みを作成してみました。
2022.06.26

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

清水です。先日(2022/05/23)のAmazon CloudFrontのアップデートでCloudFront-Viewer-TLSヘッダが利用可能になり、クライアントがCloudFrontに接続する際に使用しているTLSバージョンと暗号スイートがリアルタイムに確認可能になりました。

上記のエントリではこのCloudFront-Viewer-TLSヘッダをオリジン側で出力して確認してみました。今回はクライアントからCloudFrontへのアクセス時にCloudFront Functions内にでこのCloudFront-Viewer-TLSヘッダを確認、ヘッダの値からTLSバージョンが一定の値より小さければ本来のコンテンツを非表示にして最新のTLS使用を促すページを表示する、という仕組みをつくってみたのでまとめてみたいと思います。

CloudFront-Viewer-TLSヘッダによるTLSバージョンの確認

まずはCloudFront-Viewer-TLSヘッダによるTLSバージョンの確認方法についておさえておきましょう。CloudFront Developer Guideに記載がある通りCloudFront-Viewer-TLSヘッダは以下のフォーマットとなります。

  • SSL/TLS-version:cipher

具体的な例もいくつか確認しておきましょう。こちらもDeveloper Guide記載の例です。

  • TLSv1.3:TLS_AES_128_GCM_SHA256
  • TLSv1.2:ECDHE-ECDSA-AES128-GCM-SHA256
  • TLSv1.1:ECDHE-RSA-AES128-SHA256
  • TLSv1:ECDHE-RSA-AES256-SHA

SSL/TLSとそのバージョンのあとに:区切りで暗号スイート情報が記載されます。この文字列(ヘッダの値)を:で区切り、はじめの要素を確認すればSSL/TLSとそのバージョンが取り出せますね。以下CloudFront Functionsでの実装を想定してJavaScriptでバージョンの扱い方を確認していきます。文字列を区切り文字で分割するにはsplit()メソッドを使えば良さそうです。(参考: String.prototype.split() - JavaScript | MDN

var tlsVersionAndCipher = 'TLSv1.3:TLS_AES_128_GCM_SHA256';
console.log(tlsVersionAndCipher.split(':')[0]);  // この出力が「TLSv1.3」

さらにTLSのバージョンの数値だけを扱うには、文字vを区切り文字としてこのvに続く部分を確認すれば良さそうです。

console.log(tlsVersionAndCipher.split(':')[0].split('v')[1]);  // この出力が「1.3」

これで「1.3」とバージョンの数値が取り出せました。ただこのままだとこの「1.3」は文字列として扱われるため、parseFloat()メソッドで小数点数値に変換しておきます。(参考: JavaScriptで文字列を数値に変換する:Number(), parseInt(), parseFloat() | UX MILK なおCloudFront FunctionsはECMAScript 5.1 準拠のJavaScriptということで、最新のJavaScriptすべてのメソッド等が利用できるわけではないようなので注意します。CloudFront Functions でサポートされているランタイムについて調べてみた - michimani.net

最終的に以下で抽出・変換したtlsVersionを数値として比較することでTLSバージョンが一定以上であることを確認します。

var tlsVersion = parseFloat(tlsVersionAndCipher.split(':')[0].split('v')[1]);

TLSが一定バージョン未満であった場合の対応

CloudFront-Viewer-TLSヘッダからTLSバージョンが抽出でき、比較の準備ができました。この比較により、一定のTLSバージョン未満だった場合の挙動を決めておきます。今回はいずれのパス(request.uri)へのアクセスも、/tls-alert.htmlに準備していおいたページを返すことにします。あらかじめオリジンサーバ側にtls-alert.htmlというファイルを作成しておきます。以下の内容としました。(最新のTLSを使用するように促す内容を表示するだけのページです。)

tls-alert.html

<html>
  <head>
    <title>TLS ALERT</title>
  </head>
  <body>
    <p>最新のTLSバージョンを使用してください</p>
  </body>
</html>

https://[cloudfront-domain]/tls-alert.htmlにアクセスすれば上記の内容が返るぐあいですね。

CloudFront Functionsの作成とテスト

Functionのコード

TLSバージョンの判定方法と、一定のバージョン未満だった場合の想定する挙動を確認することができました。いよいよCloudFront Functionsを作成していきます。

Functionとして作成するコードは以下のようになります。TLSのバージョンを判定し、今回は1.2以上であれば通常のコンテンツを返すこととしました。1.2未満の場合はtls-alert.htmlの内容を返します。(最新のTLSバージョンのみを扱うということであれば1.3以上にするのが良いかと思いますが、動作確認の意味合いも込めて1.2をしきい値として選択しました。)またHTTPSではなくHTTPのアクセスではCloudFront-Viewer-TLSヘッダ自体が付与されないようです。この場合もTLS利用を促すメッセージを表示することとしました。

tls-alert.html

function handler(event) {
    var request = event.request;
    var headers = request.headers;

    if (headers['cloudfront-viewer-tls']) {
        var tlsVersionAndCipher = headers['cloudfront-viewer-tls'].value;
        var tlsVersion = parseFloat(tlsVersionAndCipher.split(':')[0].split('v')[1]);

        if (tlsVersion >= 1.2) {
            return request;
        }
    }

    request.uri = '/tls-alert.html';
    return request;
}

Functionの作成

コードを確認したところで、実際にFunctionを作成していきます。CloudFrontのマネジメントコンソール、Functionsのページから[Create function]ボタンで進みます。NameとしてTLS-Version-Check、DescriptionとしてCheck TLS Version.を入力してfunctionを作成します。

続く画面のBuildタブ、Function codeのDevelopment部分を先ほどのコードで上書きして[Save changes]します。これで作成自体は完了です。

Functionのテスト

Functionの作成ができました。続いてTestタブに移り、作成したFunctionのテストを実施します。テストのリクエストにCloudFront-Viewer-TLSヘッダを追加するため、デフォルトの状態から[Add header]ボタンでヘッダ情報を追加していきます。

Developer Guideに記載があった例のTLSv3のケース、TLSv1.3:TLS_AES_128_GCM_SHA256をcloudfront-viewer-tlsヘッダの値として指定してみました。[Test function]ボタンを押下します。

Execution resultが表示されました。request.uriは特に変更されず/index.htmlのまま、リクエスト通りのコンテンツがCloudFrontから返されるかたちですね。

TLSv1.1のヘッダの例、TLSv1.1:ECDHE-RSA-AES128-SHA256についても確認しておきましょう。Test functionを行うとテスト結果のOutput、request.uri/tls-alert.htmlとなりました。最新のTLS使用を促す警告ページを返す挙動となっていますね。

もう一つ、このcloudfront-viewer-tlsヘッダがないケースも確認しておきましょう。テスト結果のOutput、request.uri/tls-alert.htmlとなります。こちらも想定通りの挙動になっていますね。

FunctionのPublish

テスト結果も問題なさそうですので、FunctionのPublishを行います。Publishタブに移動して[Publish function]ボタンを押下しましょう。

画面上部に"Successfully published"のメッセージが表示され、またAssociated distributionsの項目が増えています。

この[Add association]ボタンから関連付けるDistribution、Behaviorを設定していくこともできますが、今回はDistributionの設定画面のほうで関連付けを行います。

DistributionへのFunctionの関連付けと動作確認

今回動作確認に使用するDistribution設定を確認しつつ、作成したCloudFront FunctionsをDistributionに関連付けていきます。

Distributionの設定内容とFunctionの関連付け

まずCloudFront Distributionの設定、カスタムドメイン(Alternate domain name (CNAME))やSSL証明書、Security policyなどは以下のように設定しました。SSL証明書とともにカスタムドメインを設定、Security Policy policyとしてはTlSv1.1を使用するかたちで、TLSv1.1以上のバージョンであればHTTPS接続できますが、TLSv1でのHTTPS接続はできない状況です。

オリジンはEC2上のApache HTTP Serverで、index.htmlとして簡単なHTMLを返すようにしています。これと先ほどのtls-alert.htmlがレスポンスとして準備されている状態です。

index.html

<html>
  <head>
    <title>Website Contents</title>
  </head>
  <body>
    <p>本来表示させるべきコンテンツページ</p>
  </body>
</html>

CloudFrontのBehaviorはDefault (*)のみを設定しています。動作確認ということでキャッシュは無効にしている(Cache policyとしてCachingDisabledを選択している)状態です。Origin request policyはCloudFront-Viewer-TLSヘッダを指定したカスタムポリシーCustom-CloudFrontViewerTLSHeaderを指定しました。

BehaviorのFunction associationsの設定箇所で関連付けるCloudFront Functionsを指定します。Viewer requestでCloudFront Functionsを選択、先ほど作成したFunctionを指定します。

Functionの動作確認

作成したFunctionの関連付けが完了しました。いざ、実際の動作を確認してみましょう。最新版のMac版Google Chromeで接続すると以下のように「本来表示させるべきコンテンツページ」が表示されました。特に問題なく通常のコンテンツが表示されているわけですね。(実際にはCloudfront-Viewer-Tls: TLSv1.3:TLS_AES_128_GCM_SHA256で接続されていました。)

続いてTLSバージョンが低い場合(今回のしきい値とした1.2未満のとき)についても確認してみましょう。同じMac版のGoogle ChromeでTLSバージョンの変更ができればよかったのですが、この設定変更方法がすぐには見つかりませんでした。いろいろと調べてみとこ、Windows上のInternet Explorerでこの使用するTLSバージョンの設定変更ができたのでこちらでの確認してみます。(なおInternet Explorerはすでにサポートが終了しています。)まずはデフォルトとなっているTLSv1.2で接続した場合です。(Cloudfront-Viewer-Tls: TLSv1.2:ECDHE-RSA-AES128-GCM-SHA256)通常のコンテンツが表示されています。

続いて「インターネット オプション」から「詳細設定」タブに進み、セキュリティの項目から使用するTLSバージョンを下げていきます。変更前は「TLS 1.2の使用」にチェックが付いていましたが、これを外して再度接続してみます。

最新のTLS使用を促す警告ページが表示されました。(リクエスト先は/のままです。)想定した挙動が実現できましたね!

さてもう一つ、Internet Explorerの設定でTLSv1.1も使用しない設定にしてみましょう。「TLS 1.1の使用」のチェックボックスもoffにしてしまいます。

今度は「このページに安全に接続できません」の表示になりました。こちらはCloudFront Functionsで警告ページを表示しているのではなく、CloudFront DistributionのSecurity policyの設定で弾かれ接続できなかった、ということになりますね。

なお今回の設定内容の場合はHTTPでの接続自体は行なえますが、最新のTLS使用を促す警告ページが表示される、という挙動となっています。

まとめ

Amazon CloudFrontのCloudFront-Viewer-TLSヘッダをCloudFront Functions内で確認して、クライアントからの接続で使用しているTLSのバージョンが一定の値よりも小さい場合にアラート用コンテンツを返し、本来のコンテンツを非表示にする仕組みを作成してみました。

クライアントが使用するSSL/TLSバージョンが一定の値よりも小さい場合、これまではSecurity Policyで接続を許可しないという選択しか行うことができませんでした。一定未満のバージョンの場合に本来のコンテンツを表示させないという点は変わりませんが、接続エラーに対するメッセージ(今回のように「最新のTLSバージョンを指定してください」だったり、「最新のブラウザ、OSを使用してください」など)を表示させることで、より利用者にやさしいサイトが実現できるのではないかと思いました。