{"id":371,"date":"2026-10-09T05:48:51","date_gmt":"2026-10-09T12:48:51","guid":{"rendered":"https:\/\/blog.canutethegreat.com\/index.php\/2026\/10\/09\/auditd-what-to-log-for-investigations\/"},"modified":"2026-10-20T20:23:18","modified_gmt":"2026-10-21T03:23:18","slug":"auditd-what-to-log-for-investigations","status":"publish","type":"post","link":"https:\/\/blog.canutethegreat.com\/index.php\/2026\/10\/09\/auditd-what-to-log-for-investigations\/","title":{"rendered":"auditd and what to log so an investigation is possible"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><em>Part 3 of 4 in the Hardening a Linux server series. Earlier parts: <a href=\"https:\/\/blog.canutethegreat.com\/index.php\/2026\/10\/07\/ssh-hardening-beyond-disable-root-login\/\">Part 1, SSH hardening<\/a> and <a href=\"https:\/\/blog.canutethegreat.com\/index.php\/2026\/10\/08\/least-privilege-with-systemd-sandboxing\/\">Part 2, systemd sandboxing<\/a>.<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The first question in almost every Linux intrusion investigation is &#8220;what did they run?&#8221;, and the honest answer on most servers is &#8220;no idea&#8221;. Shell history is a file the attacker controls; clearing it is common enough to have its own MITRE ATT&amp;CK entry (T1070.003). The application logs show the request that got them in and then nothing. The auth log shows a sudo or two. Everything in between is gone.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The Linux audit subsystem fixes that, and it&#8217;s been in the kernel since 2.6. The kernel emits records for the events you ask about, <code>auditd<\/code> writes them to disk, and the records carry the one field that makes them worth having: the audit UID.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why auid is the whole point<\/h2>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1200\" height=\"600\" src=\"https:\/\/blog.canutethegreat.com\/wp-content\/uploads\/2026\/10\/auditd-what-to-log-for-investigations-diagram.png\" alt=\"Pipeline diagram: user session with auid, kernel audit rules, kauditd backlog, auditd writing audit.log, plugins shipping events to an append-only central collector\" class=\"wp-image-369\" srcset=\"https:\/\/blog.canutethegreat.com\/wp-content\/uploads\/2026\/10\/auditd-what-to-log-for-investigations-diagram.png 1200w, https:\/\/blog.canutethegreat.com\/wp-content\/uploads\/2026\/10\/auditd-what-to-log-for-investigations-diagram-300x150.png 300w, https:\/\/blog.canutethegreat.com\/wp-content\/uploads\/2026\/10\/auditd-what-to-log-for-investigations-diagram-1024x512.png 1024w, https:\/\/blog.canutethegreat.com\/wp-content\/uploads\/2026\/10\/auditd-what-to-log-for-investigations-diagram-768x384.png 768w\" sizes=\"auto, (max-width: 1200px) 100vw, 1200px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">When a user logs in, <code>pam_loginuid<\/code> stamps their session with a login UID, <code>auid<\/code> in audit records. It survives <code>sudo<\/code>, <code>su<\/code>, setuid binaries and daemonizing. So when someone runs <code>sudo -i<\/code> and then deletes a log file, the event says <code>uid=0<\/code> and also <code>auid=1004<\/code>, which is the human who logged in. Syslog can&#8217;t give you that.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Services started by systemd at boot have no login UID. Unset shows up as 4294967295 (newer <code>auditctl<\/code> also accepts <code>unset<\/code>). Filtering on <code>auid&gt;=1000 -F auid!=4294967295<\/code> therefore means &#8220;actions traceable to a human session&#8221;, which is how you keep the volume sane.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A rule set that answers real questions<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Rules live in <code>\/etc\/audit\/rules.d\/*.rules<\/code> and are compiled by <code>augenrules --load<\/code>. Here is a compact starting point. I&#8217;m not trying to be exhaustive; Florian Roth&#8217;s public auditd rule set on GitHub is a good, much longer reference.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>## buffer and failure behaviour\n-D\n-b 8192\n--backlog_wait_time 60000\n-f 1\n\n## identity and privilege configuration\n-w \/etc\/passwd -p wa -k identity\n-w \/etc\/shadow -p wa -k identity\n-w \/etc\/group -p wa -k identity\n-w \/etc\/sudoers -p wa -k priv_config\n-w \/etc\/sudoers.d\/ -p wa -k priv_config\n-w \/etc\/ssh\/sshd_config -p wa -k sshd_config\n-w \/etc\/ssh\/sshd_config.d\/ -p wa -k sshd_config\n\n## persistence locations\n-w \/etc\/cron.d\/ -p wa -k persistence\n-w \/etc\/crontab -p wa -k persistence\n-w \/var\/spool\/cron\/ -p wa -k persistence\n-w \/etc\/systemd\/system\/ -p wa -k persistence\n-w \/root\/.ssh\/ -p wa -k persistence\n\n## commands run by humans (both ABIs)\n-a always,exit -F arch=b64 -S execve -F auid&gt;=1000 -F auid!=4294967295 -k user_exec\n-a always,exit -F arch=b32 -S execve -F auid&gt;=1000 -F auid!=4294967295 -k user_exec\n\n## kernel modules, time, tracing\n-a always,exit -F arch=b64 -S init_module -S finit_module -S delete_module -k modules\n-a always,exit -F arch=b64 -S adjtimex -S settimeofday -S clock_settime -k time_change\n-a always,exit -F arch=b64 -S ptrace -k tracing\n\n## access denied to humans\n-a always,exit -F arch=b64 -S open -S openat -S truncate -S ftruncate -F exit=-EACCES -F auid&gt;=1000 -F auid!=4294967295 -k access_denied\n-a always,exit -F arch=b64 -S open -S openat -S truncate -S ftruncate -F exit=-EPERM -F auid&gt;=1000 -F auid!=4294967295 -k access_denied\n\n## lock the configuration until reboot\n-e 2<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Every rule has a <code>-k<\/code> key. That&#8217;s what you search by later, and it&#8217;s what your SIEM will alert on. Name them for the question they answer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>arch=b32<\/code> execve rule is there on purpose. A 64-bit kernel with 32-bit compatibility still accepts 32-bit syscalls, and a rule that only matches <code>b64<\/code> misses a statically linked 32-bit binary entirely. Older hardening benchmarks list both for this reason.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Choices that matter more than the rules<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>-e 2<\/code> makes the audit configuration immutable until the next reboot. Without it, an attacker who gets root runs <code>auditctl -D<\/code> and your logging stops silently. With it, they have to reboot the machine to change the rules, and a reboot is loud.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>-f<\/code> sets what happens when the kernel can&#8217;t log: 0 is silent, 1 logs a kernel message, 2 panics the machine. Panic-on-audit-failure sounds strict and is occasionally a compliance requirement, but on most servers it hands an attacker who can flood the audit backlog a way to crash the box. I use 1 and alert on lost events instead. <code>auditctl -s<\/code> shows the <code>lost<\/code> counter, and it should stay at zero.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>-b<\/code> and <code>--backlog_wait_time<\/code> size the kernel buffer and how long a process blocks when that buffer is full. Too small and you drop events during a burst, which is exactly when the attacker is busy.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Then there&#8217;s volume. Full execve logging on a build server or a box running a lot of short-lived processes generates enormous logs. That&#8217;s why the rule above only covers human sessions. If a service account matters (the one your web app runs as, say), add a targeted rule with <code>-F uid=<\/code> for that account rather than logging every process on the machine.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Reading it back<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Raw <code>audit.log<\/code> is famously unpleasant, one event split across several records. <code>ausearch<\/code> stitches them together and <code>-i<\/code> translates numeric IDs to names:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ausearch -k user_exec --start today -i\nausearch -k identity -ts 10\/09\/2026 06:00:00 -i\naureport -x --summary\naureport --login --failed<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For anything beyond a single host, send the records somewhere else. The <code>audisp-remote<\/code> plugin ships them over the network to a central <code>auditd<\/code>, or you can have a log shipper tail the file. Laurel, a plugin from the threathunters.io folks, rewrites the multi-record mess into one JSON line per event with the process tree attached, which SIEMs handle much better.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Following a session<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Here&#8217;s why the keys and the <code>auid<\/code> field pay off. Say the <code>persistence<\/code> key fires because something wrote into <code>\/etc\/cron.d\/<\/code>. The event shows the executable that did the write, the working directory, <code>auid<\/code>, and a <code>ses=<\/code> value, which is the kernel&#8217;s ID for that login session. That session number is the thread you pull:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ausearch -k persistence --start recent -i\nausearch --session 412 --raw | aureport -x -i\nausearch -ua 1004 --start today -k user_exec -i<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The second command lists every executable that login session ran, in order, including the ones run through <code>sudo<\/code>. In practice that&#8217;s the difference between &#8220;someone added a cron job&#8221; and &#8220;this account logged in from this address, downloaded this file, ran it as root, then added a cron job&#8221;, which is the story an incident report needs.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The things auditd doesn&#8217;t cover<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A few more pieces round it out. Make the journal persistent (<code>Storage=persistent<\/code> in <code>journald.conf<\/code>), otherwise it lives in <code>\/run<\/code> and disappears at reboot. Set <code>LogLevel VERBOSE<\/code> in sshd so logins record the key fingerprint, as covered in part 1. Make sure every host runs chrony or another NTP client, because an investigation across five machines with clocks four minutes apart turns into an argument about ordering.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And send everything off the host quickly. Logs that sit only on the compromised machine are evidence the attacker can edit. The immutable flag stops them changing the rules. It doesn&#8217;t stop root from truncating <code>\/var\/log\/audit\/audit.log<\/code> after the fact. A remote copy that the server can append to and never delete from is what turns these logs into evidence.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The final part of this series is about patching, which is how you keep most of these investigations from being needed in the first place.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Other parts of the Hardening a Linux server series: <a href=\"https:\/\/blog.canutethegreat.com\/index.php\/2026\/10\/07\/ssh-hardening-beyond-disable-root-login\/\">Part 1: SSH hardening beyond &#8220;disable root login&#8221;<\/a>, <a href=\"https:\/\/blog.canutethegreat.com\/index.php\/2026\/10\/08\/least-privilege-with-systemd-sandboxing\/\">Part 2: Least privilege with systemd sandboxing<\/a>, <a href=\"https:\/\/blog.canutethegreat.com\/index.php\/2026\/10\/12\/patching-unattended-upgrades-live-patching-reboots\/\">Part 4: Patching without fear<\/a>.<\/em><\/p>\n\n","protected":false},"excerpt":{"rendered":"<p>Part 3 of 4 in the Hardening a Linux server series. Earlier parts: Part 1, SSH hardening and Part 2, systemd sandboxing. The first question in almost every Linux intrusion investigation is &#8220;what did they run?&#8221;, and the honest answer on most servers is &#8220;no idea&#8221;. Shell history is a file the attacker controls; clearing [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":373,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[22,21],"tags":[46,103,88,35,45],"class_list":["post-371","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-linux","category-security","tag-auditd","tag-forensics","tag-hardening-a-linux-server-series","tag-incident-response","tag-logging"],"_links":{"self":[{"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/posts\/371","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/comments?post=371"}],"version-history":[{"count":1,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/posts\/371\/revisions"}],"predecessor-version":[{"id":395,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/posts\/371\/revisions\/395"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/media\/373"}],"wp:attachment":[{"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/media?parent=371"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/categories?post=371"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.canutethegreat.com\/index.php\/wp-json\/wp\/v2\/tags?post=371"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}