Updated: August 8, 2022
Arguably, browsers are the weakest link in one's overall security stack, other than the user, that is. In other words, should something naughty happen to your computer, it's most likely going to involve your browser. To that end, browser vendors as well as operating system companies pay a lot of attention to making this delicate piece of software robust, secure, and isolated from the rest of the system.
A few days ago, I had some spare time, and I sat wondering if there was a way to make browsers in Linux extra secure, especially if one uses custom installations or setups? This led me to tinker with AppArmor, a security framework available in a bunch of Linux distributions. In this article, I want to show you what you can do to make your browser (Firefox) extra hardened. Let's go.
Custom Firefox releases
Most distributions ship Firefox as a default browser, packaged neatly and whatnot. Some distros also provide security hardening out of the box, including the AppArmor profile(s). But what happens if you want to install another instance of Firefox, for example? Say, you want to try the Firefox Dev or Nightly builds, and you want to use them from say inside the ~/Applications folder? This is doable, of course, because Firefox is available for download as a single archive, which you can extract anywhere and use as is.
My quest thus focused on figuring out how to isolate and harden Firefox if it sits outside the usual locations, like /usr/bin, or if it's deployed manually rather rather than through the system's tooling, whatever it may be. On one hand, the exercise sounds trivial. On the other, it was tricky enough to warrant this article.
Quick AppArmor overview
I do not want to turn this tutorial into an AppArmor guide. That is way too complicated. Indeed, AppArmor is a super-complex and nerdy tool, and it requires a lot of expertise to fully understand and master. This makes my job today hard, because I need to try to convey my message without forcing you to spend several long hours carefully reading the ins and out of AppArmor.
Anyway, to wit, this is what you need to know, for now:
- AppArmor is a Mandatory Access Control (MAC) system used to confine programs to a limited set of resources, used in distributions like Ubuntu or openSUSE.
- The confinement is defined per application, through a set of policies listed in a file called profile.
- Each profile is a text file written in AppArmor syntax and saved under /etc/apparmor or /etc/apparmor.d directories.
- The profile syntax is usually a dot-separated path for the application, e.g. usr.bin.firefox.
- AppArmor can run in two modes - enforcement and complain. The former is basically a "hard" mode that will force applications to obey the profile rules, while the later is a "soft" mode that will log violations into a system log file, but not stop them.
- AppArmor profile loading and run mode can be governed through apparmor* and system service commands, which we will discuss a bit later.
How to confine Firefox
The easiest way to get going is to grab an existing Firefox AppArmor profile, either from the Web or a system that has one in place. For instance, I decided to use the usr.bin.profile from my Kubuntu 18.04 system on the Slimbook laptop as my starting point. I created a copy of the file:
sudo cp /etc/apparmor.d/usr.bin.firefox /etc/apparmor.d/home.roger.applications.firefox
The path /home/roger/Applications/Firefox is where I intend to keep a non-standard build of Firefox, either the Dev and Nightly version, downloaded directly from the Mozilla site. The extracted archive goes there. The actual name of the AppArmor profile can be anything - what matters are the rules inside - but it helps using the correct path syntax as it's easier to understand what the profile covers.
Next I opened the file in a text editor and made modifications. Notably, the paths where the Firefox application resides. This is what the default usr.bin.firefox profile includes:
# Declare an apparmor variable to help with overrides
@{MOZ_LIBDIR}=/usr/lib/firefox
#include <tunables/global>
# We want to confine the binaries that match:
# /usr/lib/firefox/firefox
# /usr/lib/firefox/firefox
# but not:
# /usr/lib/firefox/firefox.sh
profile firefox /usr/lib/firefox/firefox{,*[^s][^h]} {
#include <abstractions/audio>
#include <abstractions/cups-client>
#include <abstractions/dbus-strict>
...
We want to change the MOZ_LIBDIR variable and the binary path:
# Declare an apparmor variable to help with overrides
#@{MOZ_LIBDIR}=/usr/lib/firefox
@{MOZ_LIBDIR}=/home/roger/Applications/Firefox
#include <tunables/global>
# We want to confine the binaries that match:
# /usr/lib/firefox/firefox
# /usr/lib/firefox/firefox
# but not:
# /usr/lib/firefox/firefox.sh
#profile firefox /usr/lib/firefox/firefox{,*[^s][^h]} {
profile firefox /home/roger/Applications/Firefox/firefox{,*[^s][^h]} {
#include <abstractions/audio>
#include <abstractions/cups-client>
#include <abstractions/dbus-strict>
...
I merely commented out the original lines rather than deleted them, so that if I want or need to debug the profile, I have the reference in place. Indeed, what matters is the path that the profile affects. In my case, Firefox resides under: /home/roger/Applications/Firefox. But it can be anything you like.
In essence, this is all you need, for now.
Load the AppArmor profile (and resolve errors)
The next step is to load the new Firefox profile into memory. Keep the browser closed. Next, run the AppArmor parser command:
sudo apparmor_parser -r "file"
For instance:
sudo apparmor_parser -r home.roger.applications.firefox.firefox
The reason why we want to do this rather than reload or restart the service (which would read and parse all available profiles) is that if you encounter errors, they will be shown on the command line output right there, the execution won't affect other profiles being used, and you will not need to go into the system logs to decipher any possible errors.
This is what you will see if you try to restart the service with an invalid profile in place:
systemd[1]: Reloading Load AppArmor profiles...
apparmor.systemd[9975]: Restarting AppArmor
apparmor.systemd[9975]: Reloading AppArmor profiles
apparmor.systemd[9995]: Skipping profile in /etc/apparmor.d/disable: usr.sbin.rsyslogd
apparmor.systemd[10014]: AppArmor parser error for /etc/apparmor.d/home.roger.applicatio>
apparmor.systemd[10070]: Skipping profile in /etc/apparmor.d/disable: usr.sbin.rsyslogd
apparmor.systemd[9975]: Error: At least one profile failed to load
systemd[1]: apparmor.service: Control process exited, code=exited, status=1/FAILURE
systemd[1]: Reload failed for Load AppArmor profiles.
Yes, it says error for "profile name" - but not the details. With apparmor_parser, you will get an exact list of errors, line by line:
sudo apparmor_parser -r home.roger.applications.firefox.firefox
Found reference to variable USER_DIR, but is never declared
Here, you do need a little bit of expertise, after all. Now, you should edit your profile and make the necessary adjustments. Like say changing the USER_DIR variable, and commenting out any clauses that do not apply to your custom installation of Firefox. Again, as an example:
sudo apparmor_parser -r home.roger.applications.firefox.firefox
Found reference to variable CONFIDENTIAL_EXCEPT_MOZILLA, but is never declared
Once you successfully resolve all of these issues (typically just commenting out extra lines), the parser command will exist without any output. There are no shortcuts here, I'm afraid, and this is where the bulk of AppArmor homework goes. At some point, you will need to understand the framework to be able to use it in a meaningful way.
Start browser, use, test, resolve errors, repeat
You should now be able to launch Firefox from your custom path, and it should run without problems. The emphasis is on the word should, because it is possible that there could be conflicting rules in your profile, which may prevent Firefox from working normally. Here, some expertise is needed, and there is no silver bullet. However, my testing shows that you need a minimal set of changes to make it work, notably:
- Firefox binary path.
- LIB_DIR variable.
- Comment out any rules that apparmor_parser complains about (say if not relevant for your distro). An example would be:
# Addons
##include <abstractions/ubuntu-browsers.d/firefox>
How do you know if Firefox is confined?
You can do that by checking the status of the AppArmor service:
sudo apparmor_status
The output should tell you the list of all the profiles in enforce and complain modes:
0 profiles are in kill mode.
0 profiles are in unconfined mode.
14 processes have profiles defined.
14 processes are in enforce mode.
/usr/sbin/cups-browsed (1602)
/usr/sbin/cupsd (681)
/usr/lib/cups/notifier/dbus (1624) /usr/sbin/cupsd
/usr/lib/cups/notifier/dbus (1625) /usr/sbin/cupsd
/usr/sbin/haveged (577)
/home/roger/Applications/Firefox/firefox-bin (4226) firefox
/home/roger/Applications/Firefox/firefox-bin (4340) firefox
/home/roger/Applications/Firefox/firefox-bin (4364) firefox
/home/roger/Applications/Firefox/firefox-bin (4388) firefox
/home/roger/Applications/Firefox/firefox-bin (4430) firefox
/home/roger/Applications/Firefox/firefox-bin (4480) firefox
/home/roger/Applications/Firefox/firefox-bin (4484) firefox
/home/roger/Applications/Firefox/firefox-bin (4485) firefox
/home/roger/Applications/Firefox/firefox-bin (4587) firefox
0 processes are in complain mode.
0 processes are unconfined but have a profile defined.
0 processes are in mixed mode.
0 processes are in kill mode.
In my case, Firefox from the custom path is isolated (in the enforce mode).
How does confinement work?
Even though AppArmor says Firefox is confined, you can verify that. You need to perform an operation that is not explicitly allowed in the rules, which will then result in an error. For instance, in my profile, Firefox is only allowed to write files to the Downloads directory. This means, if I try to create a folder like Wallpapers in the home directory (to save images for desktop backgrounds), I will see an error:
The corresponding rules are:
# Default profile allows downloads to ~/Downloads and uploads from ~/Public
owner @{HOME}/ r,
owner @{HOME}/Public/ r,
owner @{HOME}/Public/* r,
owner @{HOME}/Downloads/ r,
owner @{HOME}/Downloads/* rw,
But we can change that to:
# Default profile allows downloads to ~/Downloads and uploads from ~/Public
owner @{HOME}/ r,
owner @{HOME}/Public/ r,
owner @{HOME}/Public/* r,
owner @{HOME}/Downloads/ r,
owner @{HOME}/Downloads/* rw,
owner @{HOME}/Wallpapers/ r,
owner @{HOME}/Wallpapers/* rw,
The user still cannot create the Wallpapers directory from inside the browser, but they can save files to it. Of course, it is entirely your choice to define what the browser can and cannot do. This crude example allows you to understand the confinement, and see that it's actually active.
Things can become trickier if you need things like camera, mic, USB access, and more. But then, there are no two ways about it. If you want to use AppArmor to confine your browser, you need to understand what it does. And using an existing profile that already works (well) for the distro-supplied version of Firefox is a good starting point. Then, once you get comfortable with the concept, you can extend this to other applications, including other browsers and whatnot.
Conclusion
This brings me to the end of this tutorial, but not the end of this journey. There are many more things to cover, and cover them we shall in future articles. For now, this little guide should give you some basic grasp of how AppArmor works and how you can manually utilize it outside the predefined set of programs shipped by your distro (if any).
I focused on Firefox, because it's my favorite browser, and as an Internet-facing tool, it takes priority over something like your calculator. But the same logic applies to other browsers or programs of similar nature. Well, that would be all for now. Stay tuned for updates [sic]. Why [sic]? You will see in the sequel.
Cheers.