Running Web Browsers in FreeBSD Jail

       2058 words, 10 minutes

Using OpenBSD as a daily driver, I got used to having programs being restricted in their permissions. Especially Web Browsers from ports that are patched to implement pledge(2) and unveil(8). Long story short, this guarantees that Firefox, Chromium & friends will get killed if they try to access system resources that they were not allowed to access; be it a device or a file system space.

FreeBSD 14.1, AFAIK, does not implement such feature. And getting a bit paranoid because of “Fish Linux” , I decided my FreeBSD Web browsers should be living in jail.

Once FreeBSD is installed, it is time to read the Chapter 17. Jails and Containers section of the Handbook. Another precious reading is Jailing GUI Applications , from the FreeBSD wiki. Complete the reading session with the man pages.

Building a Thick Jail

There were two choices: Thick or Thin Jails. I went for the first one because it allows some kind of independence regarding the host system.

Jail Service

My configuration targets a laptop usage. I’ll be using Web browsers regularly. So I need to have the Jail up and ready. The Jail system is enabled at boot time.

# sysrc jail_enable="YES"
# sysrc jail_parallel_start="YES"

My laptop uses ZFS and I have no reason to use weird specific storage location. My Jail directory tree is the same as the one the Handbook documents.

# zfs create -o mountpoint=/usr/local/jails zroot/jails
# zfs create zroot/jails/media
# zfs create zroot/jails/templates
# zfs create zroot/jails/containers

I shall build a couple of jails. So I’d rather use the /etc/jail.conf.d/ configuration system. In this case, each jail has its own configuration file.

# mkdir -p /etc/jail.conf.d
# cat > /etc/jail.conf
.include "/etc/jail.conf.d/*.conf";
^D

Jail Networking

The networking model will be Host Networking Mode (IP Sharing). I don’t need the Web Browsers to expose (Web) services.

The jail has its own network and will be NATed to access Internet.

# sysrc cloned_interfaces+="lo1"
# sysrc ifconfig_lo1_aliases="inet 192.0.2.97-102/29"

# service netif cloneup lo1

# cat > /etc/pf.conf

ext_if = "lagg0"
int_if = "lo1"

set skip on lo
nat on $ext_if from ($int_if) to ! ($int_if) -> ($ext_if)
^D

# service pf enable
# service pf start

Jail Device sharing

The Web browsers will need to access some the host’s devices in order to provide a regular Web browsing experience. A basic configuration would be:

# cat > /etc/devfs.rules
[devfsrules_desktop_jail=10]
add include $devfsrules_hide_all
add include $devfsrules_unhide_basic
add include $devfsrules_unhide_login
add path 'mixer*' unhide
add path 'dsp*' unhide
^D

# service devfs restart

I’ll add more to it in the next sections.

Creating the Jail

Lets grab the FreeBSD base archive and store it in the media dataset.

# fetch https://download.freebsd.org/ftp/releases/amd64/amd64/14.1-RELEASE/base.txz \
  -o /usr/local/jails/media/14.1-RELEASE-base.txz

Then deploy the archive content and a few local files in the jail directory.

# mkdir -p /usr/local/jails/containers/jbrowsers

# tar -xf /usr/local/jails/media/14.1-RELEASE-base.txz \
  -C /usr/local/jails/containers/jbrowsers --unlink

# cp /etc/resolv.conf /etc/localtime \
  /usr/local/jails/containers/jbrowsers/etc/

Finally update the Jail to the latest patch level.

# freebsd-update -b /usr/local/jails/containers/jbrowsers/ fetch install

NB: some documentation use bsdinstall to deploy a Jail. It has the advantage of downloading and installing stuff automatically. It also allows users creation, password settings and system configuration as you would do on a real stand-alone server.

The jail being ready, a configuration file can be created and the jail started.

# cat > /etc/jail.conf.d/jbrowsers.conf
jbrowsers {
  # STARTUP/LOGGING
  exec.start = "/bin/sh /etc/rc";
  exec.stop = "/bin/sh /etc/rc.shutdown";
  exec.consolelog = "/var/log/jail_console_${name}.log";

  # PERMISSIONS
  allow.raw_sockets;
  exec.clean;
  mount.devfs;

  # HOSTNAME/PATH
  host.hostname = "${name}";
  path = "/usr/local/jails/containers/${name}";

  # NETWORK
  ip4.addr = 192.0.2.101/29;
  interface = lo0;

  devfs_ruleset = 10;
}
^D

# service jail restart

This is a basic configuration. More will be added to the configuration in the next sections.

Jail cheat sheet

Here’s a list of classical commands I used a lot while doing my testing… :)

Going through this whole section, you would get a base FreeBSD instance running in Jail. It has nothing particular related to Web browsers yet. It could be used to install whatever software you liked.

Configuring the Web Browsers Jail

I don’t want to really notice that I’m using a jailed navigator. So I will replicate a few things from the host; like the user name, the available fonts, the look etc.

Note: the Handbook recommends not logging into the Jail to operate it but use jexec and command parameters to manage it from the host. All the following commands are run from the host.

Installing packages

If I were an extreme paranoid, I’d probably create a single Jail per Web browsers. But what one can access, the others can too. So I’ll install all of them in the same Jail.

# pkg -j jbrowsers install librewolf firefox-esr iridium-browser
# pkg -j jbrowsers install twemoji-color-font-ttf webfonts
# pkg -j jbrowsers install xorg drm-kmod libva-intel-driver mesa-libs mesa-dri

I installed libva-intel-driver because the laptop running FreeBSD is a ThinkPad X280 with a Intel HD Graphics 620.

Creating an unprivileged user

To forget that I’m doing “complicated” things, the Web browsers will be running using an unprivileged user that has the same UID/GID as the one I use on the host. The Web browsers will also have access to a limited number of the user’s host directories.

# jexec jbrowsers pw groupadd joel -g 1000
# jexec jbrowsers pw useradd joel -u 1000 -g 1000 -m -w random

# jexec -U joel jbrowsers mkdir /home/joel/Downloads

To access some of the host’s directories and resources, the Jail configuration has to be amended with a few “mount” directives.

# vi /etc/jail.conf.d/jbrowsers.conf
(...)
   mount += "/tmp   $path/tmp      nullfs  rw                    0  0";
   mount += "/home/joel/Downloads $path/home/joel/Downloads  nullfs  rw  0  0";
(...)

# jexec jbrowsers sysrc clear_tmp_enable="NO"
# jexec jbrowsers sysrc clear_tmp_X="NO"

# service jail restart jbrowsers

Sharing /tmp allows access to X11 socket and resources. Not clearing /tmp content will prevent wrecking the host should I have to restart the Jail. Spoiler Alert: I will in the next sections and did wrecked my X11 session during my testing when the Jail restart deleted /tmp/.X11-unix/X0

From there, I can use the following command in a terminal and have a Web browser running for Jail:

$ doas jexec -u joel jbrowsers librewolf

Setting up wrappers to run the browsers

It is possible to simply run the browsers from the Jail using the previous doas command. But I’d rather setup some wrappers so that KDE can start them from an icon click or have Alacritty open a clicked URL.

On the host system, wrappers would look like:

$ [ ! -d ~/.local/bin ] && mkdir -p ~/.local/bin

# cat > ~/.local/bin/jbrowsers
#!/bin/sh
#
# Runs a Web browser from jbrowsers Jail
#

if [ $# -lt 2 ]; then
   echo "usage: $0 user browsers [arguments]"
   exit 1
fi

_user=$1; shift
_browser=$1; shift
_params=$@

exec jexec -U $_user jbrowsers $_browser $_params
^D
# chmod 0755 ~/.local/bin/jbrowsers

$ cat > ~/.local/bin/firefox
#!/bin/sh
_browser="$(basename $0)"

exec /usr/local/bin/doas /home/joel/.local/bin/jbrowsers "$(whoami)" "$_browser" "$@"
^D
$ ln ~/.local/bin/firefox ~/.local/bin/librewolf
$ ln ~/.local/bin/firefox ~/.local/bin/iridium

$ chmod 0755 ~/.local/bin/firefox

The problem is jexec cannot be run using an unprivileged user. To run it using doas without providing a password, a bit of configuration is required.

$ doas vi /usr/local/etc/doas.conf
(...)
# Run Web browsers in Jail
permit keepenv nopass joel as root cmd /home/joel/.local/bin/jbrowsers
(...)

Now that the wrappers can be called using doas without providing a password, let’s create “.desktop” files so they can be run using the graphical interface.

$ [ ! -d ~/.local/share/applications ] && mkdir -p ~/.local/share/applications

$ vi ~/.local/share/applications/firefox.desktop
[Desktop Entry]
Version=1.0
Name=Firefox Web Browser
Name[fr]=Navigateur Web Firefox
Comment=Browse the World Wide Web
Comment[fr]=Naviguer sur le Web
GenericName=Web Browser
GenericName[fr]=Navigateur Web
Keywords=Internet;WWW;Browser;Web;Explorer
Keywords[fr]=Internet;WWW;Browser;Web;Explorer;Fureteur;Surfer;Navigateur
Exec=/home/joel/.local/bin/firefox %U
Terminal=false
Type=Application
Icon=firefox
Categories=GNOME;GTK;Network;WebBrowser;
MimeType=text/html;text/xml;application/xhtml+xml;application/xml;\
application/rss+xml;application/rdf+xml;image/gif;image/jpeg;\
image/png;x-scheme-handler/http;x-scheme-handler/https;\
x-scheme-handler/ftp;x-scheme-handler/chrome;video/webm;application/x-xpinstall;
StartupNotify=true
Actions=NewWindow;NewPrivateWindow;

[Desktop Action NewWindow]
Name=Open a New Window
Name[fr]=Ouvrir une nouvelle fenêtre
Exec=/home/joel/.local/bin/firefox -new-window

[Desktop Action NewPrivateWindow]
Name=Open a New Private Window
Name[fr]=Ouvrir une nouvelle fenêtre de navigation privée
Exec=/home/joel/.local/bin/firefox  -private-window
$ sed -e 's/Firefox/LibreWolf/' -e 's/firefox/librewolf/' \
  ~/.local/share/applications/firefox.desktop         \
  > ~/.local/share/applications/librewolf.desktop

$ sed -e 's/Firefox/Iridium/' -e 's/firefox/iridium/' \
  ~/.local/share/applications/firefox.desktop         \
  > ~/.local/share/applications/iridium.desktop

The Web browsers can now be started as any other application installed on the host. Still, when ran from the terminal, a couple of errors still raise.

Solving D-Bus errors

When starting LibreWolf, I got errors like:

(librewolf:51776): dconf-CRITICAL **: 15:05:54.281: unable to create directory    \
   '/var/run/user/1000/dconf': Permission denied.  dconf will not work properly.
[Parent 51776, Main Thread] WARNING: unable to create directory                   \
   '/var/run/user/1000/dconf': Permission denied.  dconf will not work properly.: \
   'glib warning', file /wrkdirs/usr/ports/www/librewolf/work/librewolf-131.0.3-1/\
   toolkit/xre/nsSigHandlers.cpp:187

Indeed, /var/run/user/1000 doesn’t exist in the Jail. So let’s configure the Jail so that it inherits the host’s data.

# jexec jbrowsers mkdir /var/run/user

# vi /etc/jail.conf.d/jbrowsers.conf
(...)
mount += "/var/run/user  $path/var/run/user  nullfs  rw  0  0";
(...)

# service jail restart jbrowsers

Solving GPU errors

When starting LibreWolf, I got errors like:

Crash Annotation GraphicsCriticalError: |[0][GFX1-]: glxtest: ManageChildProcess failed
(t=0.387453) [GFX1-]: glxtest: ManageChildProcess failed
Crash Annotation GraphicsCriticalError: |[0][GFX1-]: glxtest: ManageChildProcess failed
(t=0.387453) |[1][GFX1-]: glxtest: libEGL initialize failed (t=0.387453) \
             |[2][GFX1-]: glxtest: X error, error_code=8, request_code=152, minor_code=5 (t=0.387453) \
             |[3][GFX1-]: No GPUs detected via PCI
(t=0.387453) [GFX1-]: No GPUs detected via PCI

This can be solved with sharing some more devices with the host.

# vi /etc/devfs.rules
[devfsrules_desktop_jail=10]
(...)
add path dri unhide
add path dri/* unhide
add path drm' unhide
add path drm/* unhide
add path pci unhide
(...)

# jexec jbrowsers pw usermod joel -G video

# service devfs restart
# service jail restart jbrowsers 

Solving look inconsistency

When run from the Jail, Web browsers have a few inconsistency with my overall desktop environment. The default fonts I use aren’t rendered, they use a generic theme, etc.

Have browsers use my custom fonts:

# vi /etc/jail.conf.d/jbrowsers.conf
(...)
mount += "/home/joel/.local/share/fonts $path/home/joel/.local/share/fonts  nullfs  ro  0  0";
(...)

# jexec -U joel jbrowsers mkdir -p /home/joel/.local/share/fonts

Have browsers use my custom theme:

# vi /etc/jail.conf.d/jbrowsers.conf
(...)
mount += "/home/joel/.local/share/icons       $path/home/joel/.local/share/icons       nullfs ro 0 0";
mount += "/home/joel/.local/share/themes      $path/home/joel/.local/share/themes      nullfs ro 0 0";
(...)

# jexec -U joel jbrowsers mkdir -p \
  /home/joel/.local/share/icons \
  /home/joel/.local/share/themes

The OpenBSD unveil.main file was of great help to have Firefox & friends be configured properly for being jailed.

Files and directories have to exist in the Jail file system before being NULL-mounted. So there are a few touch and mkdir commands to run before restarting the Jail for the changes to be applied.

# vi /etc/jail.conf.d/jbrowsers.conf
(...)
mount += "/home/joel/.Xauthority              $path/home/joel/.Xauthority              nullfs ro 0 0";
mount += "/home/joel/.Xresources              $path/home/joel/.Xresources              nullfs ro 0 0";
(...)
mount += "/home/joel/.config/iridium          $path/home/joel/.config/iridium          nullfs rw 0 0";
mount += "/home/joel/.librewolf               $path/home/joel/.librewolf               nullfs rw 0 0";
mount += "/home/joel/.mozilla                 $path/home/joel/.mozilla                 nullfs rw 0 0";
(...)

$ touch /usr/local/jails/containers/jbrowsers/home/joel/.Xauthority
$ touch /usr/local/jails/containers/jbrowsers/home/joel/.Xresources
(...)
$ mkdir -p /usr/local/jails/containers/jbrowsers/home/joel/.librewolf
$ mkdir -p /usr/local/jails/containers/jbrowsers/home/joel/.mozilla
$ mkdir -p /usr/local/jails/containers/jbrowsers/home/joel/.config/iridium
(...)
$ ln -s Downloads /usr/local/jails/containers/jbrowsers/home/joel/Téléchargements

Apply changes when everything is done.

# service jail restart jbrowsers

Solving KeePassXC connectivity issue

I have KeePassXC running on the host, with browser integration enabled. /tmp being accessible to the Jail via mount_null, Web browsers will still need the keepassxc-proxy program to access the KeePassXC socket.

# jexec jbrowsers pkg install keepassxc

For some reasons, LibreWolf can’t connect to KeePassXC when the extension is installed. On the contrary, Firefox has no issue. This is because LibreWolf lacks the “native-messaging-hosts” directory. And it seems to happen with most Firefox or Chrome forks. Anyway, copying the directory from a working Firefox instance solves the issue.

$ cp -pr ~/.mozilla/native-messaging-hosts ~/.librewolf/

Final thoughts

I have not tried accessing the webcam. This is not something I use, even on the host. But it should work as soon as the device is properly shared.

All browsers can access Web sites without issues. YouTube videos can be watched properly, even 4K ones. And volume management is no problem.

The last versions of my Jail and devs configuration are available here and there .

Now, my FreeBSD laptop is a Jail house that rocks! 🕺🎸