at 25.11-pre 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 '' 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}