Volver al Blog

Lo que la vulnerabilidad de contenedores de Cloudflare nos enseñó sobre la seguridad de backends de juegos multi-tenant

Publicado el 25 de septiembre de 2026
Lo que la vulnerabilidad de contenedores de Cloudflare nos enseñó sobre la seguridad de backends de juegos multi-tenant Generado con ayuda de IA

En resumen

Aprende cómo la vulnerabilidad de contenedores de Cloudflare afecta la seguridad de backends de juegos multi-tenant y cómo proteger a tus jugadores.

Tu backend de juegos containerizado levanta una VM nueva para cada partida, cada lobby, cada lote de analíticas. Cuando ese contenedor se apaga, los datos desaparecen, ¿verdad?

No necesariamente. En septiembre de 2026, el investigador de seguridad Oren Yomtov de Accomplish descubrió que Cloudflare Containers tenía una vulnerabilidad de exposición de datos cross-tenant. Los bloques de disco residuales de contenedores destruidos eran legibles por cargas de trabajo no relacionadas en el mismo host. Los datos recuperados incluían estructuras de directorios, páginas de bases de datos y bases de datos SQLite estructuralmente completas.

Si ejecutas backends de juegos en cualquier plataforma de contenedores multi-tenant, necesitas entender la causa raíz de esta vulnerabilidad: una configuración de almacenamiento de Linux llamada skip_block_zeroing en el subsistema dm-thin. Este post desglosa lo que salió mal, proporciona un runbook de detección y remediación que puedes aplicar hoy, y cubre los principios arquitectónicos que evitan que esta clase de vulnerabilidad llegue a los datos de tus jugadores.

Cómo el aprovisionamiento thin crea exposición de datos cross-tenant

Cloudflare Containers ejecuta cargas de trabajo dentro de micro-VMs de Firecracker en hardware multi-tenant. Cada contenedor obtiene un disco raíz escribible respaldado por thin provisioning de Linux (dm-thin). Firecracker expone este disco a la VM invitada como /dev/vdc.

El aprovisionamiento thin funciona difiriendo la asignación de almacenamiento físico. Cuando creas un disco virtual de 10 GiB, consume casi nada de espacio físico. Los bloques se asignan bajo demanda desde un pool compartido cuando el invitado escribe en una región previamente no mapeada. Los pools afectados de Cloudflare usaban un tamaño de bloque thin de 64 KiB.

Aquí es donde vive la vulnerabilidad. Cuando un contenedor se destruye, sus mapeos de volumen thin se eliminan y los bloques físicos vuelven al pool compartido para reutilizarse. El pool afectado estaba configurado con:

skip_block_zeroing

Con esta bandera, dm-thin omite el zeroing de los bloques recién asignados antes de hacerlos accesibles al siguiente inquilino. Una escritura completa de 64 KiB sobrescribe todo el bloque reciclado. Pero una escritura parcial — por ejemplo, de 4 KiB — reemplaza solo esa región. Los 60 KiB restantes pueden retener datos del propietario anterior del bloque.

Esto no es un caso límite teórico. El investigador recuperó estructuras de directorios, páginas de bases de datos y bases de datos SQLite completas de bloques residuales en 18 de 24 ubicaciones de producción y 20 de 22 nodos subyacentes en cuatro continentes.

Por qué las escrituras parciales son el vector clave

Leer una región no mapeada de un disco thin nuevo devuelve ceros: dm-thin sirve ceros sin asignar un bloque físico. Eso es seguro. El exploit funciona porque una escritura pequeña dispara la asignación de un bloque reciclado de 64 KiB sin hacerle zeroing primero.

El patrón de ataque:

  1. Crear un contenedor en una cuenta de Workers Paid.
  2. Abrir /dev/vdc (el disco raíz escribible).
  3. Identificar regiones alineadas a 64 KiB correspondientes al espacio libre de ext4.
  4. Escribir un bloque alineado de 4 KiB en cada región objetivo.
  5. Leer de vuelta los bloques completos de 64 KiB.
  6. Examinar solo los 60 KiB que el atacante no sobrescribió.

El paso 4 es el momento crítico. La escritura de 4 KiB hace que dm-thin asigne un bloque físico del pool compartido. Como el zeroing está deshabilitado, los 60 KiB restantes pueden contener datos residuales de quien haya sido el dueño anterior de ese bloque.

Qué validó realmente el investigador

Los investigadores usaron checksums de bloques de directorio de ext4 (la característica metadata_csum) para distinguir sus propios bloques de sistema de archivos de prueba de los bloques ajenos. En seis ubicaciones de producción:

  • 5,614 bloques de directorio comprobables examinados
  • 0 bloques atribuidos al propio sistema de archivos de los investigadores
  • 2,700 inodos de directorio ajenos distintos identificados mediante análisis de checksums

Los tipos de archivo recuperados no eran basura. Incluían bases de datos SQLite estructuralmente significativas: el tipo de datos que, en un contexto de backend de juegos, podría contener estados de guardado de jugadores, tokens de sesión o registros de inventario.

Runbook de detección: cómo encontrar la recolección de datos residuales en tu infraestructura

Si operas backends de juegos containerizados en infraestructura multi-tenant, necesitas dos capas de detección: auditoría de configuración y análisis de anomalías de I/O en tiempo de ejecución.

Paso 1: Audita tu configuración de dm-thin

Ejecuta este script en cada host que ejecute tus contenedores:

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

Si skip_block_zeroing aparece en cualquier parte de tu configuración de pools, tienes la misma clase de vulnerabilidad que Cloudflare parcheó. Arrégialo de inmediato: no esperes a una ventana de mantenimiento programada.

Paso 2: Monitorea la firma de I/O característica

El exploit produce una firma detectable: escrituras pequeñas seguidas de lecturas desproporcionadamente grandes. Una escritura de 4 KiB asigna un bloque de 64 KiB; una lectura posterior recupera los 64 KiB completos. La telemetría de contenedores donde el volumen de lectura supera al de escritura en >10x es sospechosa.

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

En el incidente de Cloudflare, la prueba de concepto de los investigadores produjo exactamente esta firma. El equipo de seguridad de Cloudflare creó firmas de detección a partir del PoC, las aplicó a la telemetría histórica de I/O de disco y confirmó que la única actividad coincidente provenía de los investigadores y de los propios ingenieros de Cloudflare durante la validación autorizada. No se encontró explotación por parte de terceros.

La lección: si estás recopilando telemetría de I/O de contenedores, puedes auditar de forma retroactiva. Si no la recopilas, estás volando a ciegas.

Runbook de remediación

Si tu auditoría revela skip_block_zeroing o una exposición similar, esta es la secuencia de remediación que ejecutó Cloudflare, y el orden importa.

Paso 1: Vuelve a habilitar el zeroing de bloques en todos los pools

Elimina skip_block_zeroing de tu configuración de pools dm-thin. Este es un cambio en tiempo de ejecución en el dispositivo thin-pool, pero solo protege nuevas asignaciones. Los bloques ya mapeados en contenedores en ejecución y las instantáneas de imágenes en caché siguen siendo vulnerables.

# 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 integró esta corrección en aproximadamente 6 horas desde el informe inicial. Los investigadores confirmaron de forma independiente que el PoC dejó de funcionar después de este cambio.

Paso 2: Retira todos los discos de contenedores en ejecución

Los bloques ya mapeados en dispositivos thin activos conservan su contenido (sin zeroing). La única forma de eliminar los datos residuales es destruir y recrear cada disco de contenedor.

Cloudflare drenó los hosts durante horas de baja demanda y reinició las VMs en cada host. Esta es una operación continua: para un backend de juegos, prográmala durante tu ventana de menor tráfico. Espera una breve interrupción por host. Si tu capa de matchmaking admite una transferencia sin interrupción (graceful handoff), los jugadores en hosts en drenaje se migran en lugar de desconectarse.

Paso 3: Limpia las instantáneas de imágenes en caché

Este paso es fácil de pasar por alto. Las capas de orquestación de contenedores suelen cachear las capas de imágenes OCI como instantáneas dm-thin para acelerar el arranque de contenedores. Estas instantáneas en caché se crearon antes de la corrección y pueden contener datos residuales en regiones no utilizadas (incluido el espacio libre de ext4). Un contenedor nuevo que hereda una capa en caché puede leer bytes residuales de /dev/vdc sin asignar nunca un bloque nuevo.

Cloudflare eliminó todas las instantáneas en caché previas a la mitigación en toda la flota afectada, completando la limpieza 15 días después del informe inicial. Las capas en caché se recrearon con asignaciones con zeroing.

El orden de recreación importa. Si limpias las cachés antes de habilitar el zeroing, acabas de empujar bloques sin zeroing de vuelta a las instantáneas nuevas.

Paso 4: Valida que la corrección se mantenga

Después de la remediación, ejecuta el script de detección contra la nueva telemetría de I/O durante al menos 48 horas. Compara las proporciones de escritura/lectura con tu línea base previa al parche. El patrón característico de alta proporción debería desaparecer por completo.

También debes verificar la tabla dm-thin en cada host después del reinicio:

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

Principios arquitectónicos para prevenir esta clase de vulnerabilidad

El incidente de Cloudflare es un ejemplo de una clase más amplia de fallos de aislamiento de almacenamiento multi-tenant. Estos son los principios que protegen tu backend de juegos, ya sea que administres tus propios contenedores o dependas de una plataforma.

Principio 1: Nunca deshabilites el zeroing de bloques en pools multi-tenant

La bandera skip_block_zeroing existe por rendimiento: hacer zeroing de bloques de 64 KiB en cada asignación añade sobrecarga de I/O. En hardware single-tenant donde solo se ejecuta tu código, la compensación puede ser aceptable. En infraestructura compartida donde los contenedores se reciclan entre inquilinos, es un defecto de seguridad.

Regla: Cualquier pool dm-thin que asigne bloques a más de una identidad de inquilino debe tener el zeroing de bloques habilitado. Aplícalo en la capa de infraestructura como código para que ningún host pueda aprovisionarse sin él.

Principio 2: Trata los discos de contenedores como efímeros y peligrosos

Tu backend de juegos nunca debería asumir que el disco de un contenedor está limpio al arrancar. Incluso con el zeroing de bloques habilitado, hay casos límite con capas de caché, herencia de instantáneas y bugs del kernel.

Escribe el entrypoint de tu contenedor para formatear o limpiar el volumen escribible al arrancar:

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

Esto añade unos segundos al arranque del contenedor. Para una partida que dura de 5 a 45 minutos, es aceptable. Para requisitos de cold-start de menos de un segundo, usa el enfoque más rápido: blkdiscard (que dispara TRIM y puede hacer zeroing de bloques dependiendo del backend de almacenamiento) combinado con mkfs.ext4 -F.

Principio 3: Monitorea las proporciones de I/O como señal de seguridad

La mayoría de la monitorización de contenedores se centra en CPU, memoria y red. Añade I/O de disco a tu telemetría de seguridad. La proporción de bytes leídos frente a bytes escritos es una señal fuerte de ataques de recolección de datos residuales: las cargas de trabajo legítimas de juegos escriben y leen en patrones equilibrados. Un contenedor que escribe fragmentos de 4 KiB y luego lee 60+ KiB por región es anómalo.

Si tu backend ya registra eventos del ciclo de vida de los contenedores, puedes correlacionar las anomalías de I/O con la identidad del inquilino y las marcas de tiempo de creación para reducir el radio de explosión de un posible incidente. Para desarrolladores de juegos que quieren informes de fallos y registros de usuario integrados sin gestionar ellos mismos esta infraestructura de telemetría, plataformas como horizOn ofrecen estas capacidades listas para usar: menos tiempo construyendo infraestructura y más tiempo en la lógica del juego.

Principio 4: Implementa defensa en profundidad para los datos de los jugadores

Incluso si el disco de tu contenedor filtra datos, el daño es limitado si los datos están cifrados o no significan nada sin una clave. Almacena los estados de guardado de los jugadores, los tokens de sesión y los datos de inventario cifrados en reposo. La clave de cifrado debe vivir en un gestor de secretos, no en el disco del contenedor.

Este es el mismo principio que cubrimos en nuestro análisis de la filtración de datos de Star Citizen y cómo arquitecturar backends de juegos para sobrevivir a compromisos: la defensa en profundidad significa asumir que cualquier capa individual acabará fallando.

Lista de verificación de mejores prácticas

  1. Audita cada configuración de pool dm-thin antes del despliegue. Añade una comprobación previa al vuelo (pre-flight) a tu pipeline de orquestación de contenedores que falle si skip_block_zeroing está presente. Es una comprobación de CI de cinco líneas que previene una clase entera de vulnerabilidades.

  2. Recopila telemetría de I/O de disco de contenedores con atribución al inquilino. No puedes detectar una explotación que no observas. Registra los volúmenes de escritura/lectura por contenedor, correlacionados con el ID de inquilino y el ID de host. Conserva al menos 30 días para investigación retroactiva.

  3. Haz zeroing o limpia los volúmenes escribibles de los contenedores al arrancar. No confíes solo en la capa de almacenamiento. Un mkfs.ext4 nuevo al inicio añade 2–5 GiB de sobrecarga de escritura, pero garantiza que ningún dato residual sobreviva al reciclaje del contenedor.

  4. Drena y recicla los contenedores durante los parches de seguridad, no solo después. Habilitar el zeroing de bloques solo protege nuevas asignaciones. Los mapeos existentes en contenedores en ejecución y las instantáneas de imágenes en caché deben limpiarse explícitamente. Siempre sigue la corrección con un reciclaje en toda la flota.

  5. Cifra los datos sensibles de los jugadores en reposo con gestión externa de claves. Si un bloque residual se filtra, el texto cifrado sin la clave es inútil. Los estados de guardado gestionados a través del Cloud Save de horizOn vinculado a la cuenta usan escrituras con control de revisiones y resolución de conflictos en el cliente, pero el aislamiento de disco subyacente sigue siendo tu responsabilidad si auto-alojas contenedores.

Cronología: cómo respondió Cloudflare

Como referencia, así de rápido puede moverse un equipo con buenos recursos ante una vulnerabilidad crítica de infraestructura:

Hora (UTC) Acción
Sep 4, 15:26 El investigador reporta a través de HackerOne
Sep 4, 18:45 Se abre el incidente de seguridad, se confirma la configuración de producción
Sep 4, 21:27 Se integra la corrección en runtime con prueba de reutilización
Sep 4, 23:15 Comienza el despliegue
Sep 7, 06:13 Despliegue completado, comienza la limpieza
Sep 14, 10:50 El investigador confirma que el PoC ya no funciona
Sep 19, 15:03 Todas las instantáneas en caché previas a la mitigación eliminadas

Eso es menos de 3 horas desde el informe hasta la confirmación de la causa raíz, y menos de 8 horas hasta una corrección integrada. La limpieza completa de la flota tomó 15 días: algo normal para operaciones continuas en una infraestructura global.

La velocidad importa. Cada hora entre un informe de vulnerabilidad y una corrección es una hora en la que la explotación es posible. Si operas tu propia infraestructura de contenedores, construye el runbook antes de necesitarlo.

Conclusión final

La vulnerabilidad de contenedores de Cloudflare no fue un zero-day en el sentido criptográfico. Fue una compensación de configuración de almacenamiento conocida — rendimiento sobre seguridad — que era inapropiada para cargas de trabajo multi-tenant. La corrección fue un único cambio de configuración más un reciclaje de flota.

Si ejecutas backends de juegos en infraestructura de contenedores compartida, audita tus pools dm-thin hoy. Ejecuta el script de detección contra tu telemetría de I/O. Y si prefieres no gestionar tú mismo el aislamiento de discos de contenedores, el reciclaje de flotas y las tuberías de telemetría de I/O, horizOn gestiona autenticación, informes de fallos, cloud save, leaderboards y los demás servicios backend que tu juego necesita, para que puedas centrarte en lanzar tu juego, no en depurar la seguridad de la capa de almacenamiento en producción a las 3 AM.


Fuente: Cómo Cloudflare abordó una vulnerabilidad de exposición de datos cross-tenant en Containers