Skip to main content

Collecting Linux audit logs

Sending auditd records to Qorionix: the two routes in, which fields are extracted, and which record types reach the shipped execution detections.

Written for whoever runs IT10 min readUpdated

The Linux audit daemon is the closest thing Linux has to the Windows Security log. It records who ran what, who became root, who opened which file and which system calls a process made, in a format that has not changed in twenty years and is not designed to be read by humans. Qorionix has a parser for it, called auditd, and this article covers what that parser does and does not extract.

The two routes in

There is no Linux-specific collector and no auditd plugin to install. The parser is reached the same two ways every other parser is reached.

How to get auditd records to the parser. Both routes end at the same code.
RouteHowTrade
HTTP Event CollectorIssue an ingest token with parser_kind set to auditd and post lines to the collector as text/plain, one record per line. Any shipper that can POST will do — a cron job with curl, Vector, Fluent Bit, rsyslog's HTTP output.Authenticated with a token, encrypted, and it tells you whether the events were stored. This is the route to prefer.
Syslog over TLSRegister the host's public address in the console as a syslog source with the parser set to auditd, install the Qorionix CA on the host, then configure audisp-syslog (or rsyslog reading /var/log/audit/audit.log) to forward with the omfwd TLS snippet the wizard gives you — gtls driver, x509/name verification, port 6514.Encrypted and verified towards us, but attributed by source address only: the host does not authenticate itself. Use it when the host already ships syslog and you want one forwarder for everything.

See Ingest tokens for issuing the token and Collecting syslog for the address-to-tenant mapping the second route needs. One token per format, as always: a token used for both auditd and plain syslog will run the auditd parser over both.

What the parser extracts

The parser reads key=value pairs across the whole line, honouring double quotes so comm="sudo" and exe="/usr/bin/sudo" parse correctly. From those pairs it fills six things.

The six auditd fields the parser reads. Everything else is preserved but not extracted.
Audit fieldBecomesNote
msg=audit(<epoch>:<seq>)The event timestamp and metadata.audit_idThe record's own clock, including the fractional part, so the four or five records a single action emits keep their order. The audit id is extracted too, which is what you join them back together on.
type=The record's verb, and its categoryEXECVE and an execve SYSCALL become process / process_launch; USER_LOGIN becomes authentication; the rest keep the type verbatim in upper case. See the next section.
res= or success=outcomeThe USER_* family writes res=success|failed; the SYSCALL family writes success=yes|no. Both are read. This used to be res= only, which left every SYSCALL record unknown.
uid=user_idThe numeric user id, as written: 0, 1000. Not a username. auid and euid are not read.
hostname=The hostRarely present in real records. When it is absent and the record arrived over syslog, the sending address is used instead.
exe=asset_idThe executable path. Yes, this is unusual — see below.

On an execution record two more fields are filled in, because OCSF Process Activity needs them and because the detection rules read them: metadata.process_name from exe, comm or the first argument, and metadata.command_line. An EXECVE record names no executable at all — the exe is on the paired SYSCALL record — so its command line is rebuilt from the argv vector, a0 a1 a2 …, which is the only thing an EXECVE record actually contains.

The asset_id behaviour is worth a sentence of its own, because it is surprising. Auditd records name a program, not a machine. With no hostname in the record, the normalisation layer's only candidate for what this event is about is the executable path, so asset_id on an auditd event typically holds something like /usr/bin/sudo. If you want to filter by machine, the sending host lands in source_ip, not in asset_id.

Where each record type lands

The searchable category is chosen from the record's type. Only the types whose meaning is unambiguous are mapped; everything else keeps its type as the action and is classified by the generic fallback, which is the honest answer for a record family whose meaning depends on audit rules we cannot see.

Auditd record types and the category and action they become. The right-hand columns are what you would type into a search.
Record typeCategoryAction
EXECVEprocessprocess_launch. Matched by the shipped execution rules.
SYSCALL where the syscall is execve or execveatprocessprocess_launch. Recognised from arch= plus syscall=; see below.
SYSCALL for any other syscallGeneric fallback (usually api or file)SYSCALL, verbatim and upper case.
USER_LOGIN, USER_AUTH, USER_ACCT, CRED_ACQauthenticationlogin_success or login_failed. These behave like a Windows 4624 or 4625 and are matched by the same brute-force rules.
USER_LOGOUTauthenticationlogout
ADD_USER, DEL_USER, ADD_GROUP, DEL_GROUP, USER_CHAUTHTOK, USER_ROLE_CHANGEiamuser_created, user_deleted, role_assigned, permission_revoked, password_changed
PATH, CWD, PROCTITLE, and everything elseGeneric fallbackThe record type, verbatim and upper case.

One further quirk, mentioned so that it does not read as inconsistency. For records that fall through to the generic classifier, the fallback looks at the recorded path and treats something that looks like a filename as file activity, so a record whose exe contains a dot — /usr/bin/python3.11, /opt/app/run.sh — is categorised file rather than api. Same record type, different category, decided by whether the binary's path happens to contain a full stop. It is a side effect of a heuristic that is right far more often than it is wrong, and on auditd data specifically it produces this odd split. It no longer affects execution records, which are categorised by type before the fallback runs.

Search these events in upper case

The search language compares values case-sensitively with =, and auditd record types are stored exactly as auditd writes them, which is upper case. This trips people up on the first query.

Queries against auditd data
# Every execution record, from both EXECVE and execve SYSCALL records
category = process AND action = process_launch

# Non-execution system calls. Note the case — these keep the auditd spelling.
action = SYSCALL AND user_id = "0"

# Wrong — matches nothing; the search language is case-sensitive with =
action = syscall

# Logins recorded by auditd, failed only
category = authentication AND outcome = failure AND source_module = "siem-ingest"

# Anything an audit rule tagged with a key you defined
raw_log contains "key=\"privileged\""

# Failed system calls — now answerable with outcome, not just raw_log
action = SYSCALL AND outcome = failure

Note the case rule still applies to the record types that keep their auditd spelling: the search language compares case-sensitively with =, and auditd writes its types in upper case. Execution records no longer need it, because process_launch is the platform's own spelling.

The fifteen searchable fields carry a summary of an auditd record; the record itself is intact in raw_log, and contains is how you reach the other forty fields. A detection rule has the better deal — it can address any extracted pair directly as metadata.ocsf.unmapped.<field>, so metadata.ocsf.unmapped.comm and metadata.ocsf.unmapped.key are both available to a rule and neither is available to a search.

What to audit, if you are starting from nothing

We are not going to reproduce an auditd tutorial, but the question what should I turn on has a short answer for a small organisation, and turning on everything is the wrong one — a busy server with unfiltered syscall auditing will produce tens of thousands of records a minute and cost you both disk and ingest.

  1. Privilege escalation. Audit execve for /usr/bin/sudo and /bin/su, tagged with a key you will recognise. This is the highest value per line of any rule you can write.
  2. Changes to accounts and authentication config. Watch /etc/passwd, /etc/shadow, /etc/sudoers and /etc/sudoers.d/. Small volume, high signal, and directly useful in an audit.
  3. Changes to the audit configuration itself. Watch /etc/audit/ and /etc/audit/rules.d/. Somebody turning off logging is a finding.
  4. SSH authorised keys. Watch ~/.ssh/authorized_keys where you can. A key appended to a service account is a classic persistence step and produces exactly one line.
  5. Only then consider broad syscall auditing, and only on the machines that justify it. Check your event volume against your plan's retention before you turn it on — see Log retention.

The first of those — auditing execve for sudo and su — produces SYSCALL and EXECVE records that now arrive as category = process, action = process_launch, with a real outcome, and are seen by the shipped execution rules. The file-watch rules produce SYSCALL and PATH records that arrive with the record type as the action and an outcome read from success=, and are found by searching on the audit key you tagged them with.

What to read next

  1. Sending events with the HTTP Event Collector — the preferred route in, with a copyable request.
  2. Searching your logs — the fifteen fields, and why raw_log contains does so much work on this source.
  3. What a firewall log actually becomes — the same analysis for the three appliance parsers.

Was this article wrong?

If a procedure here does not match what you see, or a limit we described has changed, tell us and we will fix the page. Email us about this article, or see how to get help if you need an answer rather than a correction.

Everything in logs and detection