CoursesLinux hardeningauditd rules that matter

auditd rules that matter

Identity files, privileged execs, module loads.

Advanced14 min · lesson 14 of 16

A lock stops most break-ins. A camera and a till receipt tell you what happened on the one night someone got through. Hardening a Linux box is the lock work: fewer open ports, tighter permissions, less that can go wrong. Auditing is the camera and the receipt. It does not stop an attacker. It writes down what they did, in an order you can replay later. auditd (the Linux audit daemon, a background program that writes down security events) is that camera, and the rules you give it decide where the camera points.

Here is why the recording holds up. The audit machinery does not watch your applications from the outside. It sits underneath them, down in the kernel (the core of the operating system that talks to the hardware). When a program opens /etc/shadow or loads a kernel module, that action has to pass through the kernel, and the kernel writes down what happened and hands it to auditd before the program's own logging ever runs. A web server that has been taken over can lie in its own log file. It cannot un-ask the kernel for the file it just opened. That is the whole job of writing good rules: capture the handful of events an attacker cannot avoid triggering, and none of the millions they never touch.

Where auditd Sits

The recording path is short. Your rules live in text files under /etc/audit/rules.d/. At boot, and whenever you reload, those files are compiled into a set of live rules held inside the kernel. From then on, every system call that matches a rule produces an audit record, which the kernel hands to the auditd process over a netlink socket (a private channel the kernel uses to talk to programs). auditd writes each record to /var/log/audit/audit.log in a fixed field format, one record per line. A single event often spans several of those lines, all stamped with the same ID. Think of auditd as a court stenographer. It does not judge and it does not summarize. It types down exactly what happened, with a timestamp and who did it, so the transcript still means something when you read it back weeks later.

Two Shapes Of Rule

Almost every useful rule is one of two shapes. A watch is a camera pointed at one file or directory. You write -w and the path, then -p and the kinds of access you care about, then -k and a label so you can find the events later. The four access letters are r (read), w (write), x (execute), and a (attribute change, meaning permissions or ownership). So -w /etc/shadow -p wa says: tell me about any write to the shadow password file, or any change to its permissions.

The other shape is a syscall rule, which taps one specific action no matter where it happens. It reads -a always,exit, then -F arch=b64 to say which build of the call you mean, then -S and the call's name, then -k and a label. -a always,exit means record this event as the call returns, so you also capture whether it succeeded. A syscall (system call, the request a program makes to the kernel to do real work like open a file or load code) is the lowest honest layer you can watch. Here is a starting ruleset that earns the disk it takes.

/etc/audit/rules.d/hardening.rules
## Wipe any rules already loaded, so this file is the whole truth
-D
## Kernel event buffer, and what to do if it ever fills
-b 8192
-f 1
## Identity: accounts, passwords, and who may become root
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/gshadow -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/sudoers -p wa -k identity
-w /etc/sudoers.d/ -p wa -k identity
## Privilege escalation: record every run of these binaries
-w /usr/bin/sudo -p x -k priv-esc
-w /bin/su -p x -k priv-esc
## Kernel module loads and unloads, on both the 64-bit and 32-bit paths
-a always,exit -F arch=b64 -S init_module,finit_module,delete_module -k modules
-a always,exit -F arch=b32 -S init_module,finit_module,delete_module -k modules
## Lock the configuration: no rule changes until the next reboot
-e 2

This file is a trimmed version of the audit rules the CIS (Center for Internet Security) benchmarks ship, and starting from theirs beats a blank file because they already encode the events worth watching. Walk it top to bottom. The identity block watches the files that decide who exists and who has power. /etc/passwd lists accounts, /etc/shadow holds the password hashes, /etc/group and /etc/gshadow define groups, and /etc/sudoers plus anything dropped into /etc/sudoers.d/ decide who may run commands as root. An attacker with a foothold wants a way back in. The quiet moves are: add a new account, add an existing account to a powerful group, or drop a one-line file into /etc/sudoers.d/ that grants themselves password-free root. Every one of those is a write to a file on this list, and every one lands in your log with the identity label.

The privilege block records each time someone runs sudo or su (switch user), the two normal doors from an ordinary account up to root. On its own that is a list of who reached for power. The field that makes it useful is auid, the login user ID, also called the loginuid. When you log in, the kernel stamps your session with your original user ID and does not change it when you run sudo and become root. So even after alice becomes root and edits a file as root, the record still carries auid=alice. uid tells you who someone is right now. auid tells you who they were when they walked in the door. For a defender chasing an incident, auid is the name that matters.

The last block watches kernel module loads. A kernel module is code that runs inside the kernel itself, below every user account and below most security tools. That is exactly where a rootkit (malware that hides itself and hands an attacker lasting control) wants to live, because from there it can hide files and processes from the very commands you would use to find it. init_module and finit_module load a module, delete_module unloads one. Notice the two nearly identical lines, one saying arch=b64 and one saying arch=b32. More on why in a moment.

Rules that matter: three high-signal targets
Identity files
/etc/passwd, /etc/shadow
new account or planted password hash
/etc/sudoers.d/
one-line file granting silent root
key: identity
any write or permission change
Privileged execs
sudo, su
the normal doors up to root
auid field
who they were at login, not only now
key: priv-esc
every execution recorded
Kernel modules
init_module, finit_module
code loaded below the OS
delete_module
unloading to cover tracks
b64 and b32
cover both, or you leave a door open
Watch what an attacker cannot avoid touching; skip the millions of events they never touch.

Loading And Reading The Trail

Editing the rules file changes nothing by itself. Two commands turn text into live monitoring. auditctl talks to the kernel directly and takes effect instantly, but its changes vanish on reboot, like a sticky note on the monitor. augenrules reads every file in rules.d, stitches them into one ordered set, and loads that, which is the version written into the rulebook. Use augenrules for anything you want to survive a restart.

~/secopslog — bash
$ sudo augenrules --load sudo auditctl -s sudo auditctl -l
enabled 2 failure 1 pid 743 rate_limit 0 backlog_limit 8192 lost 0 backlog 0 backlog_wait_time 60000 loginuid_immutable 0 unlocked -w /etc/passwd -p wa -k identity -w /etc/shadow -p wa -k identity -w /etc/gshadow -p wa -k identity -w /etc/group -p wa -k identity -w /etc/sudoers -p wa -k identity -w /etc/sudoers.d -p wa -k identity -w /usr/bin/sudo -p x -k priv-esc -w /bin/su -p x -k priv-esc -a always,exit -F arch=b64 -S init_module,finit_module,delete_module -F key=modules -a always,exit -F arch=b32 -S init_module,finit_module,delete_module -F key=modules

Two things confirm the change took. enabled 2 means auditing is on and the configuration is locked, which is the -e 2 line taking hold. And the rules read back match what you wrote, with the file watches keyed by -k and the syscall rules showing the key as -F key=modules, which is how the kernel prints them. One quiet detail: /etc/sudoers.d/ comes back without its trailing slash, because auditctl normalizes the path. The rule still works.

Now read what the camera caught. Raw records are dense and full of numbers. ausearch pulls records by the label you tagged, and its -i flag interprets the numbers into names: user IDs become usernames, syscall numbers become call names, timestamps become readable dates. aureport rolls records up into a summary.

~/secopslog — bash
$ sudo ausearch -k identity -ts today -i sudo aureport --auth
---- type=PROCTITLE msg=audit(07/17/2026 10:14:33.221:4412) : proctitle=vim /etc/shadow type=PATH msg=audit(07/17/2026 10:14:33.221:4412) : item=0 name=/etc/shadow inode=262176 dev=fd:00 mode=file,640 ouid=root ogid=shadow nametype=NORMAL type=CWD msg=audit(07/17/2026 10:14:33.221:4412) : cwd=/home/alice type=SYSCALL msg=audit(07/17/2026 10:14:33.221:4412) : arch=x86_64 syscall=openat success=yes exit=3 a2=O_WRONLY|O_CREAT|O_TRUNC items=1 ppid=2101 pid=2140 auid=alice uid=root gid=root euid=root suid=root fsuid=root tty=pts0 ses=3 comm=vim exe=/usr/bin/vim key=identity Authentication Report ============================================ # date time acct host term exe success event ============================================ 1. 07/17/2026 03:41:07 admin 203.0.113.9 ssh /usr/sbin/sshd no 2098 2. 07/17/2026 09:02:11 alice 10.0.0.5 /dev/pts/0 /usr/sbin/sshd yes 2199 3. 07/17/2026 09:14:52 root ? pts0 /usr/bin/sudo yes 2316

Read the SYSCALL line. alice, sitting in /home/alice, opened /etc/shadow for writing with vim, and it succeeded. uid=root, because she had used sudo. auid=alice, because the login stamp followed her through. Without a single extra tool you now know who touched the password file, as which identity, when, and whether it worked. The authentication report underneath is your quick scan for the ugly pattern: line 1 is a failed SSH (Secure Shell, an encrypted remote login) attempt from an outside address at three in the morning, exactly the kind of line worth a second look.

Cutting The Noise

A camera that films everything films nothing you can use. Backups read /etc/passwd. Package updates rewrite files under /etc. Record all of it and the one write that matters drowns. Three moves keep the signal high. First, filter by auid so you watch humans, not daemons: adding -F auid>=1000 -F auid!=unset to a syscall rule limits it to real login accounts and skips system services. Second, drop known-noisy record types with an exclude rule instead of deleting the whole watch. Third, when you are hunting for probing, match failed access with -F exit=-EACCES or -F exit=-EPERM, because a burst of permission-denied errors on sensitive files is often someone quietly testing the locks.

One rule, two doors: watch both ABIs
A 64-bit Linux kernel can still run 32-bit programs, and a 32-bit program calls the kernel through a different set of syscall numbers (a second ABI, or application binary interface, the low-level calling convention a program uses to ask the kernel for work). A rule with only -F arch=b64 watches the 64-bit door and leaves the 32-bit door wide open. An attacker who knows this loads a module or opens a file through the 32-bit path and your rule never fires. For any syscall rule that guards something an attacker cares about, write the b64 and b32 lines as a pair. That is why the module rules above appear twice.

Protecting The Trail

A recording is only evidence if the suspect cannot edit it. Two habits keep the log honest. The -e 2 line at the very bottom of the ruleset locks the configuration: once loaded, no one can add, remove, or disable a rule until the machine reboots, and a reboot of a production box is loud and visible. It goes last because nothing after it can load. The second habit is to ship audit records off the host as they are written, to a central collector the attacker does not control, since the local log on a machine they own is the first thing they reach for. The detection course covers that pipeline. One more file matters here: /etc/audit/auditd.conf decides what auditd does as the disk fills, through settings like max_log_file, num_logs, and disk_full_action.

There is a failure mode that bites from the other side. Set the ruleset too broad and the disk fills with events nobody reads, burying the one that mattered. Worse, if disk_full_action is set to halt, or the ruleset uses -f 2 (panic the kernel when the audit backlog overflows), a full disk or a flood of events can freeze the whole host on purpose, and your monitoring becomes an outage. Watch the high-signal targets, tune out the known noise, ship the rest off-box, and leave the failure mode at -f 1 (write the problem to the kernel log) unless you have a specific reason and a tested plan for -f 2.

Quick check
01alice logs in over SSH, runs sudo vim /etc/shadow, and saves a change. In the audit record, uid=root. Which field still identifies alice, and why?
Correct — the loginuid is set when the session starts and follows it through privilege changes, so it survives her becoming root.
Incorrect — uid is her identity right now, which sudo changed to root. That is why the record shows uid=root.
Incorrect — comm is the program name (vim), not a user. It tells you the command, not who ran it.
Incorrect — exe is the binary's path (/usr/bin/vim). It names the program on disk, not the person.
02The kernel-module rules are written twice, one line with -F arch=b64 and one with -F arch=b32. Why both?
Incorrect — arch selects the syscall ABI (application binary interface), not the access type; -S names the calls being watched.
Incorrect — both ABIs are live at once on a modern 64-bit kernel; this is not a version fallback.
Correct — the lesson warns an attacker can load a module through the 32-bit door if you only watch b64.
Incorrect — there is one backlog buffer; the two lines cover two calling conventions, not two buffers.
03An engineer sets -f 2 in the ruleset and disk_full_action = halt in auditd.conf, on the logic that stricter is always safer. What is the real-world consequence?
Incorrect — both are active settings that change how the host behaves when the audit system is under pressure.
Incorrect — these settings control failure behavior, not throughput.
Incorrect — noise filtering is done with -F exit and exclude rules, not with -f or disk_full_action.
Correct — the lesson says leave the failure mode at -f 1 unless you have a tested plan, precisely because -f 2 and halt can freeze the box.

When you next change a rule, prove it before you trust it. Load with augenrules --load, confirm enabled and the rule list with auditctl -s and auditctl -l, then trigger the event on purpose (touch a watched file, or run sudo) and pull it straight back with ausearch -k. A rule you have watched fire once is a rule you can rely on at three in the morning.

Try this

Work through “Protecting The Trail” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: one rule, two doors: watch both ABIs. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related