7.3 Per-Field Control

A descriptor can grant read access to some fields of a record and not others, using KACS object ACEs and object type lists. A caller authorized for a pattern but not for a field receives the records with that field absent — indistinguishable from a record that never carried it (PSPU §3.28).

7.3.1 Object type lists #

For each access check eventd builds an object type list: a tree with the data type's root at level 0 and, beneath it, the fields' dotted paths, one node per segment. subject is at level 1, subject.token at level 2, subject.token.sid at level 3. A field's node is the one at the end of its path, and every node above it is a prefix of the path, ending before a ., named as a field would be (below): the node for subject.token has the GUID of the name subject.token. A prefix that several fields share is one node, so the list names no GUID twice, and the list is in preorder, each node's subtree directly after it, as KACS requires.

Level 0: root GUID for the data type
  Level 1: event
    Level 2: event.time
    Level 2: event.type
    Level 2: event.cpu
  Level 1: emitter
    Level 2: emitter.class
    Level 2: emitter.token
      Level 3: emitter.token.guid
    Level 2: emitter.true-token
      Level 3: emitter.true-token.guid
    Level 2: emitter.process
      Level 3: emitter.process.guid
  Level 1: access
    Level 2: access.granted
  Level 1: subject
    Level 2: subject.token
      Level 3: subject.token.sid
  Level 1: source
    Level 2: source.name

The list is as deep as the deepest path, with no level limit. MS-DTYP's object type lists stop at level 4; KACS sets no depth limit, only one on the number of nodes, 1024 to a list (KACS_ACCESS_CHECK_MAX_OBJECT_TYPE_COUNT). Every prefix counts toward it. A record whose list would be longer cannot be checked, and a query reaching one fails rather than showing it.

A name with no . — a log field, a fixed metric field, most metric labels — is a node at level 1. A metric label key may contain ., and is then a path like any other: a label disk.read is beneath the node disk.

kacs_access_check_list returns a verdict per node, and eventd uses the verdict on each field's node to include or exclude that field. The verdicts on the prefix nodes are not used.

The three root GUIDs — one for events, one for logs, one for metrics — are in §B.

7.3.2 Field GUIDs are derived, not registered #

A field's GUID is computed deterministically with UUID v5 (RFC 4122):

field_guid = uuid_v5(EVENTD_FIELD_NAMESPACE, field_name)

with the namespace UUID in §B, and field_name the field's query-language name as UTF-8.

There is no registry of field GUIDs and no allocation step. The same name always yields the same GUID, so an administrator writing a descriptor computes the GUID from the field name with the same algorithm that eventd will use when it builds the list. This is the one part of eventd's access control that a third party reproduces rather than merely consumes.

Derivation rather than registration is what makes payload fields tractable at all: event payload schemas belong to the emitting subsystems, eventd has no catalogue of them, and a new event type carrying a new field needs no registration anywhere before a descriptor can name it.

Which names are used:

Datafield_name
Event header fieldits field path (PSPU §3.22) — event.time, event.type, event.cpu — never its column's name
Event payload fieldthe flattened dot path — access.granted, subject.token.sid, source.name
Log fieldthe column name — origin, message, is_error
Fixed metric fieldtimestamp, boot_id, name, type, value
Metric labelthe label key — core, device

Payload fields suppressed by flattening or by a header collision — a value or map at a header field's path — are not query-language fields, so they get no GUID (PSPU §3.22). A GUID naming a header field's path is the header field's, whatever the payload holds there.

A descriptor written before header fields were named by path, granting timestamp or cpu_id by GUID, now grants the payload field of that name rather than the header field, because the GUID is the name's. Such a grant must be rewritten with the path to keep its meaning. Metric label keys cannot collide with the fixed metric fields, because ingestion rejects records whose labels do.

7.3.3 The GUID does not encode scope #

A field GUID names a field and nothing else. access.granted produces the same GUID whatever event type carries it.

Scoping comes from the descriptor hierarchy: an object ACE naming the access.granted GUID inside the descriptor for pattern kacs means "the access.granted field of KACS events". The same ACE in a different pattern's descriptor means the same field of that pattern's records.

7.3.4 Granting a subtree #

An object ACE applies to its node and to every node beneath it, so an ACE naming a prefix applies to every field whose path it begins: subject covers subject.token.sid, subject.process.pid and any subject field an emitter adds later. The header fields are paths like the rest: event covers every event.* header field, and emitter every emitter.* header field together with the payload fields beside them, such as emitter.process.pid.

KACS decides each node once, first ACE first. A field may therefore be read when, of the ACEs that match the caller and name its GUID, the GUID of any prefix of its path, the data type's root, or no object type at all, the first in DACL order to decide EVENTD_READ allows it. Neither the more specific nor the more general ACE wins as such:

DACL, in ordersubject.token.sidsubject.process.pid
deny subject, allow subject.token.siddenieddenied
deny subject.token.sid, allow subjectdeniedread
allow subject.token.sid, deny subjectreaddenied

A canonically ordered DACL (PCDS §5.5) puts every deny before every allow, so there a deny anywhere on a field's path hides it.

A deny on a field does not reach its siblings: denying subject.token.sid leaves subject.token.auth-id and subject.process.pid as the rest of the DACL decides them, and denying subject.token hides every token field and leaves subject.process. KACS does carry a deny up to the node's ancestors, and a grant up to a node all of whose children are granted, but only onto the prefix nodes and the root, never onto another field.

A field can also be a prefix of another in the same record, since a payload is opaque: a payload value at emitter sits beside the header's emitter.class. Its node in the record's list then has nodes beneath it, whose grants and denies KACS carries up to it. eventd therefore checks such a field again in a list of its own, holding only its prefixes and itself, and uses that verdict, so that the same first-ACE rule decides it as decides any other field.

A descriptor written before the list was a tree keeps its meaning for every field it names by full path: that field's GUID, and so its node, is unchanged. What is new is that an ACE naming a prefix, which named a node in no list before, now covers the fields beneath it.

7.3.5 Writing one #

An object ACE with no object type GUID applies to the root and therefore to every field. One with a field GUID applies to that field.

To grant a security team full read access to KACS events, and a monitoring team only the time, type and CPU:

  • Allow SecurityAdmins, EVENTD_READ, no object GUID
  • Allow MonitoringTeam, EVENTD_READ, object GUID = event.time
  • Allow MonitoringTeam, EVENTD_READ, object GUID = event.type
  • Allow MonitoringTeam, EVENTD_READ, object GUID = event.cpu

MonitoringTeam querying KACS events receives records containing exactly those three keys. Payload fields, identity GUIDs and the remaining header fields are absent.

To let an auditing team see who acted but not the token's groups:

  • Deny AuditTeam, EVENTD_READ, object GUID = subject.token.groups
  • Allow AuditTeam, EVENTD_READ, object GUID = event
  • Allow AuditTeam, EVENTD_READ, object GUID = subject

AuditTeam receives every event.* header field and every subject field except subject.token.groups.

7.3.6 Building the list per record #

The list is constructed from the fields actually present in the record being checked: the root node, then the tree of their paths.

Event records contribute every header field plus every non-suppressed flattened payload field present in that particular payload. Different event types produce different lists, because they carry different payloads — and two events of the same type can too, since a payload is opaque MessagePack and nothing requires two of a type to agree.

Log records have a fixed field set — timestamp, origin, is_error, message, job_id, boot_id — so the list is the same for every log record.

Metric records contribute the five fixed fields plus the series' label keys, which vary per series.

Derived aggregate outputs — count, sum, avg, min, max — are omitted from the list entirely. They are not source fields, have no GUID and no access identity of their own, and are visible when the caller is authorized for the records and source fields they were computed from (PSPU §3.28).

7.3.7 Metric aggregate headers #

A metric aggregate or window result does not restore timestamp, name or type if source-field authorization removed that header. The private time used for window placement and ordering is independent of the returned record, so hiding timestamp does not change the numeric result or move samples into another window.

When an output combines several contributing series or metric identifiers, each returned header must be visible in every contributing source. A first contributor with broader access cannot restore a header denied by another. Per-series output retains each series' own visibility. This also applies to latest-value aggregates, transformed windows, and cached rollup results: a cached value does not bypass the current source-field check. Explicit references to a denied field still filter the source records as specified by PSPU §3.28.

Edit this page