8.2 KiB
Linux sandboxing with Firejail and Bubblewrap
This page shows how to run meshchatx under Firejail or Bubblewrap (bwrap) on Linux. The legacy CLI name meshchat installs the same entry point and can be substituted in these examples. Use this when you install MeshChatX natively (wheel, package, or Poetry) and want an extra layer of filesystem and process isolation compared to running the binary directly.
These tools do not replace a full virtual machine or hardware-enforced boundary. They reduce exposure of your home directory and other paths the process can write to, when you configure them with tight whitelists or bind mounts.
MeshChatX also applies optional in-process Linux sandboxes when available:
- Landlock for filesystem path rules (
MESHCHAT_LANDLOCK=0to disable) - Seccomp-BPF syscall denylist via libseccomp (
MESHCHAT_SECCOMP=0to disable)
Those layers fall back cleanly when unsupported. Firejail and Bubblewrap remain useful as an outer wrapper.
Landlock and user-local tools: When Landlock is active, MeshChatX whitelists common pipx paths (~/.local/bin, ~/.local/share/pipx) and Argos Translate data under ~/.local/share/argos-translate so local translation and similar CLIs keep working. Tools installed elsewhere (for example only under ~/.nvm) or symlink shims that point outside those trees may still fail with permission errors. Disable Landlock temporarily with MESHCHAT_LANDLOCK=0 while debugging PATH-only failures.
Landlock and USB serial: Landlock read roots include /sys so pyserial can read USB product strings for RNode listing. /dev is already a write root (including IOCTL_DEV on ABI 5+) so opening /dev/ttyACM* still works. Seccomp does not block serial ioctl. The user still needs dialout (or equivalent) group membership, and Ubuntu/Kubuntu brltty can steal CDC ACM devices before MeshChatX sees them.
Landlock and custom code: Interface modules under the Reticulum interfaces/ directory, Mesh Server executable pages under identity storage, and installed MeshChatX plugins under storage/plugins stay readable and writable. location_cmd, PipeInterface commands, and Sideband plugin folders must already exist on an allowed root at process start (/usr, ~/.local/bin, storage, or the configured Sideband path). Restart after pointing Sideband at a new directory.
Containers: If you already run MeshChatX with Docker or Podman, that is a different isolation model, this document is aimed at host-installed meshchatx (or meshchat).
Prerequisites
Install one or both from your distribution:
- Firejail: package name is usually
firejail. - Bubblewrap: package name is usually
bubblewrap, the binary isbwrap.
You need a working meshchatx on your PATH (for example after pipx install, pip install --user, or a distro package). The meshchat command is the same binary if both entry points are installed.
Pick a dedicated data directory for sandboxed runs so you do not mix permissions or policies with a non-sandboxed install. The examples below use:
DATA="${XDG_DATA_HOME:-$HOME/.local/share}/meshchatx-sandbox"
mkdir -p "$DATA/storage" "$DATA/.reticulum"
Adjust paths if you prefer another location.
Firejail
Firejail applies a profile (or defaults) on top of your command. For MeshChatX you typically want:
- Network left available so Reticulum and the web UI can work (do not use
--net=noneunless you know you need it). - Writable only your chosen data directory (and any other paths the app needs).
Installed meshchatx (pip, pipx, or system package)
DATA="${XDG_DATA_HOME:-$HOME/.local/share}/meshchatx-sandbox"
mkdir -p "$DATA/storage" "$DATA/.reticulum"
firejail --quiet \
--whitelist="$DATA" \
meshchatx --headless --host 127.0.0.1 \
--storage-dir="$DATA/storage" \
--reticulum-config-dir="$DATA/.reticulum"
If the default profile blocks something MeshChatX needs, you can start from a looser base and tighten later, for example:
firejail --noprofile --whitelist="$DATA" \
meshchatx --headless --host 127.0.0.1 \
--storage-dir="$DATA/storage" \
--reticulum-config-dir="$DATA/.reticulum"
--noprofile disables many Firejail restrictions. Treat it as a stepping stone, not the final hardening.
From source with UV
Poetry needs the project tree and the virtualenv. Example:
cd /path/to/reticulum-meshchatX
DATA="${XDG_DATA_HOME:-$HOME/.local/share}/meshchatx-sandbox"
VENV="$(pwd)/.venv"
mkdir -p "$DATA/storage" "$DATA/.reticulum"
firejail --quiet \
--whitelist="$(pwd)" \
--whitelist="$VENV" \
--whitelist="$DATA" \
uv run python -m meshchatx.meshchat --headless --host 127.0.0.1 \
--storage-dir="$DATA/storage" \
--reticulum-config-dir="$DATA/.reticulum"
You may need extra --whitelist= entries if UV or dependencies read config elsewhere (for example under $HOME/.config).
USB serial (RNode or similar)
If Reticulum talks to a radio over a serial device, Firejail may need explicit access to TTY devices, for example:
firejail --noblacklist=/dev/ttyACM0 --noblacklist=/dev/ttyUSB0 \
...
Use the device nodes your system actually exposes (dmesg, ls /dev/tty*).
Bubblewrap (bwrap)
Bubblewrap does not ship profiles, so you must list every mount and option. The pattern below keeps the whole root filesystem read-only, mounts a writable tmpfs on /tmp, and makes only your data directory writable at its normal path. Network namespaces are not changed, so Reticulum and TCP/UDP behave like an unsandboxed process unless you add --unshare-net (which usually breaks mesh networking).
Installed meshchatx
DATA="${XDG_DATA_HOME:-$HOME/.local/share}/meshchatx-sandbox"
mkdir -p "$DATA/storage" "$DATA/.reticulum"
exec bwrap \
--die-with-parent \
--new-session \
--proc /proc \
--dev /dev \
--ro-bind / / \
--tmpfs /tmp \
--bind "$DATA" "$DATA" \
--uid "$(id -u)" --gid "$(id -g)" \
meshchatx --headless --host 127.0.0.1 \
--storage-dir="$DATA/storage" \
--reticulum-config-dir="$DATA/.reticulum"
Notes:
- If
meshchatxlives only inside a venv that is not under$DATA, the read-only root still allows reading that path, you do not have to bind-mount the venv separately unless you also need writes there. - Distributions that merge
/and/usr(merged-usr) still work with--ro-bind / /on typical glibc setups. Ifbwrapfails with missing library paths, add the extra--ro-bindlines your distro documents (for example/lib64).
From source with UV
Bind the repository and the UV venv read-only, and keep DATA writable:
cd /path/to/reticulum-meshchatX
DATA="${XDG_DATA_HOME:-$HOME/.local/share}/meshchatx-sandbox"
VENV="$(pwd)/.venv"
mkdir -p "$DATA/storage" "$DATA/.reticulum"
PROJ="$(pwd)"
exec bwrap \
--die-with-parent \
--new-session \
--proc /proc \
--dev /dev \
--ro-bind / / \
--tmpfs /tmp \
--bind "$DATA" "$DATA" \
--ro-bind "$PROJ" "$PROJ" \
--ro-bind "$VENV" "$VENV" \
--uid "$(id -u)" --gid "$(id -g)" \
--setenv PATH "$VENV/bin:$PATH" \
--chdir "$PROJ" \
uv run python -m meshchatx.meshchat --headless --host 127.0.0.1 \
--storage-dir="$DATA/storage" \
--reticulum-config-dir="$DATA/.reticulum"
uv itself must be reachable on PATH inside the sandbox (often under /usr or $HOME/.local/bin, both visible with --ro-bind / /). If uv run fails because it cannot read ~/.cache/uv, add a read-only bind for that directory or invoke the venv interpreter directly instead of uv run:
exec bwrap \
... same mounts as above ... \
--setenv PATH "$VENV/bin:$PATH" \
--chdir "$PROJ" \
meshchatx --headless --host 127.0.0.1 \
--storage-dir="$DATA/storage" \
--reticulum-config-dir="$DATA/.reticulum"
(Use the meshchatx script, the legacy meshchat alias, or python -m entry point from $VENV/bin if your install exposes it there.)
USB serial under Bubblewrap
You may need a clearer view of devices than the minimal --dev /dev provides. Options include --dev-bind /dev /dev (broader device exposure) or binding only the specific character device. Balance convenience against attack surface.