hazmat

0.1.0

hazmat runs one command in a bubblewrap sandbox with the packages you name. The command gets a read-only system, a temporary home and none of your files. With hazmat -s it sandboxes runit services. silo is the persistent version with its own packages and home. security describes the limits of the isolation.

The source is recipes/uranium-tools/src/bin/hazmat.rs, part of the uranium-tools package.

Synopsis

hazmat [-n] [-r PATH] [-w PATH] PACKAGE... -- COMMAND...
hazmat -s COMMAND...
-n
No network. Without it the command shares the network of the host.
-r PATH
Makes an existing path visible at the same path, read-only. Can be repeated.
-w PATH
The same, writable. Changes go to the real files, since the path is bound and not copied. Can be repeated.
PACKAGE...
One or more names from the package index at /rod/nixpkgs. They are given to nix shell, which fetches or builds them into the store if needed and does not add them to any profile.
-- COMMAND...
The command. Both parts are required: at least one package before -- and at least one word after it.
-s COMMAND...
Service mode, see below. It must be the first argument.

-n, -r and -w must come before the first package. The first word that is not an option starts the package list. The shell expands ~ before hazmat sees it, and hazmat binds the path as it recieves it.

What it runs

hazmat replaces itself with nix, which starts bubblewrap inside the environment of the packages. With ROD_ROOT at the default /rod the call is:

nix shell -f /rod/nixpkgs PACKAGE... -c
  bwrap --unshare-all [--share-net] --die-with-parent
    --ro-bind /nix /nix
    --ro-bind /rod/profile /rod/profile
    --ro-bind /bin /bin
    --ro-bind /usr /usr
    --ro-bind /lib /lib
    --ro-bind /lib64 /lib64
    --ro-bind /etc/resolv.conf /etc/resolv.conf
    --ro-bind /etc/ssl /etc/ssl
    --proc /proc --dev /dev --tmpfs /tmp --tmpfs /home
    [--ro-bind PATH PATH | --bind PATH PATH]...
    --setenv HOME /tmp --chdir /tmp
    sh -c COMMAND

-n leaves out --share-net. --unshare-all creates new user, mount, PID, IPC, UTS and cgroup namespaces and a new network namespace, and --share-net keeps the network of the host. The binds from -r and -w come after --tmpfs /home and --tmpfs /tmp, so a path under /home or /tmp is visible again inside the empty directories. --ro-bind needs the source to exist, so on a system without /lib64 the call fails with a bubblewrap message.

The environment is the one nix shell builds: the bin directories of the packages come first in PATH and the variables of the caller stay, except HOME, which is /tmp. The command words are joined with spaces and read by sh -c, so quotes are removed once before the inner shell sees them:

$ hazmat coreutils -- echo "a   b"          prints a b
$ hazmat coreutils -- "echo 'a   b'"        prints a   b

What the command sees

/nixThe whole store, read-only. Any store path on the host is readable.
/rod/profile, /bin, /usr, /lib, /lib64The system profile and the links into it, read-only. The programs of the system work and a prebuilt binary finds its loader.
/etc/resolv.conf, /etc/sslThe resolver configuration and the certificates. The rest of /etc is absent, including /etc/passwd, /etc/hostname and the ssh configuration.
/tmp, /homeNew and empty memory filesystems. /tmp is the working directory and $HOME. They are gone when the command ends.
/proc, /devA procfs of the new PID namespace and a minimal /dev.
Paths from -r and -wWhere you put them.

The process has the user and group id of the caller, since hazmat passes no --uid or --gid, and it can read and write what that user can through a bound path. The kernel and the terminal are shared. hazmat does not pass --new-session and the kernel has LEGACY_TIOCSTI=y, so a kernel bug, or a program that pushes input into the terminal with TIOCSTI, gets out of the sandbox. See security.

Examples

$ hazmat hello -- hello
Hello, world!
$ hazmat -r /etc/hostname coreutils -- cat /etc/hostname
$ hazmat -r /etc/hostname coreutils -- test ! -e /etc/passwd
$ hazmat -n imagemagick -- convert photo.png photo.jpg
$ hazmat -r /srv/photos -w /srv/out imagemagick -- convert /srv/photos/a.png /srv/out/a.jpg

The second command prints the host name and the third succeeds, because only the listed path is present. The fourth does not work as written: photo.png is in a directory the sandbox does not show. Bind the directory with -r or -w and use absolute paths, like the last example, or work in /tmp.

Exit status and messages

When everything starts, the exit status is the status of the command.

Services

hazmat -s COMMAND... runs a service under a sandbox that a file describes. It is meant for the run script of a runit service in /etc/sv/NAME:

#!/rod/profile/bin/sh
export PATH=/rod/bin:/rod/profile/bin
exec hazmat -s daemon --foreground

hazmat reads the file named by HAZMAT_FILE, default ./sandbox. runit starts run in the service directory, so the default is /etc/sv/NAME/sandbox. If the file does not exist hazmat runs the command without a sandbox, so deleting the file removes the sandbox. Each line has a keyword and a path split on white space. Empty lines and lines that start with # are skipped, and anything after the path is ignored, so a path cannot contain spaces.

ro PATHVisible at the same path, read-only.
rw PATHVisible and writable. The directory is created, with its parents, if it does not exist.
netShare the network of the host.
nonetNo network. This is the default. The last of net and nonet wins.

Any other keyword stops the service from starting. The call, with the lines of the file as FILE BINDS in order:

bwrap --unshare-all [--share-net] --die-with-parent
  --proc /proc --dev /dev --tmpfs /tmp --chdir /
  FILE BINDS
  --ro-bind X X      for each of /nix, /rod/profile, /bin, /usr, /lib, /lib64,
                     /etc/ssl, /etc/resolv.conf that exists
  COMMAND...

The command runs directly and not through a shell, and the environment of the run script stays, so HOME and PATH are unchanged. A service sees the store and the system profile read-only, the paths in the file and a new empty /tmp. It does not see /etc beyond ssl and resolv.conf, so a daemon that reads /etc/passwd needs ro /etc/passwd. A service that needs raw network access or the system clock, like dhcp and ntpd, cannot run this way, because the sandbox has no privileges on the host. sshd does not use it in 0.1.0.

Example: a daemon that writes to one directory and has no network.

$ cat /etc/sv/mydaemon/sandbox
nonet
ro /etc/passwd
rw /var/lib/mydaemon

Files and variables

/rod/nixpkgs
The package index. Made by rod +s.
ROD_ROOT
Replaces /rod for the index and for the profile in service mode. The bind /rod/profile in the first mode is fixed.
HAZMAT_FILE
The sandbox file for -s.

Limits