Let us assume we have several applications (maybe described inside .desktop files) which we want to start as systemd user units. We already know that they are not malware, but at the same time we do not fully trust their developers, maybe because in the past we have found several critical vulnerabilities in released code, or maybe they collect as much data as possible from your home directory (especially if the application is closed-source). Therefore, you want to set up a simple sandbox for those programs.
Assume also that you do not want to rely on a battle-tested sandbox engine like Firejail, Bubblejail, or a full container like LXC or systemd-nspawn: you want to do everything by yourself through systemd units. Indeed, systemd already provides you a lot of sandboxing options to limit access to your home directory. We also assume that you have user namespaces enabled for unprivilegied users in order to allow systemd user units to use namespaces.
First to all, systemd has several service units types. As suggested in systemd.service(5), we will use the type exec for most part of our units. Therefore, we edit the configuration file located at ${XDG_CONFIG_HOME}/tomloader/groups.kdl with the definition of a group named App with two mandatory ordered parameters:
The configuration file will be
// ${XDG_CONFIG_HOME}/tomloader/groups.kdl def-group App 2 { sd { (section) Unit { (set) Description "${1}" } (section) Service { (set) Type "exec" (set) ExecStart "${0}" } } }
See Group configuration for information about groups and the syntax of the configuration file. For simplicity, we will sandbox just two applications as systemd user units: LibreOffice Writer and Firefox. For each application/systemd unit, we will create an unit configuration file (see Unit configuration for information about unit configuration files) inside the ~/units/ directory:
// ~/units/libreoffice_writer.service.kdl pull { App "/usr/bin/libreoffice --writer" \ "LibreOffice Writer -- sandboxed" }
// ~/units/firefox.service.kdl pull { App "/usr/lib/firefox/firefox" \ "Firefox -- sandboxed" }
Now if we run
tomloader sd-v0.2 --directory ~/units/ --target-directory \
${XDG_CONFIG_HOME}/systemd/user/
then the following systemd user units will be generated in your home:
// ${XDG_CONFIG_HOME}/systemd/user/libreoffice_writer.service
[Unit]
Description=LibreOffice Writer -- sandboxed
[Service]
Type=exec
ExecStart=/usr/bin/libreoffice --writer
// ${XDG_CONFIG_HOME}/systemd/user/firefox.service
[Unit]
Description=Firefox -- sandboxed
[Service]
Type=exec
ExecStart=/usr/lib/firefox/firefox
For the sandbow, we will implement a whitelist approach:
Sandbox will pull all the restrictions in each unit in which it is loaded;
Allow, may be loaded in order to relax the very strong limitations brought by Sandbox.
In this way, we will have a greater control on the application we will execute with a lesser risk of information leaks.
The following restrictions listed in Sandbox will always be enabled:
NoNewPrivileges=truewe do not want a sandboxed application to gain root privileges through SUID binaries;
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6other socket families are not required and could be used to exploit vulnerabilities;
SystemCallArchitectures=nativein this example we do not need to execute x86 binaries on a x86_64 machine.
Our configuration files become:
// ${XDG_CONFIG_HOME}/tomloader/groups.kdl def-group Sandbox { sd { (section) Service { (set) NoNewPrivileges #true (add) RestrictAddressFamilies \ "AF_UNIX" "AF_INET" "AF_INET6" (set) SystemCallArchitectures native } } } def-group App 2 { sd { (section) Unit { (set) Description "${1}" } (section) Service { (set) Type "exec" (set) ExecStart "${0}" } } }
// ~/units/libreoffice_writer.service.kdl pull { App "/usr/bin/libreoffice --writer" \ "LibreOffice Writer -- sandboxed" Sandbox }
// ~/units/firefox.service.kdl pull { App "/usr/lib/firefox/firefox" \ "Firefox -- sandboxed" Sandbox }
We used an (add) node instead of a (set) node for RestrictAddressFamilies in order to allow more socket families without necessarily generate conflicts (see Conflicts for a brief description of conflicts and how to resolve them).
At this point we can add personalized restrictions for our units. First to all, we want to limit network access for those applications that should be able to connect through Internet. The easiest way to implement it is through the PrivateNetwork systemd field which we will put in a group called NoNet:
def-group NoNet { sd { (set) PrivateNetwork #true } }
This group will be set as a load dependency of Sandbox in order to disallow network access by default:
def-group Sandbox { pull { NoNet } sd { (section) Service { (set) NoNewPrivileges #true (add) RestrictAddressFamilies \ "AF_UNIX" "AF_INET" "AF_INET6" (set) SystemCallArchitectures native } } }
In order to allow network access for Firefox, we create another group called AllowNet which only purpose is to remove NoNet when loaded:
def-group AllowNet { replace { NoNet } }
and modify our unit configuration files as follows:
// ~/units/libreoffice_writer.service.kdl pull { App "/usr/bin/libreoffice --writer" \ "LibreOffice Writer -- sandboxed" Sandbox }
// ~/units/firefox.service.kdl pull { App "/usr/lib/firefox/firefox" \ "Firefox -- sandboxed" Sandbox AllowNet }
Next, we want to disallow unrestricted access to our home directory. We can implement this in Systemd by mounting a temporary filesystem at our HOME and at our XDG_RUNTIME_DIR, which are represented in a systemd unit file with ‘%h’ and ‘%t’ respectively:
def-group NoHome { sd { (add) TemporaryFileSystem "%h" "%t" } } def-group Sandbox { pull { NoNet NoHome } sd { (section) Service { (set) NoNewPrivileges #true (add) RestrictAddressFamilies \ "AF_UNIX" "AF_INET" "AF_INET6" (set) SystemCallArchitectures native } } }
Instead of introducing a group which would allow unrestricted access to the home directory, we could instead provide access to just few authorized subdirectories. Since we have mounted a temporary filesystem on our home, we can use bind mounts to give access to underlying directories:
def-group AllowDir 1 { pull { NoHome } sd { (add) BindPaths "-%h/${0}" } }
but we can also use ConfigurationDirectory, RuntimeDirectory and CacheDirectory systemd fields to provide access to safe directories depending on XDG_* environment variables:
def-group AllowXDGDir 1 { pull { NoHome } sd { (set) ConfigurationDirectory "${0}" (set) RuntimeDirectory "${0}" (set) CacheDirectory "${0}" // Currently, systemd does not provide a similar // option for XDG_DATA_HOME, therefore // we should use BindPaths here (add) BindPaths "-%D/${0}" } }
Notice that, since both our applications are graphical, we need to enable access to the Wayland socket. For simplicity, we will assume here that its position is at ${XDG_RUNTIME_DIR}/wayland-0:
def-group AllowWayland { pull { NoHome } sd { (add) BindPaths "-%t/wayland-0" } }
Then, we will modify our unit configuration file as follows:
// ~/units/libreoffice_writer.service.kdl pull { App "/usr/bin/libreoffice --writer" \ "LibreOffice Writer -- sandboxed" Sandbox AllowXDGDir "libreoffice" AllowWayland AllowDir "Documents/" }
// ~/units/firefox.service.kdl pull { App "/usr/lib/firefox/firefox" \ "Firefox -- sandboxed" Sandbox AllowNet AllowWayland AllowDir "Downloads/" AllowDir ".mozilla/" }
It is not a problem to load AllowDir several times, even with different arguments, because several ‘(add)’ operations on the same field will always lead to a deterministic value that will not depend on the order of these operations. Therefore, even if we load AllowDir ".mozilla/" before AllowDir "Downloads/" the final value assigned to BindPaths will be the same. Tomloader achieves this by reordering values set by ‘(set)’ and ‘(add)’ operations in a deterministic way which at the moment is not stabilized and may change in future.
Therefore, you should use ‘(add)’ with several values only on systemd fields that doesn’t depend on the ordering. If ordering matters then the full preformatted string should be used through a single ‘(set)’ operation.
This example ends here. You can use it as a basis to implement a basic sandbox for your (sufficiently-trusted) applications. Additional sandboxing options for systemd can be found at systemd.service(5).