Назад к блогу

Чему нас научила уязвимость контейнеров Cloudflare в безопасности мультитенантных игровых бэкендов

Опубликовано 25 сентября 2026 г.
Чему нас научила уязвимость контейнеров Cloudflare в безопасности мультитенантных игровых бэкендов Создано с помощью ИИ

Коротко о главном

Узнайте, как уязвимость контейнеров Cloudflare раскрыла риски мультитенантных игровых бэкендов и как защитить данные игроков от остаточных данных.

Ваш контейнеризированный игровой бэкенд поднимает свежую VM для каждого матча, каждого лобби, каждой пачки аналитики. Когда контейнер останавливается, данные исчезают — верно?

Не обязательно. В сентябре 2026 года исследователь безопасности Орен Йомтов из Accomplish обнаружил, что Cloudflare Containers содержат уязвимость межтенантного раскрытия данных. Остаточные дисковые блоки уничтоженных контейнеров были читаемы для несвязанных рабочих нагрузок на том же хосте. Восстановленные данные включали структуры каталогов, страницы баз данных и структурно полные SQLite-базы.

Если вы запускаете игровые бэкенды на любой мультитенантной контейнерной платформе, вам нужно понять первопричину этой уязвимости — конфигурацию хранилища Linux под названием skip_block_zeroing в подсистеме dm-thin. В этом посте разбирается, что пошло не так, приводится runbook по обнаружению и устранению, который можно применить уже сегодня, и рассматриваются архитектурные принципы, не позволяющие этому классу уязвимостей добраться до данных ваших игроков.

Как thin provisioning создает межтенантное раскрытие данных

Cloudflare Containers запускают рабочие нагрузки внутри Firecracker micro-VM на мультитенантном оборудовании. Каждый контейнер получает записываемый корневой диск на основе Linux device-mapper thin provisioning (dm-thin). Firecracker предоставляет этот диск гостевой VM как /dev/vdc.

Thin provisioning работает за счет отложенного выделения физического хранилища. Когда вы создаете виртуальный диск на 10 GiB, он потребляет почти нет физического пространства. Блоки выделяются по требованию из общего пула, когда гость пишет в ранее неразмеченную область. Затронутые пулы Cloudflare использовали размер тонкого блока 64 KiB.

Вот где живет уязвимость. Когда контейнер уничтожается, его thin-volume отображения удаляются, и физические блоки возвращаются в общий пул для повторного использования. Затронутый пул был сконфигурирован с:

skip_block_zeroing

С этим флагом dm-thin пропускает обнуление вновь выделенных блоков перед тем, как сделать их доступными следующему тенанту. Полная запись 64 KiB перезаписывает весь переработанный блок. Но частичная запись — скажем, 4 KiB — заменяет только эту область. Оставшиеся 60 KiB могут сохранять данные предыдущего владельца блока.

Это не теоретический пограничный случай. Исследователь восстановил структуры каталогов, страницы баз данных и полные SQLite-базы из остаточных блоков в 18 из 24 продакшн-размещений и на 20 из 22 базовых узлов, охватывающих четыре континента.

Почему частичные записи — ключевой вектор

Чтение неразмеченной области нового тонкого диска возвращает нули — dm-thin отдает нули без выделения физического блока. Это безопасно. Эксплойт работает потому, что небольшая запись запускает выделение переработанного блока 64 KiB без его предварительного обнуления.

Паттерн атаки:

  1. Создайте контейнер в аккаунте Workers Paid.
  2. Откройте /dev/vdc (записываемый корневой диск).
  3. Определите области, выровненные по 64 KiB, соответствующие свободному пространству ext4.
  4. Запишите один выровненный блок 4 KiB в каждую целевую область.
  5. Прочитайте полные блоки 64 KiB.
  6. Изучите только те 60 KiB, которые атакующий не перезаписал.

Шаг 4 — критический момент. Запись 4 KiB заставляет dm-thin выделить физический блок из общего пула. Поскольку обнуление отключено, оставшиеся 60 KiB могут содержать остаточные данные того, кто ранее владел этим блоком.

Что именно подтвердил исследователь

Исследователи использовали контрольные суммы блоков каталогов ext4 (функция metadata_csum), чтобы отличить собственные блоки тестовой файловой системы от посторонних. В шести продакшн-размещениях:

  • Изучено 5 614 тестируемых блоков каталогов
  • 0 блоков, относящихся к собственной файловой системе исследователей
  • Выявлено 2 700 отдельных посторонних inode каталогов с помощью анализа контрольных сумм

Восстановленные типы файлов не были мусором. Они включали структурно значимые SQLite-базы — те данные, которые в контексте игрового бэкенда могут содержать сохранения игроков, сессионные токены или записи инвентаря.

Playbook по обнаружению: поиск сбора остаточных данных в вашей инфраструктуре

Если вы эксплуатируете контейнеризированные игровые бэкенды на мультитенантной инфраструктуре, вам нужны два уровня обнаружения: аудит конфигурации и анализ аномалий ввода-вывода в рантайме.

Шаг 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

Если skip_block_zeroing встречается где-либо в конфигурации вашего пула, у вас тот же класс уязвимости, который закрыла Cloudflare. Исправьте это немедленно — не ждите планового окна обслуживания.

Шаг 2: Отслеживайте характерную сигнатуру ввода-вывода

Эксплойт создает обнаруживаемую сигнатуру: маленькие записи, за которыми следуют непропорционально большие чтения. Запись 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 proof-of-concept исследователей дал именно эту сигнатуру. Команда безопасности Cloudflare построила детектирующие сигнатуры на основе PoC, применила их к исторической телеметрии дискового ввода-вывода и подтвердила, что единственная совпавшая активность исходила от исследователей и собственных инженеров Cloudflare во время авторизованной валидации. Следов эксплуатации третьими лицами не обнаружено.

Урок: если вы собираете телеметрию ввода-вывода контейнеров, вы можете провести ретроспективный аудит. Если не собираете — вы действуете вслепую.

Runbook по устранению уязвимости

Если ваш аудит выявил skip_block_zeroing или аналогичную проблему, вот последовательность устранения, которую выполнила Cloudflare — и порядок здесь важен.

Шаг 1: Повторно включите обнуление блоков во всех пулах

Удалите skip_block_zeroing из конфигурации вашего dm-thin пула. Это изменение на лету для thin-pool устройства, но оно защищает только новые выделения. Блоки, уже отображенные в работающие контейнеры и кэшированные снапшоты образов, остаются уязвимыми.

# 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: Выведите из эксплуатации все диски работающих контейнеров

Блоки, уже отображенные в активные thin-устройства, сохраняют свое (необнуленное) содержимое. Единственный способ устранить остаточные данные — уничтожить и пересоздать каждый диск контейнера.

Cloudflare выводила хосты из эксплуатации в часы низкой нагрузки и перезапускала VM на каждом хосте. Это раскатывающаяся операция — для игрового бэкенда планируйте ее в окно минимального трафика. Ожидайте кратковременного прерывания на каждом хосте. Если ваш слой matchmaking поддерживает плавную передачу, игроки на выводимых хостах будут мигрированы, а не отключены.

Шаг 3: Очистите кэшированные снапшоты образов

Этот шаг легко пропустить. Слои оркестрации контейнеров обычно кэшируют слои OCI-образов как dm-thin снапшоты для быстрого запуска контейнеров. Эти кэшированные снапшоты были созданы до исправления и могут содержать остаточные данные в неиспользуемых областях (включая свободное пространство ext4). Новый контейнер, наследующий кэшированный слой, может читать остаточные байты из /dev/vdc, вообще не выделяя новый блок.

Cloudflare удалила все кэшированные снапшоты, созданные до устранения, по всему затронутому флоту, завершив очистку через 15 дней после первоначального отчета. Кэшированные слои были пересозданы с использованием обнуленных выделений.

Порядок пересоздания важен. Если вы очистите кэши до включения обнуления, вы просто вернете необнуленные блоки прямо в свежие снапшоты.

Шаг 4: Подтвердите, что исправление работает

После устранения запустите скрипт обнаружения на новой телеметрии ввода-вывода в течение как минимум 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 при каждом выделении добавляет накладные расходы на ввод-вывод. На однотенантном оборудовании, где работает только ваш код, такой компромисс может быть приемлем. На общей инфраструктуре, где контейнеры перерабатываются между тенантами, это дефект безопасности.

Правило: Любой dm-thin пул, выделяющий блоки более чем одному тенанту, должен иметь включенное обнуление блоков. Обеспечьте это на уровне infrastructure-as-code, чтобы ни один хост не мог быть развернут без него.

Принцип 2: Относитесь к дискам контейнеров как к эфемерным и опасным

Ваш игровой бэкенд никогда не должен предполагать, что диск контейнера чист при запуске. Даже с включенным обнулением блоков существуют пограничные случаи с кэширующими слоями, наследованием снапшотов и багами ядра.

Напишите entrypoint контейнера так, чтобы он форматировал или затирал записываемый том при загрузке:

#!/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: Отслеживайте соотношения ввода-вывода как сигнал безопасности

Большинство систем мониторинга контейнеров фокусируются на CPU, памяти и сети. Добавьте дисковый ввод-вывод в свою телеметрию безопасности. Соотношение прочитанных байтов к записанным — сильный сигнал для атак по сбору остаточных данных: легитимные игровые нагрузки пишут и читают в сбалансированных паттернах. Контейнер, который пишет куски по 4 KiB, а затем читает обратно 60+ KiB на область, аномален.

Если ваш бэкенд уже логирует события жизненного цикла контейнеров, вы можете коррелировать аномалии ввода-вывода с идентичностью тенанта и временными метками создания, чтобы сузить радиус поражения потенциального инцидента. Для разработчиков игр, которые хотят встроенные отчеты о крашах и пользовательские логи без самостоятельного управления этой телеметрической инфраструктурой, такие платформы, как horizOn, предоставляют эти возможности из коробки — то есть вы тратите меньше времени на создание plumbing и больше на игровую логику.

Принцип 4: Реализуйте эшелонированную защиту данных игроков

Даже если диск контейнера утечет, ущерб ограничен, если данные на нем зашифрованы или бессмысленны без ключа. Храните сохранения игроков, сессионные токены и данные инвентаря зашифрованными в состоянии покоя. Ключ шифрования должен жить в secrets manager, а не на диске контейнера.

Это тот же принцип, который мы рассматривали в нашем анализе утечки данных Star Citizen и архитектуры игровых бэкендов, способных пережить компрометацию — эшелонированная защита означает допущение, что любой отдельный слой рано или поздно выйдет из строя.

Чек-лист лучших практик

  1. Проверяйте конфигурацию каждого dm-thin пула перед развертыванием. Добавьте pre-flight проверку в ваш пайплайн оркестрации контейнеров, которая завершается ошибкой, если присутствует skip_block_zeroing. Это проверка CI из пяти строк, предотвращающая целый класс уязвимостей.

  2. Собирайте телеметрию дискового ввода-вывода контейнеров с привязкой к тенанту. Вы не можете обнаружить эксплуатацию, которую не наблюдаете. Логируйте объемы записи/чтения по каждому контейнеру с корреляцией по 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 дней — нормально для раскатывающихся операций по глобальной инфраструктуре.

Скорость имеет значение. Каждый час между отчетом об уязвимости и исправлением — это час, когда возможна эксплуатация. Если вы управляете собственной контейнерной инфраструктурой, создайте runbook до того, как он понадобится.

Суть

Уязвимость контейнеров Cloudflare не была zero-day в криптографическом смысле. Это был известный компромисс конфигурации хранилища — производительность в ущерб безопасности, — неуместный для мультитенантных нагрузок. Исправление заключалось в одном изменении конфигурации плюс переработке флота.

Если вы запускаете игровые бэкенды на общей контейнерной инфраструктуре, проверьте свои dm-thin пулы уже сегодня. Запустите скрипт обнаружения на вашей телеметрии ввода-вывода. А если вы предпочитаете не управлять самостоятельно изоляцией дисков контейнеров, переработкой флота и пайплайнами телеметрии ввода-вывода, horizOn берет на себя аутентификацию, отчеты о крашах, облачные сохранения, лидерборды и другие бэкенд-сервисы, необходимые вашей игре, — чтобы вы могли сосредоточиться на выпуске игры, а не на отладке безопасности слоя хранилища в продакшене в 3 часа ночи.


Источник: Как Cloudflare устранила уязвимость межтенантного раскрытия данных в контейнерах