ブログに戻る

Cloudflareのコンテナ脆弱性が教えたマルチテナントゲームバックエンドのセキュリティ

公開日 2026年9月25日
Cloudflareのコンテナ脆弱性が教えたマルチテナントゲームバックエンドのセキュリティ AIを活用して生成

要点まとめ

Cloudflareのコンテナ脆弱性から学ぶ、マルチテナントゲームバックエンドのセキュリティ対策。dm-thinのskip_block_zeroingによるテナント間データ露出の今すぐ使える検出・修復ランブックと、プレイヤーデータを守る実践的なアーキテクチャ原則について解説します。

コンテナ化されたゲームバックエンドは、マッチ、ロビー、分析バッチのたびに新しいVMを起動します。そのコンテナがシャットダウンすると、データは消える——そう思っていますか?

必ずしもそうとは限りません。2026年9月、Accomplishのセキュリティ研究者Oren Yomtov氏は、Cloudflare Containersにテナント間のデータ露出脆弱性があることを発見しました。破棄されたコンテナの残存ディスクブロックが、同じホスト上の無関係なワークロードから読み取り可能だったのです。回復されたデータには、ディレクトリ構造、データベースページ、構造的に完全なSQLiteデータベースが含まれていました。

もしマルチテナントのコンテナプラットフォーム上でゲームバックエンドを運用しているなら、この脆弱性の根本原因——dm-thinサブシステムにおけるskip_block_zeroingというLinuxストレージ設定——を理解する必要があります。この記事では、何が問題だったのかを分解し、今日から適用できる検出と修復のランブックを提供し、この種の脆弱性がプレイヤーのデータに到達するのを防ぐアーキテクチャ原則を解説します。

シンプロビジョニングがテナント間データ露出を生む仕組み

Cloudflare Containersは、マルチテナントハードウェア上のFirecrackerマイクロVM内でワークロードを実行します。各コンテナには、Linuxデバイスマッパーのシンプロビジョニング(dm-thin)をバックエンドとする書き込み可能なルートディスクが割り当てられます。FirecrackerはこのディスクをゲストVMに/dev/vdcとして公開します。

シンプロビジョニングは、物理ストレージの割り当てを遅延させることで機能します。10 GiBの仮想ディスクを作成しても、物理スペースはほとんど消費されません。ゲストが以前にマップされていない領域に書き込むと、共有プールからブロックがオンデマンドで割り当てられます。Cloudflareの影響を受けたプールでは、64 KiBのシンブロックサイズが使用されていました。

ここに脆弱性が存在します。コンテナが破棄されると、そのシンボリュームのマッピングが削除され、物理ブロックは再利用のために共有プールに戻ります。影響を受けたプールは次のように構成されていました:

skip_block_zeroing

このフラグがあると、dm-thinは新しく割り当てられたブロックを次のテナントがアクセスできるようにする前にゼロ化する処理をスキップします。64 KiBのフル書き込みは、リサイクルされたブロック全体を上書きします。しかし、部分書き込み——たとえば4 KiB——はその領域だけを置き換えます。残りの60 KiBには、ブロックの以前の所有者のデータが残る可能性があります。

これは理論上のエッジケースではありません。研究者は、4大陸にまたがる24の本番配置のうち18、22の基盤ノードのうち20で、残存ブロックからディレクトリ構造、データベースページ、完全なSQLiteデータベースを回復しました。

部分書き込みが主要なベクターとなる理由

新しいシンディスクの未マップ領域を読み取るとゼロが返ります——dm-thinは物理ブロックを割り当てずにゼロを提供します。これは安全です。このエクスプロイトが機能するのは、小さな書き込みが、ゼロ化されていないリサイクル済み64 KiBブロックの割り当てをトリガーするからです。

攻撃パターン:

  1. Workers Paidアカウントでコンテナを作成する。
  2. /dev/vdc(書き込み可能なルートディスク)を開く。
  3. ext4の空き領域に対応する64 KiBアラインされた領域を特定する。
  4. 各ターゲット領域にアラインされた4 KiBブロックを1つ書き込む。
  5. 64 KiBブロック全体を読み戻す。
  6. 攻撃者が上書きしなかった60 KiBだけを調べる。

ステップ4が決定的な瞬間です。4 KiBの書き込みにより、dm-thinは共有プールから物理ブロックを割り当てます。ゼロ化が無効になっているため、残りの60 KiBには、以前にそのブロックを所有していた誰かの残存データが含まれる可能性があります。

研究者が実際に検証したこと

研究者らは、ext4ディレクトリブロックチェックサム(metadata_csum機能)を使用して、自分たちのテストファイルシステムのブロックと外部のブロックを区別しました。6つの本番配置にわたって:

  • 5,614 個のテスト可能なディレクトリブロックを調査
  • 0 個が研究者自身のファイルシステムに起因
  • 2,700 個の異なる外部ディレクトリinodeをチェックサム分析で特定

回復されたファイルタイプはジャンクではありませんでした。構造的に意味のあるSQLiteデータベースが含まれており、ゲームバックエンドの文脈では、プレイヤーのセーブ状態、セッショントークン、インベントリレコードを含む可能性のある種類のデータです。

検出プレイブック:インフラストラクチャ内の残存データ収穫を発見する

マルチテナントインフラストラクチャ上でコンテナ化されたゲームバックエンドを運用している場合、設定監査とランタイムI/O異常分析という2つの検出レイヤーが必要です。

ステップ1:dm-thin設定を監査する

コンテナを実行しているすべてのホストでこのスクリプトを実行してください:

#!/bin/bash
set -euo pipefail

echo "=== dm-thin Pool Configuration Audit ==="
echo ""

# Find all thin pool devices on this host
for pool in $(dmsetup status --target thin-pool 2>/dev/null | awk '{print $1}'); do
    echo "Pool: $pool"
    TABLE=$(dmsetup table "$pool")
    echo "  Full table: $TABLE"
    
    if echo "$TABLE" | grep -q "skip_block_zeroing"; then
        echo "  ⚠️  WARNING: skip_block_zeroing is ENABLED"
        echo "  Recycled blocks may retain previous tenant data."
        echo "  Action: Remove skip_block_zeroing from pool table."
    else
        echo "  ✅ Block zeroing is active (default dm-thin behavior)"
    fi
    echo ""
done

echo "=== Thin-block size check ==="
for pool in $(dmsetup status --target thin-pool 2>/dev/null | awk '{print $1}'); do
    BLOCK_SECTORS=$(dmsetup table "$pool" | grep -oP '\d+ \d+ thin-pool' | awk '{print $1}')
    BLOCK_KB=$((BLOCK_SECTORS * 512 / 1024))
    echo "  Pool $pool: block size = ${BLOCK_KB} KiB"
    if [ "$BLOCK_KB" -le 64 ]; then
        echo "    ⚠️  Small block size amplifies residual data exposure."
        echo "    Larger block sizes reduce the ratio of leftover bytes per partial write."
    fi
done

ステップ2:特徴的なI/Oシグネチャを監視する

このエクスプロイトは検出可能なシグネチャを生成します:小さな書き込みの後に不釣り合いに大きな読み取り。4 KiBの書き込みは64 KiBブロックを割り当て、その後の読み取りで64 KiB全体を回復します。読み取り量が書き込み量の10倍を超えるコンテナテレメトリは疑わしいです。

#!/usr/bin/env python3
"""
Detect anomalous write-then-read patterns characteristic of
cross-tenant residual data harvesting in thin-provisioned containers.

Usage: python detect_residual_harvesting.py io_events.jsonl

Input: JSONL file with one event per line:
  {"container_id": "abc123", "operation": "write", "size_bytes": 4096}
"""

import json
import sys
from collections import defaultdict

ANOMALY_THRESHOLD = 10.0  # read_bytes : write_bytes ratio

def analyze_io_events(events_file):
    with open(events_file) as f:
        events = [json.loads(line) for line in f]

    containers = defaultdict(
        lambda: {"writes": 0, "reads": 0, "write_bytes": 0, "read_bytes": 0}
    )

    for event in events:
        cid = event.get("container_id", "unknown")
        op = event.get("operation")
        size = event.get("size_bytes", 0)

        if op == "write":
            containers[cid]["writes"] += 1
            containers[cid]["write_bytes"] += size
        elif op == "read":
            containers[cid]["reads"] += 1
            containers[cid]["read_bytes"] += size

    print("=" * 60)
    print("CONTAINERS WITH ANOMALOUS READ/WRITE RATIOS")
    print("=" * 60)

    flagged = 0
    for cid, stats in sorted(containers.items()):
        if stats["write_bytes"] == 0:
            continue
        ratio = stats["read_bytes"] / stats["write_bytes"]
        if ratio > ANOMALY_THRESHOLD:
            flagged += 1
            print(f"\n🚩 Container: {cid}")
            print(f"   Writes: {stats['writes']:,} ops | "
                  f"{stats['write_bytes']:,} bytes")
            print(f"   Reads:  {stats['reads']:,} ops | "
                  f"{stats['read_bytes']:,} bytes")
            print(f"   Ratio:  {ratio:.1f}x — investigate")

    if flagged == 0:
        print("\n✅ No anomalous containers detected.")
    print(f"\nScanned {len(containers)} containers. Flagged: {flagged}")
    return flagged

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python detect_residual_harvesting.py <io_events.jsonl>")
        sys.exit(1)
    sys.exit(1 if analyze_io_events(sys.argv[1]) > 0 else 0)

Cloudflareのインシデントでは、研究者の概念実証(PoC)がまさにこのシグネチャを生成しました。CloudflareのセキュリティチームはPoCから検出シグネチャを構築し、過去のディスクI/Oテレメトリに適用して、一致するアクティビティが研究者とCloudflare自身のエンジニアによる承認済み検証中のみであることを確認しました。第三者による悪用は見つかりませんでした。

教訓:コンテナI/Oテレメトリを収集していれば、遡及的に監査できます。収集していなければ、手探りで進んでいるようなものです。

修復ランブック

監査でskip_block_zeroingまたは同様の露出が明らかになった場合、Cloudflareが実行した修復手順は次のとおりです——順序が重要です。

ステップ1:すべてのプールでブロックゼロ化を再度有効にする

dm-thinプール設定からskip_block_zeroingを削除します。これはシンプールデバイスに対するランタイム変更ですが、新しい割り当てのみを保護します。実行中のコンテナにすでにマップされているブロックとキャッシュされたイメージスナップショットは脆弱なままです。

# This is a simplified representation — actual dm-thin 
# pool recreation commands vary by your orchestration layer.
# The core principle: restart the pool WITHOUT skip_block_zeroing.

dmsetup reload <pool-name> --table "0 <size> thin-pool <meta_dev> <data_dev> <data_block_size> 0"
dmsetup suspend <pool-name>
dmsetup resume <pool-name>

Cloudflareは最初の報告から約6時間以内にこの修正をマージしました。研究者らは、この変更後にPoCが機能しなくなったことを独自に確認しました。

ステップ2:実行中のすべてのコンテナディスクを破棄する

アクティブなシンデバイスにすでにマップされているブロックは、(ゼロ化されていない)コンテンツを保持します。残存データを排除する唯一の方法は、すべてのコンテナディスクを破棄して再作成することです。

Cloudflareはオフピーク時間にホストをドレインし、各ホストでVMを再起動しました。これはローリング運用です——ゲームバックエンドでは、最もトラフィックが少ない時間帯にスケジュールしてください。ホストごとに短い中断が発生します。マッチメイキングレイヤーがグレースフルハンドオフをサポートしている場合、ドレイン中のホスト上のプレイヤーは切断ではなく移行されます。

ステップ3:キャッシュされたイメージスナップショットをクリアする

このステップは見落としがちです。コンテナオーケストレーションレイヤーは通常、高速なコンテナ起動のためにOCIイメージレイヤーをdm-thinスナップショットとしてキャッシュします。これらのキャッシュされたスナップショットは修正前に作成されたもので、未使用領域(ext4の空き領域を含む)に残存データが含まれる可能性があります。キャッシュされたレイヤーを継承する新しいコンテナは、新しいブロックを割り当てることなく/dev/vdcから残存バイトを読み取ることができます。

Cloudflareは影響を受けたフリート全体の緩和前のキャッシュスナップショットをすべて削除し、最初の報告から15日後にクリーンアップを完了しました。キャッシュされたレイヤーはゼロ化された割り当てを使用して再作成されました。

再作成の順序が重要です。 ゼロ化を有効にする前にキャッシュをクリアすると、ゼロ化されていないブロックを新しいスナップショットにすぐに戻してしまうことになります。

ステップ4:修正が維持されていることを検証する

修復後、新しいI/Oテレメトリに対して検出スクリプトを少なくとも48時間実行します。書き込み/読み取り比率をパッチ前のベースラインと比較してください。特徴的な高比率パターンは完全に消えるはずです。

再起動後、各ホストのdm-thinテーブルも検証してください:

# Confirm no host still has skip_block_zeroing
dmsetup table 2>/dev/null | grep -q "skip_block_zeroing" && \
    echo "CRITICAL: Host $(hostname) still has skip_block_zeroing" || \
    echo "OK: $(hostname) — block zeroing active"

この種の脆弱性を防ぐためのアーキテクチャ原則

Cloudflareのインシデントは、より広範なマルチテナントストレージ分離障害の一例です。ここでは、自前のコンテナを管理する場合でもプラットフォームに依存する場合でも、ゲームバックエンドを保護する原則を紹介します。

原則1:マルチテナントプールでブロックゼロ化を無効にしない

skip_block_zeroingフラグはパフォーマンスのために存在します——割り当てのたびに64 KiBブロックをゼロ化するとI/Oオーバーヘッドが追加されます。自分のコードだけが実行されるシングルテナントハードウェアでは、そのトレードオフは許容できるかもしれません。コンテナがテナント間でリサイクルされる共有インフラストラクチャでは、これはセキュリティ上の欠陥です。

ルール: 複数のテナントIDにブロックを割り当てるdm-thinプールは、ブロックゼロ化を有効にする必要があります。これをInfrastructure-as-Codeレイヤーで強制し、ゼロ化なしではホストをプロビジョニングできないようにします。

原則2:コンテナディスクをエフェメラルで危険なものとして扱う

ゲームバックエンドは、起動時にコンテナのディスクがクリーンであると想定すべきではありません。ブロックゼロ化が有効でも、キャッシュレイヤー、スナップショット継承、カーネルバグに関するエッジケースがあります。

コンテナのエントリポイントで、起動時に書き込み可能なボリュームをフォーマットまたはワイプするように記述してください:

#!/bin/bash
# Ensure a clean writable disk on container start
if [ -b /dev/vdc ]; then
    # Overwrite with zeros (slow but thorough)
    # For a game backend, the writable disk is typically small (1-4 GiB)
    dd if=/dev/zero of=/dev/vdc bs=1M count=4096 status=progress
    
    # Create a fresh filesystem
    mkfs.ext4 -F /dev/vdc
    
    mount /dev/vdc /workspace
fi

これによりコンテナの起動に数秒追加されます。5〜45分実行されるゲームマッチでは許容範囲です。サブ秒のコールドスタート要件がある場合は、より高速なアプローチを使用してください:blkdiscard(TRIMをトリガーし、ストレージバックエンドによってはブロックをゼロ化する場合があります)とmkfs.ext4 -Fの組み合わせです。

原則3:I/O比率をセキュリティシグナルとして監視する

ほとんどのコンテナ監視はCPU、メモリ、ネットワークに焦点を当てています。ディスクI/Oをセキュリティテレメトリに追加してください。読み取りバイトと書き込みバイトの比率は、残存データ収穫攻撃の強力なシグナルです——正当なゲームワークロードはバランスの取れたパターンで書き込みと読み取りを行います。4 KiBチャンクを書き込んだ後、領域ごとに60+ KiBを読み戻すコンテナは異常です。

バックエンドがすでにコンテナライフサイクルイベントをログに記録している場合、I/O異常をテナントIDや作成タイムスタンプと関連付けて、潜在的なインシデントの爆発半径を狭めることができます。このテレメトリインフラストラクチャを自分で管理せずに、組み込みのクラッシュレポートとユーザーログを必要とするゲーム開発者には、horizOnのようなプラットフォームがこれらの機能をすぐに提供します——つまり、配管の構築に費やす時間を減らし、ゲームロジックに多くの時間を費やすことができます。

原則4:プレイヤーデータに多層防御を実装する

コンテナディスクが漏洩しても、その上のデータが暗号化されているか、キーなしでは無意味であれば、被害は限定的です。プレイヤーのセーブ状態、セッショントークン、インベントリデータを保存時暗号化で保存してください。暗号化キーはコンテナディスクではなくシークレットマネージャーに置くべきです。

これは、Star Citizenのデータ漏洩と、侵害を生き延びるためのゲームバックエンドの設計方法の分析で取り上げたのと同じ原則です——多層防御とは、単一のレイヤーはいつか必ず失敗すると想定することです。

ベストプラクティスチェックリスト

  1. デプロイ前にすべてのdm-thinプール設定を監査する。 skip_block_zeroingが存在する場合に失敗するプリフライトチェックをコンテナオーケストレーションパイプラインに追加してください。これは、脆弱性のクラス全体を防ぐ5行のCIチェックです。

  2. テナント属性付きのコンテナディスクI/Oテレメトリを収集する。 観測しない悪用は検出できません。コンテナごとの書き込み/読み取り量をテナントIDとホストIDに関連付けてログに記録してください。遡及調査のために少なくとも30日間保持します。

  3. 起動時にコンテナの書き込み可能ボリュームをゼロ化またはワイプする。 ストレージレイヤーだけに依存しないでください。起動時の新しいmkfs.ext4は2〜5 GiBの書き込みオーバーヘッドを追加しますが、コンテナのリサイクル後も残存データが残らないことを保証します。

  4. セキュリティパッチ中にコンテナをドレインしてリサイクルする。 ブロックゼロ化を有効にしても新しい割り当てしか保護されません。実行中のコンテナとキャッシュされたイメージスナップショットの既存マッピングは明示的にクリアする必要があります。修正後は必ずフリート全体のリサイクルを行ってください。

  5. 外部キー管理を使用して機密性の高いプレイヤーデータを保存時暗号化する。 残存ブロックが漏洩しても、キーなしの暗号文は役に立ちません。horizOnのアカウント連携Cloud Saveで管理されるプレイヤーのセーブ状態は、リビジョン認識型の書き込みとクライアント側の競合解決を使用します——ただし、コンテナをセルフホストする場合、基盤となるディスク分離は依然としてあなたの責任です。

タイムライン:Cloudflareの対応

参考までに、リソースが十分にあるチームが重大なインフラストラクチャ脆弱性にどれだけ迅速に対応できるかを示します:

時刻 (UTC) アクション
Sep 4, 15:26 研究者がHackerOneを通じて報告
Sep 4, 18:45 セキュリティインシデント発行、本番構成を確認
Sep 4, 21:27 再利用テスト付きのランタイム修正をマージ
Sep 4, 23:15 ロールアウト開始
Sep 7, 06:13 ロールアウト完了、クリーンアップ開始
Sep 14, 10:50 研究者がPoCが機能しなくなったことを確認
Sep 19, 15:03 緩和前のキャッシュスナップショットをすべてクリア

報告から根本原因の確認まで3時間未満、修正のマージまで8時間未満です。フリート全体のクリーンアップには15日かかりました——グローバルインフラストラクチャ全体でのローリング運用としては正常です。

スピードは重要です。脆弱性の報告から修正までの1時間ごとに、悪用が可能な時間が生まれます。自前のコンテナインフラを運用しているなら、必要になる前にランブックを構築してください。

結論

Cloudflareのコンテナ脆弱性は、暗号化の意味でのゼロデイではありませんでした。これは、マルチテナントワークロードには不適切な、パフォーマンスをセキュリティより優先するという既知のストレージ設定のトレードオフでした。修正は単一の設定変更とフリートのリサイクルでした。

共有コンテナインフラストラクチャ上でゲームバックエンドを運用しているなら、今すぐdm-thinプールを監査してください。I/Oテレメトリに対して検出スクリプトを実行してください。コンテナディスク分離、フリートリサイクル、I/Oテレメトリパイプラインを自分で管理したくない場合は、horizOnが認証、クラッシュレポート、クラウドセーブ、リーダーボード、その他ゲームに必要なバックエンドサービスを処理します——午前3時に本番環境のストレージレイヤーのセキュリティをデバッグするのではなく、ゲームのリリースに集中できます。


ソース: CloudflareがContainersのテナント間データ露出脆弱性にどう対応したか

このダッシュボードは以下のチームによって愛情を込めて作られています Projectmakers

© 2026 projectmakers.de

unknown-v1.103.4 / unknown-v--