Why auditd
When you need to answer "who, when, what was read or changed" with forensic precision, the Linux audit subsystem (kauditd in the kernel plus auditd in userspace) is the tool. Scenarios:
- Compliance (PCI-DSS, HIPAA, GDPR, FZ-152): mandatory logging of access to sensitive files
- Forensics after a breach: exactly what the attacker changed
- Insider threat: which admin read the HR database
- Debugging "the file suddenly disappeared": which process deleted it
- SELinux AVC denials: tied to audit.log
This is not for general logging. It is for a precise, targeted watch. Without rules, auditd by default records only login events.
Architecture
┌──────────────┐
│ userspace │
│ ┌────────┐ │
│ │auditd │ │ ←─── netlink ───┐
│ └────────┘ │ │
│ │ │ │
└───────┼──────┘ │
│ /var/log/audit/audit.log │
│
┌───────▼──────────────────────────┴────┐
│ kernel │
│ ┌──────────┐ ┌──────────────┐ │
│ │ kauditd │◄───│ audit_filter │◄── syscall enter/exit
│ └──────────┘ └──────────────┘ │
│ │
└───────────────────────────────────────┘
- The kernel checks the audit rules on every syscall. On a match, the event goes into the queue for kauditd.
- kauditd (a kernel thread) sends it over netlink to userspace.
- auditd (PID-1-launched, the only reader of the netlink socket)
formats and writes to
/var/log/audit/audit.log. - If auditd crashes, the failure mode applies:
panic(kernel panic),printk(to dmesg), orsilent(no message).
Installation
apt install auditd # Debian/Ubuntu
dnf install audit # RHEL
systemctl enable --now auditd
Config is /etc/audit/auditd.conf:
log_file = /var/log/audit/audit.log
log_format = ENRICHED # resolves UID names, ports, etc
max_log_file = 100 # MB
num_logs = 10 # rotation
space_left = 1000 # MB free
space_left_action = email
admin_space_left_action = halt # halt the server if there is no space at all
disp_qos = lossy # on overload, drop (vs lossless = block the syscall)
In production, use space_left_action = email or syslog. Never halt
unless you know why.
auditctl, runtime rules
There are two kinds of rules:
File watches -w
auditctl -w /etc/passwd -p wa -k passwd-changes
auditctl -w /etc/shadow -p rwa -k shadow-access
auditctl -w /etc/sudoers -p wa -k sudoers-changes
auditctl -w /etc/ssh/sshd_config -p wa -k sshd-config-changes
auditctl -w /var/log/audit/ -p wa -k audit-log-access
Permissions:
r, readw, writex, executea, attribute change (chmod, chown, setxattr)
-k KEY is a label for searching with ausearch.
Syscall rules -a
# Any exec
auditctl -a always,exit -F arch=b64 -S execve -k exec-events
# Any chmod/chown
auditctl -a always,exit -F arch=b64 -S chmod -S fchmod -S fchmodat -k perm-changes
# File deletions by ordinary users
auditctl -a always,exit -F arch=b64 -S unlink -S unlinkat -F auid>=1000 -F auid!=4294967295 -k user-deletes
always,exitlogs on return from the syscall (so the exit code is available)arch=b64is for 64-bit syscalls (for 32-bit ones, useb32)-Sis a specific syscall-Fis a filter on a field (auid, uid, pid, and so on)auidis the audit UID (the original one, not replaced by sudo)
Persistent rules
Runtime rules are lost on reboot. Persistent rules go in
/etc/audit/rules.d/*.rules:
# /etc/audit/rules.d/audit.rules
-D # clear everything
-b 8192 # buffer size
-f 1 # failure mode: 1=printk
# File watches
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k identity
-w /etc/sudoers.d/ -p wa -k identity
-w /etc/ssh/sshd_config -p wa -k sshd
# Critical syscalls
-a always,exit -F arch=b64 -S execve -F auid>=1000 -F auid!=-1 -k user-exec
-a always,exit -F arch=b64 -S adjtimex -S settimeofday -k time-change
# Network config
-w /etc/sysconfig/network-scripts/ -p wa -k network-config
-w /etc/hosts -p wa -k network-config
# Make immutable: once the rules are loaded, no one can change them
-e 2
augenrules --load # load all *.rules
auditctl -l # currently active
-e 2 (immutable) is the last line. After it, only a reboot can
change the rules. This protects against tampering.
ausearch, searching events
# Everything by key
ausearch -k passwd-changes
# A specific user
ausearch -ua alice
ausearch -ui 1001 # by UID
# Over a time range
ausearch -ts today # today
ausearch -ts yesterday -te now
ausearch -ts 06/01/2026 12:00:00
# By event type
ausearch -m USER_LOGIN # USER_LOGIN, USER_AUTH, EXECVE, CWD, PATH, AVC...
ausearch -m AVC # SELinux denials
# By file
ausearch -f /etc/passwd
# PID
ausearch -p 12345
# Summary
ausearch -k passwd-changes --interpret # human-readable UID, syscall name
Pipe into aureport for aggregation:
ausearch -m USER_LOGIN -ts today | aureport --login -i
aureport, reports
aureport # summary
aureport --login # successful and failed logins
aureport --auth # auth events
aureport --executable # top executables
aureport --file --summary -i # top accessed files
aureport --user # activity by UID
aureport --pid # activity by PID
aureport --auth --failed --interpret # all failed auth
Example: insider-threat detection
Suppose you want to log who reads /etc/shadow:
auditctl -w /etc/shadow -p r -k shadow-read
An hour later:
ausearch -k shadow-read -i | grep -A2 SYSCALL | grep -oP 'auid=\d+ uid=\d+'
If someone opened shadow with no clear reason, there is a record of it.
Performance
Audit adds overhead on every matched syscall. Rules must be precise:
| Rule | Overhead |
|---|---|
-w /etc/passwd -p wa | minimal (write/attr happen rarely) |
-a always,exit -S execve -F auid>=1000 | low (execs are rare) |
-a always,exit -S read (no filters) | crushing (read happens extremely often) |
-a always,exit -S openat -F dir=/var | medium (depends on load) |
Do not write open rules without -F filters. Test in a dev environment.
SELinux AVC and audit
When [[selinux-apparmor|SELinux]] blocks something, the event is written
to audit.log as an AVC denial:
ausearch -m AVC -ts recent
This shows exactly what triggered. Then use audit2allow:
ausearch -m AVC -ts recent | audit2allow -M my-fix
semodule -i my-fix.pp # load the policy module
More in selinux-policy.
When things go wrong
auditctl -lis empty: the rules are not loaded. Runaugenrules --loadorsystemctl restart auditd.auditctl -e 2does not work: the kernel configCONFIG_AUDITSYSCALL=yis missing. Checkcat /proc/sys/kernel/audit_enabled.- audit.log balloons in a day: a rule is too open.
aureport --summaryshows the top events; refactor the rules. auditctl: failed to set rule: the rules are immutable (-e 2was already set). Only a reboot allows a change.ausearch: no matches: events are not being written at all. Check thatauditdis running and that/var/log/audit/audit.logis growing. If the file is missing, the file mode is 600, the dir mode is 700, owner root.- Performance dropped after auditd: a global read rule. Remove it.
- No ENRICHED: an old version. Without
log_format = ENRICHED, UIDs and numbers are not resolved. Update theauditpackage.
Alternatives
- eBPF + bpftrace: for debugging without rules, not for compliance
- fapolicyd / fanotify: file-access policy, complements auditd
- Falco / sysdig: userspace tracing for containers
- OSSEC / Wazuh: file integrity monitoring plus alerting on top of auditd