1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 inherit (lib) types;
9 inherit (config.environment) etc;
10 cfg = config.security.apparmor;
11 enabledPolicies = lib.filterAttrs (n: p: p.state != "disable") cfg.policies;
12 buildPolicyPath = n: p: lib.defaultTo (pkgs.writeText n p.profile) p.path;
13
14 # Accessing submodule options when not defined results in an error thunk rather than a regular option object
15 # We can emulate the behavior of `<option>.isDefined` by attempting to evaluate it instead
16 # This is required because getting isDefined on a submodule is not possible in global module asserts.
17 submoduleOptionIsDefined = value: (builtins.tryEval value).success;
18in
19
20{
21 imports = [
22 (lib.mkRemovedOptionModule [
23 "security"
24 "apparmor"
25 "confineSUIDApplications"
26 ] "Please use the new options: `security.apparmor.policies.<policy>.state'.")
27 (lib.mkRemovedOptionModule [
28 "security"
29 "apparmor"
30 "profiles"
31 ] "Please use the new option: `security.apparmor.policies'.")
32 apparmor/includes.nix
33 apparmor/profiles.nix
34 ];
35
36 options = {
37 security.apparmor = {
38 enable = lib.mkEnableOption ''
39 the AppArmor Mandatory Access Control system.
40
41 If you're enabling this module on a running system,
42 note that a reboot will be required to activate AppArmor in the kernel.
43
44 Also, beware that enabling this module privileges stability over security
45 by not trying to kill unconfined but newly confinable running processes by default,
46 though it would be needed because AppArmor can only confine new
47 or already confined processes of an executable.
48 This killing would for instance be necessary when upgrading to a NixOS revision
49 introducing for the first time an AppArmor profile for the executable
50 of a running process.
51
52 Enable [](#opt-security.apparmor.killUnconfinedConfinables)
53 if you want this service to do such killing
54 by sending a `SIGTERM` to those running processes'';
55 policies = lib.mkOption {
56 description = ''
57 AppArmor policies.
58 '';
59 type = types.attrsOf (
60 types.submodule {
61 options = {
62 state = lib.mkOption {
63 description = "How strictly this policy should be enforced";
64 type = types.enum [
65 "disable"
66 "complain"
67 "enforce"
68 ];
69 # should enforce really be the default?
70 # the docs state that this should only be used once one is REALLY sure nothing's gonna break
71 default = "enforce";
72 };
73
74 profile = lib.mkOption {
75 description = "The profile file contents. Incompatible with path.";
76 type = types.lines;
77 };
78
79 path = lib.mkOption {
80 description = "A path of a profile file to include. Incompatible with profile.";
81 type = types.nullOr types.path;
82 default = null;
83 };
84 };
85 }
86 );
87 default = { };
88 };
89 includes = lib.mkOption {
90 type = types.attrsOf types.lines;
91 default = { };
92 description = ''
93 List of paths to be added to AppArmor's searched paths
94 when resolving `include` directives.
95 '';
96 apply = lib.mapAttrs pkgs.writeText;
97 };
98 packages = lib.mkOption {
99 type = types.listOf types.package;
100 default = [ ];
101 description = "List of packages to be added to AppArmor's include path";
102 };
103 enableCache = lib.mkEnableOption ''
104 caching of AppArmor policies
105 in `/var/cache/apparmor/`.
106
107 Beware that AppArmor policies almost always contain Nix store paths,
108 and thus produce at each change of these paths
109 a new cached version accumulating in the cache'';
110 killUnconfinedConfinables = lib.mkEnableOption ''
111 killing of processes which have an AppArmor profile enabled
112 (in [](#opt-security.apparmor.policies))
113 but are not confined (because AppArmor can only confine new processes).
114
115 This is only sending a gracious `SIGTERM` signal to the processes,
116 not a `SIGKILL`.
117
118 Beware that due to a current limitation of AppArmor,
119 only profiles with exact paths (and no name) can enable such kills'';
120 };
121 };
122
123 config = lib.mkIf cfg.enable {
124 assertions = lib.concatLists (
125 lib.mapAttrsToList (policyName: policyCfg: [
126 {
127 assertion = builtins.match ".*/.*" policyName == null;
128 message = "`security.apparmor.policies.\"${policyName}\"' must not contain a slash.";
129 # Because, for instance, aa-remove-unknown uses profiles_names_list() in rc.apparmor.functions
130 # which does not recurse into sub-directories.
131 }
132 {
133 assertion = lib.xor (policyCfg.path != null) (submoduleOptionIsDefined policyCfg.profile);
134 message = "`security.apparmor.policies.\"${policyName}\"` must define exactly one of either path or profile.";
135 }
136 ]) cfg.policies
137 );
138
139 environment.systemPackages = [
140 pkgs.apparmor-utils
141 pkgs.apparmor-bin-utils
142 ];
143 environment.etc."apparmor.d".source = pkgs.linkFarm "apparmor.d" (
144 # It's important to put only enabledPolicies here and not all cfg.policies
145 # because aa-remove-unknown reads profiles from all /etc/apparmor.d/*
146 lib.mapAttrsToList (name: p: {
147 inherit name;
148 path = buildPolicyPath name p;
149 }) enabledPolicies
150 ++ lib.mapAttrsToList (name: path: { inherit name path; }) cfg.includes
151 );
152 environment.etc."apparmor/parser.conf".text =
153 ''
154 ${if cfg.enableCache then "write-cache" else "skip-cache"}
155 cache-loc /var/cache/apparmor
156 Include /etc/apparmor.d
157 ''
158 + lib.concatMapStrings (p: "Include ${p}/etc/apparmor.d\n") cfg.packages;
159 # For aa-logprof
160 environment.etc."apparmor/apparmor.conf".text = '''';
161 # For aa-logprof
162 environment.etc."apparmor/severity.db".source = pkgs.apparmor-utils + "/etc/apparmor/severity.db";
163 environment.etc."apparmor/logprof.conf".source =
164 pkgs.runCommand "logprof.conf"
165 {
166 header = ''
167 [settings]
168 # /etc/apparmor.d/ is read-only on NixOS
169 profiledir = /var/cache/apparmor/logprof
170 inactive_profiledir = /etc/apparmor.d/disable
171 # Use: journalctl -b --since today --grep audit: | aa-logprof
172 logfiles = /dev/stdin
173
174 parser = ${pkgs.apparmor-parser}/bin/apparmor_parser
175 ldd = ${lib.getExe' pkgs.stdenv.cc.libc "ldd"}
176 logger = ${pkgs.util-linux}/bin/logger
177
178 # customize how file ownership permissions are presented
179 # 0 - off
180 # 1 - default of what ever mode the log reported
181 # 2 - force the new permissions to be user
182 # 3 - force all perms on the rule to be user
183 default_owner_prompt = 1
184
185 custom_includes = /etc/apparmor.d ${
186 lib.concatMapStringsSep " " (p: "${p}/etc/apparmor.d") cfg.packages
187 }
188
189 [qualifiers]
190 ${pkgs.runtimeShell} = icnu
191 ${pkgs.bashInteractive}/bin/sh = icnu
192 ${pkgs.bashInteractive}/bin/bash = icnu
193 ${config.users.defaultUserShell} = icnu
194 '';
195 footer = "${pkgs.apparmor-utils}/etc/apparmor/logprof.conf";
196 passAsFile = [ "header" ];
197 }
198 ''
199 cp $headerPath $out
200 sed '1,/\[qualifiers\]/d' $footer >> $out
201 '';
202
203 boot.kernelParams = [ "apparmor=1" ];
204 security.lsm = [ "apparmor" ];
205
206 systemd.services.apparmor = {
207 after = [
208 "local-fs.target"
209 "systemd-journald-audit.socket"
210 ];
211 before = [
212 "sysinit.target"
213 "shutdown.target"
214 ];
215 conflicts = [ "shutdown.target" ];
216 wantedBy = [ "multi-user.target" ];
217 unitConfig = {
218 Description = "Load AppArmor policies";
219 DefaultDependencies = "no";
220 ConditionSecurity = "apparmor";
221 };
222 # Reloading instead of restarting enables to load new AppArmor profiles
223 # without necessarily restarting all services which have Requires=apparmor.service
224 reloadIfChanged = true;
225 restartTriggers = [
226 etc."apparmor/parser.conf".source
227 etc."apparmor.d".source
228 ];
229 serviceConfig =
230 let
231 killUnconfinedConfinables = pkgs.writeShellScript "apparmor-kill" ''
232 set -eu
233 ${pkgs.apparmor-bin-utils}/bin/aa-status --json |
234 ${pkgs.jq}/bin/jq --raw-output '.processes | .[] | .[] | select (.status == "unconfined") | .pid' |
235 xargs --verbose --no-run-if-empty --delimiter='\n' \
236 kill
237 '';
238 commonOpts =
239 n: p:
240 "--verbose --show-cache ${
241 lib.optionalString (p.state == "complain") "--complain "
242 }${buildPolicyPath n p}";
243 in
244 {
245 Type = "oneshot";
246 RemainAfterExit = "yes";
247 ExecStartPre = "${pkgs.apparmor-utils}/bin/aa-teardown";
248 ExecStart = lib.mapAttrsToList (
249 n: p: "${pkgs.apparmor-parser}/bin/apparmor_parser --add ${commonOpts n p}"
250 ) enabledPolicies;
251 ExecStartPost = lib.optional cfg.killUnconfinedConfinables killUnconfinedConfinables;
252 ExecReload =
253 # Add or replace into the kernel profiles in enabledPolicies
254 # (because AppArmor can do that without stopping the processes already confined).
255 lib.mapAttrsToList (
256 n: p: "${pkgs.apparmor-parser}/bin/apparmor_parser --replace ${commonOpts n p}"
257 ) enabledPolicies
258 ++
259 # Remove from the kernel any profile whose name is not
260 # one of the names within the content of the profiles in enabledPolicies
261 # (indirectly read from /etc/apparmor.d/*, without recursing into sub-directory).
262 # Note that this does not remove profiles dynamically generated by libvirt.
263 [ "${pkgs.apparmor-utils}/bin/aa-remove-unknown" ]
264 ++
265 # Optionally kill the processes which are unconfined but now have a profile loaded
266 # (because AppArmor can only start to confine new processes).
267 lib.optional cfg.killUnconfinedConfinables killUnconfinedConfinables;
268 ExecStop = "${pkgs.apparmor-utils}/bin/aa-teardown";
269 CacheDirectory = [
270 "apparmor"
271 "apparmor/logprof"
272 ];
273 CacheDirectoryMode = "0700";
274 };
275 };
276 };
277
278 meta.maintainers = lib.teams.apparmor.members;
279}