Last updated:

Sandboxing Zoom using AppArmor and Firejail

It is no secret that the Zoom video conferencing system has dubious reputation for lying to its users, mistreating their data and censoring their speech. In spite of all that, it has become a standard communication tool that I am expected to use at work several times a week. Since avoiding it unfortunately is not an option that would be available to me, I decided to at least mitigate any potential damage from Zoom by running it inside a sandbox. I was inspired to do this after reading this article.

While there exist various technical implementations, all sandboxes generally achieve the same result – they let the user control individual capabilities of programs that are confined inside. Since Zoom relies on active Internet connection, I had no illusions that I would be able to increase the protection of my calls or deny the app from sending telemetry. Instead I decided to focus mostly on restricting its capability to snoop around my file system and collect information about me without my permission. The rest of this post documents the steps I took to achieve that.

Step 1: Install AppArmor

For this effort, I am using AppArmor – a Linux Security Module that runs directly in the kernel and acts as a middle man between user-space programs and the rest of the system. This gives it the capability to restrict anything interesting that programs might want to do (for instance read files or communicate with the rest of the World) while introducing as little overhead as possible.

After installing the AppArmor kernel module using your favorite package manager, bear in mind that you also need to ensure that it gets loaded on boot. In Archlinux this is done by enabling the apparmor.service in systemd and adding the following kernel parameters:

lsm=landlock,lockdown,yama,apparmor,bpf

Following a reboot, AppArmor should be loaded and ready for use. This can be verified by running:

$ aa-enabled
Yes

Step 2: Install Firejail

AppArmor on its own is just a kernel module, not a sandbox. To supplement it I am using Firejail – a namespace-driven sandboxing program that can exploit AppArmor to efficiently enforce user-defined policies. These policies can be viewed as lists of rules that explicitly define what the confined program is allowed to do.

After Firejail is installed using your favorite package manager, you will need to put together a profile that works for Zoom. This is the balancing act that entails the most trial and error. Fortunately, Firejail profile syntax is really easy to learn, and there exist many examples for inspiration. Based on what I found on GitHub, I put together the following profile that I saved in ~/.config/firejail/zoom.local, ensuring that it is automatically applied whenever the zoom binary is executed:

protocol unix,inet,inet6,netlink
ignore seccomp
seccomp !chroot

This is what the profile above translates to:

  • Zoom is permitted to use network sockets of the UNIX, IPv4, IPV6 and Netlink families.
  • Override any previous setting enabling seccomp syscall filters.
  • Explicitly enable seccomp filter and configure it to disallow the chroot syscall.

Note that this configuration is overlaid on any other default profiles that come with your installation of Firejail. In my environment, Firejail applies 13 other profiles that blacklist user directories and whitelist common library paths etc.

Step 3: Create a wrapper script

The next step is to create a shell script that runs Zoom within the Firejail sandbox. This script can then be used instead of the Zoom client’s original binary.

My script looks like this:

#!/bin/bash
# A wrapper that confines zoom in a sandbox, faking home directory in ~/.zoom

QT_QPA_PLATFORM=xcb # NOTE: needed otherwise zoom crashes on Wayland :/

function cleanup()
{
	# Auto-mute after zoom terminates
	pactl set-source-mute 1 1
}

trap cleanup EXIT
firejail --apparmor --private=${HOME}/.zoom zoom ${@}

Let me explain the code above bit by bit:

  • QT_QPA_PLATFORM=xcb is a compatibility hack that is needed because I use a Wayland compositor. It forces Zoom to fallback to a X11 frontend with Xwayland instead of using its native Wayland frontend. While the latter option would of course be preferable, it unfortunately seems to be unstable at this time.

  • The cleanup function always runs after Zoom exits, and makes sure that my microphone stays muted when I am not in a meeting. I have noticed that Zoom has the nasty habit of unmuting the microphone whenever I unmute myself in meetings, and not bothering to restore the original state afterwards. I would often forget to do this manually, so I created a function that does this on my behalf using PulseAudio1.

  • The firejail call is where the most time is spent, as it blocks throughout the entire lifetime of the Zoom client. As you suspect, the --apparmor flag instructs Firejail to enforce the policy using the AppArmor kernel module2. The --private flag hides my home directory from Zoom and replaces it with a blank directory that I created in ~/.zoom. The ${@} variable forwards all shell options to the zoom executable; more about that later.

You may be interested to know that over my multi-year usage of Zoom, it has created the following file tree inside my decoy home directory:

.zoom
├── data
│   ├── com.zoom.ipc.assistantapp__req
│   ├── com.zoom.ipc.assistantapp__res
│   ├── com.zoom.ipc.confapp__req
│   ├── com.zoom.ipc.confapp__res
│   ├── zoomus.enc.db
│   ├── zoomus.enc.db.malformed
│   └── zoomus.tmp.enc.db
├── Downloads
├── im
├── logs
│   └── zoom_stdout_stderr.log
├── reports
└── screenCapture

Step 4: Replace Zoom with the wrapper

The final step involves replacing all links to the original Zoom client with links to the wrapper script. In my case, this meant creating a secondary XDG desktop entry that mimics the contents of /usr/share/applications/Zoom.desktop that is by default installed along with Zoom.

I saved my variant to ~/.local/share/applications/JailedZoom.desktop, so that it is discovered automatically by launchers etc. The contents of the file are as follows.

[Desktop Entry]
Name=Zoom (jailed)
Comment=Zoom Video Conference (confined to firejail)
Exec=/home/petr/bin/pm_jailed_zoom %U
Icon=Zoom
Terminal=false
Type=Application
Encoding=UTF-8
Categories=Network;Application;
StartupWMClass=zoom
MimeType=x-scheme-handler/zoommtg;x-scheme-handler/zoomus;x-scheme-handler/tel;x-scheme-handler/callto;x-scheme-handler/zoomphonecall;application/x-zoom
X-KDE-Protocols=zoommtg;zoomus;tel;callto;zoomphonecall;
Name[en_US]=Zoom

Note that my wrapper script is located at ~/bin/pm_jailed_zoom, you may want to customize the Exec path on your system. Also notice that the %U wildcard makes it possible for meeting URLs to be passed to the wrapper, which then forwards it to Zoom inside Firejail through the ${@} variable. This means that one-click meeting URLs should still work with this setup.

After adding the custom desktop entry it is necessary to rebuild the desktop database using the following command:

$ update-desktop-database ~/.local/share/applications

You can then set JailedZoom.desktop to be the default handler for Zoom URLs:

$ xdg-mime default JailedZoom.desktop x-scheme-handler/zoommtg

…and verify by re-querying:

$ xdg-mime query default x-scheme-handler/zoommtg
JailedZoom.desktop

Conclusion

If you reached this point and did everything right, you now have the capability to confine Zoom in a sandbox that keeps it away from your data. In my opinion, this is the least worst way of running Zoom on your system if you have to like me.

Just a couple of obligatory security disclaimers:

  1. Even with this setup Zoom is still present on your system, and can be executed outside of the sandbox if you (or a malicious actor) run it directly without the wrapper script.
  2. While these measures increase protection of your file system, they do not affect your communications conducted over Zoom. You should still treat Zoom as an untrustworthy channel.
  3. I am not a security engineer. This setup may be sufficient for my purposes, but you may want to consider consulting an expert.

In spite of all this effort I would still recommend not using Zoom at all. To anyone who listens, you can get the same set of features without any of the downsides with Jitsi for free.


  1. The "1 1" audio source corresponds to my laptop’s built-in microphone. If you would like to use the auto-muting feature, you may need to choose a different identifier. Run pactl list sources to retrieve a complete list of sinks available in your system. ↩︎

  2. Firejail can sandbox programs even without AppArmor. This, however, comes at the expense of narrower rule support and slightly increased user-space overhead. ↩︎