🛡️ sandbox-labs GitHub ↗

Referencia de políticas#

Enforcement#

  • strict: falla cerrado si falta cualquier requiredControl.
  • best-effort: ejecuta y registra controles no soportados.

Controles válidos: filesystem, network, processes, memory, cpu, timeout, capabilities, syscalls, devices, environment, output.

Filesystem#

  • root: ephemeral, host-readonly o custom.
  • readOnly: rutas esperadas como solo lectura.
  • writable: rutas con escritura explícita.
  • maxWorkspaceMb: cuota lógica; requiere implementación del runtime para considerarse efectiva.
  • followSymlinks: debe permanecer false para cargas no confiables.

Network#

Solo dos de los cuatro modos crean un namespace de red propio. Esa es la línea que decide si el control network puede declararse efectivo:

modeNamespace propioSalida al exteriorPuerto TCP publicableControl network
nonesínonoefectivo
loopbacksínono — solo socket Unixefectivo
allowlistsísolo por el canal explícitonoefectivo
unrestrictednosísínunca efectivo
  • none: sin interfaz de red utilizable hacia fuera.
  • loopback: la carga habla consigo misma dentro de su propio namespace. Un

servicio con este modo no puede enlazar él mismo un puerto del host: el que abra nace dentro del sandbox. Sí puede publicarlo el supervisor por él —ver publish: proxy más abajo—, y si no hace ninguna de las dos cosas, sandboxctl service up falla en cerrado explicando ambas salidas.

  • allowlist: la carga no tiene red. Lo único que cruza la frontera es un

socket Unix por el que pide CONNECT host:puerto, y un proxy del supervisor compara con hosts, abre o rechaza con 403, y registra todos los intentos en networkEvents de la evidencia.

Tres cosas que conviene saber antes de usarlo:

- La salida es una capacidad, no una propiedad del entorno. Un cliente HTTP corriente no usa el canal solo: hay que abrir el socket a propósito. La ruta llega en SANDBOX_EGRESS_SOCKET. - No hay comodines. *.ejemplo.com parece cómodo y es cómo una lista de permitidos deja de serlo: basta un subdominio ajeno para atravesarla. - unshare no monta el canal, así que con él allowlist deja a la carga sin salida ninguna.

Detalle y medición en B-04.

  • unrestricted: la red del host, escrito con todas sus letras. Ninguna

política que lo use puede exigir el control network, porque ahí no hay ninguno que exigir.

Un servicio puede contener la red y seguir publicando un puerto#

Parecen incompatibles y no lo son. La clave es separar cómo escucha el servicio (transport, en su manifiesto) de cómo llega el host (publish):

publishQuién enlaza el puerto del hostnetwork puede ser
directel propio servicioallowlist o unrestricted
proxyel supervisor, empalmando con el socket Unix del sandboxnone o loopback
nonenadie: la única puerta es el socketnone o loopback

Con proxy, sandboxctl service up levanta un reenviador que escucha en

El servicio sigue hablando HTTP y se abre en el navegador igual, pero corre en un namespace de red propio y no tiene por dónde salir.

Es lo que usa service-isolated, ahora en modo strict: si bubblewrap, cgroups o seccomp no están disponibles, el servicio no cae a un runtime más débil sino que bloquea el arranque. service-sandbox y web-application se quedan en

Recursos#

El timeout y el límite de salida los aplica el supervisor común a las cargas que terminan. Los servicios persistentes no los declaran como controles efectivos; sus políticas estrictas exigen en cambio los controles del kernel que sí se mantienen durante toda su vida.

El mecanismo es systemd-run --user --scope, que envuelve el árbol entero —scope → prlimit → bwrap → carga— y traduce la política así:

Campo de la políticaPropiedad del scopeFichero del kernel
memoryMbMemoryMax=<n>Mmemory.max
processesTasksMax=<n>pids.max
cpuCPUQuota=<n×100>%cpu.max

Nada de eso se declara por estar escrito aquí. Antes de la primera ejecución el sistema levanta un scope real con los tres límites puestos y comprueba que el kernel los acepta; si falla, los controles memory, processes y cpu no aparecen en effectiveControls y una política estricta que los exija no ejecuta. sandboxctl doctor muestra el resultado de ese sondeo.

Dónde suele fallar: hosts sin gestor de usuario de systemd (contenedores, CI, sesiones no interactivas). Ahí systemd-run --user no encuentra el bus.

Y lo que se consumió de verdad#

Aplicar un límite y medir el consumo son cosas distintas. La evidencia lleva las dos: limits.effective dice lo que el kernel impedía y limits.observed lo que la carga gastó, leído del cgroup mientras corría —systemd lo retira en cuanto el scope termina, así que mirar al final no encuentra nada—:

Campo observadoDe dónde sale
memoryPeakBytesmemory.peak
pidsPeakpids.peak
cpuUsageUsecusage_usec de cpu.stat
oomKillsoom_kill de memory.events

él, un proceso que el kernel mató por memoria se parece a uno que falló solo.

las cifras del cgroup de la sesión, que es lo que /proc/<pid>/cgroup devuelve sin envoltorio: serían números reales de la máquina equivocada.

pero no sustituye a cgroups: acota el espacio de direcciones virtual, no la memoria residente. Cuando los dos están, la evidencia nombra el cgroup, que es el que manda. RLIMIT_NPROC no se usa nunca: cuenta los procesos del UID real en todo el host, no los de la carga.

Proceso#

  • capabilities: lista de capabilities a conservar. Vacía en todas las

políticas del catálogo, y bubblewrap aplica --cap-drop ALL tanto en cargas breves como en servicios.

  • user y group: la identidad dentro del sandbox. Bubblewrap los aplica

con --uid/--gid, que exigen user namespace. El mapeo es «uid de dentro → uid real», así que los montajes de escritura siguen siendo accesibles aunque el número cambie.

No es cosmético. Sin ellos la carga corre con el uid de quien la lanzó y hereda sus grupos suplementarios — es decir, con la identidad que tiene acceso al repositorio, al llavero y a la sesión:

| | uid dentro | grupos | |---|---|---| | sin --uid | 1000 (el tuyo) | 1000, 65534 | | con --uid 65534 | 65534 (nobody) | 65534 |

unshare no los aplica: usa --map-root-user, que es otro mecanismo. Es una de las razones por las que sigue clasificado como runtime parcial.

  • environment: variables que sí entran. El resto no: bubblewrap limpia el

entorno con --clearenv antes de fijar estas.

  • allowedEnvironment: nombres de secretos que la política autoriza. Un secreto

que el servicio pide y la política no declara no entra, y eso no es un fallo sino la política haciendo su trabajo.

Syscalls#

  • deny: llamadas al sistema que la carga no puede hacer. Se compilan a un

filtro seccomp BPF y bubblewrap lo recibe por descriptor. Las denegadas devuelven EPERM.

  • allow y profile: se parsean y no se aplican todavía. El filtro que se

construye sale de deny.

Nombres reconocidos hoy: mount, umount2, ptrace, reboot, kexec_load,

kernel no conozca se ignora sin tumbar el resto del filtro —clone3 no existe en kernels antiguos—, y si ninguno se reconoce no se aplica filtro y el control no se declara.

Por qué denegación y no lista de permitidos, y por qué EPERM y no matar el proceso: B-05.

Solo bubblewrap. unshare no tiene forma de recibir un filtro.

Devices#

  • allow: dispositivos que se montan en /dev. Bubblewrap monta un /dev

mínimo; la lista es la intención declarada, y el control devices se declara porque ese /dev reducido sí se aplica.