grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / security / apparmor.nix
bloba4b3807e4e0f302befbdfcd41654f59f4281ceb4
1 { config, lib, pkgs, ... }:
3 with lib;
5 let
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 name // {
11     default = true;
12     example = false;
13   };
14   enabledPolicies = filterAttrs (n: p: p.enable) cfg.policies;
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   ];
25   options = {
26     security.apparmor = {
27       enable = mkEnableOption ''
28         the AppArmor Mandatory Access Control system.
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.
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.
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 = ''
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 = "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 = ''
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 = "List of packages to be added to AppArmor's include path";
74       };
75       enableCache = mkEnableOption ''
76         caching of AppArmor policies
77         in `/var/cache/apparmor/`.
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 ''
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).
87         This is only sending a gracious `SIGTERM` signal to the processes,
88         not a `SIGKILL`.
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   };
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);
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
134           parser = ${pkgs.apparmor-parser}/bin/apparmor_parser
135           ldd = ${pkgs.glibc.bin}/bin/ldd
136           logger = ${pkgs.util-linux}/bin/logger
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
145           custom_includes = /etc/apparmor.d ${concatMapStringsSep " " (p: "${p}/etc/apparmor.d") cfg.packages}
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     '';
160     boot.kernelParams = [ "apparmor=1" "security=apparmor" ];
162     systemd.services.apparmor = {
163       after = [
164         "local-fs.target"
165         "systemd-journald-audit.socket"
166       ];
167       before = [ "sysinit.target" "shutdown.target" ];
168       conflicts = [ "shutdown.target" ];
169       wantedBy = [ "multi-user.target" ];
170       unitConfig = {
171         Description="Load AppArmor policies";
172         DefaultDependencies = "no";
173         ConditionSecurity = "apparmor";
174       };
175       # Reloading instead of restarting enables to load new AppArmor profiles
176       # without necessarily restarting all services which have Requires=apparmor.service
177       reloadIfChanged = true;
178       restartTriggers = [
179         etc."apparmor/parser.conf".source
180         etc."apparmor.d".source
181       ];
182       serviceConfig = let
183         killUnconfinedConfinables = pkgs.writeShellScript "apparmor-kill" ''
184           set -eu
185           ${pkgs.apparmor-bin-utils}/bin/aa-status --json |
186           ${pkgs.jq}/bin/jq --raw-output '.processes | .[] | .[] | select (.status == "unconfined") | .pid' |
187           xargs --verbose --no-run-if-empty --delimiter='\n' \
188           kill
189         '';
190         commonOpts = p: "--verbose --show-cache ${optionalString (!p.enforce) "--complain "}${p.profile}";
191         in {
192         Type = "oneshot";
193         RemainAfterExit = "yes";
194         ExecStartPre = "${pkgs.apparmor-utils}/bin/aa-teardown";
195         ExecStart = mapAttrsToList (n: p: "${pkgs.apparmor-parser}/bin/apparmor_parser --add ${commonOpts p}") enabledPolicies;
196         ExecStartPost = optional cfg.killUnconfinedConfinables killUnconfinedConfinables;
197         ExecReload =
198           # Add or replace into the kernel profiles in enabledPolicies
199           # (because AppArmor can do that without stopping the processes already confined).
200           mapAttrsToList (n: p: "${pkgs.apparmor-parser}/bin/apparmor_parser --replace ${commonOpts p}") enabledPolicies ++
201           # Remove from the kernel any profile whose name is not
202           # one of the names within the content of the profiles in enabledPolicies
203           # (indirectly read from /etc/apparmor.d/*, without recursing into sub-directory).
204           # Note that this does not remove profiles dynamically generated by libvirt.
205           [ "${pkgs.apparmor-utils}/bin/aa-remove-unknown" ] ++
206           # Optionally kill the processes which are unconfined but now have a profile loaded
207           # (because AppArmor can only start to confine new processes).
208           optional cfg.killUnconfinedConfinables killUnconfinedConfinables;
209         ExecStop = "${pkgs.apparmor-utils}/bin/aa-teardown";
210         CacheDirectory = [ "apparmor" "apparmor/logprof" ];
211         CacheDirectoryMode = "0700";
212       };
213     };
214   };
216   meta.maintainers = with maintainers; [ julm ];