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… :)
- List the running jail(s):
# jls
- Start a jail:
# service jail start jbrowsers
- Stop a jail:
# service jail stop jbrowsers
- Restart a jail:
# service jail restart jbrowsers
- Destroy a jail:
# service jail stop jbrowsers
# chflags -R 0 /usr/local/jails/containers/jbrowsers
# rm -rf /usr/local/jails/containers/jbrowsers
- Log into a jail:
# jexec -u root jbrowsers
# jexec -U joel jbrowsers
- Run command(s) in a jail:
# pkg -j jbrowsers install vim
# jexec -l jbrowsers service pf status
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! 🕺🎸