Cloudflare 컨테이너 취약점이 멀티 테넌트 게임 백엔드 보안에 대해 알려준 교훈
핵심 요약
멀티 테넌트 게임 백엔드 보안을 강화하세요: Cloudflare 컨테이너 취약점의 dm-thin `skip_block_zeroing` 데이터 노출 위험을 진단하고, 탐지 런북·대응 절차·예방 아키텍처 원칙을 실제 운영 환경에 적용하는 방법을 제시합니다.
컨테이너화된 게임 백엔드는 매치, 로비, 분석 배치마다 새 VM을 스핀업합니다. 컨테이너가 종료되면 데이터는 사라지겠죠? 사실 꼭 그렇지 않습니다.
꼭 그렇지 않습니다. 2026년 9월, Accomplish의 보안 연구원 Oren Yomtov는 Cloudflare Containers에 크로스 테넌트 데이터 노출 취약점이 있음을 발견했습니다. 파괴된 컨테이너의 잔여 디스크 블록을 같은 호스트의 무관한 워크로드가 읽을 수 있었습니다. 복구된 데이터에는 디렉터리 구조, 데이터베이스 페이지, 구조적으로 완전한 SQLite 데이터베이스가 포함되어 있었습니다.
멀티 테넌트 컨테이너 플랫폼에서 게임 백엔드를 운영한다면 이 취약점의 근본 원인 — dm-thin 서브시스템의 skip_block_zeroing이라는 Linux 스토리지 구성 — 을 반드시 이해해야 합니다. 이 글은 무엇이 잘못됐는지 분석하고, 지금 바로 적용할 수 있는 탐지 및 대응 런북을 제공하며, 이런 부류의 취약점이 플레이어 데이터에 도달하지 못하게 하는 아키텍처 원칙을 다룹니다.
Thin Provisioning이 크로스 테넌트 데이터 노출을 만드는 방식
Cloudflare Containers는 멀티 테넌트 하드웨어에서 Firecracker 마이크로 VM 내부에서 워크로드를 실행합니다. 각 컨테이너는 Linux device-mapper 씬 프로비저닝(dm-thin)으로 뒷받침되는 쓰기 가능한 루트 디스크를 받습니다. Firecracker는 이 디스크를 게스트 VM에 /dev/vdc로 노출합니다.
Thin provisioning은 물리 스토리지 할당을 지연시키는 방식으로 동작합니다. 10GiB 가상 디스크를 만들면 물리 공간을 거의 소비하지 않습니다. 게스트가 이전에 매핑되지 않은 영역에 쓰면 공유 풀에서 온디맨드로 블록이 할당됩니다. Cloudflare의 영향을 받은 풀은 64KiB 씬 블록 크기를 사용했습니다.
취약점은 바로 여기에 있습니다. 컨테이너가 파괴되면 씬 볼륨 매핑이 삭제되고 물리 블록은 재사용을 위해 공유 풀로 돌아갑니다. 영향을 받은 풀은 다음과 같이 구성되어 있었습니다:
skip_block_zeroing
이 플래그가 있으면 dm-thin은 새로 할당된 블록을 다음 테넌트가 접근할 수 있게 만들기 전에 제로잉을 건너뜁니다. 64KiB 전체 쓰기는 재활용된 블록 전체를 덮어씁니다. 하지만 부분 쓰기 — 예를 들어 4KiB — 는 해당 영역만 교체합니다. 나머지 60KiB에는 이전 소유자의 데이터가 남아 있을 수 있습니다.
이것은 이론적인 엣지 케이스가 아닙니다. 연구원은 24개 프로덕션 배치 중 18개, 4개 대륙에 걸친 22개 기반 노드 중 20개에서 잔여 블록으로 디렉터리 구조, 데이터베이스 페이지, 완전한 SQLite 데이터베이스를 복구했습니다.
부분 쓰기가 핵심 벡터인 이유
새 씬 디스크의 매핑되지 않은 영역을 읽으면 0이 반환됩니다. dm-thin은 물리 블록을 할당하지 않고 0을 제공합니다. 그건 안전합니다. 이 익스플로잇은 작은 쓰기가 제로잉 없이 재활용된 64KiB 블록의 할당을 유발하기 때문에 성립합니다.
공격 패턴:
- Workers 유료 계정으로 컨테이너를 생성합니다.
/dev/vdc(쓰기 가능한 루트 디스크)를 엽니다.- ext4 여유 공간에 해당하는 64KiB 정렬 영역을 식별합니다.
- 각 대상 영역에 정렬된 4KiB 블록 하나를 씁니다.
- 전체 64KiB 블록을 다시 읽습니다.
- 공격자가 덮어쓰지 않은 60KiB만 조사합니다.
4단계가 결정적인 순간입니다. 4KiB 쓰기는 dm-thin이 공유 풀에서 물리 블록을 할당하게 만듭니다. 제로잉이 비활성화되어 있으므로 나머지 60KiB에는 이전에 그 블록을 소유했던 누군가의 잔여 데이터가 포함될 수 있습니다.
연구원이 실제로 검증한 것
연구원들은 ext4 디렉터리 블록 체크섬(metadata_csum 기능)을 사용해 자신들의 테스트 파일시스템 블록과 외부 블록을 구분했습니다. 6개의 프로덕션 배치에서:
- 검사된 테스트 가능한 디렉터리 블록 5,614개
- 연구원 자신의 파일시스템으로 귀속된 블록 0개
- 체크섬 분석으로 식별된 고유한 외부 디렉터리 inode 2,700개
복구된 파일 유형은 쓰레기가 아니었습니다. 구조적으로 의미 있는 SQLite 데이터베이스가 포함되어 있었습니다. 게임 백엔드 맥락에서 플레이어 저장 상태, 세션 토큰, 인벤토리 기록이 들어 있을 수 있는 종류의 데이터입니다.
탐지 런북: 인프라에서 잔여 데이터 하베스팅 찾기
멀티 테넌트 인프라에서 컨테이너화된 게임 백엔드를 운영한다면 구성 감사와 런타임 I/O 이상 분석이라는 두 가지 탐지 계층이 필요합니다.
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단계: 특징적인 I/O 시그니처 모니터링
이 익스플로잇은 감지 가능한 시그니처를 만듭니다: 소규모 쓰기 후 이어지는 불균형적으로 큰 읽기입니다. 4KiB 쓰기는 64KiB 블록을 할당하고, 이후 읽기는 64KiB 전체를 복구합니다. 읽기 볼륨이 쓰기 볼륨보다 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 자체 엔지니어의 활동뿐임을 확인했습니다. 제3자 악용은 발견되지 않았습니다.
교훈: 컨테이너 I/O 텔레메트리를 수집하고 있다면 사후 감사가 가능합니다. 수집하지 않는다면 맹목적으로 운영하는 것입니다.
대응 런북
감사에서 skip_block_zeroing 또는 유사한 노출이 발견되면 Cloudflare가 실행한 대응 순서를 따르세요. 순서가 중요합니다.
1단계: 모든 풀에서 블록 제로잉 다시 활성화
dm-thin 풀 구성에서 skip_block_zeroing을 제거하세요. 이것은 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단계: 실행 중인 모든 컨테이너 디스크 폐기
활성 씬 디바이스에 이미 매핑된 블록은 (제로잉되지 않은) 콘텐츠를 유지합니다. 잔여 데이터를 제거하는 유일한 방법은 모든 컨테이너 디스크를 파괴하고 다시 만드는 것입니다.
Cloudflare는 트래픽이 낮은 시간대에 호스트를 드레인하고 각 호스트에서 VM을 재시작했습니다. 이것은 롤링 운영입니다. 게임 백엔드라면 트래픽이 가장 낮은 시간대에 예약하세요. 호스트당 짧은 중단이 발생할 수 있습니다. 매치메이킹 레이어가 무중단 핸드오프를 지원하면 드레인 중인 호스트의 플레이어는 연결이 끊기지 않고 마이그레이션됩니다.
3단계: 캐시된 이미지 스냅샷 정리
이 단계는 놓치기 쉽습니다. 컨테이너 오케스트레이션 레이어는 일반적으로 빠른 컨테이너 시작을 위해 OCI 이미지 레이어를 dm-thin 스냅샷으로 캐시합니다. 이 캐시된 스냅샷은 수정 전에 생성되었으며 사용되지 않은 영역(ext4 여유 공간 포함)에 잔여 데이터가 있을 수 있습니다. 캐시된 레이어를 상속받은 새 컨테이너는 새 블록을 할당하지 않고도 /dev/vdc에서 잔여 바이트를 읽을 수 있습니다.
Cloudflare는 영향을 받은 플릿 전체에서 수정 전 캐시된 스냅샷을 모두 제거하여 초기 신고 후 15일 만에 정리를 완료했습니다. 캐시된 레이어는 제로잉된 할당으로 다시 생성되었습니다.
재생성 순서가 중요합니다. 제로잉을 활성화하기 전에 캐시를 정리하면 제로잉되지 않은 블록을 새 스냅샷에 다시 밀어 넣는 꼴입니다.
4단계: 수정이 유지되는지 검증
대응 후 최소 48시간 동안 새 I/O 텔레메트리에 탐지 스크립트를 실행하세요. 쓰기/읽기 비율을 패치 전 기준과 비교하세요. 특징적인 높은 비율 패턴이 완전히 사라져야 합니다.
재부팅 후 각 호스트의 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 플래그는 성능을 위해 존재합니다. 모든 할당에서 64KiB 블록을 제로잉하면 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분 동안 실행되는 게임 매치라면 허용 가능합니다. 1초 미만의 콜드 스타트 요구사항이 있다면 더 빠른 방법인 blkdiscard(TRIM을 트리거하고 스토리지 백엔드에 따라 블록을 제로잉할 수 있음)와 mkfs.ext4 -F를 함께 사용하세요.
원칙 3: I/O 비율을 보안 신호로 모니터링
대부분의 컨테이너 모니터링은 CPU, 메모리, 네트워크에 집중합니다. 보안 텔레메트리에 디스크 I/O를 추가하세요. 읽은 바이트 대비 쓴 바이트 비율은 잔여 데이터 하베스팅 공격의 강력한 신호입니다. 정상적인 게임 워크로드는 쓰기와 읽기가 균형 잡힌 패턴을 보입니다. 4KiB 청크를 쓴 다음 영역당 60KiB 이상을 읽어내는 컨테이너는 이상 징후입니다.
백엔드가 이미 컨테이너 라이프사이클 이벤트를 로깅하고 있다면 I/O 이상을 테넌트 ID 및 생성 타임스탬프와 상관분석하여 잠재적 인시던트의 피해 범위를 좁힐 수 있습니다. 이러한 텔레메트리 인프라를 직접 관리하지 않고 내장된 크래시 리포트와 사용자 로그를 원하는 게임 개발자라면 horizOn 같은 플랫폼이 이런 기능을 기본 제공합니다. 즉, 파이프라인을 만드는 시간을 줄이고 게임 로직에 더 많은 시간을 쓸 수 있습니다.
원칙 4: 플레이어 데이터에 심층 방어 구현
컨테이너 디스크가 유출되더라도 그 위의 데이터가 암호화되어 있거나 키 없이는 의미가 없다면 피해는 제한됩니다. 플레이어 저장 상태, 세션 토큰, 인벤토리 데이터를 저장 시 암호화하세요. 암호화 키는 컨테이너 디스크가 아니라 시크릿 매니저에 있어야 합니다.
이것은 Star Citizen 데이터 유출 사례와 침해에서 살아남는 게임 백엔드 설계 방법 분석에서 다룬 것과 같은 원칙입니다. 심층 방어는 어떤 단일 레이어도 결국 실패할 것이라고 가정하는 것입니다.
모범 사례 체크리스트
배포 전 모든 dm-thin 풀 구성을 감사하세요.
skip_block_zeroing이 있으면 실패하도록 컨테이너 오케스트레이션 파이프라인에 사전 점검을 추가하세요. 이것은 다섯 줄짜리 CI 체크로 전체 취약점 클래스를 예방합니다.테넌트 귀속 정보가 포함된 컨테이너 디스크 I/O 텔레메트리를 수집하세요. 관찰하지 못한 악용은 탐지할 수 없습니다. 컨테이너별 쓰기/읽기 볼륨을 테넌트 ID 및 호스트 ID와 함께 로깅하세요. 사후 조사를 위해 최소 30일간 보관하세요.
부팅 시 컨테이너 쓰기 가능 볼륨을 제로잉하거나 지우세요. 스토리지 레이어에만 의존하지 마세요. 시작 시 새
mkfs.ext4는 2~5GiB의 쓰기 오버헤드를 추가하지만 컨테이너 재활용 후 잔여 데이터가 남지 않음을 보장합니다.보안 패치 후에만이 아니라 패치 중에도 컨테이너를 드레인하고 재활용하세요. 블록 제로잉 활성화는 새 할당만 보호합니다. 실행 중인 컨테이너의 기존 매핑과 캐시된 이미지 스냅샷은 명시적으로 정리해야 합니다. 항상 수정 후 플릿 전체 재활용을 수행하세요.
민감한 플레이어 데이터를 외부 키 관리로 저장 시 암호화하세요. 잔여 블록이 유출되어도 키가 없는 암호문은 쓸모가 없습니다. horizOn의 계정 바인딩 Cloud Save를 통해 관리되는 플레이어 저장 상태는 리비전 인식 쓰기와 클라이언트 측 충돌 해결을 사용합니다. 하지만 컨테이너를 자체 호스팅한다면 기본 디스크 격리는 여전히 여러분의 책임입니다.
타임라인: Cloudflare의 대응
참고로, 자원이 충분한 팀이 중요한 인프라 취약점에 얼마나 빠르게 대응할 수 있는지 보여드립니다:
| 시간 (UTC) | 조치 |
|---|---|
| 9월 4일 15:26 | 연구원이 HackerOne을 통해 신고 |
| 9월 4일 18:45 | 보안 인시던트 개시, 프로덕션 설정 확인 |
| 9월 4일 21:27 | 재사용 테스트와 함께 런타임 수정 병합 |
| 9월 4일 23:15 | 롤아웃 시작 |
| 9월 7일 06:13 | 롤아웃 완료, 정리 시작 |
| 9월 14일 10:50 | 연구원이 PoC가 더 이상 동작하지 않음을 확인 |
| 9월 19일 15:03 | 수정 전 캐시된 스냅샷 전체 정리 완료 |
신고부터 근본 원인 확인까지 3시간 미만, 수정 병합까지 8시간 미만이었습니다. 전체 플릿 정리에는 15일이 걸렸습니다. 글로벌 인프라 전반의 롤링 운영으로는 정상적인 기간입니다.
속도는 중요합니다. 취약점 신고와 수정 사이의 모든 시간은 악용이 가능한 시간입니다. 자체 컨테이너 인프라를 운영한다면 필요해지기 전에 런북을 만들어 두세요.
결론
Cloudflare 컨테이너 취약점은 암호학적 의미의 제로데이가 아니었습니다. 멀티 테넌트 워크로드에 부적절했던, 보안보다 성능을 택한 알려진 스토리지 구성 트레이드오프였습니다. 수정은 단일 구성 변경과 플릿 재활용이 전부였습니다.
공유 컨테이너 인프라에서 게임 백엔드를 운영한다면 오늘 dm-thin 풀을 감사하세요. I/O 텔레메트리에 탐지 스크립트를 실행하세요. 컨테이너 디스크 격리, 플릿 재활용, I/O 텔레메트리 파이프라인을 직접 관리하고 싶지 않다면 horizOn이 인증, 크래시 리포트, 클라우드 세이브, 리더보드 등 게임에 필요한 다른 백엔드 서비스를 처리해 줍니다. 새벽 3시에 프로덕션 스토리지 레이어 보안을 디버깅하는 대신 게임 출시에 집중할 수 있습니다.