# Auditing

---

# Auditing

_Peios / Peios Security Fundamentals / Auditing_

> Auditing records what happened, separate from what was allowed. Audit ACEs, token audit policy, and delivery through KMES to userspace consumers.

**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](/peios/security-fundamentals/auditing/audit-aces.md). Privilege-use audit and the token-forced subset of object-access audit are covered in [Policy-forced auditing](/peios/security-fundamentals/auditing/policy-forced-auditing.md). Event schemas and the KMES transport are in [Events and transport](/peios/security-fundamentals/auditing/events-and-transport.md).

## Where audit fits in the pipeline

```mermaid
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](/peios/security-fundamentals/auditing/events-and-transport.md).

## 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.
- **`eventd`** is 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 `eventd` has 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_policy` may 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](/peios/security-fundamentals/auditing/audit-aces.md).

If you want privilege-use audit and the token-forced audit policy — events that fire independent of any SACL ACE — read [Policy-forced auditing](/peios/security-fundamentals/auditing/policy-forced-auditing.md).

If you want the wire-level details — event schemas, what each field means, how KMES delivers events to consumers — read [Events and transport](/peios/security-fundamentals/auditing/events-and-transport.md).

---

# Audit ACEs

_Peios / Peios Security Fundamentals / Auditing_

> SYSTEM_AUDIT ACEs fire once per access check; SYSTEM_ALARM ACEs audit every operation on the handle. Polarity, conditional variants, and inheritance.

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:

1. Does the caller's identity match the ACE's SID? If not, the ACE does not apply.
2. Does the requested access overlap the ACE's mask? If not, the ACE does not apply.
3. If the access succeeded and the ACE has `SUCCESSFUL_ACCESS_ACE_FLAG` set, emit an event.
4. If the access failed and the ACE has `FAILED_ACCESS_ACE_FLAG` set, emit an event.
5. 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](/peios/security-fundamentals/auditing/policy-forced-auditing.md)). 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:

1. Computes a continuous-audit mask — the union of all matching alarm ACEs' masks.
2. Returns the mask to the caller as part of the access check's output.
3. 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](/peios/security-fundamentals/security-descriptors/acls-and-aces.md). 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](/peios/security-fundamentals/central-access-policies/overview.md)). 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](/peios/security-fundamentals/auditing/policy-forced-auditing.md).

For what the fired events contain and how they reach userspace, read [Events and transport](/peios/security-fundamentals/auditing/events-and-transport.md).

For where audit ACEs live and who may read or write them, read [The SACL](/peios/security-fundamentals/security-descriptors/the-sacl.md).

---

# Policy-forced auditing

_Peios / Peios Security Fundamentals / Auditing_

> Privilege-use audit and the token's audit_policy bitmask fire events without any SACL ACE. When each fires and how they compose with SACL-driven audit.

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:

1. Step 4: SeBackup grants read-category bits via the privilege.
2. Step 8: DACL walk grants nothing additional.
3. Step 11: confinement intersection strips the privilege-granted bits (confinement does not preserve privilege grants).
4. 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](/peios/security-fundamentals/auditing/events-and-transport.md).

## 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_SUCCESS` is 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_FAILURE` set 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_policy` has the corresponding bit set. A token with `audit_policy = 0` produces 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](/peios/security-fundamentals/logon-sessions/lifecycle.md)) 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](/peios/security-fundamentals/auditing/events-and-transport.md).

For the privilege model behind privilege-use audit, read [Privileges](/peios/security-fundamentals/privileges/overview.md).

---

# Events and transport

_Peios / Peios Security Fundamentals / Auditing_

> The audit event schemas and the KMES transport that carries them — access-audit, continuous-audit, privilege-use, and logon-session-destroyed.

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](/peios/using-peios/wire-formats-reference/overview.md); 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_access` is what *this operation* asks for. A `read` on a file requires read; an `mmap` requires execute or write depending on mode.
- `matched_access` is the subset of `requested_access` that 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_access` is 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:

1. The kernel produces an event (e.g. an audit fires during step 14).
2. The kernel writes the event into each subscriber's KMES buffer.
3. Userspace consumers read from their KMES buffers when they are ready.
4. 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](/peios/advanced-peios/pspk/kmes-event-stream/scope.md), 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-audit` for compliance, just `privilege-use` for 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-audit` with 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](/peios/security-fundamentals/auditing/overview.md) — the model these events come from.
- [Audit ACEs](/peios/security-fundamentals/auditing/audit-aces.md) — the SACL mechanisms behind access-audit and continuous-audit events.
- [Policy-forced auditing](/peios/security-fundamentals/auditing/policy-forced-auditing.md) — the token policy behind privilege-use and forced object-access events.
- [Wire formats reference](/peios/using-peios/wire-formats-reference/overview.md) — byte-level encoding of the SIDs and ACEs carried in events.
