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 ${if cfg.enableCache then "write-cache" else "skip-cache"}
154 cache-loc /var/cache/apparmor
155 Include /etc/apparmor.d
156 ''
157 + lib.concatMapStrings (p: "Include ${p}/etc/apparmor.d\n") cfg.packages;
158 # For aa-logprof
159 environment.etc."apparmor/apparmor.conf".text = '''';
160 # For aa-logprof
161 environment.etc."apparmor/severity.db".source = pkgs.apparmor-utils + "/etc/apparmor/severity.db";
162 environment.etc."apparmor/logprof.conf".source =
163 pkgs.runCommand "logprof.conf"
164 {
165 header = ''
166 [settings]
167 # /etc/apparmor.d/ is read-only on NixOS
168 profiledir = /var/cache/apparmor/logprof
169 inactive_profiledir = /etc/apparmor.d/disable
170 # Use: journalctl -b --since today --grep audit: | aa-logprof
171 logfiles = /dev/stdin
172
173 parser = ${pkgs.apparmor-parser}/bin/apparmor_parser
174 ldd = ${lib.getExe' pkgs.stdenv.cc.libc "ldd"}
175 logger = ${pkgs.util-linux}/bin/logger
176
177 # customize how file ownership permissions are presented
178 # 0 - off
179 # 1 - default of what ever mode the log reported
180 # 2 - force the new permissions to be user
181 # 3 - force all perms on the rule to be user
182 default_owner_prompt = 1
183
184 custom_includes = /etc/apparmor.d ${
185 lib.concatMapStringsSep " " (p: "${p}/etc/apparmor.d") cfg.packages
186 }
187
188 [qualifiers]
189 ${pkgs.runtimeShell} = icnu
190 ${pkgs.bashInteractive}/bin/sh = icnu
191 ${pkgs.bashInteractive}/bin/bash = icnu
192 ${config.users.defaultUserShell} = icnu
193 '';
194 footer = "${pkgs.apparmor-utils}/etc/apparmor/logprof.conf";
195 passAsFile = [ "header" ];
196 }
197 ''
198 cp $headerPath $out
199 sed '1,/\[qualifiers\]/d' $footer >> $out
200 '';
201
202 boot.kernelParams = [ "apparmor=1" ];
203 security.lsm = [ "apparmor" ];
204
205 systemd.services.apparmor = {
206 after = [
207 "local-fs.target"
208 "systemd-journald-audit.socket"
209 ];
210 before = [
211 "sysinit.target"
212 "shutdown.target"
213 ];
214 conflicts = [ "shutdown.target" ];
215 wantedBy = [ "multi-user.target" ];
216 unitConfig = {
217 Description = "Load AppArmor policies";
218 DefaultDependencies = "no";
219 ConditionSecurity = "apparmor";
220 };
221 # Reloading instead of restarting enables to load new AppArmor profiles
222 # without necessarily restarting all services which have Requires=apparmor.service
223 reloadIfChanged = true;
224 restartTriggers = [
225 etc."apparmor/parser.conf".source
226 etc."apparmor.d".source
227 ];
228 serviceConfig =
229 let
230 killUnconfinedConfinables = pkgs.writeShellScript "apparmor-kill" ''
231 set -eu
232 ${pkgs.apparmor-bin-utils}/bin/aa-status --json |
233 ${pkgs.jq}/bin/jq --raw-output '.processes | .[] | .[] | select (.status == "unconfined") | .pid' |
234 xargs --verbose --no-run-if-empty --delimiter='\n' \
235 kill
236 '';
237 commonOpts =
238 n: p:
239 "--verbose --show-cache ${
240 lib.optionalString (p.state == "complain") "--complain "
241 }${buildPolicyPath n p}";
242 in
243 {
244 Type = "oneshot";
245 RemainAfterExit = "yes";
246 ExecStartPre = "${pkgs.apparmor-utils}/bin/aa-teardown";
247 ExecStart = lib.mapAttrsToList (
248 n: p: "${pkgs.apparmor-parser}/bin/apparmor_parser --add ${commonOpts n p}"
249 ) enabledPolicies;
250 ExecStartPost = lib.optional cfg.killUnconfinedConfinables killUnconfinedConfinables;
251 ExecReload =
252 # Add or replace into the kernel profiles in enabledPolicies
253 # (because AppArmor can do that without stopping the processes already confined).
254 lib.mapAttrsToList (
255 n: p: "${pkgs.apparmor-parser}/bin/apparmor_parser --replace ${commonOpts n p}"
256 ) enabledPolicies
257 ++
258 # Remove from the kernel any profile whose name is not
259 # one of the names within the content of the profiles in enabledPolicies
260 # (indirectly read from /etc/apparmor.d/*, without recursing into sub-directory).
261 # Note that this does not remove profiles dynamically generated by libvirt.
262 [ "${pkgs.apparmor-utils}/bin/aa-remove-unknown" ]
263 ++
264 # Optionally kill the processes which are unconfined but now have a profile loaded
265 # (because AppArmor can only start to confine new processes).
266 lib.optional cfg.killUnconfinedConfinables killUnconfinedConfinables;
267 ExecStop = "${pkgs.apparmor-utils}/bin/aa-teardown";
268 CacheDirectory = [
269 "apparmor"
270 "apparmor/logprof"
271 ];
272 CacheDirectoryMode = "0700";
273 };
274 };
275 };
276
277 meta.maintainers = lib.teams.apparmor.members;
278}