Terug naar Blog

Wat Cloudflare's containerkwetsbaarheid ons leerde over multi-tenant game-backend security

Gepubliceerd op 25 september 2026
Wat Cloudflare's containerkwetsbaarheid ons leerde over multi-tenant game-backend security Gegenereerd met behulp van AI

Kort samengevat

Audit je dm-thin pools en versterk je multi-tenant game-backend security na de Cloudflare-containerkwetsbaarheid: detectie, herstel en preventie.

Je gecontaineriseerde game-backend start voor elke match, elke lobby en elke analytics-batch een verse VM op. Wanneer die container wordt afgesloten, is de data weg — toch?

Niet per se. In september 2026 ontdekte security-onderzoeker Oren Yomtov van Accomplish dat Cloudflare Containers een cross-tenant data-exposure-kwetsbaarheid had. Resterende schijfblokken van vernietigde containers waren leesbaar voor niet-gerelateerde workloads op dezelfde host. De teruggevonden data omvatte directorystructuren, databasepagina's en structureel complete SQLite-databases.

Als je game-backends draait op een multi-tenant containerplatform, is de grondoorzaak van deze kwetsbaarheid — een Linux-opslagconfiguratie genaamd skip_block_zeroing in het dm-thin-subsysteem — iets dat je moet begrijpen. In dit artikel lees je wat er misging, krijg je een detectie- en herstel-runbook dat je vandaag nog kunt toepassen, en behandelen we de architectuurprincipes die voorkomen dat dit type kwetsbaarheid ooit de data van je spelers bereikt.

Hoe thin provisioning cross-tenant data exposure creëert

Cloudflare Containers draait workloads binnen Firecracker-micro-VM's op multi-tenant hardware. Elke container krijgt een beschrijfbare rootdisk, ondersteund door Linux device-mapper thin provisioning (dm-thin). Firecracker stelt deze disk in de gast-VM beschikbaar als /dev/vdc.

Thin provisioning werkt door fysieke opslagtoewijzing uit te stellen. Wanneer je een virtuele disk van 10 GiB aanmaakt, verbruikt die vrijwel geen fysieke ruimte. Blokken worden on-demand toegewezen uit een gedeelde pool zodra de gast schrijft naar een voorheen niet-toegewezen (unmapped) gebied. De getroffen pools van Cloudflare gebruikten een thin-blockgrootte van 64 KiB.

Hier zit de kwetsbaarheid. Wanneer een container wordt vernietigd, worden de thin-volume-toewijzingen verwijderd en keren de fysieke blokken terug naar de gedeelde pool voor hergebruik. De getroffen pool was geconfigureerd met:

skip_block_zeroing

Met deze vlag slaat dm-thin het nullen (zeroing) van nieuw toegewezen blokken over voordat ze toegankelijk worden voor de volgende tenant. Een volledige 64 KiB-schrijfbewerking overschrijft het volledige gerecyclede blok. Maar een partiële schrijfbewerking — bijvoorbeeld 4 KiB — vervangt alleen dat gebied. De resterende 60 KiB kunnen data van de vorige eigenaar van het blok bevatten.

Dit is geen theoretisch randgeval. De onderzoeker haalde directorystructuren, databasepagina's en complete SQLite-databases terug uit resterende blokken, op 18 van de 24 productie-placements en 20 van de 22 onderliggende nodes verspreid over vier continenten.

Waarom partiële schrijfbewerkingen de belangrijkste vector zijn

Het lezen van een niet-toegewezen (unmapped) gebied van een nieuwe thin disk geeft nullen terug — dm-thin levert nullen zonder een fysiek blok toe te wijzen. Dat is veilig. De exploit werkt omdat een kleine schrijfbewerking de toewijzing van een gerecycled 64 KiB-blok triggert, zonder het eerst te nullen.

Het aanvalspatroon:

  1. Maak een container aan op een Workers Paid-account.
  2. Open /dev/vdc (de beschrijfbare rootdisk).
  3. Identificeer 64 KiB-uitgelijnde gebieden die overeenkomen met ext4-vrije ruimte.
  4. Schrijf één uitgelijnd 4 KiB-blok naar elk doelgebied.
  5. Lees de volledige 64 KiB-blokken terug.
  6. Onderzoek alleen de 60 KiB die de aanvaller niet overschreef.

Stap 4 is het kritieke moment. De 4 KiB-schrijfbewerking zorgt ervoor dat dm-thin een fysiek blok uit de gedeelde pool toewijst. Omdat zeroing is uitgeschakeld, kunnen de resterende 60 KiB restdata bevatten van wie dat blok eerder bezat.

Wat de onderzoeker daadwerkelijk valideerde

De onderzoekers gebruikten ext4-directoryblok-checksums (de metadata_csum-feature) om hun eigen testbestandssysteemblokken te onderscheiden van vreemde blokken. Over zes productie-placements:

  • 5.614 testbare directoryblokken onderzocht
  • 0 blokken toegeschreven aan het eigen bestandssysteem van de onderzoekers
  • 2.700 verschillende vreemde directory-inodes geïdentificeerd via checksumanalyse

De teruggevonden bestandstypen waren geen rommel. Ze omvatten structureel betekenisvolle SQLite-databases — het soort data dat in een game-backendcontext spelersopslagstanden (save states), sessietokens of inventarisrecords zou kunnen bevatten.

Detectie-Playbook: residual data harvesting vinden in je infrastructuur

Als je gecontaineriseerde game-backends draait op multi-tenant infrastructuur, heb je twee detectielagen nodig: configuratie-audits en runtime-I/O-anomalieanalyse.

Stap 1: Controleer je dm-thin-configuratie

Draai dit script op elke host die jouw containers draait:

#!/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

Als skip_block_zeroing ergens in je poolconfiguratie voorkomt, heb je dezelfde kwetsbaarheidsklasse die Cloudflare patchte. Los het onmiddellijk op — wacht niet op een gepland onderhoudsvenster.

Stap 2: Monitor op de karakteristieke I/O-signatuur

De exploit produceert een detecteerbare signatuur: kleine schrijfbewerkingen gevolgd door onevenredig grote leesbewerkingen. Een 4 KiB-schrijfbewerking wijst een 64 KiB-blok toe; een daaropvolgende leesbewerking haalt de volledige 64 KiB op. Containertelemetrie waarbij het leesvolume het schrijfvolume met >10x overtreft, is verdacht.

#!/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)

In het Cloudflare-incident produceerde de proof-of-concept van de onderzoekers exact deze signatuur. Het securityteam van Cloudflare bouwde detectiesignaturen op basis van de PoC, paste ze toe op historische disk-I/O-telemetrie en bevestigde dat de enige overeenkomende activiteit afkomstig was van de onderzoekers en Cloudflare's eigen engineers tijdens geautoriseerde validatie. Er werd geen uitbuiting door derden gevonden.

De les: als je container-I/O-telemetrie verzamelt, kun je achteraf auditen. Als je het niet verzamelt, vlieg je blind.

Herstel-Runbook

Als je audit skip_block_zeroing of vergelijkbare blootstelling aan het licht brengt, volgt hier de herstelsequentie die Cloudflare uitvoerde — en de volgorde is belangrijk.

Stap 1: Schakel block zeroing op alle pools opnieuw in

Verwijder skip_block_zeroing uit je dm-thin-poolconfiguratie. Dit is een runtime-wijziging op het thin-pool-apparaat, maar het beschermt alleen nieuwe toewijzingen. Blokken die al zijn toegewezen aan actieve containers en gecachte image-snapshots blijven kwetsbaar.

# 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 voerde deze fix binnen ongeveer 6 uur na het eerste rapport door. De onderzoekers bevestigden onafhankelijk dat de PoC na deze wijziging niet meer werkte.

Stap 2: Vervang alle actieve containerdisks

Blokken die al zijn toegewezen aan actieve thin-devices behouden hun (niet-genulde) inhoud. De enige manier om restdata te elimineren, is elke containerdisk vernietigen en opnieuw aanmaken.

Cloudflare leegde hosts buiten de piekuren en herstartte VM's op elke host. Dit is een rolling-operatie — plan het voor een game-backend in tijdens je venster met de minste verkeersdrukte. Verwacht een korte onderbreking per host. Als je matchmaking-laag een graceful handoff ondersteunt, worden spelers op leeggelopen hosts gemigreerd in plaats van verbroken.

Stap 3: Wis gecachte image-snapshots

Deze stap wordt makkelijk over het hoofd gezien. Containerorchestratielagen cachen doorgaans OCI-image-layers als dm-thin-snapshots voor een snelle containerstart. Deze gecachte snapshots zijn vóór de fix aangemaakt en kunnen restdata bevatten in ongebruikte gebieden (inclusief ext4-vrije ruimte). Een nieuwe container die een gecachte layer erft, kan resterende bytes uit /dev/vdc lezen zonder ooit een nieuw blok toe te wijzen.

Cloudflare verwijderde alle vóór de mitigatie gecachte snapshots in de getroffen fleet en voltooide de opschoning 15 dagen na het eerste rapport. De gecachte layers werden opnieuw aangemaakt met genulde toewijzingen.

De volgorde van het opnieuw aanmaken is belangrijk. Als je caches wist vóór je zeroing inschakelt, duw je niet-genulde blokken meteen terug in verse snapshots.

Stap 4: Valideer dat de fix standhoudt

Draai na de herstelwerkzaamheden het detectiescript minimaal 48 uur tegen nieuwe I/O-telemetrie. Vergelijk de schrijf-/leesratio's met je baseline van vóór de patch. Het karakteristieke hoge-ratio-patroon zou volledig moeten verdwijnen.

Je moet ook de dm-thin-tabel op elke host na een reboot verifiëren:

# 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"

Architectuurprincipes om dit type kwetsbaarheid te voorkomen

Het Cloudflare-incident is één geval van een bredere klasse van multi-tenant storage-isolatiefouten. Hier zijn de principes die je game-backend beschermen — of je nu je eigen containers beheert of afhankelijk bent van een platform.

Principe 1: Schakel block zeroing nooit uit in multi-tenant pools

De skip_block_zeroing-vlag bestaat voor prestaties — het nullen van 64 KiB-blokken bij elke toewijzing voegt I/O-overhead toe. Op single-tenant hardware waar alleen jouw code draait, is die afweging misschien acceptabel. Op gedeelde infrastructuur waar containers over tenants worden gerecycled, is het een security-defect.

Regel: Elke dm-thin-pool die blokken toewijst aan meer dan één tenant-identiteit moet block zeroing ingeschakeld hebben. Handhaaf dit op de infrastructure-as-code-laag, zodat geen enkele host zonder kan worden geprovisiond.

Principe 2: Behandel containerdisks als ephemeral en gevaarlijk

Je game-backend mag er nooit van uitgaan dat de disk van een container schoon is bij het opstarten. Zelfs met block zeroing ingeschakeld zijn er edge cases met caching-layers, snapshot-overerving en kernelbugs.

Schrijf je container-entrypoint zo dat het het beschrijfbare volume bij het opstarten formatteert of wist:

#!/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

Dit voegt een paar seconden toe aan de containerstart. Voor een gamematch die 5–45 minuten duurt, is dat acceptabel. Voor cold-start-eisen onder de seconde gebruik je de snellere aanpak: blkdiscard (dat TRIM triggert en afhankelijk van de storage-backend blokken kan nullen) in combinatie met mkfs.ext4 -F.

Principe 3: Monitor I/O-ratio's als security-signaal

De meeste containermonitoring richt zich op CPU, geheugen en netwerk. Voeg disk-I/O toe aan je security-telemetrie. De verhouding tussen gelezen en geschreven bytes is een sterk signaal voor residual-data-harvesting-aanvallen — legitieme game-workloads schrijven en lezen in gebalanceerde patronen. Een container die 4 KiB-blokken schrijft en vervolgens 60+ KiB per gebied terugleest, is afwijkend.

Als je backend al gebeurtenissen in de containerlevenscyclus logt, kun je I/O-afwijkingen correleren met tenant-identiteit en aanmaaktijdsstempels om de blast radius van een potentieel incident te beperken. Voor gameontwikkelaars die ingebouwde crashrapporten en gebruikerslogs willen zonder die telemetrie-infrastructuur zelf te beheren, bieden platforms als horizOn deze mogelijkheden out of the box — zodat je minder tijd besteedt aan het bouwen van leidingwerk en meer tijd aan gamelogica.

Principe 4: Implementeer defense in depth voor spelersdata

Zelfs als je containerdisk lekt, is de schade beperkt als de data erop versleuteld of betekenisloos is zonder een sleutel. Sla spelersopslagstanden, sessietokens en inventarisdata versleuteld op (at rest). De versleutelingssleutel moet in een secrets-manager leven, niet op de containerdisk.

Dit is hetzelfde principe dat we bespraken in onze analyse van de Star Citizen-data-inbreuk en hoe je game-backends architecteert om compromittering te overleven — defense in depth betekent aannemen dat elke afzonderlijke laag uiteindelijk zal falen.

Checklist best practices

  1. Audit elke dm-thin-poolconfiguratie vóór deployment. Voeg een pre-flight-check toe aan je containerorchestratiepipeline die faalt als skip_block_zeroing aanwezig is. Dit is een CI-check van vijf regels die een hele klasse kwetsbaarheden voorkomt.

  2. Verzamel containerdisk-I/O-telemetrie met tenant-toerekening. Je kunt geen uitbuiting detecteren die je niet observeert. Log schrijf-/leesvolumes per container, gecorreleerd met tenant-ID en host-ID. Bewaar minimaal 30 dagen voor retroactief onderzoek.

  3. Null of wis beschrijfbare containervolumes bij het opstarten. Vertrouw niet alleen op de opslaglaag. Een verse mkfs.ext4 bij het opstarten voegt 2–5 GiB schrijfoverhead toe, maar garandeert dat geen restdata containerrecycling overleeft.

  4. Leeg en recycle containers tijdens security-patches, niet alleen daarna. Het inschakelen van block zeroing beschermt alleen nieuwe toewijzingen. Bestaande toewijzingen in actieve containers en gecachte image-snapshots moeten expliciet worden gewist. Volg de fix altijd op met een fleet-wide recycle.

  5. Versleutel gevoelige spelersdata at rest met extern sleutelbeheer. Als een restblok lekt, is cijfertekst zonder de sleutel nutteloos. Spelersopslagstanden die via horizOn's accountgebonden Cloud Save worden beheerd, gebruiken revisiebewuste schrijfbewerkingen en client-side conflictresolutie — maar de onderliggende disk-isolatie blijft jouw verantwoordelijkheid als je containers zelf host.

Tijdlijn: hoe Cloudflare reageerde

Ter referentie: zo snel kan een goed uitgerust team bewegen bij een kritieke infrastructuurkwetsbaarheid:

Tijd (UTC) Actie
4 sep, 15:26 Onderzoeker meldt via HackerOne
4 sep, 18:45 Security-incident geopend, productiesetup bevestigd
4 sep, 21:27 Runtime-fix samengevoegd met hergebruiktest
4 sep, 23:15 Rollout begint
7 sep, 06:13 Rollout voltooid, opschoning begint
14 sep, 10:50 Onderzoeker bevestigt dat PoC niet meer werkt
19 sep, 15:03 Alle pre-mitigatie gecachte snapshots gewist

Dat is minder dan 3 uur van rapport tot bevestigde grondoorzaak, en minder dan 8 uur tot een samengevoegde fix. De volledige fleet-opschoning duurde 15 dagen — normaal voor rolling-operations over een wereldwijde infrastructuur.

De snelheid doet ertoe. Elk uur tussen een kwetsbaarheidsrapport en een fix is een uur waarin uitbuiting mogelijk is. Als je je eigen container-infra beheert, bouw het runbook voordat je het nodig hebt.

Conclusie

De Cloudflare-containerkwetsbaarheid was geen zero-day in cryptografische zin. Het was een bekende opslagconfiguratie-afweging — prestaties boven security — die ongepast was voor multi-tenant workloads. De fix was één configuratiewijziging plus een fleet-recycle.

Als je game-backends draait op gedeelde containerinfrastructuur, audit dan vandaag nog je dm-thin-pools. Draai het detectiescript tegen je I/O-telemetrie. En als je liever niet zelf containerdisk-isolatie, fleet-recycling en I/O-telemetriepipelines beheert, regelt horizOn authenticatie, crashrapporten, cloud save, leaderboards en de andere backenddiensten die je spel nodig heeft — zodat jij je kunt richten op het uitbrengen van je spel, in plaats van om 3 uur 's nachts storage-layer-security in productie te debuggen.


Bron: Hoe Cloudflare een cross-tenant data-exposure-kwetsbaarheid in Containers aanpakte