Auditing
Single-page view · as markdown
Auditing
Peios / Peios Security Fundamentals / Auditing
Auditing is the access check's record-keeping layer. Where the DACL and the rest of the pipeline decide whether access is granted, auditing decides what to remember about that decision. An audit event records an access attempt — who made it, what they wanted, what they got, what triggered the recording — and ships the event through KMES to whatever userspace consumer is listening.
Auditing runs after the access decision is made. By the time an audit event fires, the granted mask is already final; the audit walk is not part of the gate, only of the record. This means audit events are observational, not decisional: a fired audit event reports what already happened; it does not affect whether the access happens.
This page covers the model — what kinds of audit exist, where each one fires in the pipeline, and what they have in common.
What auditing is for #
Auditing answers questions of the form "did this happen?". It does not answer "is this allowed?" — that is the access check's job, and it has already been done by the time audit runs.
The questions audit handles well:
- "Who accessed this sensitive file in the last hour?"
- "Did this principal exercise SeDebugPrivilege today?"
- "How many failed write attempts were there against this object since Monday?"
- "Which sessions are currently writing to this directory?"
The questions audit does not handle well:
- "Should this access have been allowed?" — that is policy, not audit.
- "Will this access succeed?" — audit is post-decision, not predictive.
- "What rights does this user have on this object?" — audit observes events; computing rights requires the access check.
Auditing is most valuable for compliance and incident response. A regulated environment may need to log every access to certain classes of data; an incident-response team needs to reconstruct what happened in the moments before a compromise. Auditing produces the raw record those use cases consume.
The three audit categories #
Peios produces three categories of audit event from the access pipeline:
| Category | Triggered by | When it fires |
|---|---|---|
| Object-access audit | SYSTEM_AUDIT* ACEs in an object's SACL, or the calling token's audit_policy bitmask | At AccessCheck completion (one event per matching audit ACE per access) |
| Continuous audit | SYSTEM_ALARM* ACEs in an object's SACL | Per-operation on an open handle (every operation whose required mask overlaps the handle's continuous-audit mask) |
| Privilege-use audit | A privilege contributed to the granted mask, and the token's audit_policy requested it | At AccessCheck completion (one event per privilege that fired) |
Plus a fourth, tangentially related category produced by session lifecycle:
| Category | Triggered by | When it fires |
|---|---|---|
| Session destruction | A logon session loses its last token reference | When the session is destroyed |
Object-access and continuous audit are covered in Audit ACEs. Privilege-use audit and the token-forced subset of object-access audit are covered in Policy-forced auditing. Event schemas and the KMES transport are in Events and transport.
Where audit fits in the pipeline #
flowchart LR
A["AccessCheck pipeline steps 0-12"] --> B["Granted mask + privilege contributions"]
B --> C["Step 13: privilege-use audit emission"]
C --> D["Step 14: SACL audit walk"]
D --> E["Step 14b: token audit_policy forced events"]
E --> F["AccessCheck returns: granted mask, continuous_audit mask, staging_mismatch"]
F --> G["KMES delivers events to consumers"]
Step 13 fires privilege-use events for privileges that contributed bits. Step 14 walks the object's SACL (plus any CAAP effective SACLs collected during step 12) looking for audit and alarm ACEs that match the access. Step 14b checks the calling token's audit_policy bitmask for forced events that should fire regardless of any SACL ACE.
Each of these steps can produce zero or more events. A single access check might produce no events (no audit ACEs matched, no privileges used, no token-forced auditing); or it might produce many (multiple matching audit ACEs, multiple privileges, multiple forced events).
The events are sent through KMES — Peios's kernel-to-userspace event transport — to whichever consumer is subscribed. The kernel does not retain them, does not buffer them past the transport, does not retry them; KMES is fire-and-forget at the kernel-to-userspace boundary, and everything past that boundary is the transport pipeline's concern, not the audit layer's.
What an audit event records #
Every audit event includes:
- The subject — who was making the access. The token's user SID, group SIDs, integrity level, PIP type and trust, projected UID.
- The object context — a caller-supplied opaque blob identifying the object being accessed. Audit consumers use this to correlate events with the objects they cover.
- The access — what was requested, what was granted, whether the access succeeded.
- The trigger — which ACE matched (for SACL-driven events), which privilege fired (for privilege-use events), or which audit-policy bit (for token-forced events).
- The process context — pid, name, executable path. Useful for correlating events with running processes.
The exact field set varies by event type and is covered in Events and transport.
What audit does not do #
A few clarifications:
- Audit is not the access decision. An audit event firing does not mean access was granted; an event for a denied access is just as legitimate. Read the success flag in the event to know what happened.
- Audit is not retroactive. An audit ACE added to an SACL after some accesses have happened does not generate events for those past accesses. Auditing observes the present; the past is the past.
- Audit does not survive crash. An event in flight when the kernel crashes is lost. KMES is designed for the typical case where the kernel keeps running; for crash-resilient audit, a userspace consumer that persists events to disk is what provides durability.
- Audit does not authenticate. The recorded subject is whatever the calling token says they are. If the token has been issued correctly, the audit record reflects reality. The audit layer does not independently verify the principal's identity beyond what the token provides.
Where audit lives #
A handful of components are involved in producing and consuming audit:
- The kernel runs the audit walk during AccessCheck and produces the events.
- KMES transports the events from the kernel to userspace.
eventdis the userspace daemon that subscribes to KMES and writes events to wherever they should go (a local file, a remote log collector, a SIEM endpoint).- Audit consumers are tools that read what
eventdhas captured — query interfaces, dashboards, alerting systems.
This separation is what lets the kernel stay simple. The kernel's only job is generating events; everything else — buffering, persistence, querying, alerting — is userspace. A failure in the audit-storage layer does not affect the access check; the access decision happens regardless of whether the corresponding event was ever recorded.
Auditing is best-effort #
The audit layer aims to record every meaningful event, but it does not guarantee that every event is recorded. The reasons:
- An event in flight when the kernel crashes is lost.
- A consumer that has fallen behind may have its KMES backlog dropped (KMES's flow control is a per-consumer concern; the kernel does not block the access check on consumer back-pressure).
- A misconfigured SACL or
audit_policymay simply fail to generate the events that would have been useful for forensics.
Best-effort means audit is a signal, not a proof. A deployment that needs cryptographic non-repudiation of every access would need additional infrastructure on top of the audit layer. For the typical use cases — compliance, alerting, incident response — best-effort is enough.
The model deliberately puts performance ahead of guaranteed delivery. An access check that has to block until its audit event has been written to disk would be intolerably slow. The kernel chooses to record what it can and not slow down the actual work; the userspace audit pipeline is responsible for getting the events from the transport buffer to whatever destination they need to land at.
Where to start #
If you want to understand audit ACEs — how they work, the difference between standard (SYSTEM_AUDIT) and continuous (SYSTEM_ALARM) ACEs, audit polarity, and the conditional audit case — read Audit ACEs.
If you want privilege-use audit and the token-forced audit policy — events that fire independent of any SACL ACE — read Policy-forced auditing.
If you want the wire-level details — event schemas, what each field means, how KMES delivers events to consumers — read Events and transport.
Audit ACEs
Peios / Peios Security Fundamentals / Auditing
The primary mechanism for object-access auditing is audit ACEs in an object's SACL. An audit ACE says: when a specific principal (or principals matching a SID) attempts specific access rights, fire an event. The ACE can be conditioned on whether the attempt succeeded, whether it failed, or both. The event records the access; the access itself is decided by the DACL and the rest of the pipeline.
This page covers the two audit ACE families — SYSTEM_AUDIT* (one-shot at handle creation) and SYSTEM_ALARM* (continuous per-operation) — and the rules that govern when each fires.
SYSTEM_AUDIT — one event per access #
A SYSTEM_AUDIT ACE fires once when an access check completes. The ACE has the standard single-SID structure:
| Field | Meaning |
|---|---|
| AceType | SYSTEM_AUDIT (0x02), or one of the object/callback variants (0x07, 0x0D, 0x0F) |
| AceFlags | Includes SUCCESSFUL_ACCESS_ACE_FLAG (0x40) and/or FAILED_ACCESS_ACE_FLAG (0x80) |
| Mask | The access mask the ACE is interested in |
| SID | The principal the ACE targets |
When an access check is in step 14 (the SACL audit walk), the kernel scans the SACL for audit ACEs. For each SYSTEM_AUDIT ACE, it asks:
- Does the caller's identity match the ACE's SID? If not, the ACE does not apply.
- Does the requested access overlap the ACE's mask? If not, the ACE does not apply.
- If the access succeeded and the ACE has
SUCCESSFUL_ACCESS_ACE_FLAGset, emit an event. - If the access failed and the ACE has
FAILED_ACCESS_ACE_FLAGset, emit an event. - Otherwise the ACE applies but does not fire.
One event per matching ACE. An object with three matching audit ACEs produces three events on a single access. A separate access later — a new handle, a new access check — produces a separate set of events.
The event is for the access check as a whole. It does not fire per-operation on the open handle; once the handle is open, subsequent operations do not re-fire the audit ACE. (For per-operation audit, see SYSTEM_ALARM below.)
Common patterns #
The SUCCESSFUL_ACCESS_ACE_FLAG and FAILED_ACCESS_ACE_FLAG flags can both be set in one ACE:
| Flag combination | Audit behaviour |
|---|---|
SUCCESSFUL_ACCESS_ACE_FLAG only | Audit only when the access succeeded. |
FAILED_ACCESS_ACE_FLAG only | Audit only when the access was denied. |
| Both set | Audit every access attempt, regardless of outcome. |
| Neither set | The ACE applies but never fires. |
The "both set" pattern gives a complete audit log for an object — every attempt, allowed or denied. The "failed only" pattern is useful for security monitoring: alert me whenever someone tries to do X but is refused. The "successful only" pattern is useful for compliance: record everyone who has actually touched the object.
A SACL with no audit ACEs produces no audit events (unless audit_policy on the token fires; see Policy-forced auditing). A SACL with audit ACEs but no relevant flags set is auditing nothing — the ACEs are essentially comments at that point. The flags are what makes them active.
SYSTEM_ALARM — continuous, per-operation #
A SYSTEM_ALARM ACE works differently. It does not fire at access-check time; instead, it configures a continuous audit mask on the open handle, and every subsequent operation through the handle whose required access overlaps that mask produces an event.
The ACE has the same structure as SYSTEM_AUDIT:
| Field | Meaning |
|---|---|
| AceType | SYSTEM_ALARM (0x03), or one of the variants (0x08, 0x0E, 0x10) |
| AceFlags | Same flag bits as audit |
| Mask | The mask to record on the handle |
| SID | The targeted principal |
During step 14 of access check, when a SYSTEM_ALARM ACE matches, the kernel:
- Computes a continuous-audit mask — the union of all matching alarm ACEs' masks.
- Returns the mask to the caller as part of the access check's output.
- The caller (typically FACS for files) stores the mask on the open handle. For registry keys the enforcement point is LCS, the kernel registry subsystem — never the registryd source, which stores data but makes no security decisions (and LCS v0.21 defines only open-time SACL audit for keys, not continuous audit).
Then, on each subsequent operation through the handle, the enforcement point compares the operation's required-access mask against the handle's continuous audit mask. If they overlap (any bit shared), the kernel emits a continuous-audit event for the operation.
Audit vs alarm: granularity #
The difference between SYSTEM_AUDIT and SYSTEM_ALARM is fundamentally about granularity:
| Aspect | SYSTEM_AUDIT | SYSTEM_ALARM |
|---|---|---|
| Event timing | Once, at access-check completion | Per operation on the open handle |
| Event count per handle | One (or one per matching ACE) | Many — one per matching operation |
| What is recorded | The access-check decision | The specific operation that was performed |
| Storage of state | None — fires and forgets | Continuous audit mask on the handle |
| Use case | "Who opened this file with these rights" | "Every read this principal performed on this object" |
For most auditing, SYSTEM_AUDIT is the right tool. The event volume is bounded by the number of distinct accesses, which is usually small.
For high-sensitivity objects where every operation matters — auditing a credential store's every read, recording every write to a security-relevant log file — SYSTEM_ALARM is the right tool. The event volume is bounded by operation count, which can be high, but the visibility into what is actually being done is also higher.
Both can coexist on the same SACL. A SACL with both SYSTEM_AUDIT and SYSTEM_ALARM produces an at-open event from the audit ACE and per-operation events from the alarm ACE.
Audit polarity for SID matching #
A subtle but important rule: audit ACEs match SIDs with deny polarity rather than allow polarity.
For an ACCESS_ALLOWED ACE in a DACL, a group on the token matches only if it has SE_GROUP_ENABLED set. Groups marked SE_GROUP_USE_FOR_DENY_ONLY are invisible to allow ACEs.
For an ACCESS_DENIED ACE, both regular groups and deny-only groups match. The broader view exists so denies cannot be sidestepped by demoting a group to deny-only.
Audit ACEs use the same broad view as deny ACEs. A token's deny-only groups match audit ACEs. Logon SIDs, disabled groups in some cases, every SID associated with the token contributes to audit matching.
The reasoning: the purpose of auditing is to capture the most complete picture of the identity making the access. A token that has been restricted (groups marked deny-only, say) is still that token, with that identity, and the audit log should reflect that. Limiting audit to the narrower allow-polarity view would let auditing miss accesses by tokens that had been deliberately downgraded.
Practical effect: if you write an audit ACE matching a specific group SID, it will fire whether the calling token currently has that group enabled, disabled, or marked deny-only. The audit ACE catches the broadest possible identity match.
Conditional audit ACEs #
The callback variants of audit and alarm ACEs (SYSTEM_AUDIT_CALLBACK, SYSTEM_AUDIT_CALLBACK_OBJECT, SYSTEM_ALARM_CALLBACK, SYSTEM_ALARM_CALLBACK_OBJECT) add a conditional expression. The ACE fires only when the expression evaluates appropriately for an audit.
The rule: an audit-callback ACE fires when its expression evaluates to TRUE or UNKNOWN. It does not fire on FALSE.
This is the opposite of the rule for ACCESS_ALLOWED_CALLBACK (where TRUE fires the allow, UNKNOWN does not) and matches the rule for ACCESS_DENIED_CALLBACK (TRUE or UNKNOWN fires the deny).
The reasoning: an audit ACE's job is to record events. Erring on the side of recording — including when the conditional cannot be definitively evaluated — produces a more complete audit log. A missed event is more dangerous than an extra one. UNKNOWN, therefore, fires the audit.
Practical pattern: an audit ACE conditioned on "the request came from outside the trusted network" — @Local.Source != "internal" — fires on accesses where the local-claims do not include a Source value (the expression evaluates UNKNOWN). The audit log captures the access even though the condition could not be evaluated; an investigator reviewing the log can decide whether the missing claim is meaningful.
Object-scoped audit ACEs #
The object variants (SYSTEM_AUDIT_OBJECT, etc.) carry one or two GUIDs that scope the ACE to specific properties of a directory-style object. The audit fires only when the access is to a property whose GUID matches.
For most objects (files, registry keys, processes), object ACEs are not used and the basic single-SID variants are what appears in SACLs. Object ACEs come up in directory-style objects where per-property audit is meaningful — auditing "who read the manager property of this user object" rather than just "who read this user object".
The mechanics of object ACEs are the same as elsewhere — the GUID-scoped match — and are covered in ACLs, ACEs, and access masks. The audit case is identical to the access-check case structurally; only the firing semantics differ.
Inherit-only audit ACEs #
An audit or alarm ACE with INHERIT_ONLY_ACE set does not fire on the object it sits on. It is for inheritance to children only — when a child is created, the audit ACE is copied to the child's SACL (subject to inheritance flags) and from then on fires on the child.
This is the same inheritance semantics that DACL ACEs use. An inherit-only audit ACE on a parent directory does not audit accesses to the parent; it just propagates to child files.
What audit ACEs do not produce #
A few clarifications:
- They do not skip pre-decided accesses. If MIC or PIP pre-decided write bits as denied at step 5, and the access check ends with the write denied, the audit walk still runs and audit ACEs fire normally. The pre-decision does not skip the audit walk.
- They do not produce duplicate events on the same handle for the same access. An access check produces one event per matching ACE. If a process accesses the same object twice (two separate opens, two access checks), each access check produces its own set of events.
- They do not record per-operation activity on an open handle. For per-operation events, alarm ACEs are the mechanism. Standard audit ACEs are "at the moment of opening, was this access allowed?".
- They do not affect access. The audit walk is observational. A matching audit ACE does not change what gets granted, even if the audit ACE has a different mask than the matching DACL ACE.
CAAP effective SACLs #
A SYSTEM_SCOPED_POLICY_ID_ACE in the object's SACL references a central access policy. That policy can have its own effective SACL containing audit ACEs (see Central access policies). The policy's audit ACEs are collected during CAAP evaluation at step 12 and merged into the audit walk at step 14.
From the audit consumer's perspective, an event triggered by a CAAP's effective SACL looks the same as one triggered by the object's own SACL. The event records what fired, and where the matched ACE came from is part of the event metadata, but the structure is uniform.
Practical implication: a policy administrator who wants to audit a class of objects can put the audit ACE in the policy's effective SACL once, and every object referencing the policy contributes audit events — better than putting a copy of the audit ACE on every object's SACL individually.
Where to go next #
For the events that fire without any SACL ACE — privilege-use audit and the token's audit_policy — read Policy-forced auditing.
For what the fired events contain and how they reach userspace, read Events and transport.
For where audit ACEs live and who may read or write them, read The SACL.
Policy-forced auditing
Peios / Peios Security Fundamentals / Auditing
Not every audit event comes from an audit ACE in an SACL. Two mechanisms produce events that fire regardless of whether an SACL ACE matched: privilege-use audit (fires when a privilege contributes bits to the granted mask) and the token's audit_policy bitmask (forces object-access or privilege-use events whenever the corresponding flag is set on the token).
Both mechanisms are forced by something other than an SACL ACE — by the privilege exercise itself, or by the token's per-principal audit policy. They run during the same audit walk as SACL ACEs but are independent of any specific ACE matching.
This page covers both mechanisms, when each fires, and how they compose with SACL-driven audit.
Privilege-use audit #
When AccessCheck completes, the pipeline knows which privileges contributed bits to the granted mask. A backup tool that used SeBackup to bypass the DACL has the privilege recorded against the bits it contributed; an administrator who used SeTakeOwnership to gain WRITE_OWNER has that privilege recorded.
If the calling token's audit_policy requests it, the kernel fires a privilege-use event for each privilege that contributed. The events fall into two flavours:
| Flavour | Triggered by | Fires when |
|---|---|---|
| Successful privilege use | audit_policy & PRIVILEGE_USE_SUCCESS (0x04) | A privilege contributed bits AND those bits survived to the final granted mask. |
| Failed privilege use | audit_policy & PRIVILEGE_USE_FAILURE (0x08) | A privilege contributed bits BUT those bits did NOT survive (stripped by a later layer like confinement or CAAP). |
The "failed" case is the interesting one. A privilege that fires but does not actually grant access tells the audit log that the caller attempted to exercise a privilege and was prevented by some narrowing layer. This is observable separately from a plain "access denied" — the privilege got far enough to fire and then was stripped.
A worked example. A token with SeBackupPrivilege enabled tries to read a confined object. The pipeline:
- Step 4: SeBackup grants read-category bits via the privilege.
- Step 8: DACL walk grants nothing additional.
- Step 11: confinement intersection strips the privilege-granted bits (confinement does not preserve privilege grants).
- Final granted mask: empty.
If audit_policy & PRIVILEGE_USE_SUCCESS is set, no event fires (no bits survived). If audit_policy & PRIVILEGE_USE_FAILURE is set, a failed-use event fires, recording "SeBackupPrivilege contributed FILE_READ_DATA but the bits were stripped".
Either configuration is useful. Tracking only success tells you when privileges actually grant access; tracking only failure tells you when privileges try to grant access but cannot. Tracking both gives you a complete picture of privilege exercise.
The event itself includes the privilege name (e.g. SeBackupPrivilege), the bits the privilege contributed pre-narrowing, the bits that survived to the final result, and a success boolean. The exact schema is in Events and transport.
Token audit_policy #
The token's audit_policy field is a small bitmask with four flags:
| Flag | Value | Effect |
|---|---|---|
OBJECT_ACCESS_SUCCESS | 0x01 | Force an audit event on every successful access by this token, regardless of SACL ACE matches. |
OBJECT_ACCESS_FAILURE | 0x02 | Force an audit event on every failed access by this token. |
PRIVILEGE_USE_SUCCESS | 0x04 | Emit successful privilege-use events. |
PRIVILEGE_USE_FAILURE | 0x08 | Emit failed privilege-use events. |
PRIVILEGE_USE_* controls privilege-use audit (above). OBJECT_ACCESS_* controls a separate kind of force: object-access events that fire even when no SACL audit ACE matched.
How OBJECT_ACCESS_SUCCESS / FAILURE work #
When the calling token's audit_policy has OBJECT_ACCESS_SUCCESS set, step 14b of the pipeline fires an object-access event for every access by this token where the granted mask is non-empty — regardless of whether any SACL audit ACE would have matched.
When OBJECT_ACCESS_FAILURE is set, an event fires for every access where the granted mask did not satisfy the requested mask (i.e. some requested rights were denied).
These flags effectively say: "audit every access this principal makes". The audit log gets an event whether or not the object's owner thought to put an audit ACE in the SACL.
The use case: surveillance of specific principals. A security team responding to a suspected compromised account can set OBJECT_ACCESS_SUCCESS | OBJECT_ACCESS_FAILURE on that account's tokens (via authd reissuing the token) and capture every access the account makes. The objects' SACLs do not need to be modified; the audit comes from the token policy.
Another use case: high-sensitivity contractors or third parties who need temporary access. Their tokens are issued with OBJECT_ACCESS_* flags set so every access is logged independently of which specific objects they touch.
Setting audit_policy #
The audit_policy field is set at token creation and is immutable for the lifetime of the token. authd specifies the value when minting the token via kacs_create_token. There is no AdjustPrivileges-style call to modify audit_policy at runtime.
This is consistent with the rest of the immutable token state. A token's audit policy is decided when it is issued; changing it requires a new token.
For administrators wanting to toggle audit policy on a principal, the mechanism is: have authd reissue the principal's tokens with the new policy. Existing sessions continue to operate under the old policy; new sessions get the new policy. (For more immediate effect, revoke the existing sessions and force re-authentication.)
Composition with SACL audit #
The SACL audit walk (step 14) and the token audit policy (step 14b) run independently. Both can fire events for the same access.
A worked composition example. A SACL has one audit ACE on Everyone for FILE_READ_DATA with SUCCESSFUL_ACCESS_ACE_FLAG. The calling token has audit_policy = OBJECT_ACCESS_SUCCESS. An access for read succeeds.
- Step 14 (SACL walk): the audit ACE matches Everyone, the access succeeded, the success flag is set, fire one event with trigger = "sacl" and the matched ACE.
- Step 14b (token-forced):
OBJECT_ACCESS_SUCCESSis set, the access succeeded, fire one event with trigger = "policy".
Two events for one access. They are not duplicates — they have different triggers, and an audit consumer can distinguish them. The SACL-driven event records that this specific audit ACE matched; the policy-driven event records that the token's audit policy required logging.
In practice, audit consumers either treat both events as one entry (deduplicating by some criteria like access context) or keep them separate. The kernel does not merge them; both fire.
What policy-forced auditing does not do #
A few clarifications:
- Token audit_policy does not affect the access decision. The flags fire events; they do not gate access. A token with
OBJECT_ACCESS_FAILUREset is not somehow more likely to be denied; the access check runs identically. - Token audit_policy does not fire on every kernel surface. It fires on AccessCheck-mediated accesses. Operations that do not go through AccessCheck (a syscall the kernel handles directly without an SD lookup, an internal kernel operation) are not subject to it. Privilege-use audit fires only on privileges that fire during AccessCheck, not on privileges exercised in kernel-standalone paths.
- Privilege-use audit does not always fire. It fires only when the token's
audit_policyhas the corresponding bit set. A token withaudit_policy = 0produces no privilege-use events, even when privileges are exercised. The policy is opt-in per token.
The four bits in detail #
A summary table of what each bit controls:
| Bit | Controls | When event fires |
|---|---|---|
OBJECT_ACCESS_SUCCESS (0x01) | Successful object access | After step 14b, when granted mask is non-empty |
OBJECT_ACCESS_FAILURE (0x02) | Failed object access | After step 14b, when requested mask is not fully granted |
PRIVILEGE_USE_SUCCESS (0x04) | Privilege use that succeeded | After step 13, when a privilege's contributed bits survived |
PRIVILEGE_USE_FAILURE (0x08) | Privilege use that failed | After step 13, when a privilege's contributed bits were stripped |
The bits are independent. A token can have all four, none, or any combination. The most common configurations:
audit_policy | Use case |
|---|---|
0 | Default. Audit only from SACL ACEs. |
OBJECT_ACCESS_FAILURE | PRIVILEGE_USE_FAILURE | Capture every denial. Useful for security monitoring. |
OBJECT_ACCESS_SUCCESS | OBJECT_ACCESS_FAILURE | Surveillance — every access by this principal. |
| All four | Maximum visibility. Compliance audit, forensics. |
The choice is operational: how much audit volume can the deployment afford to handle, and what is the audit pipeline configured to do with the events. There is no security benefit to setting more bits than the consumer can actually process — events that are dropped because the consumer fell behind are not useful audit.
Putting it together #
The full set of auditing channels:
| Source | Fires when |
|---|---|
SYSTEM_AUDIT* ACE in SACL | The ACE matches the caller's identity and access |
SYSTEM_ALARM* ACE in SACL | Per-operation on an open handle when the operation overlaps the ACE's mask |
| CAAP effective SACL audit ACE | The CAAP rule applied and its audit ACE matched |
PRIVILEGE_USE_SUCCESS audit policy | A privilege contributed surviving bits to the granted mask |
PRIVILEGE_USE_FAILURE audit policy | A privilege contributed bits that were stripped by a later layer |
OBJECT_ACCESS_SUCCESS audit policy | The access succeeded (granted mask non-empty), no SACL ACE match required |
OBJECT_ACCESS_FAILURE audit policy | The access failed (some requested bits denied), no SACL ACE match required |
Plus the session-destruction event (covered in Logon sessions) which fires independently of any access check.
A single access can produce events from many of these channels at once. An audit consumer sees them, deduplicates or correlates as appropriate, and persists what is needed for downstream use.
Where to go next #
For the schemas of the events these mechanisms emit and how they reach consumers, read Events and transport.
For the privilege model behind privilege-use audit, read Privileges.
Events and transport
Peios / Peios Security Fundamentals / Auditing
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 #
Five event types come out of the access-check layer and its surroundings:
| 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 kernel enforcement point for the operation (FACS for file handles; other kernel subsystems such as LCS for their own objects) |
privilege-use | A privilege contributed bits AND the token's audit_policy PRIVILEGE_USE_* requested it | Step 13 of AccessCheck |
caap-policy-diagnostic | A CAAP SACL evaluation error, or a staged/effective result mismatch | Step 12 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 a non-empty surviving_access; 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 PSPK §2, the kernel-boundary protocol standard in which 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.
See also #
- Auditing — the model these events come from.
- Audit ACEs — the SACL mechanisms behind access-audit and continuous-audit events.
- Policy-forced auditing — the token policy behind privilege-use and forced object-access events.
- Wire formats reference — byte-level encoding of the SIDs and ACEs carried in events.