Referencia de políticas#
Enforcement#
strict: falla cerrado si falta cualquierrequiredControl.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-readonlyocustom.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 permanecerfalsepara 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:
mode | Namespace propio | Salida al exterior | Puerto TCP publicable | Control network |
|---|---|---|---|---|
none | sí | no | no | efectivo |
loopback | sí | no | no — solo socket Unix | efectivo |
allowlist | sí | solo por el canal explícito | no | efectivo |
unrestricted | no | sí | 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):
publish | Quién enlaza el puerto del host | network puede ser |
|---|---|---|
direct | el propio servicio | allowlist o unrestricted |
proxy | el supervisor, empalmando con el socket Unix del sandbox | none o loopback |
none | nadie: la única puerta es el socket | none 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 → prlimit → bwrap → carga— y traduce la política así:
| Campo de la política | Propiedad del scope | Fichero del kernel |
|---|---|---|
memoryMb | MemoryMax=<n>M | memory.max |
processes | TasksMax=<n> | pids.max |
cpu | CPUQuota=<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 observado | De dónde sale |
|---|---|
memoryPeakBytes | memory.peak |
pidsPeak | pids.peak |
cpuUsageUsec | usage_usec de cpu.stat |
oomKills | oom_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.
userygroup: 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.
allowyprofile: 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.