1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.security.auditd;
9
10 settingsType =
11 with lib.types;
12 nullOr (oneOf [
13 bool
14 nonEmptyStr
15 path
16 int
17 ]);
18
19 pluginOptions = lib.types.submodule {
20 options = {
21 active = lib.mkEnableOption "Whether to enable this plugin";
22 direction = lib.mkOption {
23 type = lib.types.enum [
24 "in"
25 "out"
26 ];
27 default = "out";
28 description = ''
29 The option is dictated by the plugin. In or out are the only choices.
30 You cannot make a plugin operate in a way it wasn't designed just by
31 changing this option. This option is to give a clue to the event dispatcher
32 about which direction events flow.
33
34 ::: {.note}
35 Inbound events are not supported yet.
36 :::
37 '';
38 };
39 path = lib.mkOption {
40 type = lib.types.path;
41 description = "This is the absolute path to the plugin executable.";
42 };
43 type = lib.mkOption {
44 type = lib.types.enum [ "always" ];
45 readOnly = true;
46 default = "always";
47 description = ''
48 This tells the dispatcher how the plugin wants to be run. There is only
49 one valid option, `always`, which means the plugin is external and should
50 always be run. The default is `always` since there are no more builtin plugins.
51 '';
52 };
53 args = lib.mkOption {
54 type = lib.types.nullOr (lib.types.listOf lib.types.nonEmptyStr);
55 default = null;
56 description = ''
57 This allows you to pass arguments to the child program.
58 Generally plugins do not take arguments and have their own
59 config file that instructs them how they should be configured.
60 '';
61 };
62 format = lib.mkOption {
63 type = lib.types.enum [
64 "binary"
65 "string"
66 ];
67 default = "string";
68 description = ''
69 Binary passes the data exactly as the audit event dispatcher gets it from
70 the audit daemon. The string option tells the dispatcher to completely change
71 the event into a string suitable for parsing with the audit parsing library.
72 '';
73 };
74 settings = lib.mkOption {
75 type = lib.types.nullOr (
76 lib.types.submodule {
77 freeformType = lib.types.attrsOf settingsType;
78 }
79 );
80 default = null;
81 description = "Plugin-specific config file to link to /etc/audit/<plugin>.conf";
82 };
83 };
84 };
85
86 prepareConfigValue =
87 v:
88 if lib.isBool v then
89 (if v then "yes" else "no")
90 else if lib.isList v then
91 lib.concatStringsSep " " (map prepareConfigValue v)
92 else
93 builtins.toString v;
94 prepareConfigText =
95 conf:
96 lib.concatLines (
97 lib.mapAttrsToList (k: v: if v == null then "#${k} =" else "${k} = ${prepareConfigValue v}") conf
98 );
99in
100{
101 options.security.auditd = {
102 enable = lib.mkEnableOption "the Linux Audit daemon";
103
104 settings = lib.mkOption {
105 type = lib.types.submodule {
106 freeformType = lib.types.attrsOf settingsType;
107 options = {
108 # space_left needs to be larger than admin_space_left, yet they default to be the same if left open.
109 space_left = lib.mkOption {
110 type = lib.types.either lib.types.int (lib.types.strMatching "[0-9]+%");
111 default = 75;
112 description = ''
113 If the free space in the filesystem containing log_file drops below this value, the audit daemon takes the action specified by
114 {option}`space_left_action`. If the value of {option}`space_left` is specified as a whole number, it is interpreted as an absolute size in mebibytes
115 (MiB). If the value is specified as a number between 1 and 99 followed by a percentage sign (e.g., 5%), the audit daemon calculates
116 the absolute size in megabytes based on the size of the filesystem containing {option}`log_file`. (E.g., if the filesystem containing
117 {option}`log_file` is 2 gibibytes in size, and {option}`space_left` is set to 25%, then the audit daemon sets {option}`space_left` to approximately 500 mebibytes.
118
119 ::: {.note}
120 This calculation is performed when the audit daemon starts, so if you resize the filesystem containing {option}`log_file` while the
121 audit daemon is running, you should send the audit daemon SIGHUP to re-read the configuration file and recalculate the correct per‐
122 centage.
123 :::
124 '';
125 };
126 admin_space_left = lib.mkOption {
127 type = lib.types.either lib.types.int (lib.types.strMatching "[0-9]+%");
128 default = 50;
129 description = ''
130 This is a numeric value in mebibytes (MiB) that tells the audit daemon when to perform a configurable action because the system is running
131 low on disk space. This should be considered the last chance to do something before running out of disk space. The numeric value for
132 this parameter should be lower than the number for {option}`space_left`. You may also append a percent sign (e.g. 1%) to the number to have
133 the audit daemon calculate the number based on the disk partition size.
134 '';
135 };
136 };
137 };
138
139 default = { };
140 description = "auditd configuration file contents. See {auditd.conf} for supported values.";
141 };
142
143 plugins = lib.mkOption {
144 type = lib.types.attrsOf pluginOptions;
145 default = { };
146 defaultText = lib.literalExpression ''
147 {
148 af_unix = {
149 path = lib.getExe' pkgs.audit "audisp-af_unix";
150 args = [
151 "0640"
152 "/var/run/audispd_events"
153 "string"
154 ];
155 format = "binary";
156 };
157 remote = {
158 path = lib.getExe' pkgs.audit "audisp-remote";
159 settings = { };
160 };
161 filter = {
162 path = lib.getExe' pkgs.audit "audisp-filter";
163 args = [
164 "allowlist"
165 "/etc/audit/audisp-filter.conf"
166 (lib.getExe' pkgs.audit "audisp-syslog")
167 "LOG_USER"
168 "LOG_INFO"
169 "interpret"
170 ];
171 settings = { };
172 };
173 syslog = {
174 path = lib.getExe' pkgs.audit "audisp-syslog";
175 args = [ "LOG_INFO" ];
176 };
177 }
178 '';
179 description = "Plugin definitions to register with auditd";
180 };
181 };
182
183 config = lib.mkIf cfg.enable {
184 assertions = [
185 {
186 assertion =
187 let
188 cfg' = cfg.settings;
189 in
190 (
191 (lib.isInt cfg'.space_left && lib.isInt cfg'.admin_space_left)
192 -> cfg'.space_left > cfg'.admin_space_left
193 )
194 && (
195 let
196 get_percent = s: lib.toInt (lib.strings.removeSuffix "%" s);
197 in
198 (lib.isString cfg'.space_left && lib.isString cfg'.admin_space_left)
199 -> (get_percent cfg'.space_left) > (get_percent cfg'.admin_space_left)
200 );
201 message = "`security.auditd.settings.space_left` must be larger than `security.auditd.settings.admin_space_left`";
202 }
203 ];
204
205 # Starting the userspace daemon should also enable audit in the kernel
206 security.audit.enable = lib.mkDefault true;
207
208 # setting this to anything other than /etc/audit/plugins.d will break, so we pin it here
209 security.auditd.settings.plugin_dir = "/etc/audit/plugins.d";
210
211 environment.etc = {
212 "audit/auditd.conf".text = prepareConfigText cfg.settings;
213 }
214 // (lib.mapAttrs' (
215 pluginName: pluginDefinitionConfigValue:
216 lib.nameValuePair "audit/plugins.d/${pluginName}.conf" {
217 text = prepareConfigText (lib.removeAttrs pluginDefinitionConfigValue [ "settings" ]);
218 }
219 ) cfg.plugins)
220 // (lib.mapAttrs' (
221 pluginName: pluginDefinitionConfigValue:
222 lib.nameValuePair "audit/audisp-${pluginName}.conf" {
223 text = prepareConfigText pluginDefinitionConfigValue.settings;
224 }
225 ) (lib.filterAttrs (_: v: v.settings != null) cfg.plugins));
226
227 security.auditd.plugins = {
228 af_unix = {
229 path = lib.getExe' pkgs.audit "audisp-af_unix";
230 args = [
231 "0640"
232 "/var/run/audispd_events"
233 "string"
234 ];
235 format = "binary";
236 };
237 remote = {
238 path = lib.getExe' pkgs.audit "audisp-remote";
239 settings = { };
240 };
241 filter = {
242 path = lib.getExe' pkgs.audit "audisp-filter";
243 args = [
244 "allowlist"
245 "/etc/audit/audisp-filter.conf"
246 (lib.getExe' pkgs.audit "audisp-syslog")
247 "LOG_USER"
248 "LOG_INFO"
249 "interpret"
250 ];
251 settings = { };
252 };
253 syslog = {
254 path = lib.getExe' pkgs.audit "audisp-syslog";
255 args = [ "LOG_INFO" ];
256 };
257 };
258
259 systemd.services.auditd = {
260 description = "Security Audit Logging Service";
261 documentation = [ "man:auditd(8)" ];
262 wantedBy = [ "sysinit.target" ];
263 after = [
264 "local-fs.target"
265 "systemd-tmpfiles-setup.service"
266 ];
267 before = [
268 "sysinit.target"
269 "shutdown.target"
270 ];
271 conflicts = [ "shutdown.target" ];
272
273 unitConfig = {
274 DefaultDependencies = false;
275 RefuseManualStop = true;
276 ConditionVirtualization = "!container";
277 ConditionKernelCommandLine = [
278 "!audit=0"
279 "!audit=off"
280 ];
281 };
282
283 serviceConfig = {
284 LogsDirectory = "audit";
285 ExecStart = "${pkgs.audit}/bin/auditd -l -n -s nochange";
286 Restart = "on-failure";
287 # Do not restart for intentional exits. See EXIT CODES section in auditd(8).
288 RestartPreventExitStatus = "2 4 6";
289
290 # Upstream hardening settings
291 MemoryDenyWriteExecute = true;
292 LockPersonality = true;
293 RestrictRealtime = true;
294 };
295 };
296 };
297}