【テクニカル・上級編】HTTP/1.1のVaryヘッダーとキャッシュの多様性 – HTTPプロトコル・通信規格実践ガイド

Varyヘッダーの深淵:CDN・プロキシキャッシュ時代のルーレットと、キャッシュ汚染という名の「静かなる破滅」

ネットワークの現場に身を置く者なら、誰もが一度は「なぜ、あのユーザーには古いページが見えているんだ?」「CDNのヒット率が急落したと思ったら、今度は全く関係ないユーザーに管理画面のモックが露出している!」という悪夢のようなインシデントに直面したことがあるはずだ。

原因の多くは、オリジンサーバーとエッジキャッシュ(CDNやリバースプロキシ)の間で交わされるHTTPヘッダー、特に `Vary` ヘッダーの不適切な扱いに起因している。

HTTP/1.1が策定されて以来、Webのスケールアウトを支えてきたキャッシュ機構だが、その根幹である `Vary` は、一歩使い方を誤ればパフォーマンスの劇的な改善をもたらす魔法の杖から、インフラ全体を崩壊させるパンドラの箱へと姿を変える。今回は、パケットの往来とエッジのメモリ構造にまで踏み込み、この `Vary` ヘッダーの真の挙動と、実務で絶対に踏んではならない地雷について解説しよう。

—

1. パケットとキャッシュキーの裏側:Varyヘッダーは何をしているのか?

まず、HTTP/1.1(RFC 7230 / RFC 9110)におけるキャッシュの基本原理を思い出してほしい。
ブラウザやCDNなどのキャッシュサーバーは、通常 `URI`(Request Target)をキャッシュの主キー(Primary Key)としてメモリやストレージにデータを保持している。

しかし、モダンなWebアプリケーションは単一のURLに対して千変万化するレスポンスを返す。例えば、以下のようなケースだ。

  • リクエストヘッダー `Accept-Encoding: gzip, deflate` を見て、圧縮版と非圧縮版を切り替える。
  • `User-Agent` を見て、スマートフォン向けとデスクトップ向けのHTMLを動的に生成する。
  • `Accept-Language: ja` を見て、多言語対応のコンテンツを返す。

ここで `Vary` ヘッダーが登場する。オリジンサーバーがレスポンスに以下のように付与したとする。

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Vary: Accept-Encoding, User-Agent
Cache-Control: public, max-age=3600

この瞬間、エッジプロキシ(Varnish、Cloudflare、CloudFrontなど)の内部で何が起きるか?
キャッシュエンジンは、単なるURL(例: `/index.html`)のハッシュ値だけでなく、`Vary` で指定されたリクエストヘッダーの値も含めて複合的なキャッシュキー(Secondary Key)を生成するようになる。

エッジのメモリ内では、以下のようなイメージでエントリが管理されることになる。

  • キャッシュキーA: `/index.html` + `Accept-Encoding: gzip` + `User-Agent: Mobile`
  • キャッシュキーB: `/index.html` + `Accept-Encoding: gzip` + `User-Agent: Desktop`
  • キャッシュキーC: `/index.html` + `Accept-Encoding: identity` + `User-Agent: Desktop`

つまり、`Vary` とは、キャッシュサーバーに対して「おい、URLだけで判断するな。このリクエストヘッダーたちの組み合わせが変わったら、別のコンテンツとして別個にキャッシュを保存し、適切にルーレットを回して出し分けろよ」と命じる指令なのだ。

—

2. 致命的な罠:User-AgentをVaryに指定することの罪

ここで、実務における最大の罠について語らなければならない。
よく見かけるアンチパターンとして、以下のような設定がある。

絶対にやってはいけない最悪の例
Vary: User-Agent

これをやってしまったが最後、あなたのWebサイトのインフラは緩慢な死、あるいは瞬時のリソース枯渇へと向かう。

なぜか?
世の中に存在する `User-Agent`(ブラウザの識別文字列)の種類は、数千、数万どころではない。マイナーなブラウザ、スクレイピングツール、古びたフィーチャーフォン、そして数日おきに行われるChromeやSafariのマイナーバージョンアップのたびに、文字列は完全に別物になる。

`Vary: User-Agent` を指定した瞬間、CDNのエッジサーバーのメモリ上には、「わずか数人しかアクセスしないレアなUser-Agentごとの独立したキャッシュ」が無限に生成されることになる。

結果として何が起きるか。
1. キャッシュヒット率(Cache Hit Ratio)の急落: エッジがキャッシュを保持していても、ほとんどのユーザーで「Cache Miss」となり、オリジンサーバーへのリクエスト(Origin Shieldの貫通)が激増する。オリジン側のCPUとDBは悲鳴を上げる。
2. メモリの圧迫(Cache Bloat / DDOS状態): エッジプロキシのメモリ(RAM)が無限のバリエーションで埋め尽くされ、LRU(Least Recently Used)アルゴリズムが高速に働き始め、本来ヒットすべき重要なキャッシュまで追い出されてしまう(Thrashing現象)。

モダンなWebアーキテクチャにおいて、レスポンスの出し分けは `User-Agent` 全体で行うべきではない。もしデバイスごとにHTML構造を変える必要があるならば、サーバーサイドで User-Agent をパースして `X-Device-Type: mobile` などの極めて限られたバリュー(高々2〜3種類)を持つカスタムヘッダーに変換し、そちらを `Vary` の対象にするべきだ。

まだマシな設計例:バリエーションを限定する
Vary: Accept-Encoding, X-Device-Type

—

3. セキュリティの暗部:キャッシュ汚染(Cache Poisoning)のメカニズム

インフラエンジニアやセキュリティスペシャリストにとって、`Vary` ヘッダーの誤設定は単なるパフォーマンス低下に留まらない。それは「Cache Poisoning(キャッシュ汚染)」という致命的な脆弱性の温床になる。

攻撃者がどのようにこの仕組みを悪用するか、パケットの流れを見てみよう。

1. 攻撃者が、意図的に特殊なリクエストヘッダー(例えば、アプリケーションのフレームワークが誤って解釈し、内部状態を変えてしまうような未知のヘッダーや、不正な `X-Forwarded-Host` など)を付与してオリジンにリクエストを投げる。
2. アプリケーションのバグや設定ミスにより、オリジンサーバーがそのヘッダーを動的な処理に組み込み、かつレスポンスに `Vary: <その特殊なヘッダー名>` を含めて返す。または、そもそも `Vary` に指定されていないにもかかわらず、そのヘッダーに応じた出力を返してしまう。
3. CDNなどのキャッシュサーバーは、そのレスポンスを「攻撃者が意図した不正な状態(XSSペイロードが埋め込まれたHTMLや、機密情報が含まれる画面など)」のまま、複合キャッシュキーに紐づけて保存してしまう。
4. その後、何食わぬ顔でアクセスしてきた一般のユーザー(被害者)が、同じキャッシュキーにヒットする通常のリクエストを送ると、エッジサーバーは攻撃者が仕込んだ「汚染されたキャッシュ」を平然と返してしまう。

これが、Webアプリケーションの脆弱性を直接突くのではなく、インフラのキャッシュレイヤーをハッキングする「Web Cache Poisoning」の恐るべき実態だ。

防御策:Unkeyed Headersの排除とVaryの厳格化

この脅威からシステムを守るためには、以下の鉄則を遵守する必要がある。

  • Varyするヘッダーのホワイトリスト化: アプリケーションが必要とするヘッダー以外(例えば、プロキシが追加する `X-Forwarded-` や、トラッキング用の `Cookie` の一部など)がキャッシュキーに影響を与えないよう、CDNやリバースプロキシ側で「Varyの対象に含めるべきヘッダー」を厳格に制限する。
  • Unkeyed Headers(キャッシュキーに含まれないヘッダー)のサニタイジング: オリジンに到達する前に、リバースプロキシ(NginxやVarnishなど)の段階で、アプリケーションの挙動を狂わせる可能性のある不審なヘッダーを強制的に削除または正規化する。

以下に、Nginxリバースプロキシのコンフィグで、不要なヘッダーをシャットアウトしつつ安全にアップストリームへ流す実践的な設定例を示す。

Nginx リバースプロキシにおけるヘッダーの正規化とキャッシュ制御の例
server {
listen 443 ssl http2;
server_name api.example.com;

# SSL/TLSの終端とセキュリティ設定(省略)

location / {
# 【重要】キャッシュポイズニングを防ぐため、
# アプリケーションが予期しない不正なカスタムヘッダーを強制的にクリアまたは安全な値に上書きする
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;

# 攻撃者が悪用しうる未検証のヘッダーをバニッシュ(削除)
proxy_set_header X-Original-URL “”;
proxy_set_header X-Rewrite-URL “”;

# アップストリーム(オリジン)へ転送
proxy_pass http://backend_cluster;

# キャッシュの挙動設定
proxy_cache my_cache;
proxy_cache_valid 200 302 10m;

# オリジンからの Vary ヘッダーを尊重しつつ、
# 過剰なバリエーションを生むヘッダー(例: User-Agent)がキャッシュキーを汚染するのを防ぐ
# ※Varnishの場合は vcl_fetch / vcl_backend_response での制御が必要
proxy_ignore_headers “Set-Cookie”;

# パフォーマンスとデバッグのためのヘッダー付与
add_header X-Cache-Status $upstream_cache_status;
}
}

—

4. HTTP/2, HTTP/3時代のVary:HPACK/QPACKとヘッダー圧縮の罠

時代はHTTP/1.1からHTTP/2、そしてHTTP/3(QUIC)へと移行している。しかし、プロトコルがどれほどモダンになろうとも、HTTPのセマンティクス(意味論)において `Vary` ヘッダーが担う役割の本質は変わらない。

それどころか、HTTP/2以降のヘッダー圧縮(HPACK / QPACK)のコンテキストにおいて、`Vary` は新たなパフォーマンスの課題を生む。

HTTP/2では、すべてのリクエストおよびレスポンスヘッダーが Huffman符号化 や 動的テーブル(Dynamic Table)を用いて圧縮され、ラウンドトリップのオーバーヘッドを極限まで削ぎ落としている。
しかし、もしあなたのサーバーが過剰な `Vary` ヘッダーを返し、それによってリクエストごとに微妙に異なるカスタムヘッダーや、無駄に長い文字列(例: 数百文字に及ぶ `Vary` の列挙)を毎度送信させようものならどうなるか。

1. 動的テーブルの圧迫: クライアントとサーバー間で共有されるHPACKの動的テーブルに、ユニークなヘッダー値が次々と登録され、テーブルのフラッシュやサイズ超過による圧縮効率の低下(圧縮率の悪化=パケットサイズの肥大化)を引き起こす。
2. RTTとバッファの無駄遣い: TCP/QUICの輻輳ウィンドウ(Congestion Window: cwnd)の初期段階において、無駄に肥大化したヘッダーブロックが最初のパケット(Initial Window)に収まりきらなくなり、不要な追加の往復(RTT)が発生する。

特に、HTTP/3(QUIC)環境下では、UDPベースのパケットロス耐性とストリーム多重化の恩恵を最大限に受けるために、「送受信するメタデータ(ヘッダー)のサイズは可能な限り小さく保つ」ことが鉄則となる。

—

5. アーキテクトが実践すべきVary設計のチェックリスト

現場のアーキテクトやテックリードとして、明日からコードベースとインフラストラクチャの設計を見直すための「Vary最適化チェックリスト」を提示しよう。

1. `Vary: User-Agent` は即座に廃止せよ

  • デバイスごとの出し分けが必要な場合は、サーバーサイドまたはエッジワーカー(Cloudflare Workers, Fastly VCLなど)でパースし、`Vary: X-Device-Type` のような数パターンの高エントロピーを排除したヘッダーに置き換える。

2. CDNのエッジキャッシュ仕様を熟知せよ

  • 使用しているCDN(CloudFront, Akamai, Fastly, Cloudflareなど)が、複数指定された `Vary` ヘッダー(例: `Vary: Accept-Encoding, Accept-Language`)をどのようにハンドリングし、キャッシュキーの組み合わせ爆発を防いでいるか、ドキュメントの仕様を必ず検証する。

3. 不要なレスポンスヘッダーを削ぎ落とせ

  • アプリケーションフレームワークのデフォルト設定で、無駄に多くの `Vary` が付与されていないか確認する。本当にそのヘッダーごとのキャッシュ分離が必要か、1つずつ疑うこと。

4. Cache-Controlとの協調関係を確認する

  • `Vary` は、`Cache-Control: private` と併用されるとエッジではキャッシュされない(ブラウザのみ)。パブリックにキャッシュさせたいリソースでのみ `Vary` を有効にし、プライベートなデータにはそもそも `Vary` を意識する必要がない設計に落とし込む。

—

結び:インフラの美しさは「目に見えない調和」にある

プロトコルの仕様書をめくれば、`Vary` ヘッダーの定義はわずか数行のドライなテキストに過ぎない。しかし、その数行の解釈と実装の甘さが、数百万人のユーザーへのレスポンス遅延を引き起こしたり、最悪の場合はセキュリティインシデントへと直結する。

ネットワークスペシャリストやインフラアーキテクトの仕事とは、パケットが流れるその瞬間に、エッジのメモリ上で何が起き、クライアントのCPUがどう反応しているのかを脳内で完全にトレースすることだ。

`Vary` ヘッダーという名の小さなルーレット。それを正しく制御し、高速かつ安全なコンテンツ配信の歯車として噛み合わせることこそが、プロフェッショナルなエンジニアリングの醍醐味である。あなたのシステムのキャッシュは、本当に美しく機能しているか? 今一度、レスポンスヘッダーの嵐をパケットキャプチャで覗いてみてほしい。

コメント

タイトルとURLをコピーしました