The Linux kernel's implementation of Filesystem in Userspace
requires root permissions, despite its use in unprivileged programs.
This has normally been solved via libfuse's setuid helper program
fusermount/fusermount3.
This does means that certain kinds of security policies cannot be applied,
specifically no_new_privileges process flag.
$ mkdir _lower _mnt
$ # Without no_new_privileges
$ fuse-overlayfs -o lowerdir=_lower _mnt
$ fusermount3 -u _mnt
$ # With no_new_privileges
$ setpriv --no-new-privs -- fuse-overlayfs -o lowerdir=_lower _mnt
/usr/bin/fusermount3: mount failed: Operation not permitted
fuse-overlayfs: cannot mount: Operation not permittedThe no_new_privileges flag is important for proper application sandboxing,
as Linux features such as landlock and seccomp-bpf can only be used
after a call to prctl(PR_SET_NO_NEW_PRIVS, 1).
Using Unix domain sockets like defused does also means that the FUSE-mounting
capability can be granted to applications by allow-listing the socket in the
application's AppArmor or Landlock configuration.
Doing so with fusermount3 is much more challenging, as it is not compatible
with Landlock.
Defused requires Linux 6.13 or later: the service authorizes unmounts by
resolving a client's pidfd to its pid via the PIDFD_GET_INFO ioctl, which
was added in 6.13. Support for older kernels is possible by falling back to
parsing /proc/self/fdinfo/<pidfd>'s Pid: line, but that fallback is not
currently implemented.
This project provides the following:
- A system service that listens on
/run/defused/defused.sock. - A replacement
fusermount3binary to communicate with the service.
The system service is written to use systemd socket activation with
Accept=yes.
For testing or on systems without systemd, defused --daemon can be used
to create the Varlink socket and fork off child processes to handle
accepted connections.
Root callers are delegated directly to libfuse's fusermount3, since they do
not need the unprivileged service path.
This means libfuse's fusermount3 should still be installed, just not in
/usr/bin (ex. /usr/lib/fuse3/fusermount3).
Defused uses a different mountpoint ownership model than libfuse's setuid
fusermount3.
For non-root mounts, the mountpoint must be a directory or regular file owned
by the caller.
It must be writable by that caller, and directories must also be searchable.
This means defused rejects mounts on writable shared directories owned by
another user, even when libfuse's setuid helper would allow them because the
directory is not sticky.
The stricter rule keeps the privileged service's authorization decision tied
to the mountpoint file descriptor it receives, instead of trying to reproduce
libfuse's path-based access(W_OK) check across the client/service protocol.
This does lead to some additional mounting possibilities, all due to other
filesystem restrictions.
If a given file path is owned by the user, but the process is unable to write
to the path due to POSIX ACLs, LSMs like SELinux, AppArmor, or Landlock,
libfuse's setuid implementation will deny the mount while this implementation
will still perform it.
I do not believe this is an issue, however, as sandboxed applications should
deny access to /dev/fuse or /run/defused/defused.sock.
See protocol.md for more information on how defused works.
I am using cachix as a binary cache:
# Add to nix.conf
extra-substituters = https://defused.cachix.org
extra-trusted-public-keys = defused.cachix.org-1:/YD+2Bmle49JSliBhGRqTKpLYhvruoFyMPPU071YCAY=
See contributing.md.
This project copies a some helpers from libfuse in util.h, which are either GPL-2.0-only or LGPL-2.1-only (marked via SPDX snippets in util.c). All of my code is licensed under GPL-2.0-or-later, but the resulting binary will be GPL-2.0-only.