Auditd doesn’t just log what happened, it logs how it happened at the deepest system level.

Let’s say you want to know every time a user on your system tries to execve (which is the underlying system call for running any program) a binary located in /usr/local/bin.

Here’s how you’d set that up in auditd. First, you need to tell auditd what to watch. This is done through audit rules, typically managed in /etc/audit/rules.d/. We’ll create a new file for our specific rule, say 10-exec-local.rules.

Inside 10-exec-local.rules, you’d add a line like this:

-a always,exit -F arch=b64 -S execve -F dir=/usr/local/bin -k local-exec

Let’s break that down:

  • -a always,exit: This means the rule is always active and triggers on the exit of the system call.
  • -F arch=b64: This specifies the architecture. b64 is for 64-bit systems. You might also see b32 for 32-bit.
  • -S execve: This is the core of the rule – we’re watching the execve system call.
  • -F dir=/usr/local/bin: This is a filter. We only care about execve calls where the target executable is within the /usr/local/bin directory.
  • -k local-exec: This is a key. It’s a human-readable tag you can use to filter your audit logs later.

After saving this file, you need to load the rules. The auditctl command is used for this, but it’s best practice to have your rules managed by the auditd service itself. The augenrules command compiles rules from all files in /etc/audit/rules.d/ into a single, efficient format that auditd loads on startup.

Run sudo augenrules --load to compile and load your new rules. If auditd is already running, it will reload them.

Now, if a user tries to run something like /usr/local/bin/my-custom-script.sh, auditd will log it. You can see these logs using the ausearch command. To find logs matching our key:

sudo ausearch -k local-exec

This will output something like:

type=SYSCALL msg=audit(1678886400.123:456): arch=c000003e syscall=59 success=yes exit=0 a0=7ffd12345678 a1=7ffd12345680 a2=7ffd12345690 a3=1 items=1 ppid=1234 pid=5678 auid=1000 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 tty=pts/0 ses=1 comm="my-custom-scr" exe="/usr/local/bin/my-custom-script.sh" subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c102 key="local-exec"
type=EXECVE msg=audit(1678886400.123:456): argc=2 a0="my-custom-scr" a1="/usr/local/bin/my-custom-script.sh"

The SYSCALL record shows the system call (syscall=59 which is execve), the process ID (pid=5678), the user ID (uid=0), and the executable path (exe="/usr/local/bin/my-custom-script.sh"). The EXECVE record provides the arguments to the system call.

You can audit based on specific file paths, users, groups, even whether a system call succeeded or failed. For example, to audit all failed attempts to open files in /etc/shadow:

sudo augenrules --load
# Add to a new rule file, e.g., /etc/audit/rules.d/20-shadow-open.rules
-a always,exit -F arch=b64 -S open,openat -F path=/etc/shadow -F success=no -k shadow-open-fail

Then run sudo augenrules --load. ausearch -k shadow-open-fail would then show you any user who tried and failed to open that sensitive file.

The most surprising thing about auditd rules is how granular you can get with -F options. You can filter not just on paths and success/failure, but also on file modes (-F mode=0777), user IDs (-F uid=1000), group IDs (-F gid=500), and even specific system call arguments. This allows for incredibly precise monitoring of system activity.

The auditd system generates a massive amount of data, and understanding how to filter it effectively with ausearch and keys is paramount.

Next, you’ll want to look into how to manage audit log rotation and retention.

Want structured learning?

Take the full Cdk course →