On this page
Events and transport
The audit layer is the part of the access check that generates events. Once an event has been produced, its further life is the transport layer's concern — getting the event from the kernel to whatever userspace consumer is interested. Peios uses KMES (Kernel Message Event Stream) for this. The kernel writes events to KMES; userspace daemons subscribe and receive them; eventually they are written to wherever the deployment's audit pipeline wants them.
This page covers the event schemas — what each event type contains — and the transport mechanics at a conceptual level.
The event types
Four event types come out of the access-check layer:
| Event type | When it fires | Fired by |
|---|---|---|
access-audit |
An SACL audit ACE matched, or a token audit_policy OBJECT_ACCESS_* flag forced it |
Step 14 and 14b of AccessCheck |
continuous-audit |
An operation on an open handle matched the handle's continuous audit mask | The enforcement point for the operation (FACS, registryd, etc.) |
privilege-use |
A privilege contributed bits AND the token's audit_policy PRIVILEGE_USE_* requested it |
Step 13 of AccessCheck |
logon-session-destroyed |
A logon session lost its last token reference | Session destruction path (independent of access check) |
Each event is a self-describing record. Consumers do not need to know which check produced an event in order to parse it; the event itself carries enough metadata to identify itself and its context.
Event encoding
All audit events are encoded as msgpack maps with UTF-8 string keys. msgpack is the binary serialisation format Peios uses across kernel/userspace boundaries; it produces compact records that are quick to parse and round-trip cleanly.
SIDs and ACEs appear as bin (binary blob) values in the msgpack — the same binary forms used in the kernel. UIDs, timestamps, and bitmasks appear as uint values. Strings appear as msgpack strings (UTF-8).
Consumers are expected to ignore unknown keys. Future kernel versions may add fields to event records without changing the existing ones. A consumer that processes only the fields it knows about and ignores the rest will continue to work across kernel upgrades.
The msgpack format and the key conventions are stable across the v0.20 line. The exact byte-level layout is documented in Wire formats reference; this page covers the logical structure.
The subject record
A subject record appears in every event that involves a calling principal. It identifies the token under which the operation ran:
| Field | Type | Meaning |
|---|---|---|
user_sid |
bin | The token's user SID. |
group_sids |
array of bin | The token's group SIDs (with attributes, in the same SID_AND_ATTRIBUTES form the token holds). |
integrity_level |
uint | The token's integrity level (one of the well-known integrity RIDs). |
pip_type |
uint | The PIP type of the calling process. |
pip_trust |
uint | The PIP trust level of the calling process. |
For events that fire from inside an impersonating thread, the subject reflects the effective token — the impersonation token — not the primary. This is what the access check ran against, and it is what should be recorded.
For continuous-audit events specifically, the subject is the effective token at the moment of the operation, not at the moment the handle was opened. A process whose effective token has changed since opening the handle gets the up-to-date subject in each continuous-audit event.
The process record
A process record identifies the kernel-level context the event came from:
| Field | Type | Meaning |
|---|---|---|
pid |
uint | Process ID. |
name |
string | Process executable name. |
executable_path |
string | Full path to the executable. |
These fields let a consumer correlate the event with a running (or recently-running) process. The pid is useful in the short term — the process may still exist when the consumer is processing the event. The name and path are useful for human-readable correlation and for long-term records where the pid may have been reused.
access-audit event schema
The most common event type. Fired by step 14 (SACL audit walk) and step 14b (token audit_policy).
| Field | Type | Meaning |
|---|---|---|
subject |
map | The subject record. |
object_context |
bin or nil | Caller-supplied opaque object identifier. nil if not provided. |
requested_access |
uint | The access mask the caller requested (after generic mapping). |
granted_access |
uint | The mask of rights actually granted. |
success |
bool | Whether the access succeeded (granted contains all of requested). |
trigger |
map | Trigger record — see below. |
process |
map | The process record. |
The trigger map identifies why this event fired:
| Field | Type | Meaning |
|---|---|---|
kind |
string | Either "sacl" (a SACL audit ACE matched) or "policy" (token audit_policy forced it). |
ace |
bin or nil | For "sacl" triggers, the matched ACE bytes. For "policy" triggers, nil. |
A consumer can use trigger.kind to filter: events from SACL ACEs versus events forced by token policy. The ace field for SACL triggers includes the bytes of the matched ACE so a consumer can reconstruct what specific rule produced the event.
continuous-audit event schema
Fired per-operation on a handle whose continuous audit mask overlaps the operation's required mask.
| Field | Type | Meaning |
|---|---|---|
subject |
map | The subject record (operation-time effective token, not handle-open token). |
object_context |
bin or nil | Object identifier, if retained by the enforcement point. |
operation |
string | The operation name. For FACS operations, prefixed with file. (e.g. file.read, file.write). |
requested_access |
uint | The required mask for this operation. |
matched_access |
uint | The subset of required_access that overlapped the handle's continuous audit mask. |
granted_access |
uint | The mask cached on the handle at the time of the operation. |
success |
bool | Whether the operation succeeded. |
process |
map | The process record. |
The two access-mask fields require explanation:
requested_accessis what this operation asks for. Areadon a file requires read; anmmaprequires execute or write depending on mode.matched_accessis the subset ofrequested_accessthat intersected the handle's continuous audit mask. The event fires because of this overlap; the mask is recorded so consumers know which alarm ACEs were relevant.granted_accessis the cached access mask on the handle from the original open. Operations can only succeed if their required mask is a subset of the cached granted mask, so this field tells consumers what the handle was actually authorised for.
privilege-use event schema
Fired at step 13 of AccessCheck for each privilege that contributed bits.
| Field | Type | Meaning |
|---|---|---|
subject |
map | The subject record. |
object_context |
bin or nil | Object identifier (the same one the access check was for). |
privilege |
string | The canonical privilege name (e.g. SeBackupPrivilege). |
requested_access |
uint | The bits the caller requested that this privilege might address. |
granted_access |
uint | The bits the privilege actually contributed. |
surviving_access |
uint | The subset of granted_access that survived to the final granted mask. |
success |
bool | True if surviving_access is non-empty (the privilege contributed bits that made it through). |
process |
map | The process record. |
The three access masks tell the full story: what the caller wanted, what the privilege tried to grant, what actually survived. A successful privilege-use event has granted_access == surviving_access (or close to it — the surviving subset is what mattered). A failed event has surviving_access == 0 — the privilege tried to grant bits but they were stripped.
logon-session-destroyed event schema
Fired when a logon session loses its last token reference and is destroyed.
| Field | Type | Meaning |
|---|---|---|
session_id |
uint | The destroyed session's LUID. |
user_sid |
bin | The user SID the session belonged to. |
logon_type |
uint | The session's logon type. |
auth_package |
string | The auth-package name (e.g. "Kerberos", "NTLM", "local"). |
created_at |
uint | The session's creation timestamp. |
No subject record (the subject was this session). No process record (the session is identified by its ID, not by any specific process).
The event lets userspace consumers — notably authd — release session-scoped state (Kerberos tickets, cached directory data, per-session credentials).
KMES transport
The kernel does not directly write audit events to disk, send them over a network, or hand them to a specific userspace process. The kernel writes events to KMES — Kernel Message Event Stream — which is a per-subscriber ring buffer with kernel-side production and userspace-side consumption.
Conceptually:
- The kernel produces an event (e.g. an audit fires during step 14).
- The kernel writes the event into each subscriber's KMES buffer.
- Userspace consumers read from their KMES buffers when they are ready.
- Once a buffer is drained, the kernel can continue producing into it.
The buffers are per-subscriber. A subscriber that falls behind has its own buffer fill up; eventually KMES applies flow control to that buffer (typically dropping the oldest events). The kernel's production of events to other subscribers is unaffected.
The audit subsystem typically has at least one subscriber: eventd, the userspace audit-daemon. eventd subscribes to the audit event types, reads them from KMES, and writes them to wherever the deployment's audit pipeline goes (a local file, a remote syslog, a SIEM collector). Other subscribers — debugging tools, real-time monitors — can also exist.
The exact KMES protocol, buffer sizes, flow-control mechanics, and reliability guarantees are outside the scope of this auditing topic; they are defined in the observability documentation where KMES is the core abstraction. For auditing purposes, what matters is:
- Events are not retained by the kernel beyond writing them to KMES.
- A subscriber that falls behind may lose events.
- Reliable persistence is the job of whoever drains the KMES buffer, not the kernel.
What consumers should look at
For someone writing or operating an audit consumer:
- Filter by event type. Most consumers care about specific types (just
access-auditfor compliance, justprivilege-usefor privilege monitoring, etc.). Filtering early reduces processing volume. - Correlate by subject + object_context. A consumer that wants to track "everything user X did to object Y" should index events by these two fields.
- Use the trigger field to distinguish event sources. An
access-auditwith trigger "policy" was forced by token audit policy; one with trigger "sacl" came from a specific ACE. The distinction matters for some compliance scenarios. - Be tolerant of new fields. Future kernel versions may add fields to event records. Ignoring unknown keys is the rule; rejecting events with unfamiliar fields is a bug.
- Plan for event loss. A KMES subscriber that falls behind loses events. Consumers that need lossless audit must implement their own persistence layer above KMES and handle backpressure appropriately.
What the audit transport does not provide
A few clarifications:
- No reliable delivery to the kernel's edge. Events written to KMES are best-effort; a crashing subscriber loses its buffer. The kernel does not block access checks on subscriber readiness.
- No replay. A consumer that joins late, or one that loses its connection, cannot ask for old events. The events are produced once; missing them means missing them.
- No event correlation across boots. Each boot starts fresh. Audit logs that span reboots are assembled by userspace persistence (eventd writing to disk, log aggregators collecting across systems).
- No authentication of events at the consumer. Events are produced by the kernel and received by KMES subscribers. There is no signature on events; the trust model is "the kernel said this, so it is true". A subscriber that needs cryptographic non-repudiation of events would need to add a signing layer in userspace.
The model is built for performance and simplicity over absolute guarantees. For deployments that need stronger properties, the userspace audit pipeline is where additional layers belong — not in the kernel-to-KMES boundary.