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
76 changes: 29 additions & 47 deletions nixos/modules/security/audit.nix
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,21 @@
}:
let
cfg = config.security.audit;
enabled = cfg.enable == "lock" || cfg.enable;

failureModes = {
silent = 0;
printk = 1;
panic = 2;
};

disableScript = pkgs.writeScript "audit-disable" ''
#!${pkgs.runtimeShell} -eu
# Explicitly disable everything, as otherwise journald might start it.
auditctl -D
auditctl -e 0 -a task,never
'';

# TODO: it seems like people like their rules to be somewhat secret, yet they will not be if
# put in the store like this. At the same time, it doesn't feel like a huge deal and working
# around that is a pain so I'm leaving it like this for now.
startScript = pkgs.writeScript "audit-start" ''
#!${pkgs.runtimeShell} -eu
# Clear out any rules we may start with
auditctl -D

# Put the rules in a temporary file owned and only readable by root
rulesfile="$(mktemp)"
${lib.concatMapStrings (x: "echo '${x}' >> $rulesfile\n") cfg.rules}

# Apply the requested rules
auditctl -R "$rulesfile"

# Enable and configure auditing
auditctl \
-e ${if cfg.enable == "lock" then "2" else "1"} \
-b ${toString cfg.backlogLimit} \
-f ${toString failureModes.${cfg.failureMode}} \
-r ${toString cfg.rateLimit}
'';

stopScript = pkgs.writeScript "audit-stop" ''
#!${pkgs.runtimeShell} -eu
# Clear the rules
auditctl -D

# Disable auditing
auditctl -e 0
# The order of the fixed rules is determined by augenrules(8)
rules = pkgs.writeTextDir "audit.rules" ''
-D
-b ${toString cfg.backlogLimit}
-f ${toString failureModes.${cfg.failureMode}}
-r ${toString cfg.rateLimit}
${lib.concatLines cfg.rules}
-e ${if cfg.enable == "lock" then "2" else "1"}
'';
in
{
Expand Down Expand Up @@ -110,23 +80,35 @@ in
};
};

config = {
systemd.services.audit = {
description = "Kernel Auditing";
wantedBy = [ "basic.target" ];
config = lib.mkIf (cfg.enable == "lock" || cfg.enable) {
systemd.services.audit-rules = {
description = "Load Audit Rules";
wantedBy = [ "sysinit.target" ];
before = [
"sysinit.target"
"shutdown.target"
];
conflicts = [ "shutdown.target" ];

unitConfig = {
DefaultDependencies = false;
ConditionVirtualization = "!container";
ConditionSecurity = [ "audit" ];
ConditionKernelCommandLine = [
"!audit=0"
"!audit=off"
];
};

path = [ pkgs.audit ];

serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "@${if enabled then startScript else disableScript} audit-start";
ExecStop = "@${stopScript} audit-stop";
ExecStart = "${lib.getExe' pkgs.audit "auditctl"} -R ${rules}/audit.rules";
ExecStopPost = [
# Disable auditing
"${lib.getExe' pkgs.audit "auditctl"} -e 0"
# Delete all rules
"${lib.getExe' pkgs.audit "auditctl"} -D"
];
};
};
};
Expand Down
25 changes: 18 additions & 7 deletions nixos/modules/security/auditd.nix
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
options.security.auditd.enable = lib.mkEnableOption "the Linux Audit daemon";

config = lib.mkIf config.security.auditd.enable {
boot.kernelParams = [ "audit=1" ];
Copy link
Member

Choose a reason for hiding this comment

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

This might need a release note? Not sure. I am a little bit out of the loop what the kernel param does. I assume it causes the audit subsystem to start before userspace even starts up. Does it buffer the events?

Copy link
Member

@arianvp arianvp Aug 4, 2025

Choose a reason for hiding this comment

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

from the auditd docs they do say:

A boot param of audit=1 should be added to ensure that all processes that run before the audit daemon starts is marked as auditable by the kernel. Not doing that will make a few processes impossible to properly audit.

Copy link
Contributor

Choose a reason for hiding this comment

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

i'll try to look into this soon. Ideally there'd be a VM test to actually test this stuff and get immediate results. My approach to looking into this will be trying to break it (i.e. try to audit pid 1), and if that breaks see whether adding that kernel parameter fixes 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.

We should just add the kernelParam back. But to the audit module and not the auditd module!.

Copy link
Contributor

@LordGrimmauld LordGrimmauld Aug 6, 2025

Choose a reason for hiding this comment

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

huh? No that makes little sense. auditd is there for the userspace auditing, our audit module is only there for rules. And you can enable audit(d) without adding any rules.

Copy link
Contributor

@LordGrimmauld LordGrimmauld Aug 6, 2025

Choose a reason for hiding this comment

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

iirc you can attach audit to existing processes using auditctl later, but only if those processes were started either after auditd or with the kernel flag. Which would mean that is entirely independent of our rule-loading module.
I might be mistaken, i have some reading to do to get this right.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

our audit module is only there for rules. And you can enable audit(d) without adding any rules.

No, our audit module enables the audit subsystem in the kernel right now (via the -e rule). This is what our test shows. Enabling just the audit daemon does not enable the audit subsystem (because of the -s nochange, see https://man.archlinux.org/man/auditd.8.en#s=).

IMO the audit module is there to (a) enable the audit subsystem and (b) load the rules. You can then use whatever audit daemon you like (go-audit, auditd, etc.). That's why I also think this module should set the kernelParam.

The auditd module needs the audit module (to enable the audit subsystem) to receive any audit logs from the kernel.

We shouldn't couple enabling the audit subsystem direclty to the daemon used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Anyways, let's finish this discussion here and take it elsewhere.

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

environment.systemPackages = [ pkgs.audit ];

systemd.services.auditd = {
description = "Linux Audit daemon";
description = "Security Audit Logging Service";
documentation = [ "man:auditd(8)" ];
wantedBy = [ "sysinit.target" ];
after = [
Expand All @@ -28,16 +29,26 @@
conflicts = [ "shutdown.target" ];

unitConfig = {
ConditionVirtualization = "!container";
ConditionSecurity = [ "audit" ];
DefaultDependencies = false;
RefuseManualStop = true;
ConditionVirtualization = "!container";
ConditionKernelCommandLine = [
"!audit=0"
"!audit=off"
];
};

path = [ pkgs.audit ];

serviceConfig = {
ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p /var/log/audit";
LogsDirectory = "audit";
ExecStart = "${pkgs.audit}/bin/auditd -l -n -s nochange";
Restart = "on-failure";
# Do not restart for intentional exits. See EXIT CODES section in auditd(8).
RestartPreventExitStatus = "2 4 6";

# Upstream hardening settings
MemoryDenyWriteExecute = true;
LockPersonality = true;
RestrictRealtime = true;
};
};
};
Expand Down
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ in
atticd = runTest ./atticd.nix;
atuin = runTest ./atuin.nix;
ax25 = runTest ./ax25.nix;
audit = runTest ./audit.nix;
audiobookshelf = runTest ./audiobookshelf.nix;
auth-mysql = runTest ./auth-mysql.nix;
authelia = runTest ./authelia.nix;
Expand Down
37 changes: 37 additions & 0 deletions nixos/tests/audit.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{

name = "audit";

nodes = {
machine =
{ lib, pkgs, ... }:
{
security.audit = {
enable = true;
rules = [
"-a always,exit -F exe=${lib.getExe pkgs.hello} -k nixos-test"
];
};
security.auditd.enable = true;

environment.systemPackages = [ pkgs.hello ];
};
};

testScript = ''
machine.wait_for_unit("audit-rules.service")
machine.wait_for_unit("auditd.service")

with subtest("Audit subsystem gets enabled"):
assert "enabled 1" in machine.succeed("auditctl -s")

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

with subtest("Stopping audit-rules.service disables the audit subsystem"):
machine.succeed("systemctl stop audit-rules.service")
assert "enabled 0" in machine.succeed("auditctl -s")
'';

}
2 changes: 2 additions & 0 deletions pkgs/by-name/au/audit/package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
enablePython ? stdenv.hostPlatform == stdenv.buildPlatform,
nix-update-script,
testers,
nixosTests,
}:
stdenv.mkDerivation (finalAttrs: {
pname = "audit";
Expand Down Expand Up @@ -90,6 +91,7 @@ stdenv.mkDerivation (finalAttrs: {
tests = {
musl = pkgsCross.musl64.audit;
pkg-config = testers.testMetaPkgConfig finalAttrs.finalPackage;
audit = nixosTests.audit;
};
};

Expand Down
Loading