🛡️ sandbox-labs GitHub ↗

Referencia de políticas#

Enforcement#

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

Filesystem#

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
nonenonoefectivo
loopbacknono — solo socket Unixefectivo
allowlistsolo por el canal explícitonoefectivo
unrestrictednonunca efectivo

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.

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.

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 usan service-isolated y los casos 02 y 03. service-sandbox y

puerto ellos mismos.

Recursos#

El timeout y el límite de salida los aplica el supervisor común.

El mecanismo es systemd-run --user --scope, que envuelve el árbol entero —scope → prlimitbwrap → 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#

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

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.

entorno con --clearenv antes de fijar estas.

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#

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

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#

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