The first time I enabled auditd with the default rules on a small VPS, /var/log/audit/audit.log grew to 2 GB in eight hours and I watched the disk fill in real time. The default ruleset is built for compliance audits — STIG, CIS, PCI-DSS — where the goal is “log everything anyone asked us to log” and someone else is paying for the storage. For a small server you’re trying to defend, that’s the wrong shape entirely.
Here are the 12 rules I actually run. They catch the things you’d want to know after a breach, without producing a log file you’ll never read.
The ruleset
Drop this in /etc/audit/rules.d/50-small-server.rules and run augenrules --load:
# 1-3: identity changes
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k identity
# 4-5: SSH server config and authorized_keys
-w /etc/ssh/sshd_config -p wa -k ssh-config
-w /root/.ssh/ -p wa -k ssh-keys
# 6: cron edits (the "i'll come back later" attacker payload)
-w /etc/crontab -p wa -k cron
-w /etc/cron.d/ -p wa -k cron
-w /var/spool/cron/ -p wa -k cron
# 7: kernel module loads (rootkits)
-w /sbin/insmod -p x -k kmod
-w /sbin/modprobe -p x -k kmod
-a always,exit -F arch=b64 -S init_module,delete_module,finit_module -k kmod
# 8: time change (covering tracks)
-a always,exit -F arch=b64 -S adjtimex,settimeofday,clock_settime -k time-change
# 9: privilege escalation through setuid/setgid binary execution
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -F auid!=4294967295 -k root-cmd
# 10: failed file opens (recon, not the attacker but a sign)
-a always,exit -F arch=b64 -S open,openat -F exit=-EACCES -F auid>=1000 -k file-denied
# 11: outbound connection metadata (rare on a small server, often a sign)
-a always,exit -F arch=b64 -S connect -F a2=16 -F success=1 -k outbound-conn
# 12: the catch-all I lock down at the end
-e 2
-e 2 at the bottom makes the configuration immutable until reboot. An attacker who got root can’t reflexively disable auditd; they have to reboot the box, which is itself a loud signal.
What each rule catches
- identity (1-3): any write to
passwd,shadow, orsudoers. Catches a planted backdoor account before it’s used. - ssh-config + ssh-keys (4-5): anyone modifying sshd_config (changing PermitRootLogin, AllowUsers) or appending to
~/.ssh/authorized_keys. The classic post-exploitation persistence move. - cron (6): cron edits are how attackers come back tomorrow without leaving a process running. If a row appears in
/var/spool/cron/rootand it wasn’t you, it’s bad news. - kmod (7): kernel modules being loaded. On a small server you almost never load modules at runtime — when you do, it’s usually you, and you’ll know. Anyone else doing it is suspicious.
- time-change (8): clock manipulation is a track-covering move. Backdating
last, log timestamps, etc. The attempt itself is the signal. - root-cmd (9): any command run as root by a non-system UID. Catches sudo escalation, setuid binaries fired by ordinary users.
- file-denied (10): failed file-access attempts by ordinary users. One or two are normal (a misconfigured app); a flood is recon.
- outbound-conn (11): outbound IPv4 connections that succeeded. Most of these are normal (apt update, etc), but the rate-of-change is the signal — a sudden spike means data exfil or C2 calling home.
Reading the logs
Don’t grep audit.log directly — it’s binary-ish and structured. Use ausearch:
# Identity changes in the last 24 hours
sudo ausearch -k identity --start recent
# SSH config touches today
sudo ausearch -k ssh-config --start today
# Root commands run by ordinary users
sudo ausearch -k root-cmd --start today | aureport -i
# Outbound connection bursts
sudo ausearch -k outbound-conn --start today --raw | wc -l
aureport -i resolves UIDs to names; without it you’re staring at numeric IDs.
The disk-budget question
This ruleset on a quiet personal server produces ~5 MB of logs per day. Configure /etc/audit/auditd.conf to rotate at 8 MB and keep 30 files (~240 MB total budget):
max_log_file = 8
num_logs = 30
max_log_file_action = ROTATE
space_left_action = SYSLOG
admin_space_left_action = SUSPEND
disk_full_action = SUSPEND
SUSPEND on disk-full is intentional: better to stop logging than to crash the audit subsystem and lose visibility entirely. SYSLOG on low-space mirrors the warning to journalctl.
The thing this ruleset doesn’t catch
Network-based attacks that don’t touch any of the watched paths. If an attacker exploits a WordPress RCE and runs entirely inside the PHP process, never writes to disk, never escalates, never opens an outbound connection — auditd won’t see it. That’s the fail2ban / CrowdSec layer’s job, not auditd’s.
auditd is the kernel-syscall layer of defence. It catches the things that involve real changes to the system — which, in practice, is most of what attackers actually do once they’re in. The 12 rules above cost almost nothing to run and answer the post-incident question that matters most: what did they touch?
Cover photo: Pixabay on Pexels.
