AppArmor and apparmor.d on NixOS
Resources and general info
AppArmor will be shortened as aa.
AppArmor is a MAC [Mandatory Access Control]. As such, aa is a security-relevant component. This article will not guarantee your security. You are responsible for that yourself. This article is meant for people to learn something useful and potentially new.
While researching aa, there have been a few good resources. Incredibly useful was the SuSe enterprise AppArmor documentation.
AppArmor: Design and Goals
AppArmor is an attempt to restrict what parts of the OS can be
touched by different Programs, enforced by the Linux kernel. For this
purpose, a set of rules is defined in /etc/apparmor.d/ and
loaded into use with aa-complain oraa-enforce.
In NixOS, this can be done decoratively. More to NixOS specifics
later.
User space programs are by default not very separated from each other. This means (by default) all my installed apps with outdated electron (totally not discord :P) have the capability to access and exfiltrate my ssh and gpg keys. Needless to say, this is bad. Controlling what processes my user space apps can launch and what files they can access is generally an improvement.
AppArmor itself comes with very minimal rules. This makes sense, as rules differ between Linux distributions, and any rule set will ultimately be opinionated. This is however also a problem: No pre-built rules means aa is very inaccessible to DIY. My personal goal is to get aa to a state where i can start incrementally increasing coverage, while keeping rules quite strict.
Profiles
The smallest unit in aa is a profile. A profile defines a
pattern of an executable path for which the profile is valid, as well as
what that profile can access. What profiles are installed on a system
can be checked using aa-status command.
Abstractions
Further, aa supports abstractions. Typically (but not
necessarily) placed in /etc/apparmor.d/abstractions, they
are importable reusable snippets that can be reused across profiles.
Most important is the abstraction <abstractions/base>
which is an abstraction typically included in all profiles. For example
it might make sense to allow any program on a system to read
/share/zoneinfo, so that would go into the base
abstraction. Other abstractions, for example for graphics or audio, make
sense too.
Tunables
Tunables are typically stored in
/etc/apparmor.d/tunables and contain variables. Tunables
are a subset of abstractions that can be included but don’t actually
contain rules. Rather, the tunables only contain variable definitions.
It can for example make sense to define a @{bin} variable
instead of hard-coding {/usr,}/bin everywhere, as the
change of the variable is instantly applied about all profiles using
that variable.
Why AppArmor is a better choice than SE Linux on NixOS
Both SE Linux and AppArmor are in active use to harden Linux systems across the industry. I do not claim either to be more reliable or more secure, but the work required to use those solutions in the nix ecosystem is vastly different. SE Linux is label-based, leveraging xattrs. Defining those attributes on the nix store is not trivial, requiring significant work. AppArmor is path-based, with profiles that match patterns against paths. This is the case both for matching which profile to use via arg0, as well as what access is granted via rules defining path access.
AppArmor on NixOS: A nightmare
AppArmor on NixOS has one huge issue on NixOS: LACK OF RULES. It is obviously absurd to assume rules working on Ubuntu would work on NixOS, right?? Well! apparmor.d is a project with the ultimate goal of thorough and strict rules for a LOT of apps on a LOT of Linux distributions. So getting those rules to work would solve that issue. And it is very much possible to make it work!
Some smaller issues than just lack of rules exist: AppArmor logging
is broken by default. However, it plays nicely with
audit.d. Further, a lot of apps try to read their own
resources, or try to load shared object files using mmap
syscall. Both is by default denied in apparmor. However, the
/nix/store is world readable anyways, and also not supposed
to contain any sensitive information. Thus it makes sense to allow read
on the whole store, as well as mmap for lib and bin paths
in the store. This makes for some configuration in NixOS already:
security.auditd.enable = true;
security.apparmor.packages = [ apparmor-d ];
security.apparmor.enable = true;
security.apparmor.includes = {
"abstractions/base" = ''
/nix/store/*/bin/** mr,
/nix/store/*/lib/** mr,
/nix/store/** r,
${getExe' pkgs.coreutils "coreutils"} rix,
${getExe' pkgs.coreutils-full "coreutils"} rix,
'';
};Because security.apparmor.includes.<name> is a is
of strings joined by a newline, we can just add that part without
outright overriding the base abstraction defined
in the nixos modules. This should be a safe thing to do. I
don’t really like it personally, but this really should be
fine.
Further, a LOT of things depend on coreutils. Therefore i chose to add the coreutils binary as something that can always be executed if staying in the limitations of the original profile (ix = execute inherit).
With auditd, logs will end up in
/var/log/audit/audit.log which you can tail-follow when
doing testing.
Now to fix the lack of rules!
Basic Rule in NixOS
security.apparmor.policies = {
hello = {
enable = true;
enforce = true;
profile = ''
abi <abi/4.0>,
include <tunables/global>
profile hello ${lib.getExe pkgs.hello} {
include <abstractions/base>
}
'';
};
include <tunables/global> imports some useful
variables for later. include <abstractions/base> is
the earlier mentioned base include that should be part of (almost) any
profile. abi <abi/4.0>, defines the abi version with
which that profile is compatible. All my profiles have only been tested
on 4.x versions of apparmor, just be aware this breaks on NixOS 24.05 as
aa is on 3.x versions there.
Preventing boot issues
Notice the NixOS options enable and enforce. DO NOT put enforce true for system components without first checking the log is clean using complain mode for a week or so. Enforcing rules on system components can leave the system unable to boot. This isn’t particularly bad with NixOS keeping old generations around, but you should not tempt fate.
Yes, AppArmor can block important system components. That is the whole point. I’d personally advise to always include an escape hatch for when things go awfully wrong. This can be done with a NixOS specialisation that just force-disables aa:
specialisation.no-apparmor.configuration = {
security.apparmor.enable = lib.mkForce false;
};apparmor.d
The apparmor.d project and its documentation has been immensely useful. AppArmor requires rules to make any sense. Writing rules by hand is possible, but ideally a set of community-maintained rules would be available on NixOS. apparmor.d is one such set of community-maintained rules, and also structured in a quite extendable way. THis is why i want to try adapting aa.d rules for NixOS, both as a feasibility study as well as to actively use those rules on a daily basis.
Getting apparmor.d ready for NixOS
apparmor.d needs a package, that is just the most convenient way to make use of nix tooling. A naive approach is to just take the repo and throw the relevant folder containing all the profiles and abstractions into the store. An example of this can be found on my gitea.
This is not enough by itself: apparmor.d assumes /bin,
/usr/bin, /sbin or /usr/sbin to
be the only relevant paths for executables. Similar assumptions were
made for libraries. Needles to say, those assumptions do not work on
NixOS. This means we have to patch apparmor.d. Conveniently, aa and
apparmor.d both support and make use of variables. This means we only
have to patch the variable definitions for @{lib} and
@{bin} in apparmor.d, we do not have to patch all the rules
individually! The easy patching in a central location is part of why i
chose apparmor.d for this. That patch
can also be found on my gitea.
Eventually package and patch will be upstreamed. However, there is more complex issues with this approach that i am currently working on fixing. That includes making use of the intended distribution-specific tooling already provided by aa.d and improving the user experience for people just writing their nixos config.
using apparmor.d profiles on NixOS
Following codesnippets assume apparmor-d variable in nix
to contain the package derivation, e.g. by let-binding
apparmor-d = pkgs.callPackage ./apparmor-d.nix {};.
First, apparmor.d needs to be added to the include paths of apparmor, or else the apparmor.d abstractionsand tunables would not be importable. This can be achieved by adding the apparmor-d package to the apparmor packages:
security.apparmor.packages = [ apparmor-d ];Note the simple addition of the package to
security.apparmor.packages does not register and
activate the profiles!
Including the profiles into NixOS can be done using (clunky) include useage:
security.apparmor.policies = {
spotify = {
enable = true;
enforce = true;
profile = ''
include "${apparmor-d}/etc/apparmor.d/profiles-s-z/spotify"
'';
};
};The UX for this is terrible, essentially digging through the apparmor.d github for whether there is a profile for an app, then copying the path over. Ideally this can be improved, but for now this works.
fixing up profiles that broke
For spotify, this profile just works. But some other apps might not be so lucky. Let’s take a look into forefox.
First, firefox is actually not one profile, but two. Second, i run the gnupass extension as password manager in firefox. This requires some tricks.
security.apparmor.policies = {
passff = { # passff profile is the profile of the plugin helper
enable = true;
enforce = true;
profile = ''
abi <abi/4.0>,
include <tunables/global>
profile passff ${pkgs.passff-host}/share/passff-host/passff.py {
include <abstractions/base> # read access to /nix/store, basic presets for most apps
include <abstractions/python>
${getExe pkgs.pass} Px, # allowed to execute pass and jump into pass profile
}
'';
};
firefox-glxtest = {
enable = true;
enforce = true;
profile = ''
include "${apparmor-d}/etc/apparmor.d/groups/browsers/firefox-glxtest"
'';
};
firefox = {
enable = true;
enforce = true;
profile = ''
include "${apparmor-d}/etc/apparmor.d/groups/browsers/firefox"
'';
};
pass = {
enable = true;
enforce = true;
profile = ''
include "${apparmor-d}/etc/apparmor.d/profiles-m-r/pass"
'';
};
};
security.apparmor.includes = {
"local/pass" = ''
${getExe' pkgs.pass ".pass-wrapped"} rix, # nix wrapping means we have to allow the wrapper executable to call the wrapped
'';
"local/pass_gpg" = ''
owner @{PROC}/@{pid}/fd/ r,
/nix/store/*/libexec/keyboxd ix, # required for password transfer from askpass
owner /run/user/*/gnupg/S.keyboxd wr,
'';
"local/firefox" = ''
${pkgs.passff-host}/share/** rPx -> passff, # Px means a profile transfer into passff profile
@{HOME}/.mozilla/firefox/** mr, # mmap and read access to the ff profile folder
'';
};Variables, common patterns, common pitfalls
apparmor.d also comes with a lot of useful variables. For example,
@{pid} can match process ids. Note it does not
match a specific PID, only the format of a pid. Therefore
process access is commonly restricted using
owner @{PROC}/@{pid}/... to make sure at least the user
owning the process matches. The variables @{bin} or
@{lib} were those that were patched earlier. Using the
variables can help if the profiles should be upstreamable to aa.d.
Execute flags always contain a note about how profiles should work on
executing new subprocesses. Px means execute and jump to
the profile matching that executable. Cx -> profile_name
means execute and use the named profile for that executed programm.
ix means to just inherit the profile of the process
starting that program. ix is often pretty safe, but might
not be sufficient. Or a bad idea if you do not want to further widen the
scope of permissions of the main process doing the execute. Keep in
mind execute permission flags can not contradict. Only specifically
defined apps take priority. Because of how we patched
@{bin} to always contain a glob on the nix store,
@{bin}/hello is not specifically defined.
Therefore, be careful when adding specific execute flags. In the case
ofhello, the nix evaluation can be used to make it
specific: ${lib.getExe pkgs.hello}.
Nix has a lot of package wrapping, especially in userspace.
This causes issues with aa.d, as the wrapped apps might not have a
profile: None of the other distros does such extensive wrapping!
Typically, apps using wrappers need an addition to their profile,
letting them execute the wrapped programm while allowing ix
(inherit execute).
Another common issue is missing profiles to jump into. Notice firefox and firefox-glxtest being two profiles. This is because of a profile transition defined in the firefox profile. AppArmor will complain about transitions into non-existent profiles. Check your logs!
Making apparmor.d easier to use
The naive approach or just copying the rule files over does
technically work. It does however have a couple significant drawbacks.
First, just copying over the rule files bypasses any type of platform
specific setups. Part of why apparmor.d is so flexible is the fact it
has a preprocessor to collect and adjust profiles to conform todistro
specifics. The owner of the apparmor.d project has confirmed they are
indeed willing to support nixos, assuming the workflow on their side is
about comparable to the other supported distros. This means the package
should actually call the intended build processes instead of just
copying files. Second, the patch to match /nix/store/*/bin
causes a lot of problems with conflicting paths: This being a glob, any
rule referring to @{bin}/something ix, is potentially a
conflicting x flag. This can be fixed by replacing the glob with
character matching. This has issues too, most notably apparmor rule
parsing will become extremely slow, but it does work. Third, the syntax
to use apparmor.d profiles on nixos is awkward at best.
To solve those issues, i propose exposing an apparmor.d nixos module that can contain some necessary local overrides, as well as provide decent UI by managing the awkward imports.
An early iteration of this can be found on my gitea at https://git.grimmauld.de/Grimmauld/grimm-nixos-laptop/src/commit/0fc6f9d53bd82aaad7ce9835241c753e2b145b6f/common/tooling/apparmor, but i am still iterating on this. PRs into nixpkgs will come after 24.11 branchoff has happened.
Closing thoughts
apparmor.d is very powerful as a repository for aa rules. It currently supports Ubuntu, Debian, Open SUSE and Arch. Getting NixOS support would be nice.
The tooling for using apparmor.d on NixOS is awkward, but flexible enough to actually work. (Ab)using local overrides as well as the patching of the bin and lib variables makes the touch-ups minimal. Still necessary, but significantly more manageable than writing everything by hand.
Currently NixOS-defined apparmor profiles do not support
being in any other path than flat /etc/apparmor.d/.. This
is a superficial limitation by an assert in the apparmor module of
NixOS, i believe that limitation to be bogus. Yes it has an explanatory
comment. I still don’t think an arbitrary assert without further thought
might not be the right approach.
It might be worth to provide a more thorough and easier-to-use
integration of apparmor.d into NixOS, but it will likely always require
(small) manual fixes for a lot of apps. I am not sure who should
reliably maintain those fixes. Up-streaming to apparmor.d might be the
best option, but potentially also not viable seeing how nix does a lot
of app wrapping.
I have been in contact with roddhjav, owner of the apparmor.d project. They have helped a lot in my attempts to bring apparmor rules to NixOS, and are willing to support NixOS with their set of rules. I had many questions, many of which stupid, while working on this project. I am truly grateful this work is actually being appreciated, both by the NixDA community as well as from apparmor.d side, and some other Linux people i talked to. I now fully believe this can work. Obviously with quite some effort, but not unreasonable.
As to which side should support which components: I believe NixOS modules should be responsible for integrating apparmor profiles in a way where a NixOS user can easily switch which profiles are enforced/complaining/disabled. NixOS should further be responsible to keep track of any wrappers that need extra rules (typically wrappers just need to inherit rules). Apparmor.d should be responsible for cross-distribution compatibility and maintaining the actual core rules and abstractions, as is the case already. This does not mean nix should just take these rules as guaranteed: We should expect to also update some rules. Apparmor.d is similarly community-maintained as nix is, using their rules and expecting them to fix everything without interacting with them will not work.