Peios Learn
Products
PePeios pkpekit PvProvium UDUniversal Directory TrTrail PrProject WiWispist
Using Peios Security Basics Technical Documentation Source
Using Peios Security Basics Technical Documentation Source
Peios

Privileges

Single-page view · as markdown

Privileges

Peios / Peios Security Fundamentals / Privileges

A privilege is a system-wide right carried on a token. Where group membership decides "is this principal allowed to do things by virtue of who they are", a privilege decides "is this principal allowed to do this specific operation, regardless of who they are". Loading a kernel module, taking ownership of an object, changing the system clock, reading any file for backup — each is gated by a specific privilege, granted to principals whose role legitimately requires it.

Privileges sit on the token alongside the user SID and group list, but they are evaluated separately from identity. The access check reads a privilege bitmask, not a group membership. An ACE in a DACL cannot match a privilege the way it matches a SID. Privileges are their own axis of authority.

What a privilege is #

A privilege has three properties:

  • A name — a specific string like SeBackupPrivilege. Most privilege names start with Se and end with Privilege; the convention is shared across the catalog.
  • A LUID — a 64-bit identifier the kernel uses internally to reference the privilege. Privileges occupy specific bit positions in the 64-bit privilege bitmask on every token.
  • A specific operation it gates — every privilege has a defined effect: which kernel operation, which AccessCheck pathway, which capability it controls.

Privileges are global. The catalog is fixed at build time; there is no API to define new privileges at runtime. A token either has a specific named privilege or does not. The number of distinct privileges in v0.20 is in the low tens.

Where privileges come from #

A token's privileges are decided at one moment: when the token is minted. That happens either:

  • During boot, when the kernel constructs the SYSTEM token (which holds every privilege).
  • When authd authenticates a principal and constructs their token, applying the privilege policy authd has loaded for the principal's role.
  • When peinit mints a token for a service it is launching.
  • When existing code creates a derived token via DuplicateToken (preserves privileges) or FilterToken (removes the listed privileges).

There is no path to gain a privilege after a token is minted. AdjustPrivileges can enable or disable a privilege the token already has, and can permanently remove a privilege, but cannot add one that was not present at creation. A token's privilege bitmask is at most what was put there at the start.

The privilege policy authd consults — which principals get which privileges — is local to the machine, and lives in the registry under Machine\Generic\Authn\Policy, one record per principal. authd reads it at every logon. See assigning privileges for the record format and the rules.

That it is local is the design rather than an implementation detail. A principal source — the local store, or a directory — says who someone is: their SID, their memberships, their POSIX identifiers. It never says how much this machine trusts them, and the protocol it speaks gives it no way to. So a directory can tell this machine that you are a member of Domain Admins; whether that membership carries SeLoadDriverPrivilege here is this machine's answer, not the directory's.

From the kernel's point of view the policy is invisible: it only ever sees the resulting token.

The four states of a privilege #

At any moment, each privilege on a token is in one of four states:

StateMeaning
AbsentThe privilege is not on this token. The token's bitmask has the relevant bit clear in both the "present" and "enabled" fields.
Present, disabledThe privilege is on the token but not currently in effect. The access check ignores it. The token may enable it via AdjustPrivileges.
Present, enabledThe privilege is on the token and is in effect. AccessCheck will use it where applicable.
UsedThe privilege has been exercised at least once. A sticky audit bit; never cleared.

The states layer rather than replace. A privilege that has been used remains in whatever present/enabled state it was in; the "used" bit is recorded alongside, for auditing.

Disabling rather than removing is the common pattern. A service runs most of the time with sensitive privileges present but disabled, enabling them only at the moment they need to be exercised and disabling them afterwards. The reason: the access check ignores disabled privileges entirely, so the service is not at risk of accidentally exercising a privilege it had not meant to use.

The full lifecycle — transitions, AdjustPrivileges semantics, FilterToken-based permanent removal, the "used" bit — lives in Privilege lifecycle.

Privileges in the access check #

Privileges interact with the access check in three ways:

Direct gating #

Some operations are gated directly by a privilege held at the moment of the call, with no DACL involved.

  • SeLoadDriverPrivilege gates kernel module loads. No DACL applies; either you have the privilege enabled or the load fails.
  • SeSystemtimePrivilege gates settimeofday.
  • SeCreateTokenPrivilege gates kacs_create_token.

These privileges are direct. The check is "is this privilege enabled on the calling token" and nothing else.

Influencing the DACL walk #

A handful of privileges, when enabled, change what the DACL walk would conclude. These are the AccessCheck-influencing privileges, covered in Privilege categories:

  • SeSecurityPrivilege grants ACCESS_SYSTEM_SECURITY (SACL read/write) regardless of the DACL.
  • SeTakeOwnershipPrivilege grants WRITE_OWNER on any object.
  • SeBackupPrivilege grants read access to any object (with the backup intent flag set).
  • SeRestorePrivilege grants write access and ownership-change rights (with the restore intent flag).
  • SeRelabelPrivilege permits raising an object's integrity label above the caller's own.

These privileges produce grants that no DACL would have produced. They are recorded in the access check's audit state so audit events can record "this access was granted by privilege X" rather than appearing as a DACL grant.

Intent-gated privileges #

Two of the influencing privileges — SeBackupPrivilege and SeRestorePrivilege — only fire when the caller passes a specific intent flag to AccessCheck. Without the flag, the privileges might as well not be present. This avoids accidentally exercising a backup privilege when doing a normal read.

The intent model is covered in Intent-gated privileges.

Privileges vs groups #

The split between privileges and group membership is one of the deliberate design decisions of the model:

Group membershipPrivileges
What it grantsWhatever ACEs name the groupA specific operation, named directly
Where it livesThe token's groups fieldThe token's privilege bitmask
How it is matchedACE SID matches token group SIDPrivilege code is consulted in specific kernel paths
Who manages itWhoever holds the accounts (a directory, or lpsd) and the per-object administrator (DACL entries)This machine's administrator, in local policy
GranularityGroup-wide; the same group across all objectsPer-operation; the same privilege across all objects

A user can be in a group and still not hold a privilege the rest of the group has, or vice versa, because the two are managed independently — and, more than that, they are managed by different parties. Memberships come from whoever holds the account; privileges come from this machine.

The classical example: BUILTIN\Administrators is a group, and being in it grants whatever ACEs in DACLs name the group. But the dangerous privileges — SeLoadDriver, SeBackup, SeRestore — are not automatic by group membership. Local policy specifies which administrators get which privileges, and it does so by naming principals: a record for one account's SID can grant it privileges the rest of Administrators does not have, or withhold ones they do.

This separation matters because the failure modes are different. A misconfigured DACL grants too much group access; a misconfigured privilege policy grants too much system-level authority. Both are fixable in different places.

Privileges and inheritance #

Group memberships propagate down through identity (you remain in your groups until your token changes). Privileges work the same way: a token holds the privileges authd minted it with, and the bitmask is preserved through fork, exec, and inheritance into child processes — exactly like the rest of the token.

Privileges do not propagate to ACEs or objects. There is no "privilege-bearing" ACE; no SD can be annotated with a privilege requirement; no object can demand "you must have SeBackup to open me". The privilege model and the SD model do not overlap that way. They interact only through the access check, when an influencing privilege is enabled on the caller's token and the AccessCheck pipeline decides whether to use it.

Where to start #

If you want the lifecycle in detail — the present/enabled/used/removed transitions, what AdjustPrivileges actually does, how FilterToken removes a privilege permanently — read Privilege lifecycle.

If you want to understand intent-gated privileges — why SeBackup and SeRestore require an explicit flag and how that flag is passed — read Intent-gated privileges.

If you want to see how privileges are organised — the kernel-standalone group, the AccessCheck-influencing group, the application-level group, the reserved group — read Privilege categories.

If you want the full catalog of named privileges with their numeric LUIDs and one-line descriptions, that is in the Constants and catalogs reference.

Privilege lifecycle

Peios / Peios Security Fundamentals / Privileges

A privilege's state on a token is more than just "yes, the token has it". The four-state model — absent, present-disabled, present-enabled, used — exists so a service can hold a sensitive privilege most of the time without exercising it, enable it briefly when it actually needs it, disable it again afterwards, and have all of those transitions auditable.

This page covers each transition: how privileges get added (only one moment), how they get enabled and disabled (many times), how they get permanently removed (one-way), and what the "used" bit means for auditing.

The four states #

From the Privileges overview:

StateBitmask representationMeaning
Absentpresent bit clearNot on the token. AccessCheck cannot use it.
Present, disabledpresent set, enabled clearOn the token but not in effect. AccessCheck ignores it.
Present, enabledpresent set, enabled setIn effect. AccessCheck will use it where applicable.
Used (overlaid)used bit set, independent of the othersHas been exercised at least once. Sticky.

The bitmask is a 64-bit word. Each privilege occupies a fixed bit position (its LUID determines which). The token actually carries four independent 64-bit words: present, enabled, enabled_by_default, and used. Most code only cares about the first two.

enabled_by_default records the initial state — what was enabled at token creation. The kernel uses it to implement the "reset to defaults" sentinel in AdjustPrivileges.

Adding a privilege: only at creation #

There is exactly one moment when a privilege can be added to a token: when the token is being created. Specifically:

  • During boot, the kernel constructs the SYSTEM token with every privilege present and enabled.
  • When authd calls kacs_create_token, the wire-format specification names the privileges to include and which of them should start enabled.
  • When peinit calls kacs_create_token for a service.

DuplicateToken preserves the privilege bitmask of the source — every present bit, every enabled bit, every used bit copies into the new token. The duplicate has the same privileges as the source.

That is the entire list of paths. There is no syscall, no ioctl, no privileged API to add a privilege to an existing token. A token's present bitmask is at most what it was at creation; it can only shrink over time.

This rule is one of the load-bearing parts of the security model. It is what makes "the principal's authority is fully expressed by their token" true. If a process could acquire new privileges at runtime, the kernel would need to track which privilege-acquisition paths exist and protect each one. By making creation the only entry point and authd the only entity that can use it, the kernel concentrates the privilege-policy decision into one component, audited at one moment.

Enabling and disabling: AdjustPrivileges #

The most common runtime operation on privileges is enabling or disabling them. The syscall is AdjustPrivileges (also reachable as the KACS_IOC_ADJUST_PRIVS ioctl on a token fd).

The caller passes an array of kacs_priv_entry records, each containing:

  • A LUID identifying which privilege to adjust.
  • An attributes value telling the kernel what to do with it.

The attributes value is one of:

ValueEffect
0Disable the privilege. Clears the enabled bit; leaves present set.
SE_PRIVILEGE_ENABLED (0x02)Enable the privilege. Sets the enabled bit. The privilege must already be present.
SE_PRIVILEGE_REMOVED (0x04)Permanently remove the privilege. Clears present, enabled, and enabled_by_default. Does not clear used. Irreversible.
KACS_PRIV_RESET_ALL_DEFAULTS (0x80000000)Sentinel value, used with LUID 0. Resets every privilege on the token to its enabled_by_default state.

The operation is atomic: every entry in the array is validated first, and if any one is invalid (a LUID for a privilege not present on the token, an unknown attributes value, a duplicate LUID), the entire call fails and no changes are made. There is no partial success.

The call returns the previous state of every privilege touched, as a bitmask. Callers that want to do scoped privilege enable — turn on a privilege, do work, turn it off — read the return value to know what state to restore to. The pattern:

  1. Save previous state by calling AdjustPrivileges to enable the desired privilege.
  2. Do the work.
  3. Call AdjustPrivileges again with the saved state to restore.

If the privilege was already enabled before step 1, step 3 leaves it enabled. If it was disabled, step 3 restores it to disabled. Either way the work runs with the privilege enabled and the original state is preserved.

What AdjustPrivileges cannot do #

AdjustPrivileges cannot:

  • Add a privilege. A LUID for a privilege not present on the token is rejected. The token must already have the privilege; AdjustPrivileges only changes its enabled state or removes it.
  • Resurrect a removed privilege. Once SE_PRIVILEGE_REMOVED is applied, the privilege is absent. A subsequent attempt to enable it fails (the privilege is no longer present).
  • Toggle the used bit. The kernel sets used automatically when a privilege is exercised. Userspace cannot clear it.
  • Set enabled without present. The kernel rejects this — a privilege cannot be enabled if it is not present on the token. The "enabled but absent" state is not representable.

The right to call AdjustPrivileges is gated by TOKEN_ADJUST_PRIVILEGES (0x0020) on the target token. A thread can always adjust its own token's privileges (the default token SD grants this right to the token's user identity); adjusting another process's token requires PROCESS_QUERY_INFORMATION on that process, TOKEN_ADJUST_PRIVILEGES on the token, and the appropriate PIP dominance.

The "used" bit and audit #

When AccessCheck or any other kernel path actually exercises a privilege on a token, the kernel sets the corresponding bit in the token's used bitmask. Once set, the bit remains set for the life of the token. Nothing — not AdjustPrivileges, not FilterToken, not DuplicateToken into a fresh token — clears it.

The bit is for auditing. It answers the question "did this token, at some point in its existence, exercise this privilege?". A security audit can read a token's used bitmask to see which privileges have been engaged so far. A privilege that is present but never used is materially different from one that has been used; the used bit records the difference.

This is why removal does not clear used. If a privilege has been exercised, removing it later does not erase the fact. The audit trail remains.

DuplicateToken copies used from the source. A duplicate of a token that has used a privilege is itself marked as having used it, even though the duplicate has never actually exercised it. The rationale: a duplicate inherits the source's audit history; if the source has used a privilege, anything derived from it is suspect of having access to that privilege's effects.

If you need a fresh, audit-clean token, you need a freshly-minted token (a new authentication, or a new kacs_create_token call). Duplicates carry history.

Permanent removal #

SE_PRIVILEGE_REMOVED permanently removes a privilege from a token. After removal:

  • present is clear.
  • enabled is clear.
  • enabled_by_default is clear.
  • used remains whatever it was (still set if the privilege was exercised before removal).

The removal is irreversible. There is no syscall to re-add. The only way back to a token with the privilege is to start with a different token — typically by FilterToken from a source that still has the privilege, or by a fresh authentication.

Removal is the right tool for:

  • Service hardening. A service that holds privileges by default but only needs them at startup can remove them after the startup phase. Removing rather than disabling makes the privilege unrecoverable for the remainder of the service's lifetime — even if an attacker compromises the service later, the privileges are gone.
  • Sandbox launchers. Code that forks a child to run untrusted work removes every dangerous privilege from the child's token before exec. The child has no way to get them back.
  • One-way demotion. A process starting with high authority that wants to demote itself to a lower-privileged identity, irrecoverably, removes the privileges it no longer wants. This is stronger than disabling: a disabled privilege can be enabled by code running on the token (assuming TOKEN_ADJUST_PRIVILEGES); a removed privilege cannot.

The one-way nature is the point. The model is built around the assumption that authority shrinks. A removed privilege is gone for the same reason a disabled group can be marked use-for-deny-only and not un-marked: each is a deliberate restriction the kernel will not let userspace undo.

FilterToken and bulk removal #

For removing multiple privileges at once — typically during sandbox creation — FilterToken is the appropriate API. Where AdjustPrivileges with SE_PRIVILEGE_REMOVED removes one privilege from the calling token, FilterToken produces a new token with the listed privileges removed.

The difference matters: FilterToken does not modify an existing token. It creates a copy with the listed adjustments (privileges removed, groups marked use-for-deny-only, restricted SIDs added) and returns the new token. The original is unchanged.

This is the right pattern for sandbox launchers, which want to filter a token down to a restricted version for a child process without affecting their own running identity. AdjustPrivileges with SE_PRIVILEGE_REMOVED is the right pattern for code that wants to demote itself.

FilterToken is covered in Token lifecycle and Restricted and write-restricted tokens.

The reset-to-defaults sentinel #

A token records enabled_by_default separately from enabled. The two diverge as the token's privileges get enabled and disabled at runtime; the default record stays as it was at creation.

The KACS_PRIV_RESET_ALL_DEFAULTS sentinel — a kacs_priv_entry with luid = 0 and the sentinel attributes value — tells AdjustPrivileges to copy enabled_by_default back into enabled. Every privilege that was enabled at creation is now enabled again; every privilege that was disabled at creation is now disabled again.

The sentinel is useful for code that does "enable a few privileges, do work, restore". Instead of saving the prior state and restoring it explicitly, the code can simply reset to defaults — which is the right behaviour as long as nothing else has changed the defaults (which nothing should, since defaults are immutable at creation).

The reset sentinel only affects enabled. It does not bring back removed privileges. It does not clear the used bit. It is a cheap way to "go back to how this token started" within the set of operations that still make sense.

Field mutability summary #

To pin the entire model on one table:

FieldMutability
presentOnly ever clears (via SE_PRIVILEGE_REMOVED). Set only at token creation.
enabledToggled freely between 0 and the corresponding present bit.
enabled_by_defaultImmutable after token creation.
usedSet by the kernel when the privilege is exercised. Never clears.

The asymmetric mutability is the model's spine. Authority can decrease at runtime but not increase. State for auditing accumulates. Defaults are fixed. The "easy" mutation — toggle enabled — is the only one that goes both ways, and it does not affect what the token fundamentally is.

Where to go next #

For the two privileges that require an intent flag on top of being enabled, read Intent-gated privileges.

For bulk privilege removal as part of building a sandbox token, read Restricted and write-restricted tokens.

Intent-gated privileges

Peios / Peios Security Fundamentals / Privileges

Most privileges work the same way: if they are enabled on the calling token at the moment of the call, the kernel uses them; if not, it does not. AdjustPrivileges manages enabled state; the access check reads it. There is no other input.

Two privileges are special: SeBackupPrivilege and SeRestorePrivilege. Both are present-and-enabled on the tokens of backup-and-restore tools. Both grant access that the DACL alone would not. And both will refuse to do anything unless the calling code passes a specific intent flag to AccessCheck — BACKUP_INTENT for SeBackup, RESTORE_INTENT for SeRestore.

Without the intent flag, the privilege is invisible to the access check. The token has it. The kernel knows it has it. But the privilege does not participate in the access decision. The DACL walk runs as if the privilege were not present.

This page covers why intent gating exists, how the flags are passed, and what each privilege actually does when its intent flag is set.

Why intent gating exists #

The privileges in question grant very broad access. SeBackup grants read on any object regardless of the DACL. SeRestore grants write, ownership change, and SACL access on any object. They are designed for backup-and-restore tools — programs that legitimately need to bypass discretionary access control to do their job.

The problem: if these privileges were always in effect when enabled, every access check on a token holding them would benefit from them. A backup tool that opens a configuration file as part of its initialisation — for entirely ordinary reasons — would silently exercise SeBackup, even though the configuration file's normal DACL would have granted the access anyway. The audit trail would record privilege exercises that were operationally meaningless. Worse, a buggy code path in the tool that did something unintended would silently get backup-level access to the unintended object.

Intent gating solves this by making the privilege opt-in per call. The tool's main backup loop sets BACKUP_INTENT on every AccessCheck call it makes; everything else does not. The privilege fires only where the tool deliberately asked for it. Audit events for privilege exercise are accurate (they only appear where the tool intentionally invoked the privilege), and bugs in non-backup code paths cannot accidentally exercise SeBackup.

The split is between "enabled" (the token can use the privilege) and "intent" (the caller wants to use the privilege right now). Both must be true.

The intent flags #

The flags are passed to AccessCheck as part of the privilege_intent parameter. They are independent bits:

FlagEffect
BACKUP_INTENT (0x01)Tells AccessCheck that the caller wants SeBackupPrivilege to participate, if present and enabled.
RESTORE_INTENT (0x02)Tells AccessCheck that the caller wants SeRestorePrivilege to participate.

A caller can set both, one, or neither. The flags are not exclusive — a backup-and-restore tool might do a restore-with-verification operation that wants both privileges to fire.

Without either flag, the corresponding privilege is stripped from the access check's view of the token for this call. It is not removed from the token; the token still has it; the next call from the same code can set the flag and use it. The strip happens only for the current AccessCheck.

The flags are at the AccessCheck API surface. Lower-level open and access paths (the file system's open, the registry's read) translate higher-level semantics into AccessCheck calls; whether they pass the flags depends on whether they were told to. Most do not; an opener of a file in the ordinary course of work does not pass BACKUP_INTENT.

What SeBackupPrivilege does #

With BACKUP_INTENT set and SeBackup enabled, AccessCheck grants the caller read-category rights on the object regardless of the DACL:

  • For files: FILE_READ_DATA, FILE_READ_ATTRIBUTES, FILE_READ_EA, and READ_CONTROL.
  • For registry keys: the read-category rights on the key (the specific values depend on the registry's GenericMapping).
  • For tokens: TOKEN_QUERY and READ_CONTROL.

The grant happens after the DACL walk has run. Specifically, the walk produces a granted mask for whatever the DACL alone would grant; the privilege then adds the read-category bits to that mask without consulting the DACL. If the DACL already granted some of them, the privilege grant is redundant; if the DACL granted nothing, the privilege adds them.

The grant is recorded in the access check's audit state. An audit event for this call records "the read rights came from SeBackupPrivilege", distinguishing privilege-granted access from DACL-granted access. Audit consumers that care about privilege use can filter by this marker.

SeBackup does not grant write access. A backup tool that needs to write its output also reads but cannot use SeBackup for that — write is SeRestore's territory.

What SeRestorePrivilege does #

With RESTORE_INTENT set and SeRestore enabled, AccessCheck grants the caller write-category rights and metadata-modification rights regardless of the DACL:

  • Write rights (FILE_WRITE_DATA, FILE_APPEND_DATA, FILE_WRITE_ATTRIBUTES, FILE_WRITE_EA for files; corresponding rights on other object types).
  • DELETE.
  • WRITE_OWNER and WRITE_DAC.
  • ACCESS_SYSTEM_SECURITY (SACL read/write).

Plus, separately, SeRestore bypasses the "new owner must be self or SE_GROUP_OWNER group" restriction during kacs_set_sd. A restore tool can set the owner of a restored object to any well-formed SID, not just the caller's own. This is what lets a backup restore reconstitute an object's original owner even when the original principal is not present on the running system.

Like SeBackup, the grant is recorded in audit state — write rights granted by SeRestore are distinguished from those granted by the DACL.

The reason SeRestore also grants ACCESS_SYSTEM_SECURITY (which would normally require SeSecurityPrivilege) is the same: a restore operation needs to set the SACL as part of reconstituting the object's policy. Forcing the tool to also hold SeSecurity would be redundant; SeRestore folds that authority in.

What the flags do not do #

Two clarifications:

  • The flags do not grant privileges. A token that does not have SeBackup gets nothing from setting BACKUP_INTENT. The flag tells the kernel "use this privilege if I have it"; it cannot conjure a privilege the token lacks.
  • The flags do not enable disabled privileges. A token that has SeBackup present but disabled gets nothing from BACKUP_INTENT, just as it would get nothing without the flag. The privilege must be enabled for AccessCheck to consider using it. Intent is on top of enabled, not in place of it.

The state machine is: token has the privilege AND token has the privilege enabled AND caller has set the intent flag = AccessCheck uses the privilege. Any one of the three missing means it does not.

Why only these two privileges #

The intent-gating model exists specifically for privileges that grant broad, blanket access. SeBackup grants read on everything; SeRestore grants write on everything. The risk of accidental exercise is high enough to be worth a per-call gate.

Other privileges that influence the access check — SeSecurityPrivilege (SACL access), SeTakeOwnershipPrivilege (WRITE_OWNER), SeRelabelPrivilege (raising integrity) — are not intent-gated. They are scoped enough that the "just check if enabled" model is appropriate. SeSecurity only fires when accessing the SACL; SeTakeOwnership only when changing the owner; SeRelabel only when changing the integrity label — none of which is done by accident.

The split is between "this privilege only fires when you do something specific anyway" (no intent needed) and "this privilege would fire on every access if we let it" (intent required). SeBackup and SeRestore are the only two privileges in the second class.

Calling pattern #

A typical backup tool's loop:

  1. Token at startup has SeBackupPrivilege present and enabled. (Granted by authd's privilege policy to backup-role principals.)
  2. For each object to back up:
    • Call AccessCheck with BACKUP_INTENT set in privilege_intent. The kernel grants the read-category rights via SeBackup if the DACL would not.
    • Read the object.
  3. Other operations the tool does (reading its own configuration, writing log output, opening its output file) call AccessCheck without BACKUP_INTENT. These run through the DACL like any other access. The privilege is not exercised.

A typical restore tool's loop is the mirror:

  1. Token at startup has SeRestorePrivilege present and enabled.
  2. For each object to restore:
    • Call AccessCheck with RESTORE_INTENT set. The kernel grants write-category rights via SeRestore.
    • Write the object's contents.
    • Set its SD via kacs_set_sd, including owner. The "any well-formed SID can be the owner" rule is in effect because RESTORE_INTENT was set on the AccessCheck that produced the WRITE_OWNER grant.
  3. Other operations run normally without the flag.

The pattern is symmetric. Both privileges work the same way; both follow the same intent rule.

What about privileges in audit events #

Audit events for privilege exercise carry enough information to distinguish:

  • The privilege that was exercised.
  • The bits it contributed to the final granted mask.
  • Whether those bits survived to the final access decision (i.e., were not stripped by some later layer like restricted-token intersection or confinement).

A backup tool with BACKUP_INTENT set on every AccessCheck call produces clean audit events: one privilege-use event per backup-flavoured access, none for incidental accesses. This is the audit trail intent gating was designed to produce. Without the flag, the privilege would fire on every access from the same token, and the audit would be unable to distinguish "I exercised SeBackup because I'm a backup" from "I exercised SeBackup because the file would have been readable to me anyway".

The full audit model lives in Auditing.

Where to go next #

For the four functional categories the rest of the privileges fall into, read Privilege categories.

For exactly where SeBackup and SeRestore fire during a check — and which layers can still strip their grants — read Privileges in the pipeline.

Privilege categories

Peios / Peios Security Fundamentals / Privileges

The privileges in Peios fall into four functional categories. Each category has a different relationship to the kernel and to the access check. Knowing which category a privilege is in tells you what it does, where it fires, and whether to expect it to participate in the DACL walk.

This page is organised around the four categories. The full per-privilege catalog — name, LUID bit, one-line description for every privilege — is in Constants and catalogs.

The four categories #

CategoryWhat the privileges doExamples
Kernel-standaloneGate specific kernel operations directly. Not consulted during the DACL walk.SeLoadDriverPrivilege, SeSystemtimePrivilege, SeCreateTokenPrivilege
AccessCheck-influencingCause the access check to grant access the DACL alone would not. Consulted at specific points during the access pipeline.SeSecurityPrivilege, SeTakeOwnershipPrivilege, SeBackupPrivilege, SeRestorePrivilege, SeRelabelPrivilege
Application-levelDefined for the directory and authd to interpret. The kernel records them on the token but does not enforce them itself.SeSyncAgentPrivilege, SeEnableDelegationPrivilege, SeMachineAccountPrivilege
ReservedPresent in the catalog for ABI parity but not used in v0.20.SeCreatePagefilePrivilege, SeUndockPrivilege, SeTimeZonePrivilege

The categorisation is functional, not structural. A token's privilege bitmask does not partition by category; all bits live in the same 64-bit word. The category is a property of how each individual privilege is consumed.

Kernel-standalone privileges #

These are the largest category. A kernel-standalone privilege gates a specific kernel operation: the kernel checks at the entry point whether the caller's effective token has the privilege enabled, and refuses the operation if not. The DACL walk is not involved.

Representative members:

PrivilegeWhat it gates
SeCreateTokenPrivilegekacs_create_token. Token minting. Held only by authd and peinit.
SeAssignPrimaryTokenPrivilegeInstalling a token as another process's primary. Used by peinit.
SeImpersonatePrivilegeImpersonating any user (when not running as the same user). Held by every service that handles user requests.
SeTcbPrivilege"Act as part of the TCB" — a catch-all for operations that should only happen in trusted code. Required for KACS_IOC_LINK_TOKENS, kacs_set_caap, mount-policy changes (policy=synth-*, which author security descriptors), and a handful of other system operations. It also satisfies every check SeManageVolumePrivilege satisfies, since the TCB may do anything a volume manager may.
SeLoadDriverPrivilegeLoading and unloading kernel modules. Held only by peinit on its primary token; explicitly stripped via FilterToken from every other service.
SeManageVolumePrivilegeMounting, unmounting and reshaping the mount tree, including mount policy (policy=synth-*). Granted to Administrators. The most powerful privilege routinely granted outside the TCB — see the warning below.
SeShutdownPrivilegeLocal shutdown and reboot.
SeRemoteShutdownPrivilegeShutdown from a remote connection. Requires SeShutdown as well.
SeDebugPrivilegeInspecting another process regardless of its SD. Crucially, it does not bypass PIP dominance — a SeDebug holder can bypass an unrelated process's SD but still cannot cross a PIP boundary.
SeSystemtimePrivilegeSetting the system clock.
SeIncreaseBasePriorityPrivilegeRaising another process's scheduling priority or setting its CPU affinity.
SeIncreaseQuotaPrivilegeOverriding resource limits for a process.
SeLockMemoryPrivilegeLocking pages in physical memory (mlock/mlockall).
SeAuditPrivilegeWriting entries to the audit log.
SeProfileSingleProcessPrivilegeCross-task profiling of a specific other process (perf_event_open). Own-task profiling needs no privilege.
SeSystemProfilePrivilegeSystem-wide profiling (perf_event_open with pid == -1): per-CPU, all-task, kernel-mode events.
SeBindPrivilegedPortPrivilegeBinding to TCP/UDP ports below 1024. A Peios-specific privilege.
SeChangeNotifyPrivilegeBypassing traverse checks during path resolution. Granted to every principal by default; rarely the answer to a question.
SeCreateSymbolicLinkPrivilegeCreating symbolic links. Granted to every principal by default.

For all of these, the calling pattern is the same: the kernel reads the calling token's privilege bitmask at the entry point of the gated operation; if the privilege is not enabled, the operation fails with the appropriate error.

The privileges in this category are mostly held in narrow ways. The most dangerous of them — SeCreateToken, SeTcb, SeLoadDriver — appear on only a handful of TCB token holders. Most other services hold a small, role-specific subset.

AccessCheck-influencing privileges #

These privileges, when enabled, change what the access check itself decides. They produce grants the DACL alone would not.

PrivilegeWhat it does
SeSecurityPrivilegeGrants ACCESS_SYSTEM_SECURITY on any object (SACL read/write). Also gates kernel-standalone audit-system operations.
SeTakeOwnershipPrivilegeGrants WRITE_OWNER on any object regardless of the DACL. Subject to MIC and PIP — does not bypass those.
SeBackupPrivilegeGrants read-category rights on any object when BACKUP_INTENT is set. Intent-gated.
SeRestorePrivilegeGrants write, metadata, and ownership-change rights on any object when RESTORE_INTENT is set. Intent-gated. Also bypasses the "new owner must be self or SE_GROUP_OWNER group" restriction.
SeRelabelPrivilegePermits setting an object's mandatory integrity label above the caller's own. Also acts as the kernel-standalone gate for kacs_set_sd with LABEL_SECURITY_INFORMATION set to a higher label.

These privileges fire at specific points in the access pipeline:

  • SeSecurityPrivilege is consulted when AccessCheck sees ACCESS_SYSTEM_SECURITY in the requested mask.
  • SeBackupPrivilege and SeRestorePrivilege are consulted near the start of AccessCheck, but only if the corresponding intent flag is set in privilege_intent. See Intent-gated privileges.
  • SeTakeOwnershipPrivilege is consulted after the DACL walk: if the walk did not grant WRITE_OWNER and the mandatory policy did not block it, the privilege grants it.
  • SeRelabelPrivilege is consulted by kacs_set_sd when the caller is trying to set an integrity label.

When any of these privileges contributes to the final granted mask, the access check records the fact so audit events can attribute the grant correctly. Audit consumers can distinguish "the user got read access because the DACL allowed it" from "the user got read access because they hold SeBackup and asked to use it".

Important

Not currently grantable. SeTakeOwnershipPrivilege, SeRelabelPrivilege and SeSystemProfilePrivilege are enforced by the kernel exactly as described above and named in audit records, but they are missing from the published ABI headers that userspace is built against — so no policy can name them and authd cannot put them on a token. Everything on this page about how they behave is accurate; what is not yet true is that anyone can hold one. In practice that means a Peios machine currently has no way to grant take-ownership, so the usual escape hatch for a file whose DACL excludes you — take ownership, then rewrite the DACL — is unavailable.

Application-level privileges #

These privileges are defined for authd, the directory, and federation services to interpret. The kernel records them on the token (so authd has somewhere to put them) but does not enforce them itself.

PrivilegeWhat it does
SeSyncAgentPrivilegeLets the holder read all directory objects regardless of per-object permissions. Used by directory replication agents.
SeEnableDelegationPrivilegeLets the holder mark a principal as trusted for delegation in the directory.
SeMachineAccountPrivilegeLets the holder add computer accounts to the domain.

From the kernel's point of view, these are token attributes that no kernel path checks. They appear on the token's privilege bitmask; AdjustPrivileges can enable, disable, or remove them like any other privilege; but no AccessCheck path consults them. Their effect happens entirely in user-space services that read the token and act on its privilege bitmask themselves.

The kernel still enforces the present/enabled/removed/used state machine for these privileges as it does for others — AdjustPrivileges treats them identically. The application-level distinction is about who consumes them, not how they are stored or transitioned.

What SeManageVolumePrivilege is actually worth #

Mounting is an administrative act rather than a TCB one, which is why this privilege exists separately: without it no administrator could mount anything, and peios-install could not run outside a SYSTEM shell.

But be clear about its reach before granting it more widely. Its holder may:

  • mount a filesystem whose synthesised security descriptors it chooses (policy=synth-* with --synth-sddl), and
  • mount over an existing path.

Together those are enough to author policy on a subtree and to shadow a system path — so in the hands of a determined holder it is a route to authority approaching the TCB's.

This is a deliberate design decision, not an oversight. The narrower alternatives — permitting only filesystems that carry their own descriptors, or constraining synthesis to a fixed system template — were considered and rejected, because the FAT ESP carries no descriptors of its own and must be mounted synth-ephemeral for an installation to work at all.

Treat SeManageVolumePrivilege as sitting beside SeLoadDriverPrivilege in sensitivity, not beside SeChangeNotifyPrivilege.

Mounting passes three separate checks, and the privilege covers all three: may_mount() (may I reshape my namespace), mount_capable() (may this context create a superblock), and KACS's set_mount_policy (may I choose how descriptors are synthesised). They were found one at a time by driving a real image — each fix moved the failure to the next gate — which is worth knowing if a fourth ever appears.

Reserved privileges #

A handful of privileges appear in the catalog for binary compatibility with the spec lineage but have no implementation in v0.20:

PrivilegeWhy it is reserved
SeCreateGlobalPrivilegeNo per-session object namespaces in Peios.
SeCreatePagefilePrivilegeFolded into SeTcbPrivilege.
SeCreatePermanentPrivilegeNo Linux equivalent.
SeIncreaseWorkingSetPrivilegeLinux does not gate memory-residency hints.
SeTrustedCredManAccessPrivilegeReserved for future secrets infrastructure.
SeSystemEnvironmentPrivilegeReplaced by SDs on EFI variable files under FACS.
SeTimeZonePrivilegeLinux does not gate timezone changes.
SeUndockPrivilegeServer OS; not applicable.

A reserved privilege's LUID position in the bitmask is allocated, but no kernel path consults it. They are placeholders that keep the bitmask layout stable for future use. A reserved name is not in the privilege vocabulary at all, so a policy record naming one is dropped with a warning rather than granting anything.

Default-grant privileges #

Two privileges deserve a special note: SeChangeNotifyPrivilege and SeCreateSymbolicLinkPrivilege are granted to every principal on a stock machine. The reason is that they are needed for almost every program to function normally — without SeChangeNotifyPrivilege, a process cannot traverse a directory to reach a file, so a token lacking it cannot so much as start a shell; without SeCreateSymbolicLinkPrivilege, a process cannot create the symlinks that build systems and packaging tools depend on.

They are granted by the shipped policy — a record for Everyone — rather than being built into authd, so both are visible and both can be taken away. See assigning privileges. SeChangeNotifyPrivilege alone is additionally authd's compiled floor, applied when a machine has no policy key at all, because a machine that cannot start a shell cannot be repaired from a console.

Their effect is broad-but-uninteresting: on a stock machine every token has them, so any reasoning about access that does not explicitly involve their absence can ignore them. They are mentioned here for completeness; they will rarely be the answer to a question about who can do what.

A token can have these stripped by FilterToken if a sandbox wants to operate without them. Doing so creates a token that cannot traverse arbitrary directories — useful in a tightly confined sandbox, not useful much elsewhere.

How to find the catalog #

The four-category model on this page is the conceptual structure. The byte-level catalog — every privilege name, its LUID bit position, its one-line effect — lives in Constants and catalogs. Cross-reference between the two when you need to look up a specific privilege.

The naming convention is uniform: every privilege starts with Se and ends with Privilege. The middle is descriptive: SeLoadDriver, SeBackup, SeChangeNotify. There are no privileges outside this convention.

Where to go next #

For the per-privilege reference — every name, LUID bit position, and one-line effect — see the Privilege catalog.

For how the AccessCheck-influencing category actually participates in a check, read Access decisions.

Assigning privileges

Peios / Peios Security Fundamentals / Privileges

A principal source says who someone is — their SID, their memberships, their POSIX identifiers. It never says how much this machine trusts them. Privileges and integrity are local policy: authd's alone, decided here, and there is no message with which a source could ask for one.

That policy lives in the registry:

Machine\Generic\Authn\Policy

authd reads it at every logon, not once at startup. A policy change takes effect the next time someone signs in, rather than the next time you restart the one daemon on the system that is most disruptive to restart.

One record per principal #

Each subkey is a principal, and holds everything this machine grants them:

Machine\Generic\Authn\Policy
  DeniedPrivileges  REG_MULTI_SZ  ["SeDebugPrivilege"]
  \Everyone
      Privileges    REG_MULTI_SZ  ["SeChangeNotifyPrivilege"]
  \Administrators
      Privileges    REG_MULTI_SZ  ["SeBackupPrivilege", "SeRestorePrivilege"]
      Integrity     REG_SZ        "High"
      Owner         REG_SZ        "Administrators"
      DefaultDacl   REG_SZ        "D:(A;;GA;;;SY)(A;;GA;;;BA)"

Keyed by principal rather than by privilege on purpose. One key shows the totality of a principal's authority — reg ls on \Administrators answers "what can an administrator do on this machine?" completely. Authority scattered across twenty per-privilege values is authority nobody audits: a right granted somewhere unexpected does not surface when you look at the principal, and you would have to know to check every other place.

It is also the only shape that holds more than privileges. Integrity, the default owner and the default DACL live on the same record, and logon rights will when they arrive.

Naming a principal #

A subkey's name is either a well-known name or a literal SID:

\Administrators
\S-1-5-21-2847362817-1094533892-3310298447-1000

Names are matched case-insensitively. The recognised ones are SYSTEM, Everyone, Authenticated Users, Administrators, Users, Guests, Local Service, Network Service, and the logon types Interactive, Network, Batch, Service and Anonymous.

Two limits are worth knowing before you hit them:

BUILTIN\Administrators cannot be a subkey name. A backslash is the registry's path separator, so the qualified spelling is unrepresentable. Write the bare name.

A local group's name does not resolve. authd cannot know what developers means without asking lpsd, and policy must never depend on a principal source being up — that would make "what may this principal do" unanswerable exactly when a source is broken. Name local groups by SID, which lps group list will show you.

A subkey that is neither a known name nor a parseable SID is ignored with a warning. It is worth checking for that warning after editing: a record naming nobody sits in the key looking authoritative and applying to no one.

If the key exists, the key is the whole policy #

This is the most important behaviour on the page.

authd carries a compiled-in floor, but it applies only when the key is absent entirely. It is not merged in value by value. So a policy you write is the complete policy: anything not granted below is not granted.

StateWhat a principal gets
Key absentThe compiled floor — Everyone gets SeChangeNotifyPrivilege, and nothing else
Key presentExactly what the records say
Key present but unreadableNothing, and a loud log line

The alternative — a compiled default that each record replaces — fails in the wrong direction. An administrator who writes one record believing they have locked the machine down would still be handing out whatever a compiled table said for every principal they did not mention, from a table they cannot read. That is a security failure, and a silent one. Failing towards less privilege makes things stop working, which is far easier to diagnose than authority you did not know you were granting.

An unreadable key is treated as granting nothing rather than as absent, for the same reason: reading it as absent would silently restore every privilege an administrator may have deliberately removed, at the one moment nobody can check.

What a machine ships with #

The defaults are registry data, not compiled in — authd-policy.reg, shipped to /usr/share/regim/ and applied if the image opts in. So they can be read, edited, and replaced wholesale by an image that wants a different policy, rather than being invisible inside a binary.

A stock machine grants:

PrincipalPrivilegesIntegrity
EveryoneSeChangeNotifyPrivilege, SeCreateSymbolicLinkPrivilege(Medium, by default)
Administratorsthe operational set below, plus both of the aboveHigh

The operational set is SeBackupPrivilege, SeRestorePrivilege, SeShutdownPrivilege, SeRemoteShutdownPrivilege, SeSystemtimePrivilege, SeSecurityPrivilege, SeLoadDriverPrivilege, SeImpersonatePrivilege, SeIncreaseQuotaPrivilege, SeIncreaseBasePriorityPrivilege and SeProfileSingleProcessPrivilege.

SeDebugPrivilege is deliberately not granted. It writes to any process regardless of integrity label, so it is the single privilege that most directly defeats the integrity boundary, and ordinary administration does not need it. Add it when a machine genuinely does.

Three privileges are never granted by the shipped policy and should not be added: SeCreateTokenPrivilege mints any identity without going near authd, which would make every other control here decorative; SeTcbPrivilege and SeAssignPrimaryTokenPrivilege belong to the trusted computing base.

Note that Administrators is granted SeChangeNotifyPrivilege directly, not only through Everyone. Privileges accumulate across every SID on a token, so that repetition is insurance: if someone edits the key and drops the Everyone record, ordinary users lose the ability to traverse a directory — but administrators keep working and can repair it.

How the values compose #

Privileges accumulate. A token holds the union of every record naming a SID it carries — the principal's own, and each of their groups. A group marked USE_FOR_DENY_ONLY contributes nothing.

DeniedPrivileges wins. Listed on the Policy key itself rather than on a record, it is applied after the union, so no record you have not read can defeat it. It is on the parent key so it cannot collide with a principal who happens to be called Denied.

Integrity, Owner and DefaultDacl are single values, so they cannot accumulate. The principal's own record wins outright; otherwise the groups decide:

  • Integrity takes the maximum across the groups that name one.
  • Owner and DefaultDacl have no ordering to compare, so two groups naming different values is a misconfiguration: it logs a warning and falls back to the default rather than picking by whichever the registry enumerated first. Two groups naming the same value is not a conflict.

The user's record winning outright is what makes a principal possible to lower. Under a plain maximum, a guest whose own record said Low would still come out Medium the moment any group they belonged to named Medium. The consequence is worth stating plainly: a group cannot impose an integrity floor on a member. If Administrators names High and a member's own record names Low, that member gets Low.

Writing each value #

Privileges — REG_MULTI_SZ #

Full ABI names, SeBackupPrivilege rather than SeBackup, matched case-sensitively. A name this build does not recognise is dropped with a warning and the rest of the list still applies — a policy written for a newer Peios should not lose the privileges it spelled correctly.

An empty list is meaningful: it grants nothing, and is different from the value being absent.

Integrity — REG_SZ or REG_DWORD #

A tier name, matched case-insensitively:

NameValue
Untrusted0
Low4096
Medium8192
High12288
System16384

Or a raw REG_DWORD. The kernel compares integrity numerically and any value is legal, so the numeric form reaches levels between the tiers — 8193 sits just above Medium. Use the name unless you need that.

A principal no record names gets Medium. That default is compiled in and is not affected by the key existing, because unlike a privilege it is not a grant: every token must carry some level to be valid at all.

Owner — REG_SZ #

Which principal owns objects this token creates. Names a principal, exactly like a subkey name does; authd converts it to the index the token actually carries, which is a number meaningful only within one token and different on the next logon.

Absent — the ordinary case — means objects are owned by their creator.

The case this exists for is a shared administrative estate: objects an administrator creates being owned by Administrators rather than by the individual, so they remain manageable when that person's account goes away.

DefaultDacl — REG_SZ #

The DACL objects this token creates inherit when nothing else supplies one, written as SDDL:

D:(A;;GA;;;SY)(A;;GA;;;BA)

SDDL rather than raw bytes because the whole argument for policy living in the registry is that an operator can read it. Conditional ACEs are preserved, so D:(XA;;GA;;;WD;(@USER.Department == "Engineering")) works and keeps its condition.

A value that does not parse is dropped with a warning and the system default applies — a typo costs the customisation, not the session.

Locking a machine down #

To forbid a privilege regardless of what any record says:

Machine\Generic\Authn\Policy
  DeniedPrivileges  REG_MULTI_SZ  ["SeDebugPrivilege", "SeLoadDriverPrivilege"]

That is one edit, in one place, that a record you have not read cannot defeat. Removing the privilege from each record individually relies on having found them all.

What this cannot express yet #

Whether a principal may sign in at all. Policy decides what a session gets, not whether one happens. lps disable covers the blunt case; per-logon-type restriction — allowed over the network but not at the console — is not built.

Two privileges the kernel enforces but nothing can name. SeTakeOwnershipPrivilege and SeRelabelPrivilege are honoured by the access check and appear in audit records, but are absent from the published ABI, so no policy can grant them. SeSystemProfilePrivilege is in the same position on current builds. Until that is resolved, a Peios machine has no way to grant take-ownership — which means the documented escape hatch for a file whose DACL excludes you is not currently reachable.

Seeing what a token actually got #

$ token show --all

[privileges] lists what the token holds and integrity shows the label. A privilege the tool cannot name appears as <privilege bit N> rather than being omitted, so the list is always complete even when the name table is not.

See also #

  • Privileges — the model these records feed.
  • The token command — reading the privileges and integrity a live token actually carries.
  • Managing local principals — the accounts these records apply to.

Peios Learn — documentation for the Peios project.

Built with Trail.