Docker Sandbox Kit Spec v3: la autoridad de un agente, versionada como una imagen
En mi homelab llevo meses granting permisos a agentes uno por uno: un bind mount aquí, un token con más scope del que la tarea necesita allá, una regla de firewall que abrí porque era más rápido que afinarla. Cada decisión tenía sentido por separado. En conjunto me devolvieron el aislamiento que creía tener, y ninguno de los agujeros requirió un exploit: los fui abriendo yo, a propósito, casi siempre con prisa.
El problema no es que esos permisos sean malos. Es que nadie los escribió en ningún lado. Viven en el historial de bash, en un dashboard y en mi memoria. No puedo pasárselos a un colega, no puedo diffearlos contra los de la semana pasada y no puedo responder la pregunta más básica que puede hacerle un jefe de seguridad: ¿qué tiene permitido hacer este agente?
Docker acaba de publicar la especificación que responde exactamente eso, y además la está llevando a la CNCF para que no sea un formato propietario. El Sandboxes Kit Spec v3 convierte la autoridad de un agente en un artefacto versionado que se construye, se escanea, se firma y se pinea por digest, igual que una imagen de contenedor. Ese es el tema de este post: no la nube, no el precio por segundo, sino cómo se hace reproducible el permiso cuando el software que lo pide decide en tiempo de ejecución qué hacer.
TL;DR — Lo que vas a tener al final
- Tiempo de setup: 10 minutos para correr el ejemplo del repo oficial y leer tu primer descriptor.
- Requisitos:
sbx0.45.1 o superior (brew o binario Linux), cuenta de Docker y Docker Engine para construir el Kit. - Resultado: un Kit que es una imagen OCI normal, con sus permisos declarados y visibles en el diff.
- Troubleshooting: sin runtime conforme la anotación es inerte; si compones el workload con su propio mixin, el resolutor falla.
Por qué un Dockerfile no alcanza para un agente
Vale la pena partir de la diferencia de fondo, porque es lo que justifica que exista un formato nuevo. Un contenedor empaqueta software: comparte el kernel del host y usa namespaces y cgroups para darle a una carga fija su propia vista del filesystem, la red y los procesos. Es la herramienta correcta para un programa que arranca, hace su trabajo y toca solo lo que le pasaron.
Un agente es otra cosa. Es un actor probabilístico: decide qué hacer a continuación y lo hace contra tu filesystem, tu red, tus credenciales y tu cuenta en la nube. Va a instalar un paquete que necesita root, va a abrir un puerto que nadie planeó y, cuando lo bloquean, va a intentar la siguiente vía. Un contenedor no se construyó para eso, porque el límite es el mismo kernel que el actor está presionando.
Ahí entra el sandbox: una microVM con su propio kernel, de modo que el límite queda por debajo de cualquier cosa que el modelo pueda alcanzar o reescribir. Dentro de ese límite sí puedo darle root al agente y soltarlo, porque el daño se detiene en la frontera. Christian Dupius, que escribió la especificación, lo resume así: los contenedores empaquetan aplicaciones, los sandboxes contienen agentes.
Pero un sandbox vacío no es un entorno. Algo tiene que decir qué agente corre ahí, qué herramientas recibe, qué servidores MCP ve, qué skills lo guían y, sobre todo, qué puede tocar. Eso es un Kit.
Un Kit es una imagen OCI, nada más
Este es el cambio de fondo de la versión 3 y lo que hace la especificación interesante: un Kit dejó de ser un artefacto propio con su propio media type y su propio sidecar. Ahora es una imagen OCI ordinaria. Sin media type nuevo, sin archivo aparte, sin nada que un registry tenga que aprender.
El manifiesto carga las declaraciones en una sola anotación, vnd.docker.sandbox.kit.descriptor, y las capas llevan el contenido. La consecuencia práctica es que el camino de distribución y de herramientas te lo regalan: construyes con docker buildx build, descargas con docker pull, escaneas y firmas con lo que ya corres, y funciona dentro de un FROM.
Y aquí está el punto que más me importa como sysadmin: pinear el digest pinea el contenido, las declaraciones y los metadatos juntos. Antes, el permiso vivía al lado de la imagen y se desincronizaba. Ahora viaja dentro.
Existen dos tipos de Kit. Un workload corre y aporta el filesystem raíz. Un mixin es una capa encima: una CLI con su regla de red, una binding de credencial, contexto para el agente. Levantas un solo workload y los mixins que necesites encima.
Leer el permiso como quien lee una nota de permisos
Esto es lo que mejor funciona de todo el diseño, y vale la pena verlo con calma. Este fragmento es real, del mixin de la CLI de GitHub en el repositorio oficial de la especificación:
capabilities:
- type: com.docker.sandbox/network-policy@2
config:
runtime:
allow:
- hosts: [api.github.com]
methods: [GET, HEAD, POST, PATCH, PUT, DELETE]
deny:
- hosts: [api.github.com]
methods: [DELETE]
paths: [/repos/**]
- type: com.docker.sandbox/credential@1
optional: true
config:
service: github
phase: runtime
apiKey:
name: GH_TOKEN
proxyManaged: true
inject:
- {domain: api.github.com, header: Authorization, format: "Bearer %s"}
Esto pide alcanzar GitHub y nada más, y pide casi toda la API pero no los borrados bajo /repos/**, porque deny gana. El token que abre un pull request no puede borrar el repositorio. Fíjate en que el borrado aparece en las dos listas: está permitido en general y denegado específicamente sobre las rutas peligrosas. La denegación gana, siempre.
Y fíjate también en proxyManaged: true. La credencial es gestionada por proxy: un runtime conforme inyecta el valor real en las peticiones hacia los dominios nombrados, y dentro del sandbox solo hay un centinela. El agente nunca vio la llave. Eso es lo que cierra de verdad la puerta al prompt injection sobre secretos: una inyección no puede robar lo que el agente nunca tuvo.
# Clonar el spec y entrar a los ejemplos oficiales
git clone https://github.com/docker/sandbox-kit-spec.git
cd sandbox-kit-spec/examples
# Lanzar el workload de ejemplo con el mixin de GitHub encima
sbx run ./hello --kit ./gh
Si editas un descriptor, solo ese Kit se reconstruye, y docker buildx build lo publica a cualquier registry que tengas. Cloud Sandboxes corre los mismos Kits con el mismo modelo de confianza, sobre capacidad elástica.
Para correr esto en tu servidor Linux el cliente se instala sin Docker Desktop, igual que los sandboxes locales:
# Instalar el cliente sbx y autenticarte
brew install docker/tap/sbx
sbx login
# Lanzar un workload con su mixin de permisos encima
sbx run ./hello --kit ./gh
# Listar los Kits disponibles y ver qué declara cada uno
sbx kit list
sbx kit inspect ./gh
Si no usas macOS, el mismo release está como binario en el repo docker/sbx-releases. Necesitas la versión 0.45.1 o superior.
El diff es la revisión
Este es el argumento que más me cierra, porque es el que se parece a lo que ya hago con código. El descriptor del Kit de Claude Code declara los hosts a los que pide llegar, su credencial, los volúmenes que persisten entre sesiones y sus hooks de instalación y arranque. Cuando la siguiente versión pide un host nuevo o una segunda credencial, eso ya no es una actualización de software: es un cambio de autoridad, y aparece en el pull request como líneas agregadas que un humano puede rechazar.
La especificación define una segunda puerta que no depende de que alguien lea el diff. Cada descriptor se reduce a un conjunto normalizado de todo lo que el host tendría que conceder; un runtime que controla actualizaciones registra ese conjunto y compara la versión nueva contra él. Una versión que se mantiene dentro de lo concedido se aplica sin preguntar. Cualquier ampliación se detiene y pregunta. Y quitar una regla de denegación también cuenta como ampliación: si un Kit posterior de gh dejara de declarar DELETE /repos/**, el runtime retiene la actualización.
Ese detalle es el que demuestra que las declaraciones tenían que vivir dentro del artefacto y no al lado. Si estuvieran afuera, el runtime no tendría forma de saber qué cambió respecto a la versión anterior.
Composición es una función, no una secuencia
Las imágenes de contenedor nunca resolvieron la herencia múltiple: un stage de Dockerfile tiene un solo FROM. Los mixins se ordenan por el grafo de dependencias que los Kits declaran a través de provides y requires, nunca por el orden en que escribiste los flags, así que el mismo conjunto siempre compone a la misma imagen.
El resolutor es estricto a propósito, y sus reglas son las que te ahorran sorpresas a las 2 de la mañana:
- Todo
requiresse satisface desde dentro del conjunto o la resolución falla. No se descarga nada para tapar un hueco. - Se permite exactamente un workload. Dos Kits que proporcionan el mismo nombre fallan en vez de que uno tape al otro en silencio.
- Donde hay solapamiento, las declaraciones se reconcilian: las reglas de red hacen unión, los hooks corren en orden de dependencia, las licencias hacen unión. Las peticiones incompatibles son un error, no una moneda al aire.
Un descriptor kind: set nombra otros Kits; al publicarlo se ejecutan las mismas reglas de coherencia en tiempo de build y se fusiona en un Kit ordinario. Un conjunto incoherente falla en tu build, no en el launch de otra persona.
La ventaja práctica para un sysadmin es que la resolución es determinista. El mismo directorio con los mismos descriptores produce la misma imagen, sin importar el orden en que hayas escrito los flags:
# Construir el conjunto completo de una vez
docker buildx build -t mi-registry/mi-agent-kit:v1 ./mi-kit
# Fallar rápido si el conjunto es incoherente, antes de publicar
docker buildx build --check ./mi-kit
Ese --check es la analogía directa de un linter: si dos Kits declaran el mismo provides, o si un requires no está satisfecho dentro del conjunto, el build se detiene ahí y no cuando alguien más intenta lanzar el sandbox.
Para comparar la autoridad de dos versiones del mismo Kit —que es la verificación que de verdad quieres hacer antes de actualizar un agente en producción— el diff del descriptor es el objeto que revisas:
# Traer las dos versiones y dejar solo el descriptor para comparar
for v in v1 v2; do
docker pull mi-registry/mi-kit:$v
skopeo inspect "docker://mi-registry/mi-kit:$v"
| python3 -c "import sys,json; print(json.load(sys.stdin)['Labels']['vnd.docker.sandbox.kit.descriptor'])"
> /tmp/kit-$v.json
done
# El diff que hay que leer: todo lo que se agrega es Ampliacion
diff -u /tmp/kit-v1.json /tmp/kit-v2.json
Si en ese diff aparece un host nuevo, una credencial adicional o desaparece una regla deny, no estás mirando una actualización de software: estás mirando un cambio de autoridad, y ese es exactamente el diff que un revisor debería bloquear por defecto.
Por qué es una especificación y no una funcionalidad
Si un Kit dejara de significar algo al correr en otro lado, eso no sería una frontera de confianza: sería lock-in. Por eso la gramática es normativa, cada tipo de capacidad tiene su propia página describiendo qué debe implementar un runtime conforme, y los tipos versionan de forma independiente: hoy coexisten network-policy@1 y network-policy@2.
Se envían dos suites de conformidad: una juzga si un artefacto es un Kit conforme, la otra si un runtime se comporta como las páginas dicen. Cada enunciado normativo está cubierto por una comprobación o por una renuncia escrita.
Y aquí está la razón por la que importa a un sysadmin aunque no seas usuario de Docker: Docker mantiene la especificación hoy, y su propia explicación es que no debería quedarse bajo un solo vendor. Un formato para decidir qué puede hacer un agente vale menos si pertenece a quien te vende el runtime. Docker Sandboxes va a ser la primera implementación de primera clase, no la única.
# Ver qué pide realmente un Kit ya publicado, sin ejecutarlo
docker buildx imagetools inspect ghcr.io/docker/sandbox-kit-gh:latest
# Extraer la anotación del descriptor desde la config de la imagen
skopeo inspect docker://ghcr.io/docker/sandbox-kit-gh:latest
| python3 -c "import sys,json; d=json.load(sys.stdin);
print(json.dumps(d.get('Labels',{}).get('vnd.docker.sandbox.kit.descriptor','SIN DESCRIPTOR'),indent=2))"
# Construir tu propio Kit y pinearlo por digest
docker buildx build -t mi-registry/mi-kit:v1 ./mi-kit
docker buildx imagetools inspect mi-registry/mi-kit:v1 --format '{{.manifest.digest}}' | tee /tmp/kit-digest.txt
Lo que aporta el anuncio de WeAreDevelopers
Docker presentó este material en WeAreDevelopers World Congress North America, en San Jose, entre el 23 y el 25 de septiembre, con más de 10.000 asistentes. El contexto importa porque el anuncio no fue solo técnico: hubo tres keynotes que ponen la pieza de infraestructura en su lugar.
Mark Cavage, presidente y COO de Docker, planteó el problema con un ejemplo que duele porque cualquiera que haya montado un bot lo ha vivido: un agente corriendo en un contenedor con el socket de Docker del host montado encontró una forma de leer un secreto del host. No descubrió una vulnerabilidad nueva: estaba usando el acceso que la configuración le había dado. Esa misma línea de código, dentro del sandbox, falló.
Ahí está el matiz que los blogs de anuncio suelen pasar por alto y que me parece el más honesto del discurso: Cavage también señaló los límites de esos controles. Bloquear un destino de red no aprobado es distinto de reconocer que un correo que sí está permitido vaya dirigido al cliente equivocado. Eso sigue sin resolverse. Permisos estrechos, como dejar que un agente lea y redacte mensajes sin poder enviarlos, siguen siendo parte esencial de usar agentes con responsabilidad.
Tushar Jain, CTO, mostró en su keynote una petición de borrado de un repositorio de GitHub bloqueada por una regla de default-deny: la operación devolvió HTTP 403 y la decisión quedó registrada. En el panel de preguntas, Jain reconoció que la identidad del agente sigue siendo un reto abierto para toda la industria.
Un detalle adicional que me gustó: Nous Research subió al escenario con Hermes empaquetado como Kit, como partner de lanzamiento. Es la demostración práctica del argumento de fondo, que es la libertad de elegir el agente. Un agente desarrollado por terceros, empaquetado en el formato común, corriendo sobre una base de aislamiento que no tuviste que reconstruir.
Vale la pena?
Para un homelab como el mío, la respuesta honesta es: el sandbox ya me servía, el Kit me resuelve un problema que no sabía nombrar. No es seguridad nueva, es reproducibilidad. Antes de esta especificación, la respuesta a «qué tiene permitido hacer este agente» la tenía yo, y no por diseño sino por costumbre acumulada.
También hay que decir lo incómodo. La gobernanza centralizada para empresas todavía no está: el propio anuncio dice que llega pronto a través de Docker AI Governance. Los kits oficiales cubren Claude Code, Codex, Copilot, Antigravity, Open Code y Hermes, y hay kits del ecosistema para AWS, Box, Datadog, Dynatrace, JFrog, NanoClaw, OpenClaw, Palo Alto Networks y Snyk, pero la cobertura real depende de quién mantenga qué. Y la aceptación del formato por parte de la CNCF es un compromiso, no un hecho consumado: la gobernanza neutral empieza con la presentación formal.
Mi recomendación concreta: si ya corres agentes en sandboxes, no cambies nada hoy, pero empieza a mover tus permisos hacia descriptores versionados. El día que llegue otro runtime que implemente el spec, te ahorra el trabajo. Y si hoy dependes de docker run con un socket montado para que el agente alcance lo que necesita, esa es exactamente la configuración que el ejemplo de Cavage mostró como el problema.
Preguntas frecuentes
¿Qué diferencia hay entre un Kit y una imagen de contenedor normal?
Casi ninguna en la distribución: un Kit ES una imagen OCI ordinaria, con la diferencia de que su manifiesto lleva una anotación vnd.docker.sandbox.kit.descriptor donde declara lo que pide. La imagen normal declara cómo se construye y arranca el software; el Kit declara además a qué puede llegar. Por eso se construye, escanea y firma con las mismas herramientas.
¿Qué pasa si uso un Kit en un runtime que no implementa la especificación?
La anotación queda inerte: es una imagen y nada más, sin ejecución. La imagen arranca y el agente corre, pero las peticiones declaradas no se aplican. Es el punto ciego más importante de todo el diseño, y por eso conviene verificar que tu runtime es conforme antes de confiar en las restricciones de un Kit.
¿Por qué deny gana sobre allow?
Por seguridad en la Intersección. El patrón más común es que un mixin de nivel superior abra un host con todos los métodos y el mixin específico lo restrinja. Si el allow ganara, bastaría con componer un mixin que abra api.github.com con todo para tumbar las restricciones de otro Kit. Con deny ganando, ampliar un permiso tiene que ser explícito y visible.
¿Puedo usar un Kit con un agente propio que no esté en la lista oficial?
Sí. La especificación es abierta y el formato es OCI, así que puedes construir tu propio Kit declarando provides y requires. El repo oficial trae ejemplos y la comunidad publica kits para herramientas de terceros. El punto es precisamente ese: el formato es del ecosistema, no de Docker.
¿Cómo evito que una actualización de Kit amplíe mis permisos sin que me entere?
revisa el diff del descriptor, porque una host nuevo o una credencial adicional aparece como líneas agregadas y es un cambio de autoridad, no una actualización de software. La segunda capa es el gating del runtime: si tu runtime registra el conjunto de permisos concedido y compara contra la versión nueva, cualquier ampliación detiene la actualización, y quitar un deny también cuenta como ampliación.
