Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 246 additions & 2 deletions nixos/modules/security/auditd.nix
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,260 @@
pkgs,
...
}:
let
cfg = config.security.auditd;

settingsType =
with lib.types;
nullOr (oneOf [
bool
nonEmptyStr
path
int
]);

pluginOptions = lib.types.submodule {
options = {
active = lib.mkEnableOption "Whether to enable this plugin";
direction = lib.mkOption {
type = lib.types.enum [
"in"
"out"
];
default = "out";
description = ''
The option is dictated by the plugin. In or out are the only choices.
You cannot make a plugin operate in a way it wasn't designed just by
changing this option. This option is to give a clue to the event dispatcher
about which direction events flow.

::: {.note}
Inbound events are not supported yet.
:::
'';
};
path = lib.mkOption {
type = lib.types.path;
description = "This is the absolute path to the plugin executable.";
};
type = lib.mkOption {
type = lib.types.enum [ "always" ];
readOnly = true;
default = "always";
description = ''
This tells the dispatcher how the plugin wants to be run. There is only
one valid option, `always`, which means the plugin is external and should
always be run. The default is `always` since there are no more builtin plugins.
'';
};
args = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.nonEmptyStr);
default = null;
description = ''
This allows you to pass arguments to the child program.
Generally plugins do not take arguments and have their own
config file that instructs them how they should be configured.
'';
};
format = lib.mkOption {
type = lib.types.enum [
"binary"
"string"
];
default = "string";
description = ''
Binary passes the data exactly as the audit event dispatcher gets it from
the audit daemon. The string option tells the dispatcher to completely change
the event into a string suitable for parsing with the audit parsing library.
'';
};
settings = lib.mkOption {
type = lib.types.nullOr (
lib.types.submodule {
freeformType = lib.types.attrsOf settingsType;
}
);
default = null;
description = "Plugin-specific config file to link to /etc/audit/<plugin>.conf";
};
};
};

prepareConfigValue =
v:
if lib.isBool v then
(if v then "yes" else "no")
else if lib.isList v then
lib.concatStringsSep " " (map prepareConfigValue v)
else
builtins.toString v;
prepareConfigText =
conf:
lib.concatLines (
lib.mapAttrsToList (k: v: if v == null then "#${k} =" else "${k} = ${prepareConfigValue v}") conf
);
in
{
options.security.auditd.enable = lib.mkEnableOption "the Linux Audit daemon";
options.security.auditd = {
enable = lib.mkEnableOption "the Linux Audit daemon";

settings = lib.mkOption {
type = lib.types.submodule {
freeformType = lib.types.attrsOf settingsType;
options = {
# space_left needs to be larger than admin_space_left, yet they default to be the same if left open.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be an assertion?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. Problem is, this does not have to be a number, it can also be e.g. "10%", and the other can be a number, making assertions extremely awkward (and fully impossible if one is % and the other is absolute).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrote an assert. It will not check the case where one is % and the other absolute, but it does check %/% and abs/abs

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a gut feeling that this is one of those too complex assertions that will become a maintancen burden but since it's already written I won't ask you to undo it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe, and then i can still remove it. But the regex to match for definitions is quite strict, so there shouldn't be anything unexpected happening here.

space_left = lib.mkOption {
type = lib.types.either lib.types.int (lib.types.strMatching "[0-9]+%");
default = 75;
description = ''
If the free space in the filesystem containing log_file drops below this value, the audit daemon takes the action specified by
{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
(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
the absolute size in megabytes based on the size of the filesystem containing {option}`log_file`. (E.g., if the filesystem containing
{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.

::: {.note}
This calculation is performed when the audit daemon starts, so if you resize the filesystem containing {option}`log_file` while the
audit daemon is running, you should send the audit daemon SIGHUP to re-read the configuration file and recalculate the correct per‐
centage.
:::
'';
};
admin_space_left = lib.mkOption {
type = lib.types.either lib.types.int (lib.types.strMatching "[0-9]+%");
default = 50;
description = ''
This is a numeric value in mebibytes (MiB) that tells the audit daemon when to perform a configurable action because the system is running
low on disk space. This should be considered the last chance to do something before running out of disk space. The numeric value for
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
the audit daemon calculate the number based on the disk partition size.
'';
};
};
};

default = { };
description = "auditd configuration file contents. See {auditd.conf} for supported values.";
};

plugins = lib.mkOption {
type = lib.types.attrsOf pluginOptions;
default = { };
defaultText = lib.literalExpression ''
{
af_unix = {
path = lib.getExe' pkgs.audit "audisp-af_unix";
args = [
"0640"
"/var/run/audispd_events"
"string"
];
format = "binary";
};
remote = {
path = lib.getExe' pkgs.audit "audisp-remote";
settings = { };
};
filter = {
path = lib.getExe' pkgs.audit "audisp-filter";
args = [
"allowlist"
"/etc/audit/audisp-filter.conf"
(lib.getExe' pkgs.audit "audisp-syslog")
"LOG_USER"
"LOG_INFO"
"interpret"
];
settings = { };
};
syslog = {
path = lib.getExe' pkgs.audit "audisp-syslog";
args = [ "LOG_INFO" ];
};
}
'';
description = "Plugin definitions to register with auditd";
};
};

config = lib.mkIf cfg.enable {
assertions = [
{
assertion =
let
cfg' = cfg.settings;
in
(
(lib.isInt cfg'.space_left && lib.isInt cfg'.admin_space_left)
-> cfg'.space_left > cfg'.admin_space_left
)
&& (
let
get_percent = s: lib.toInt (lib.strings.removeSuffix "%" s);
in
(lib.isString cfg'.space_left && lib.isString cfg'.admin_space_left)
-> (get_percent cfg'.space_left) > (get_percent cfg'.admin_space_left)
);
message = "`security.auditd.settings.space_left` must be larger than `security.auditd.settings.admin_space_left`";
}
];

config = lib.mkIf config.security.auditd.enable {
# Starting auditd should also enable loading the audit rules..
security.audit.enable = lib.mkDefault true;

environment.systemPackages = [ pkgs.audit ];

# setting this to anything other than /etc/audit/plugins.d will break, so we pin it here
security.auditd.settings.plugin_dir = "/etc/audit/plugins.d";

environment.etc = {
"audit/auditd.conf".text = prepareConfigText cfg.settings;
}
// (lib.mapAttrs' (
pluginName: pluginDefinitionConfigValue:
lib.nameValuePair "audit/plugins.d/${pluginName}.conf" {
text = prepareConfigText (lib.removeAttrs pluginDefinitionConfigValue [ "settings" ]);
}
) cfg.plugins)
// (lib.mapAttrs' (
pluginName: pluginDefinitionConfigValue:
lib.nameValuePair "audit/audisp-${pluginName}.conf" {
text = prepareConfigText pluginDefinitionConfigValue.settings;
}
) (lib.filterAttrs (_: v: v.settings != null) cfg.plugins));

security.auditd.plugins = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be aware of overriding parts of this config. The way config is merged based on it's priority is no t immediately obvious. See #285114

There is no action you can take right now but when this module is used, this might come up in the future.

af_unix = {
path = lib.getExe' pkgs.audit "audisp-af_unix";
args = [
"0640"
"/var/run/audispd_events"
"string"
];
format = "binary";
};
remote = {
path = lib.getExe' pkgs.audit "audisp-remote";
settings = { };
};
filter = {
path = lib.getExe' pkgs.audit "audisp-filter";
args = [
"allowlist"
"/etc/audit/audisp-filter.conf"
(lib.getExe' pkgs.audit "audisp-syslog")
"LOG_USER"
"LOG_INFO"
"interpret"
];
settings = { };
};
syslog = {
path = lib.getExe' pkgs.audit "audisp-syslog";
args = [ "LOG_INFO" ];
};
};

systemd.services.auditd = {
description = "Security Audit Logging Service";
documentation = [ "man:auditd(8)" ];
Expand Down
16 changes: 15 additions & 1 deletion nixos/tests/audit.nix
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
{ lib, ... }:
{

name = "audit";

meta = {
maintainers = with lib.maintainers; [ grimmauld ];
};

nodes = {
machine =
{ lib, pkgs, ... }:
Expand All @@ -12,7 +17,13 @@
"-a always,exit -F exe=${lib.getExe pkgs.hello} -k nixos-test"
];
};
security.auditd.enable = true;
security.auditd = {
enable = true;
plugins.af_unix.active = true;
plugins.syslog.active = true;
# plugins.remote.active = true; # needs configuring a remote server for logging
# plugins.filter.active = true; # needs configuring allowlist/denylist
};

environment.systemPackages = [ pkgs.hello ];
};
Expand All @@ -25,6 +36,9 @@
with subtest("Audit subsystem gets enabled"):
assert "enabled 1" in machine.succeed("auditctl -s")

with subtest("unix socket plugin activated"):
machine.succeed("stat /var/run/audispd_events")

with subtest("Custom rule produces audit traces"):
machine.succeed("hello")
print(machine.succeed("ausearch -k nixos-test -sc exit_group"))
Expand Down
Loading