at master 11 kB view raw
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}