firejail - pprados/pysandboxes GitHub Wiki

Firejail

Firejail is an OS-sandbox provider that runs code in an isolated environment with restricted filesystem access, its own network namespace (on a bridge with socket rules, loopback only without), and configurable seccomp. It allows limiting disk access, network access, and system calls. See the Firejail documentation for details.

Solution in brief

The host launches a Firejail sandbox with a generated profile: whitelist/read-only paths from expose-ro / expose-rw file rules, --net=<bridge> and netfilter rules (FIFO) for socket rules, --net=none (loopback only) without them. The sandbox runs main_sandbox which reads the configuration from a named pipe and starts the SSE server. The host talks to the sandbox via SSE on a local URL. With socket rules, the jail gets its own network namespace on a bridge (e.g. docker0, br0) and is reached at the address that bridge assigns. Only under an explicit restricted-network yes is no --net passed: the jail then shares the host network stack and the daemon listens on the host loopback (127.0.0.1).

Advantages

Aspect Detail
Isolation Filesystem whitelist, private network namespace (loopback only without socket rules), netfilter-based socket rules.
Ease of use Single binary, profile from template + Py-sandboxes rules; no VM or kernel feature beyond namespaces.
Security hardening Optional seccomp (allow/block lists) to restrict system calls.
Alignment with other providers Same API (SSE, call_in_sandbox) and config flow (named pipe) as bwrap/unshare.

Disadvantages

Aspect Detail
Docker / Podman Not usable in Docker, Podman or Kubernetes: inside a container, firejail runs the program without any isolation (see Using with Docker).
Network With socket rules, restricted-network no must be set in /etc/firejail/firejail.config and a host bridge must exist. Firejail requires extra host network setup compared with bwrap, unshare and qemu; see Prerequisites. The daemon mode always has a socket rule (its SSE port). Under an explicit restricted-network yes, the sandbox keeps the host network with a warning and only the Python layer enforces the socket rules.
/etc /etc inside the jail is the --private-etc copy of the template (hosts, resolv.conf, nsswitch.conf, ssl, pki, ca-certificates). An expose-* rule under /etc/ gets no --whitelist (it would put a tmpfs over /etc and break --dns), so a file outside that list stays invisible.
Debug Profile and netfilter generation can be complex; check logs and Firejail options for troubleshooting.

How it works

  1. Host: The daemon builds Firejail arguments from the template, file rules (whitelist/read-only/read-write), and optional socket rules. If socket rules are used, it generates netfilter rules and passes them via FIFOs (--netfilter, --netfilter6); without them, it adds --net=none unless firejail.net is set. Config is written to a named pipe; the sandbox reads it on startup.
  2. Launch: The host runs firejail [options] -- /usr/bin/env -i ... python -m pysandboxes.remote.main_sandbox --_named-pipe <path>. Options include --whitelist, --read-only, --net=<bridge>, --dns, --netfilter when network rules are enabled, --net=none otherwise. --x11=none is added only with --net (a bridge or none): firejail refuses it while the abstract X11 socket is reachable, i.e. without a network namespace. Without one (explicit restricted-network yes), the template still blacklists /tmp/.X11-unix and empties DISPLAY and XAUTHORITY.
  3. Inside the sandbox: main_sandbox loads the config from the pipe, applies Python guards, starts the SSE server on the expected port and waits for requests.
  4. Communication: The host sends calls over SSE to the sandbox, on the bridge-assigned IP given by --net.

For more details, see the Firejail documentation.

Using with Docker

Firejail is not compatible with Docker. Inside a container, it concludes it already runs in a sandbox, prints Warning: an existing sandbox was detected. <program> will run without any additional sandboxing features and starts the program unconfined. Neither --privileged nor --force changes this: with firejail 0.9.74 in docker run --privileged, the program keeps the container's mount and PID namespaces. The process starts and simple checks pass, but no kernel boundary applies. The project uses one image per OS provider (see unshare, bwrap, qemu); there is no container image for firejail. Use unshare, bwrap, or qemu for container-based workflows.

Using with Podman

Firejail is not compatible with Podman, for the same reason as Docker. Use unshare, bwrap, or qemu with their respective provider images for container-based workflows.

Using with Kubernetes

Firejail is not supported in Kubernetes (no python-sb-firejail image), for the same reason as Docker. Use unshare (python-sb-unshare), bwrap (python-sb-bwrap), or qemu (python-sb-qemu) and their documentation for Kubernetes.

Configuration parameters

You can add Firejail-specific options in .py-sandboxes. The keys recognized are firejail.net, firejail.seccomp, firejail.seccomp.keep, firejail.seccomp.block and firejail.rlimit-*, each passed to Firejail as --<option>=<value> when the sandbox is started. Any other firejail.<option> line is not passed to Firejail.

Network (firejail.net)

To use a specific bridge interface (e.g. for host communication), set:

  • firejail.net=my_bridge — use the given bridge for the sandbox network.

If firejail.net is not set and socket rules are used, the daemon selects docker0 when present, otherwise the first detected bridge. It exits with an error if no bridge exists; a default-route interface is not used as a fallback. Set firejail.net=<bridge> to select a bridge explicitly.

Limits (firejail.rlimit-*)

firejail.template caps every sandbox: --rlimit-as=300m, --rlimit-nproc=20, --rlimit-nofile=50, --rlimit-fsize and --rlimit-sigpending. A profile whose libraries need more sets the limit for itself, and the template's line for that option is then left out:

  • firejail.rlimit-as=600m — address space, for a framework that maps more than 300 MB at import.

The environment variables the template reads (FIREJAIL_RELIMIT, FIREJAIL_NPROC...) change the default for every sandbox of the host instead.

Seccomp (firejail.seccomp, firejail.seccomp.keep, firejail.seccomp.block)

To restrict system calls, use an allow-list or block-list:

  • firejail.seccomp=<path> — path to a seccomp list file.
  • firejail.seccomp.keep=<syscalls> — comma-separated list of allowed syscalls.
  • firejail.seccomp.block=<syscalls> — comma-separated list of blocked syscalls.

To discover which syscalls your application uses, you can run:

scripts/extract_strace.sh <command to start your application>

Then add the resulting syscalls to firejail.seccomp.keep= (or seccomp.block=), separated by commas and without spaces.

Prerequisites

  • Firejail installed (e.g. sudo apt install firejail).
  • For socket rules (including daemon transport): set restricted-network no in /etc/firejail/firejail.config and provide a host bridge. Existing Docker (docker0) or libvirt (virbr0) bridges can be selected automatically. Otherwise, an administrator can run sudo scripts/add-bridge.sh [bridge-name]; the script needs ip, iptables and sysctl. It creates a 10.10.20.0/24 bridge, enables IPv4 forwarding, adds scoped NAT/FORWARD rules, and does not attach or change a physical interface. IPv4 forwarding is a global runtime host setting. Review the script and existing firewall policy before running it.
  • For socket rules (and so for the daemon mode), the restricted-network line of /etc/firejail/firejail.config decides:
    • restricted-network no: network filtering is applied (--net, --dns, --netfilter, --netfilter6).
    • no such line: the daemon logs an error and exits.
    • restricted-network yes (the stock value, e.g. on GitHub runners): the daemon warns and starts without the network setup, on the host network: the socket rules are not enforced by firejail, only by the Python guard, so native code, ctypes or a subprocess can reach any address.
  • Without socket rules, --net=none is used whatever the restricted-network line says: firejail allows it to regular users.
  • With --net=<bridge>, the daemon listens on every interface of the jail's own network namespace: the host and any other peer of that bridge (a Docker container on docker0, for example) can reach its port. A dedicated bridge, such as the one scripts/add-bridge.sh creates, keeps it away from other workloads.

Recommendations

  • Set restricted-network no in /etc/firejail/firejail.config and configure a bridge before relying on socket rules. If no bridge exists, the daemon now stops with an actionable error instead of trying the default physical interface. Under restricted-network yes, only the Python guard enforces them, and native code, ctypes or a subprocess can reach any address.
  • Do not use Firejail in Docker, Podman or Kubernetes. Use unshare, bwrap or qemu with their provider images instead.
  • Restrict system calls with firejail.seccomp.keep=, built from the output of the strace script shown above.
  • Note that a file under /etc is visible only if it belongs to the --private-etc copy of the template.

References

⚠️ **GitHub.com Fallback** ⚠️