Signal9Signal9 — home
online
all lessons

auditd for Responders: Rules, ausearch & aureport

Advanced40 min7 steps
What you'll learn
  • Explain what the Linux audit subsystem records that journald and syslog cannot, and why it is tamper-evident
  • List the running rules with auditctl and add both a watch and a syscall rule, persisting them in /etc/audit/rules.d
  • Search the audit log by rule key, record type, user, and time window with ausearch
  • Read a multi-line SYSCALL/EXECVE/PATH record and use aureport to summarise authentication and execution activity
  • Use audit records to prove that a specific file was accessed or a specific binary was executed
  1. Why auditd exists

    Frame the audit subsystem as the kernel's tamper-evident record of who did what.

    Ordinary logs are written by the programs being logged. sshd decides what to put in the auth log; a shell writes nothing about the commands it runs unless you bolt something on. That is fine for operations but weak for investigation: a program that has been subverted can simply lie, or write nothing at all.

    The Linux audit subsystem answers a different question — did this actually happen? — by recording events inside the kernel, at the moment a syscall returns, before any user-space program gets a chance to spin the story. The kernel emits audit events to a dedicated daemon, `auditd`, which writes them to `/var/log/audit/audit.log`. It can capture things no ordinary log sees: every execve (every command run, by whom, with which arguments), every read or write of a watched file, every change to a watched directory, and every login.

    Think of auditd as the courtroom stenographer. An ordinary log is a casual diary the suspect keeps themselves — they choose what to write and can tear out a page. The stenographer sits in the corner of the kernel and transcribes every official action verbatim, in order, as it occurs. When you need to prove something in an incident, you reach for the transcript, not the diary.

  2. Is audit even running? auditctl -s

    Confirm the daemon is active and read its operational state before trusting any rule.

    Before you write rules or trust a search, confirm the subsystem is actually collecting. auditctl -s (status) prints the kernel-side state: whether auditing is enabled, the PID of the auditd process receiving events, how many events were lost (a backlog overflow means you have gaps), and the failure mode.

    sudo auditctl -s
    enabled 1
    failure 1
    pid 842
    rate_limit 0
    backlog_limit 8192
    lost 0
    backlog 0
    backlog_wait_time 60000
    -s
    Report the subsystem status (enabled flag, receiving PID, loss counters, limits).
    enabled 1 means the kernel is auditing; enabled 2 would mean locked (rules immutable until reboot). pid 842 is the live auditd — a pid 0 means no daemon is collecting, so events are being dropped. failure 1 is the action on error (1 = log to printk). The number to watch in an incident is `lost`: anything above 0 means the kernel overran its backlog and some events never reached the log — a gap you must account for.

    Check yourself

    In auditctl -s output, which field tells you the kernel dropped events because its buffer overflowed?

  3. Reading and writing rules: watches vs syscall rules

    List the active rules and understand the two kinds of rule you can add.

    Audit only records what its rules tell it to. auditctl -l lists the rules currently loaded in the kernel. There are two kinds, and knowing which to reach for is the core skill:

    • Watches answer "who touched this path?" — -w <path> -p <perms> -k <key>. Perms are r (read), w (write), x (execute), a (attribute/metadata change). A watch is really shorthand the kernel expands into syscall rules on that path.
    • Syscall rules answer "who called this syscall?" — -a always,exit -F arch=b64 -S <syscall> -k <key>. These fire on any invocation of a syscall (optionally filtered with -F on uid, success, paths, etc.), regardless of which file is involved.

    The `-k` key is a label you attach to a rule so you can later pull exactly its events with ausearch -k. Choose it carefully — it is how your future self finds these records in a million-line log.

    sudo auditctl -l
    -w /etc/passwd -p wa -k passwd_changes
    -w /etc/shadow -p wa -k shadow_changes
    -a always,exit -F arch=b64 -S execve -k exec
    -a always,exit -F arch=b64 -F euid=0 -S execve -k root_exec
    -l
    List all audit rules currently loaded in the kernel.
    Read top to bottom: two watches flag any write (w) or attribute change (a) to /etc/passwd and /etc/shadow under keys passwd_changes/shadow_changes; two syscall rules record every execve (key exec) and, more narrowly, every execve run as root (-F euid=0, key root_exec). arch=b64 scopes the rule to 64-bit syscalls — you typically pair it with a b32 copy so a 32-bit binary can't slip past.
    sudo auditctl -w /etc/passwd -p wa -k passwd_changes
    -w
    Set a watch on this file or directory path.
    -p wa
    Trigger on write (w) and attribute/metadata changes (a). Add r/x for read/execute.
    -k
    Tag matching events with this searchable key.
    This adds a watch right now, in the running kernel. Any process that writes to or changes the ownership/permissions of /etc/passwd will now generate an audit record carrying the key passwd_changes. Useful for an ad-hoc 'watch this during the next hour' — but read the mistake below before you rely on it. Silent on success — confirm it registered with auditctl -l, which lists the active watch.
    sudo tee /etc/audit/rules.d/50-responder.rules > /dev/null <<'EOF'
    -w /etc/passwd -p wa -k passwd_changes
    -a always,exit -F arch=b64 -S execve -k exec
    EOF
    sudo augenrules --load
    sudo auditctl -l | tail -2
    /usr/sbin/augenrules: No change
    -w /etc/passwd -p wa -k passwd_changes
    -a always,exit -F arch=b64 -S execve -k exec
    Persisting is two moves: write the rule text into a *.rules file under rules.d, then run `augenrules --load`, which compiles every file in that directory into one ruleset and loads it. Now the rules survive reboots (they reload from rules.d at boot). auditctl -l confirms they are live. (No change here just means the running set already matched.)

    Check yourself

    You want to record every time anyone READS /etc/shadow. Is that a watch or a syscall rule, and which perm letter?

  4. Finding events: ausearch

    Pull exactly the records you want by key, type, user, and time.

    /var/log/audit/audit.log is enormous and the records are dense, so you almost never cat it. `ausearch` is the query tool: it filters the log and, crucially, reassembles the multi-line records that belong to a single event and renders timestamps as human-readable dates.

    The filters you will reach for constantly:

    • -k <key> — events tagged with a rule key (the payoff for tagging your rules)
    • -m <type> — by record type, e.g. USER_LOGIN, EXECVE, SYSCALL, USER_AUTH
    • -ui <uid> / -ua <auid> — by user id (or audit login uid, which survives su)
    • -ts <when> / -te <when> — time start/end, e.g. -ts today, -ts recent

    Example — not runnable on your lab box

    sudo ausearch -k exec -ts today
    time->Mon Jun 23 14:02:10 2026
    type=PROCTITLE msg=audit(1750687330.501:8842): proctitle=2F62696E2F6E63002D6C2D70...
    type=PATH msg=audit(1750687330.501:8842): item=0 name="/usr/bin/nc" inode=2097 mode=0100755 ouid=0 ogid=0
    type=CWD msg=audit(1750687330.501:8842): cwd="/tmp"
    type=EXECVE msg=audit(1750687330.501:8842): argc=4 a0="nc" a1="-l" a2="-p" a3="4444"
    type=SYSCALL msg=audit(1750687330.501:8842): arch=c000003e syscall=59 success=yes exit=0 ppid=1402 pid=2207 auid=1000 uid=1000 gid=1000 comm="nc" exe="/usr/bin/nc" key="exec"
    -k exec
    Only events tagged with the rule key 'exec'.
    -ts today
    Only events from the start of today (also accepts 'recent', 'this-week', or a date/time).
    One event is several record lines sharing the same id (...:8842). Read it bottom-up: the SYSCALL line is the spine — syscall=59 is execve, success=yes, run by auid=1000 (the login user, even if they later became root), exe=/usr/bin/nc. The EXECVE line decodes the arguments: nc -l -p 4444 — a netcat listener on port 4444, a textbook backdoor. PATH shows the binary's inode/mode, CWD the working directory (/tmp). You have just proven who ran what, with which arguments, from where.

    Example — not runnable on your lab box

    sudo ausearch -m USER_LOGIN -ts today | tail -6
    time->Mon Jun 23 09:14:55 2026
    type=USER_LOGIN msg=audit(1750662895.310:512): pid=1402 uid=0 auid=1000 ses=42 msg='op=login id=1000 exe="/usr/sbin/sshd" hostname=203.0.113.9 addr=203.0.113.9 terminal=ssh res=success'
    time->Mon Jun 23 09:15:02 2026
    type=USER_LOGIN msg=audit(1750662902.778:518): pid=1455 uid=0 auid=1000 ses=43 msg='op=login id=1000 exe="/usr/sbin/sshd" hostname=203.0.113.9 addr=203.0.113.9 terminal=ssh res=failed'
    -m USER_LOGIN
    Filter to records of type USER_LOGIN (one of dozens — USER_AUTH, ADD_USER, CRED_ACQ, etc.).
    -m filters by record type rather than your own rule key — handy for the built-in events the kernel and PAM emit without any rule from you. Each line carries addr= (source IP), terminal=ssh, and res=success/res=failed. Here login auid=1000 from 203.0.113.9 succeeded at 09:14 then failed at 09:15 — a pattern worth tying to the auth log.

    Check yourself

    Which ausearch flag pulls every event recorded by a specific rule, using the label you set with -k when you wrote the rule?

  5. Summarising at a glance: aureport

    Turn the raw log into high-level summaries to spot where to dig.

    ausearch answers a specific question; `aureport` gives you the overview — counts and tables across the whole log so you can see where to point ausearch. It is your triage pass: "how many failed logins, by whom?", "what executed today?", "which users were most active?"

    Example — not runnable on your lab box

    sudo aureport --auth -ts today | tail
    Authentication Report
    ============================================
    # date time acct host term exe success event
    ============================================
    1. 06/23/2026 09:14:55 ec2-user 203.0.113.9 ssh /usr/sbin/sshd yes 512
    2. 06/23/2026 09:15:02 root 203.0.113.9 ssh /usr/sbin/sshd no 518
    3. 06/23/2026 09:15:04 root 203.0.113.9 ssh /usr/sbin/sshd no 520
    4. 06/23/2026 09:15:06 root 203.0.113.9 ssh /usr/sbin/sshd no 522
    --auth
    Summarise authentication attempts (login/auth events).
    -ts today
    Restrict the report to today (same time syntax as ausearch).
    One row per auth attempt with success yes/no and the trailing event number. Here a flurry of no results for root from a single host — a brute-force signature. The event number (520, 522…) is the handle to pivot: sudo ausearch -a 520 pulls the full record for that exact event to see the detail behind the summary line.

    Example — not runnable on your lab box

    sudo aureport -x --summary -ts today
    Executable Summary Report
    =================================
    total  file
    =================================
    1843  /usr/bin/bash
    402  /usr/bin/sshd
    77  /usr/bin/sudo
    5  /usr/bin/nc
    2  /tmp/.cache/kworkerd
    -x
    Report on executables (which binaries ran).
    --summary
    Aggregate into totals per item instead of one line per event.
    A ranked tally of every binary that ran today. The eye goes straight to the bottom: /usr/bin/nc running at all is suspicious on a server, and /tmp/.cache/kworkerd — a binary executing from a hidden temp path posing as a kernel thread — is a glaring anomaly. Now you pivot back: sudo ausearch -x /tmp/.cache/kworkerd reads exactly who launched it and with what arguments.

    Check yourself

    You want a ranked count of every binary that executed today, not a line per event. Which tool and flags?

  6. Proving it happened: anatomy of a record

    Read a full multi-line record well enough to stand behind it as evidence.

    An investigator's job often comes down to a yes/no a manager or a court can rely on: did this file get read? did this binary run, and by whom? The audit record is the artifact that settles it — but only if you can read every field. One event is several record lines sharing the same `audit(timestamp:serial)` id; ausearch groups them for you.

    Example — not runnable on your lab box

    sudo ausearch -k shadow_read -ts recent
    time->Mon Jun 23 14:40:11 2026
    type=PATH msg=audit(1750689611.220:9051): item=0 name="/etc/shadow" inode=1180 mode=0100000 ouid=0 ogid=0 nametype=NORMAL
    type=CWD msg=audit(1750689611.220:9051): cwd="/home/webadmin"
    type=SYSCALL msg=audit(1750689611.220:9051): arch=c000003e syscall=257 success=yes exit=3 a0=ffffff9c a1=7ffd2c1b a2=0 a3=0 items=1 ppid=1403 pid=3120 auid=1000 uid=1000 gid=1000 euid=0 comm="cat" exe="/usr/bin/cat" key="shadow_read"
    -k shadow_read
    Events from the watch on /etc/shadow's read permission.
    This is the proof. PATH names the exact file touched — /etc/shadow. SYSCALL carries the verdict: syscall=257 is openat, `success=yes` with exit=3 (it got file descriptor 3 — the open succeeded). The actor is pinned down: comm="cat", exe=/usr/bin/cat, pid=3120, and auid=1000 — the login user webadmin — who was running with euid=0 (they had escalated to root). You can now state, with timestamp and process lineage, that webadmin read the shadow file at 14:40:11. That is courtroom-grade.

    Check yourself

    In a SYSCALL record, which field proves the operation actually worked rather than merely being attempted?

  7. Try it in your live terminal

    Inspect the real audit subsystem yourself — robustly, even where it isn't running.

    You have a real root shell to your right. The audit subsystem often isn't active in a container, so each command below degrades gracefully — it shows you the real output where audit is running and a clear message where it isn't. Run them in order; the goal is to make the inspection workflow reflex, not to depend on a particular box's state.

    command -v auditctl || echo "auditd not installed here"
    First, is the tooling even present? command -v prints the path to auditctl if installed, otherwise the fallback message fires. No tooling means this box was built without the audit userspace — note it and move on.
    sudo auditctl -s 2>/dev/null || echo "audit not active"
    Read the subsystem status: look for enabled 1 and a non-zero pid. If the kernel has no audit support or the daemon isn't attached, the command fails quietly and you get audit not active.
    sudo auditctl -l 2>/dev/null
    List the loaded rules. Empty output usually means No rules (a clean box with nothing being watched) — which is itself worth knowing before you trust any search.
    sudo ausearch -m USER_LOGIN -ts today 2>/dev/null | tail || echo "no audit log"
    Pull today's logins by record type. Where an audit log exists you'll see the USER_LOGIN records with source addresses and success/failure; otherwise the fallback prints no audit log.
    sudo aureport --auth 2>/dev/null | tail || true
    Finally the summary view: the authentication report condensed to its last rows. || true keeps the sequence from erroring out on a box with no log — the point is to practise the aureport-then-ausearch reflex, not to find a guaranteed hit.
Recap
  • auditd records events in the kernel — every execve, watched-file access, and login — into /var/log/audit/audit.log: a tamper-evident transcript that ordinary, program-written logs can't match. It is not in the journal.
  • Rules come in two kinds: watches (-w path -p perms -k key, for 'who touched this file') and syscall rules (-a always,exit -F arch=b64 -S syscall -k key, for 'who called this syscall'). Always give a -k key.
  • auditctl rules are runtime-only and vanish on reboot — persist them in /etc/audit/rules.d/*.rules and load with augenrules --load.
  • Investigate with ausearch (-k key, -m type, -ua loginuid, -ts time) and triage with aureport (--auth, -x --summary); the event number/path from a report feeds straight back into ausearch -a / -x.
  • Read a record bottom-up: SYSCALL is the spine (success/exit, auid, exe), EXECVE decodes the command line, PATH names the file. success=yes is what turns 'attempted' into 'proven'.
Task → command
GoalCommandNotes
Is auditing on?auditctl -sCheck enabled, pid, and the lost counter
List active rulesauditctl -lWatches and syscall rules currently in the kernel
Watch a file (who touched it)auditctl -w /etc/passwd -p wa -k passwd_changesRuntime only — persist in rules.d
Record a syscall (e.g. exec)-a always,exit -F arch=b64 -S execve -k execPair with a b32 rule for 32-bit binaries
Make rules permanentedit /etc/audit/rules.d/*.rules then augenrules --loadSurvives reboot
Search by rule keyausearch -k execThe payoff for tagging rules with -k
Search by record typeausearch -m USER_LOGINBuilt-in events without your own rules
Search by user / timeausearch -ua 1000 -ts today-ua loginuid follows the user through su/sudo
Summarise auth attemptsaureport --authSpot brute-force; pivot with ausearch -a <event>
Summarise executionsaureport -x --summarySpot binaries running from /tmp or unexpected tools