nix audit

Published as part of 'nix' series.

Nix Audit

The linux kernel has an audit module which can keep track of which files are accessed, and which syscalls are invoked. This audit log can then be queried, and summarized into reports to allow for forensic investigations in post mortems, or even catching serious issues before they happen. In this post we'll build a module which sets up auditing with some simple but useful rules, and some scripts to alert you to suspicious activity.

You can find the the source mirrored here: https://codeberg.org/xvrqt/audit-flake

Options

First, let's add two options to make our live's easier. The first is enable which is self explanatory and defaults to true (otherwise why did you add this flake as an input?). The second requireReboot sets a kernel parameter which prevents rule changes without a reboot. This helps prevent someone with root access turn off rules, then do something malicious. It is much more likely you will notice tampering if they have to reboot your entire computer. This defaults to true as well, but is provided so we can disable it while we are rebuilding our NixOS system to test new rule changes.

  options = {
    security = {
      auditing = {
        enable = lib.mkOption {
          type = lib.types.bool;
          default = true;
          description = "Enable logging syscalls using autitd.";
        };
        requireReboot = lib.mkOption {
          type = lib.types.bool;
          default = true;
          description = "Require a reboot to change rules.";
        };
      };
    };
  };

Logging

First we setup auditd1 which is a userspace daemon which writes the audit records to a log. I have it set to create a maximum of 8x32MiB logs which rotate, but you can of course change this to better fit your needs or system's capacity. In addition these logs can be viewed in journald and using an optional plugin, you can further configure these logs to be sent securely off machine to prevent direct tampering or retroactive loss in the case of compromise. I will not be covering this functionality in this blog post.

  # Enable Linux Kernel Auditing
security = {
  auditd = {
    enable = lib.mkDefault true;
    settings = {
      # Number of log files to keep
      num_logs = lib.mkDefault 8;
      # Maximum logfile size, in MiB
      max_log_file = lib.mkDefault 32;
      # What to do when we're out of log files
      max_log_file_action = lib.mkDefault "rotate";
    };
  };
};

Rules

The bulk of configuring the audit system is setting up rules. Rules determine which sort of things are logged. The two types of rules I use most frequently are rules which watch filesytem inodes, and rules which watch syscalls. This is also where we can You can set them a list of strings like so:

security = {
  auditd = { }; # See above

  audit = {
    enable = if config.security.auditing.requireReboot then "lock" else false;
    rules = [
      "rule 1"
      "rule 2"
      "..."
    ];
  };
};

File System Rules

A rule which logs every time ssh_config is written to, or when its permissions change.

"-w /etc/ssh/ssh_config -p wa -k ssh_config"

-w means watch and /etc/ssh/ssh_config is the file we are watching.

-p flag lists the operations we are watching. There are 4 operations corresponding the unix file permissions you are already familiar with. r read, w write, x execute, a change in the file's attributes. In this case we are logging when our ssh config is edited or its permissions are changed.

-k means key and you can put in a tag, in this case ssh_config to make searching for logs generated by specific rules easier.

You could imagine setting a rule to record if your keys or wallet.dat (or a canary) were ever read.

Syscall Rules

A rule which logs every successful attempt to open utmp2, a file which shows which users are currently using the system.

"-a always,exit -F arch=b64 -S openat -F path=/var/run/utmp -F success=1 -k login_success"

-a sets when to record an event. In this case always means write a log (as opposed by never), and exit means write the log after the call has completed.

-F is a filter, in this case we filter by architecture to only log 64bit syscalls. This helps provide consistency between syscall numbers.

-S flag sets the syscalls we're interested in monitoring, in this case the openat3 syscall (opening file).

-F another filter, in this case we only care about calls to open /var/run/utmp and not every time the system opens any file.

-F another filter, we only care about when this syscall succeeded in opening the file (success=1)

-k means key just like the above example.

Example SSH Rules

Here are some rules which log attempts to use, or are connected to by ssh. It will also watch configuration files for tampering.

# Track changes to SSH configuration
"-w /etc/ssh/ssh_config -p wa -k ssh_config"
"-w /etc/ssh/sshd_config -p wa -k sshd_config"
"-w /etc/ssh/sshd_config.d/ -p wa -k ssh_config_dir"
# "-w /etc/ssh/sshd_config.d/*.conf -p wa -k ssh_config_files"

# Monitor SSH host key access
"-w /etc/ssh/ssh_host_rsa_key -p rx -k ssh_key_access"
"-w /etc/ssh/ssh_host_ecdsa_key -p rx -k ssh_key_access"
"-w /etc/ssh/ssh_host_ed25519_key -p rx -k ssh_key_access"

# Monitor SSH authentication logs
"-w /var/log/auth.log -p wa -k auth_log"
"-w /var/log/secure -p wa -k secure_log"

# Monitor PAM configuration for SSH
"-w /etc/pam.d/sshd -p wa -k pam_sshd"
"-w /etc/pam.d/ -p wa -k pam_config"

# Monitor when sshd is run
"-w ${config.services.openssh.package}/bin/sshd -p x -k sshd_execution"
# Use this version if you want to see which arguments are passed
# "-a exit,always -F arch=b64 -S execve -F path=/run/current-system/sw/bin/sshd -k sshd_execve"

# Track when sshd accepts incoming connections
"-a exit,always -F arch=b64 -S accept4 -F exe=${config.services.openssh.package}/bin/sshd -k sshd_accept"

Example User Login Rules

Here are some examples of rules which watch for changes to user passwords, using sudo, and logging in.

# Track successful logins via PAM
"-a always,exit -F arch=b64 -S openat -F path=/var/run/utmp -F success=1 -k login_success"
"-w /var/run/utmp -p wa -k utmp_changes"
"-a always,exit -F arch=b64 -S openat -F path=/var/log/btmp -F success=1 -k login_failure"
"-w /var/log/btmp -p wa -k btmp_changes"
"-a always,exit -F arch=b64 -S openat -F path=/var/log/wtmp -F success=1 -k login_record"
"-w /var/log/wtmp -p wa -k wtmp_changes"
"-w /var/log/lastlog -p wa -k lastlog_changes"

# Monitor user/group changes
"-w /etc/passwd -p wa -k user_changes"
"-w /etc/group -p wa -k group_changes"
"-w /etc/shadow -p wa -k shadow_changes"
"-w /etc/sudoers -p wa -k sudoers_changes"
"-w /etc/sudoers.d/ -p wa -k sudoers_dir"

Kernel Module Rules

Here are some example rules which monitor changes to kernel modules. Notice that we use a nix-store path as our file, otherwise it won't be detected.

# Kernel Module Changes
"-w ${pkgs.kmod}/bin/kmod -p x -k modules"
"-a always,exit -F arch=b64 -S init_module -S delete_module -k modules"

Audit Tools

To install tools to manage, search, and summarize audit events and rules, simply add pkgs.audit to your system.

# - auditctl: for changing auditd's function (although mostly useless
# because we use NixOS to declaritively set rules, as well as prevent
# rule changes without a reboot)
# - ausearch: for searching through audit logs
# - aureport: generate audit log summaries
environment.systemPackages = [
  (harden pkgs.audit)
];

auditctl

We don't have much use for auditctl4 on NixOS since we manage our rules declaritvely, but it can be faster to test out new rules (provided you're not locked behind a reboot) using sudo auditctl -a <rule> and then removing it with sudo auditctl -d <rule> without needing to rebuild each time.

ausearch

ausearch5 is a utility for searching through audit events. I most commonly use it with the -k keys we set on the rules earlier. So if I wanted to find each event correlating to someone connecting via ssh, I would run ausearch -k sshd_accept which would show me each event matching the rule we set with the same key earlier. There are other things you can do with it, such as show the most recent commands ausearch -ts recent and such, but I'm not going to get into a deep dive here.

time->Sun Feb  1 18:55:47 2026
type=PROCTITLE msg=audit(1770000947.662:32034): proctitle=737368643A202F6E69782F73746F72652F716930307069696A357277367232646A7668686D346E6B79357634326A3972352D6F70656E7373682D31302E3270312F62696E2F73736864202D44202D66202F6574632F7373682F737368645F636F6E666967205B6C697374656E65725D2030206F662031302D3130302073746172
type=SOCKADDR msg=audit(1770000947.662:32034): saddr=0200A1FAC0A801010000000000000000
type=SYSCALL msg=audit(1770000947.662:32034): arch=c000003e syscall=43 success=yes exit=8 a0=6 a1=7ffd96f6e6d0 a2=7ffd96f6e5e0 a3=0 items=0 ppid=1 pid=363870 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="sshd" exe="/nix/store/qi00piij5rw6r2djvhhm4nky5v42j9r5-openssh-10.2p1/bin/sshd" key="sshd_accept"

aureport

aureport6 allows you to generate reports, summarize, and even scan for suspicious activity. aureport --summary provides a report which looks a bit like this:

Range of time in logs: 01/30/2026 12:22:27.525 - 02/01/2026 19:19:12.411
Selected time for report: 01/30/2026 12:22:27 - 02/01/2026 19:19:12.411
Number of changes in configuration: 66
Number of changes to accounts, groups, or roles: 0
Number of logins: 0
Number of failed logins: 0
Number of authentications: 1
Number of failed authentications: 0
Number of users: 2
Number of terminals: 13
Number of host names: 3
Number of executables: 137
Number of commands: 129
Number of files: 414
Number of AVC's: 0
Number of MAC events: 0
Number of failed syscalls: 16941
Number of anomaly events: 0
Number of responses to anomaly events: 0
Number of crypto events: 0
Number of integrity events: 0
Number of virt events: 0
Number of keys: 25
Number of process IDs: 9605
Number of events: 32543

You can also look at a log of who authenticated:

  Authentication Report
============================================
# date time acct host term exe success event
============================================
1. 02/01/2026 19:08:39 root nyaa /dev/pts/9 /nix/store/xx0s3kr16bjb7dbvwahm0lc4zba1x0q4-shadow-4.18.0-su/bin/su yes 32173
2. 02/01/2026 19:22:55 crow nyaa /dev/pts/9 /nix/store/xx0s3kr16bjb7dbvwahm0lc4zba1x0q4-shadow-4.18.0-su/bin/su yes 32575
3. 02/01/2026 19:23:35 sshd nyaa /dev/pts/9 /nix/store/xx0s3kr16bjb7dbvwahm0lc4zba1x0q4-shadow-4.18.0-su/bin/su no 32608

There's really a lot of nice utility built into it which makes it better than hacking together your own thing with ausearch awk and sed. Ok, hope that was helpful for someone. Thanks to anonymous for asking why logging syscalls is useful - they inspired this post.

  1. https://man.archlinux.org/man/auditd.8

  2. https://www.man7.org/linux/man-pages/man5/utmp.5.html

  3. https://linux.die.net/man/2/openat

  4. https://man.archlinux.org/man/auditctl.8.en

  5. https://man.archlinux.org/man/ausearch.8.en

  6. https://man.archlinux.org/man/aureport.8.en