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