[update] Amazon CloudFrontでリクエストのヘッダ構造を判断するためのヘッダが利用可能になりました!

リクエスト時のヘッダの順序を格納する「CloudFront-Viewer-Header-Order」とヘッダの個数を格納する「CloudFront-Viewer-Header-Count」の2つのヘッダがCloudFrontで利用可能になりました。
2023.01.26

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

はじめに

清水です。AWSのCDNサービスであるAmazon CloudFrontでViewer(リクエスト)のヘッダ構造を判断するためのヘッダが利用可能になりました!(2023/01/13付でAWS What's Newにポストされたアップデート情報となります。)

具体的には、リクエスト時に以下2つのヘッダがCloudFrontで付与できるようになりました。

  • CloudFront-Viewer-Header-Order
  • CloudFront-Viewer-Header-Count

1つ目のCloudFront-Viewer-Header-OrderはViewer(リクエスト)のヘッダの名称がコロンで区切られたかたちで格納されます。例えばCloudFront-Viewer-Header-Order: Host:User-Agent:Accept:Accept-Encodingというぐあいですね。(Host:User-Agent:Accept:Accept-EncodingCloudFront-Viewer-Header-Orderヘッダの値というわけです。)

2つ目のCloudFront-Viewer-Header-CountはViewer(リクエスト)のヘッダの総数が数値で格納されます。先ほどの例であればヘッダの総数が5つなのでCloudFront-Viewer-Header-Count: 5となります。

これら2つのヘッダを用いてHTTPヘッダの順序と総数が追跡できるようになり、クライアントからのリクエストを期待される正当なパターンと比較することができます。ほかのアクセス制御ルールと組み合わせることで、リクエストの偽造を検知しブロックすることなどに役立てることが可能ということです。

なお、Amazon CloudFront Developer Guideのほうも同じく2023/01/13付で更新されており、「Adding the CloudFront request headers - Amazon CloudFront」のページに「Headers for determining the viewer's header structure」という項目が追加されています。

本エントリでは、このCloudFrontでViewerのヘッダ構造を判断するためのヘッダについて、実際に動作を確認してみたのでまとめてみます。

re:Invent 2022のセッション内でアップデートが先行発表されていた!!

さて動作確認の前ですが、これら2つのアップデート対象となるヘッダ、CloudFront-Viewer-Header-OrderCloudFront-Viewer-Header-Countをみてピンと来た方もいるかと思います。この2つのヘッダについては、昨年末のre:Invent 2022のセッション「Optimizing performance with CloudFront: Every millisecond matters (NET313)」にてWhat's Newへのポストに先行して紹介があったアップデートとなります。

AWS re:Invent 2022 - Optimizing performance with CloudFront: Every millisecond matters (NET313) - YouTube

以下の該当セッションのレポートブログ執筆時(2022年12月)にも、2つのヘッダはOrigin request policy作成時のカスタムヘッダとして個別に追加することにより利用自体は可能だったのですが、今回正式にアップデート情報として案内されたかたちです。また本ブログエントリで紹介しますが、Origin request policyでのヘッダ追加時にきちんと選択ができるようにもなっています。

CloudFrontでViewerのヘッダ構造を判断するためのヘッダを使ってみた

では実際に、今回のアップデート内容となるViewer(リクエスト)のヘッダ構造を判断するための2つのヘッダについて、実際にCloudFrontに設定してその挙動を確認してみます。使い方はほかのCloudFront HTTPヘッダーの利用方法と同様で、Origin request policyにヘッダを追加するかたちですすめていきます。

オリジンサーバの準備

CloudFrontに設定するオリジンとして、Apache HTTP ServerとPHPが稼働するEC2を準備しました。以下のPHPコードを記載したファイルindex.phpを配置し、デフォルトルート(/)にアクセスすればオリジン側で確認したリクエストヘッダをすべて出力するようにします。

index.php

<?php

foreach (getallheaders() as $name => $value) {
    echo "<p>";
    echo "$name: $value";
    echo "</p>\n";
}

?>

Origin request policyの作成

CloudFront側の設定として、まずはOrigin request policyを作成します。マネジメントコンソールのCloudFrontの画面、PoliciesのOrigin requestの項目から[Create origin request policy]で進みます。NameとDescriptionを適切に設定します。

Origin request settingsの項目ではHeadersで「All viewer headers and the following CloudFront headers」を選択しました。(All viewer headersは任意ですが、CloudFront headersを選択する必要があります。)Add headerでOrigin requestに含めるCloudFront headersとして、CloudFront-Viewer-Header-OrderCloudFront-Viewer-Header-Countを選択します。

Origin request policyが作成できました。

Distributionの作成

Origin request policyが作成できたら、続いてこのOrigin request policyを使用するCloudFront Distributionを作成します。Distribution一覧画面から[Create Distribution]ボタンで進みます。オリジンは先ほど準備したEC2のPublic IPv4 DNSを指定しました。その他は基本的にマネジメントコンソールのデフォルト設定で進みますが、Cache key and origin requestsの項目は以下のように設定しました。キャッシュを無効にしつつ、先ほど作成したOrigin request policyを使用するかたちですね。

Distributionが作成できたら、続いてCloudFront Functionsを設定します。

CloudFront Functionsの設定

オリジン側でリクエストヘッダをすべて出力するようにしていますが、今回はCloudFrontが受け取った段階のViewer(リクエスト)ヘッダの内容も確認したかったため、CloudFront Functionsでこれを出力するようにします。Functionのコードの内容は以下です。シンプルにconsole.log()event.requestをログに出力するだけとなります。

function handler(event) {
    console.log(event.request);
    return event.request;
}

CloudFrontマネジメントコンソールのFunctionsのページ、[Create functions]ボタンから進みます。NameとDescriptionを適切に設定します。

遷移後の画面でFunction codeのDevelopmentを上記のコードの内容で書き換え、[Save changes]します。

Publishのタブを開き、[Publish function]を行います。

Publishしたあと、Associated distributionの項目で[Add association]ボタンから先ほど作成したDistributionへの関連付けを行います。Event typeはViewer Requestとしました。Cache behaviorはDefault (*)に設定します。

Functionを関連付けたDistributionのDeployingか終了すれば準備完了です。

CloudFront-Viewer-Header-OrderヘッダならびにCloudFront-Viewer-Header-Countヘッダの確認

Functionを関連付けたDistributionのDeployingか終了すれば準備完了です。CloudFrontのDistribution domain nameにアクセスしてみましょう。

Safariブラウザでの確認

まずはmacOSのSafariブラウザで確認してみました。

アップデート対象である2つのヘッダの中身は以下となりました。

  • Cloudfront-Viewer-Header-Order:
    • host:user-agent:accept:accept-language:accept-encoding
  • Cloudfront-Viewer-Header-Count:
    • 5

また実際にオリジンサーバから出力されるリクエストヘッダの内容は順にHostUser-AgentX-Amz-Cf-IdConnectionViaX-Forwarded-ForAccept-LanguageAcceptAccept-Encodingの合計9個でした。

続いてCloudFront Functions側の出力、つまりCloudFrontが受け取った段階でのヘッダの内容についても確認してみます。CloudWatch Logsで確認してみると以下の内容が出力されていました。(見やすくなるように出力を整形しています。本エントリ内のほかのログ出力も同様です。)

T75MxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxqQ== 
{
    method:'GET',
    uri:'/',
    querystring:{},
    headers:{
        host:{value:'dgl3xxxxxxxxx.cloudfront.net'},
        user-agent:{value:'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.2 Safari/605.1.15'},
        accept:{value:'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'},
        accept-language:{value:'ja'},
        accept-encoding:{value:'gzip, deflate, br'},
        cloudfront-viewer-header-order:{value:'host:user-agent:accept:accept-language:accept-encoding'},
        cloudfront-viewer-header-count:{value:'5'}
        },
    cookies:{}
}

アップデート対象である2つのヘッダCloudfront-Viewer-Header-OrderCloudfront-Viewer-Header-Countについて、その中身は同一ですね。この2つより前の順序となるヘッダについても確認してみましょう、hostuser-agentacceptaccept-languageaccept-encodingCloudfront-Viewer-Header-Orderの順序どおりに並んでいることがわかります。ヘッダの総数もCloudfront-Viewer-Header-OrderCloudfront-Viewer-Header-Countを除けば5つでCloudfront-Viewer-Header-Countの値と一致しました。

オリジンサーバ側でのみ確認できたX-Amz-Cf-IdConnectionViaX-Forwarded-Forなどのヘッダについては、Viewerからのリクエスト時に存在しているヘッダではなく、CloudFrontがオリジンとの通信の際に付与するヘッダということになりますね。また順序については、Origini Request Policyの内容によってCloudFront側で置き換える可能性があるもの(今回ならHostUser-Agent)が先になり、削除される可能性があるもの(今回ならAcceptAccept-LanguageAccept-Encodingはあととなる、というような規則なのかなと推測しています。(「HTTP リクエストヘッダーと CloudFront の動作 (カスタムオリジンおよび Amazon S3 オリジン) 」 カスタムオリジンの場合のリクエストおよびレスポンスの動作 - Amazon CloudFront

curlコマンドでの確認

続いてcurlコマンドでも確認してみます。Safariブラウザの場合と同様、CloudWatch Logsで確認できるCloudFrontが受け取ったリクエストの段階のヘッダではCloudfront-Viewer-Header-Countの個数のヘッダが、Cloudfront-Viewer-Header-Orderの順序で並んでいます。オリジンサーバで受け取るリクエストではこれらにCloudFrontが付与する各種ヘッダが加わり、また順序についても一部変更があるというぐあいですね。

% curl https://dgl3xxxxxxxxx.cloudfront.net
<p>Host: dgl3xxxxxxxxx.cloudfront.net</p>
<p>User-Agent: curl/7.79.1</p>
<p>X-Amz-Cf-Id: McfZxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx8A==</p>
<p>Connection: Keep-Alive</p>
<p>Via: 2.0 c299xxxxxxxxxxxxxxxxxxxxxxxx62a2.cloudfront.net (CloudFront)</p>
<p>X-Forwarded-For: 2axx:xxxx:xxxx:xx::xx:x7f</p>
<p>Accept: */*</p>
<p>Cloudfront-Viewer-Header-Order: host:user-agent:accept</p>
<p>Cloudfront-Viewer-Header-Count: 3</p>
McfZxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx8A==
{
    method:'GET',
    uri:'/',
    querystring:{},
    headers:{
        host:{value:'dgl3xxxxxxxxx.cloudfront.net'},
        user-agent:{value:'curl/7.79.1'},
        accept:{value:'*/*'},
        cloudfront-viewer-header-order:{value:'host:user-agent:accept'},
        cloudfront-viewer-header-count:{value:'3'}
    },
    cookies:{}
}

curlコマンドでリクエスト時に独自のヘッダを追加してみましょう。挙動などは独自ヘッダを付与しないときと変わりありませんが、Cloudfront-Viewer-Header-Countがきちんと増えていることやCloudfront-Viewer-Header-Orderの順序も意図されたものとなっていることがわかります。

% curl https://dgl3xxxxxxxxx.cloudfront.net -H "x-myheader1: value1" -H "x-myheader2: value2"
<p>Host: dgl3xxxxxxxxx.cloudfront.net</p>
<p>User-Agent: curl/7.79.1</p>
<p>X-Amz-Cf-Id: Lsa1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPg==</p>
<p>Connection: Keep-Alive</p>
<p>Via: 2.0 1e30xxxxxxxxxxxxxxxxxxxxxxxx7c88.cloudfront.net (CloudFront)</p>
<p>X-Forwarded-For: 2axx:xxxx:xxxx:xx::xx:x7f</p>
<p>Accept: */*</p>
<p>X-Myheader1: value1</p>
<p>X-Myheader2: value2</p>
<p>Cloudfront-Viewer-Header-Order: host:user-agent:accept:x-myheader1:x-myheader2</p>
<p>Cloudfront-Viewer-Header-Count: 5</p>
Lsa1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPg==
{
    method:'GET',
    uri:'/',
    querystring:{},
    headers:{
        host:{value:'dgl3xxxxxxxxx.cloudfront.net'},
        user-agent:{value:'curl/7.79.1'},
        accept:{value:'*/*'},
        x-myheader1:{value:'value1'},
        x-myheader2:{value:'value2'},
        cloudfront-viewer-header-order:{value:'host:user-agent:accept:x-myheader1:x-myheader2'},
        cloudfront-viewer-header-count:{value:'5'}
    },
    cookies:{}
}

Chromeブラウザでの確認

さて、続いてはmacOSのChromeブラウザでも確認してみました。Chromeの場合はCloudfront-Viewer-Header-Countの値が「13」とSafariに比べて多くなっていますね。詳細を確認してみると、CloudFrontがViewerからのリクエストを受け取った段階、CloudFront Functionsで確認できるヘッダでCloudfront-Viewer-Header-Orderの順序と異なった状態となっていました。(何度か条件を変えて確認しても同様の結果となったので、リクエストが偽装されているといったものではないかと思います。)

Safariと比べて追加されているのはUser-Agent Client Hints関連のヘッダとFetch Metadata Request Headersと呼ばれるヘッダ群などでしょうか。今回はこれら詳細などについて深追いはしませんが、Chromeブラウザでは現在のところCloudfront-Viewer-Header-Orderの扱いについては注意が必要そうです。

kSVwxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxfg==
{
    method:'GET',
    uri:'/',
    querystring:{},
    headers:{
        user-agent:{value:'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'},
        sec-ch-ua-mobile:{value:'?0'},
        cloudfront-viewer-header-order:{value:'host:sec-ch-ua:sec-ch-ua-mobile:sec-ch-ua-platform:upgrade-insecure-requests:user-agent:accept:sec-fetch-site:sec-fetch-mode:sec-fetch-user:sec-fetch-dest:accept-encoding:accept-language'},
        host:{value:'dgl3xxxxxxxxx.cloudfront.net'},accept:{value:'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9'},
        upgrade-insecure-requests:{value:'1'},
        sec-fetch-site:{value:'none'},
        sec-fetch-dest:{value:'document'},
        accept-language:{value:'ja,en-US;q=0.9,en;q=0.8'},
        accept-encoding:{value:'gzip, deflate, br'},
        sec-ch-ua-platform:{value:'"macOS"'},
        sec-fetch-user:{value:'?1'},
        cloudfront-viewer-header-count:{value:'13'},
        sec-ch-ua:{value:'"Not_A Brand";v="99", "Google Chrome";v="109", "Chromium";v="109"'},
        sec-fetch-mode:{value:'navigate'}},
    cookies:{}
}

まとめ

Amazon CloudFrontに新たに追加された2つのヘッダCloudFront-Viewer-Header-OrderならびにCloudFront-Viewer-Header-Countについて、実際に設定して確認してみました。これらの2つのヘッダを用いることで、Viewerからのリクエスト時のヘッダの順序ならびのその個数が確認可能となります。ただし、オリジン側で参照する際にはCloudFront側で付与などを行うヘッダの影響や、Origin request policyの設定内容などを考慮する必要があります。CloudFront FunctionsやLambda@Edgeなどで参照するほうがシンプルに利用できそうですね。また今回確認した限りでは、一部ブラウザでヘッダ順序が維持されないケースがあった点についても注意して活用しましょう。