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

Tokens

Single-page view · as markdown

Tokens

Peios / Peios Security Fundamentals / Tokens

A token is the runtime carrier of identity in Peios. It is a reference-counted kernel object that holds a user SID, group SIDs, privileges, confinement capabilities, and a handful of other policy fields. Every thread on the system has exactly one effective token at any moment, and every access decision starts by reading that token.

If Identity describes who a principal is, the token is the concrete thing that says "this thread is acting as that principal, right now". Tokens appear throughout these docs because they are the input to AccessCheck for files, registry keys, processes, sockets, and the tokens themselves.

What a token contains #

A token is a structured object with around twenty fields. The full field list is covered in Token types and fields; this section sketches the shape so you have a mental model going into the rest of the topic.

GroupFieldsWhat they do
Identityuser_sid, groups, logon_sid, restricted_sidsWho the token represents.
Type and leveltoken_type, impersonation_level, integrity_level, mandatory_policyWhat kind of token this is and how it may be used.
Defaultsowner_sid_index, primary_group_index, default_daclWhat the token contributes when new objects are created without an explicit security descriptor.
Session and provenanceauth_id, source, token_id, created_at, expiration, originWhere the token came from and which authentication event it belongs to.
Privilegesprivilege bitmask (present / enabled / used / removed states)System-wide rights the token holds.
Confinementconfinement_sid, confinement_capabilities, confinement_exemptSandboxing constraints, when the token is for a confined application.
Claimsuser_claims, device_claims, device_groupsTyped attributes for conditional ACE evaluation.
Projectionprojected_uid, projected_gid, projected_supplementary_gidsLinux-compatibility identity numbers, derived from the rest of the token.
Audit policyaudit_policy bitmaskPer-token forced auditing flags.
Self SDsecurity_descriptorThe token is itself an object — this is its own SD, governing who can read or adjust it.

Most fields are set once when the token is minted and never change. A few — the enabled state of privileges, the enabled state of groups, the default DACL — are adjustable at runtime within strict rules. None of the identity fields (the SIDs themselves) ever change. See Token types and fields for the exact rules.

Tokens and threads #

flowchart LR
    A["Thread"] -->|primary token| B["Primary token"]
    A -->|impersonation token, when set| C["Impersonation token"]
    B -->|read by AccessCheck| D["Kernel"]
    C -->|read by AccessCheck when in effect| D

Every thread has a primary token — its baseline identity, inherited at fork and shared with every other thread in the process. While the thread is running normally, the primary token is what AccessCheck reads.

A thread may also temporarily install an impersonation token — a second token that overrides the primary for that one thread until it is reverted. Services use impersonation to act on behalf of a client they are handling: accept the connection, impersonate the client, do the work as them, revert. The primary token survives unchanged; only the one thread sees the impersonated identity, and only until it reverts.

Impersonation has its own topic: Impersonation. For this page it is enough to know that a thread can have two tokens at once, and the kernel reads whichever is "in effect" right now.

Where a token comes from #

A token is always minted by code holding SeCreateTokenPrivilege. In a running system that effectively means authd — the authentication authority — which mints tokens once a principal source has verified who is signing in, and peinit, which mints service tokens during boot before authd is up.

The minting flow is straightforward:

  1. Authentication succeeds, or a service is being launched.
  2. The minting component gathers what the token will carry: the identity and memberships the principal source asserted, and the privileges and integrity level this machine's policy grants the principal.
  3. It calls kacs_create_token with the resolved values.
  4. The kernel returns a token file descriptor with TOKEN_ALL_ACCESS.
  5. The minting component installs the token on the target process (the new login session's first process, or the service binary) and closes the fd.

From that point on the token lives in the kernel, referenced by every thread of every process that inherits it. The original fd is gone; the only handle into the token is through the threads carrying it (and any other process that opens it via /proc/<pid>/token or kacs_open_process_token).

No other component creates tokens. There is no API for an ordinary process to fabricate an identity for itself. A process can ask the kernel to duplicate a token it already has, or filter one to a more restricted version, but the original always comes from authd or peinit. This is deliberate: it is what makes the kernel's "where did this identity come from?" question always answerable.

Tokens are objects too #

A token is itself one of the objects KACS protects. It has a security descriptor of its own, and operations on it (querying, adjusting, impersonating, duplicating, installing) all go through AccessCheck against that SD.

RightAction it gates
TOKEN_QUERYRead token information — fields, groups, privileges, etc.
TOKEN_DUPLICATECreate an independent copy via DuplicateToken or FilterToken.
TOKEN_IMPERSONATEInstall as a thread's impersonation token.
TOKEN_ASSIGN_PRIMARYInstall as a process's primary token.
TOKEN_ADJUST_PRIVILEGESEnable, disable, or permanently remove privileges.
TOKEN_ADJUST_GROUPSEnable or disable groups.
TOKEN_ADJUST_DEFAULTChange the default DACL, owner index, or primary group index.
TOKEN_ADJUST_SESSIONIDChange interactive_session_id (additionally requires SeTcbPrivilege).

By default, a freshly minted token grants the SYSTEM identity and the creating principal full access, and grants the token's own user identity TOKEN_QUERY and the adjustment rights.

This self-protection is what stops one thread from "stealing" another's identity. Even reading the fields of another process's token requires the PROCESS_QUERY_INFORMATION right on the target process and the relevant token rights on the token itself.

The two token rules to remember #

Almost every confusion about tokens reduces to one of two rules.

Rule 1: A thread always has a token. There is no "no identity" state. A thread that has not been given a more specific token is running on whichever token its process inherited — at the very least, the SYSTEM token from boot. Code that runs before authd is up runs as SYSTEM, which has every privilege. Code that runs after authd is up runs as whichever identity authd assigned. There is no third option.

Rule 2: Identity is the token, not the process. A process does not "have an identity" except through the tokens of its threads. Two threads in the same process can be running as different principals at the same instant, because one is impersonating and the other is not. When you debug an "access denied" question, the answer is in the thread's token, not the process's notional identity.

Where to start #

If you want to understand the token field-by-field — what each value means, when it can change, how it is encoded — read Token types and fields.

If you want to know how a token moves through fork, exec, adjustment, and destruction, read Token lifecycle.

If you are interested in the restricted-token model — the sandbox primitive where a token is intersected with a secondary identity list — read Restricted and write-restricted tokens.

If you need to understand UAC-style elevation — the linked Full/Limited token pair that lets one principal have two tokens for the same session — read Elevation and linked tokens.

To work with tokens from a shell — inspect, adjust, duplicate, restrict, impersonate — read The token command.

Token types and fields

Peios / Peios Security Fundamentals / Tokens

Every token in Peios is classified along several axes at once. A given token is either a primary token or an impersonation token. If it is an impersonation token it has an impersonation level. It is either restricted or not. If it is part of an elevation pair it has an elevation type. These are not types in an inheritance sense — they are independent dials, and a token's behaviour is the product of all of them.

This page walks through each axis, then through the token's fields grouped by what they do.

token_type — primary or impersonation #

This is the first and most important classification.

A primary token is the baseline identity of a process. Every process has exactly one. It is inherited across fork — child processes start with a copy of the parent's primary token — and survives exec. When a thread runs without doing anything special, AccessCheck reads its primary token.

An impersonation token is a thread-local override. A thread installs an impersonation token to act as someone else for a span of time, then reverts back to its primary. While the impersonation is in effect, AccessCheck reads the impersonation token instead of the primary. Other threads in the same process are unaffected.

The two types are not interchangeable. Trying to install a primary token as a thread's impersonation, or vice versa, fails. The kernel's kacs_open_self_token and related syscalls return tokens of the correct type for what they were asked to do.

Use caseToken type
A login session's identityPrimary
A server thread acting on behalf of a connected clientImpersonation
A service running with the identity authd assigned to itPrimary
A thread querying its caller's identity for an audit log entryImpersonation (at the Identification level — see below)

impersonation_level — how far an identity may travel #

Every token carries an impersonation level. For primary tokens this is set to a conventional value (Anonymous) and ignored. For impersonation tokens it is what bounds what the impersonator may do.

There are four levels, ordered from least to most permissive:

LevelWhat a server may do with this token
AnonymousNo identity. The token's user SID is the well-known Anonymous SID (S-1-5-7). The server learns nothing about the client.
IdentificationThe server may inspect the client's identity — read their SIDs, groups, integrity level — but may not use the token for any access check. AccessCheck with an Identification-level token returns ACCESS_DENIED immediately, no matter what.
ImpersonationThe server may act as the client for all local operations. This is the default, and the level most server-to-client code paths assume.
DelegationSame as Impersonation locally, plus the server may forward the client's credentials to a remote machine over Kerberos. KACS only tracks the level; the actual forwarding is authd's job.

The level is set by the client, on the socket, before connecting. The server cannot raise it. It can only end up with the level the client granted, or lower if other constraints intervene (see Impersonation for the full two-gate model).

The Identification level is a common source of confusion. A frequent bug — in services not written for Peios — is to take a client's token and try to open files as them, finding that every open returns ACCESS_DENIED for reasons the code cannot diagnose. The cause is almost always that the client connected at Identification level and the server is supposed to be inspecting, not acting. The correct fix is at the client end: ask for Impersonation.

restricted — a secondary identity check #

A restricted token carries two SID lists: the normal groups list and a secondary restricted_sids list. During AccessCheck the kernel runs the DACL walk twice and intersects the results — once with the full identity, once with only the restricted SIDs. A right is granted only if both passes grant it.

The effect is to limit a token's identity-based access to what the restricted SIDs would independently receive. The user is still the same user; they just cannot use group memberships or other SIDs that are not in the restricted list to satisfy an ACE.

A write-restricted token is the same idea, but the intersection applies only to write-category rights. Reads and execute come from the normal pass alone. This is the common case: a sandbox that should be able to read system files but only write into a narrow allowed set.

Both variants are covered in detail under Restricted and write-restricted tokens. For this page it is enough to know that "restricted" is a property of the token, set at creation by FilterToken, and that the kernel handles the second pass automatically.

elevation_type — half of a linked pair #

When a principal has both a normal (Limited) token and an elevated (Full) token — the UAC-style pattern — the two tokens are linked at the session level. Each carries an elevation_type saying which half it is:

ValueMeaning
DefaultThe token is not part of a linked pair. Most tokens.
FullThe elevated half of a linked pair.
LimitedThe non-elevated half of a linked pair.

A linked pair is established by an explicit call to KACS_IOC_LINK_TOKENS. The elevation type does not change anything about the token's access rights by itself — it is a label that lets the system locate the partner token when the user requests elevation. The mechanics live in Elevation and linked tokens.

Field mutability classes #

Token fields fall into three classes. Knowing which class a field is in tells you whether you can ever change it after the token is minted.

ClassMeaningExamples
FixedSet at creation, never changes. Cannot be adjusted at runtime even by SYSTEM.user_sid, groups[].sid, logon_sid, restricted_sids, token_id, auth_id, created_at, source, confinement_sid, mandatory_policy.
AdjustableCan be changed at runtime through a specific syscall.Privilege enabled-state (via AdjustPrivileges), group enabled-state (via AdjustGroups), default DACL / owner index / primary group index (via AdjustDefault), security_descriptor (via kacs_set_sd).
One-wayCan be tightened but never loosened.Privilege presence (a privilege can be removed but not re-added), group USE_FOR_DENY_ONLY (can be set but not cleared).

This split is the source of one rule worth memorising: the identity in a token cannot change. You cannot relabel a token to a different user. You cannot add a group to a token. You can only adjust how existing identity bits are used — enable or disable a privilege, mark a group as deny-only, narrow the default DACL.

If you need a different identity, you need a different token. Either authd mints a new one for you (with a re-authentication), or you DuplicateToken / FilterToken your existing one into a more restricted copy.

Fields by purpose #

The token's fields, grouped by what they do. None of this is a low-level binary layout — that lives in the Kernel ABI reference topic. This is the conceptual field list.

Identity #

The fields that say who the token represents.

FieldWhat it is
user_sidThe primary identity of the token. The principal's SID.
groupsAn array of SID_AND_ATTRIBUTES — every group the principal is a member of, each with its current enabled/disabled state and other flags.
logon_sidThe session-specific SID (S-1-5-5-X-Y) for the logon session this token belongs to. Also appears in groups with the SE_GROUP_LOGON_ID flag.
restricted_sidsSecondary identity list for restricted tokens. Empty on unrestricted tokens.

Type and level #

FieldWhat it is
token_typePrimary or Impersonation.
impersonation_levelOne of the four levels listed above (only meaningful for Impersonation tokens).
integrity_levelNormally one of Untrusted / Low / Medium / High / System; technically any numeric S-1-16 level (see MIC).
mandatory_policyThe token's MIC enforcement flags (NO_WRITE_UP, NEW_PROCESS_MIN).

Defaults #

These fields contribute to the security descriptor of a new object when the creator does not supply one explicitly.

FieldWhat it is
owner_sid_indexIndex into [user_sid, groups[0..N-1]] selecting which SID becomes the default owner.
primary_group_indexSame idea for the default primary group.
default_daclThe DACL applied to new objects when no explicit SD is supplied.

Session and provenance #

FieldWhat it is
token_idA unique LUID identifying this token instance.
auth_idThe LUID of the logon session this token belongs to.
sourceAn 8-character name plus a LUID identifying the component that minted the token (e.g. authd).
created_atTimestamp of original minting. Copied unchanged by DuplicateToken and FilterToken.
expirationExpiry timestamp. Set by authd. Informational only in v0.20 — not enforced by AccessCheck.
originThe originating logon session for derived tokens (S4U, network logon).
modified_idA counter bumped on every adjustment. Used as a cache-invalidation key.

Privileges #

Privileges live in a 64-bit bitmask with four states per privilege:

  • Absent — not on this token at all.
  • Present, disabled — on the token but not currently in effect. May be enabled.
  • Present, enabled — in effect; AccessCheck will use it.
  • Used — a sticky audit bit set when the privilege has been exercised. Never cleared.

See Privileges for the model in detail and the catalog of named privileges.

Confinement #

FieldWhat it is
confinement_sidIf non-null, the token is for a confined application; its identity for access checks is intersected with this SID.
confinement_capabilitiesThe list of capability SIDs the confined application has declared.
confinement_exemptEscape hatch — if set, confinement is skipped entirely.
isolation_boundaryReserved for future use; not enforced in v0.20.

See Confinement for the sandbox model.

Claims #

FieldWhat it is
user_claimsTyped attributes about the user, populated from the directory at authentication time. Used by @User.* references in conditional ACEs.
device_claimsSame idea for the machine. Used by @Device.*.
device_groupsThe machine's group memberships, for compound identity.

See Claims on a token.

Projection #

These fields hold pre-computed Linux UID/GID values for compatibility. They are derived from the token, never the other way around.

FieldWhat it is
projected_uidThe Linux UID the token's identity maps to (or 65534 if unmapped).
projected_gidSame for primary GID.
projected_supplementary_gidsSame for supplementary GIDs.

See Linux compatibility for what consumes these.

Audit policy #

FieldWhat it is
audit_policyA bitmask of per-token forced-audit flags (OBJECT_ACCESS_SUCCESS, OBJECT_ACCESS_FAILURE, PRIVILEGE_USE_SUCCESS, PRIVILEGE_USE_FAILURE).

When a flag is set, AccessCheck emits the corresponding audit event regardless of the object's SACL.

Self SD #

FieldWhat it is
security_descriptorThe token is itself a protected object; this is its own SD. Governs who may query, duplicate, impersonate, install, or adjust the token.

Other #

FieldWhat it is
interactive_session_idThe interactive session number. Zero for services. Adjustable only with SeTcbPrivilege.
elevation_typeDefault / Full / Limited, for linked-pair membership.

Where to go next #

For how these fields are set, shared, adjusted, and destroyed over a token's life — mint, fork, exec, impersonation, adjustment — read Token lifecycle.

For the full rules around impersonation tokens and their levels, read Impersonation.

To read these fields off a live token from a shell, read The token command.

Token lifecycle

Peios / Peios Security Fundamentals / Tokens

A token is reference-counted. It comes into existence when a privileged component mints it, is reference-counted up and down as it gets attached to processes and threads, and is destroyed when the last reference drops. Between mint and destruction it moves through a handful of well-defined transitions. None of them change a token's identity — but each affects how the token is reached, how many references it has, or how its mutable fields are set.

Three ways to mint a token #

A token is always created by code holding the right privilege:

OperationEffectPrivilege required
kacs_create_tokenMint a token from scratch using a wire-format specification.SeCreateTokenPrivilege
DuplicateTokenMake an independent copy of an existing token. The copy is a new object; modifying it does not affect the source.TOKEN_DUPLICATE on the source
FilterTokenMake a copy of an existing token with privileges removed, groups marked deny-only, or restricted SIDs added.TOKEN_DUPLICATE on the source

kacs_create_token is what authd and peinit use to mint genuinely new identities. The other two — DuplicateToken and FilterToken — produce copies derived from a token a process already has. A process can FilterToken its own primary token down to a more restricted version and install the result as the primary token of a child it is about to launch.

In all three cases the kernel:

  1. Validates the inputs (well-formed SIDs, no duplicate luids, all index references in range, etc.).
  2. Allocates a new token object with refcount = 1.
  3. Assigns a fresh token_id LUID and modified_id = 0.
  4. Stamps the created_at timestamp.
  5. Injects the appropriate logon SID into the groups list (callers must not supply it).
  6. Returns a token file descriptor with TOKEN_ALL_ACCESS (for create) or the access mask the caller asked for (for duplicate).

Any validation failure results in no token being created — the operation is all-or-nothing.

Attaching to a process #

A token by itself is just an object the kernel holds. It does not yet identify anyone. To take effect, it has to be attached to a process or thread.

There are three attachment paths:

  • Inheritance at fork — the child's primary token is the parent's primary token. The token's reference count goes up by one.
  • Installation by a privileged caller — KACS_IOC_INSTALL on a token fd makes the token the calling process's primary. Requires TOKEN_ASSIGN_PRIMARY on the token and SeAssignPrimaryTokenPrivilege on the caller. Used by peinit when launching a service: fork, install the service token on the child, exec the binary.
  • Impersonation on a thread — KACS_IOC_IMPERSONATE on a token fd makes the token the calling thread's impersonation token. Requires TOKEN_IMPERSONATE. The process-wide primary token is unchanged.

A given token can be attached in all three ways at once: shared as the primary of N processes, also held as the impersonation token of M threads. Each attachment is a reference.

Fork, exec, and the primary token #

flowchart LR
    A["Parent process (primary token T)"] -->|fork| B["Child process (primary token T)"]
    B -->|exec| C["Child process (primary token T, possibly with lowered integrity)"]

Fork copies the parent's primary token pointer into the child. Both processes now share the same token object. Adjustments made by the parent are visible to the child instantly — they share the storage. Adjustments made by either process via AdjustPrivileges etc. affect the shared token.

What does not survive fork is the parent's impersonation. A thread that forks while impersonating gets a child whose primary token is the parent's primary (not the impersonation). The child is not impersonating; its first thread is running on the inherited primary.

Thread clone (CLONE_THREAD) is different: the new thread is part of the same process, so it shares the same primary token. Privilege or group adjustments made by any thread are visible to all of them immediately.

Exec keeps the primary token. The new binary runs as the same identity. One subtle exception: if the token's mandatory_policy has NEW_PROCESS_MIN set and the executable carries a lower integrity label than the token, the kernel creates a copy of the token with integrity lowered to match, replaces the primary with that copy, and drops the original reference. This is the mechanism that prevents Medium-integrity code from running at Medium when its image is labelled Low.

Impersonation is always reverted at exec. A thread that execs while impersonating has its impersonation token released before the new program runs. This is enforced — the new binary cannot inherit an impersonation it did not establish.

Impersonation install and revert #

A thread becomes an impersonator by installing an impersonation token. The two ways to do it:

  • kacs_impersonate_peer(fd) — extract the peer's identity from a connected Unix socket and install it at the appropriate level. The most common path for services accepting client connections.
  • KACS_IOC_IMPERSONATE on a token fd — install a specific token (for transports that do not carry a peer token, or when the server has obtained a token by some other means).

Either operation has the same effect: the thread now has a primary token (unchanged) and an impersonation token (newly installed). AccessCheck reads the impersonation token from this point.

A thread reverts with kacs_revert. It always succeeds. It drops the impersonation reference and restores the original primary as the effective identity.

If a thread that is already impersonating installs a different impersonation token, the kernel silently reverts the old one first and then installs the new one. There is no nesting.

Impersonation tokens come from one of three sources during the install:

  • A peer's identity captured at socket connect time.
  • A duplicate of an existing token (DuplicateToken to a token type of Impersonation with the desired level).
  • A new mint from authd (rare).

The level on an impersonation token is set by the client at connect time, never raised by the server. The full two-gate model — identity gate plus integrity ceiling — is in Impersonation.

Adjustment in place #

Some fields can be changed at runtime. The operations are:

Syscall / ioctlWhat it changes
AdjustPrivilegesEnable, disable, or permanently remove privileges. A reset-all sentinel restores defaults.
AdjustGroupsEnable or disable group entries. Cannot target mandatory groups, deny-only groups, the logon SID, or the user SID.
AdjustDefaultModify the default DACL, owner index, or primary group index.
kacs_set_sd on the token fdModify the token's own self-SD. Requires WRITE_DAC on the token.

All adjustments are atomic — invalid input rolls the whole operation back, no partial change. Each successful adjustment bumps modified_id so caches keyed on token state can invalidate.

Because the token storage is shared across all threads of a process, an adjustment made by one thread is visible to all of them immediately. This includes adjustments to the primary token of a process that has just installed it but not yet had every thread converge — the kernel handles the convergence asynchronously, and during the brief window other threads may still see the old token. This window is small enough not to matter in practice but is worth knowing about if you are writing tests.

Permanent privilege removal #

When a privilege is removed rather than just disabled, it is gone from the token permanently. The token's privilege bitmask loses the present and enabled-by-default bits. The used bit stays — its purpose is auditing, and the fact that the privilege was once exercised is information that should not disappear.

Removal is irreversible by design. There is no API to re-add a removed privilege. The only way back is a different token (a fresh authentication, or a duplicate from a source that still has the privilege).

The same applies to SE_GROUP_USE_FOR_DENY_ONLY set on a group: it can be marked, but never cleared. Marking a group deny-only is a one-way trip down the access ladder.

The default DACL #

Every token carries a default DACL. It is the DACL that gets applied to a new object when:

  • The creator (for example a kacs_open with CREATE disposition, or a registry-key create) does not supply an explicit SD.
  • The object's parent has no inheritable ACEs that fully cover it.

In other words, the default DACL is the fallback. It guarantees that any object a token creates has at least some DACL, even when nothing else supplies one.

The default DACL is adjustable via AdjustDefault. A typical default DACL grants the token's user identity and SYSTEM full access and excludes everyone else. Services that want a more permissive or more restrictive default can change it once at startup.

The default owner and primary group also live on the token, as indices into the [user_sid, groups[0..N-1]] array. They tell the kernel which SID to stamp as owner and which to stamp as primary_group when synthesising a new SD. They are adjustable through the same call.

Reference counts and destruction #

Every attachment is a reference. A token is alive as long as any of these are true:

  • Any process has it as a primary token.
  • Any thread has it as an impersonation token.
  • Any process has it open via a token fd.
  • It is part of an established linked pair on a still-existing logon session.

When the last reference drops, the kernel destroys the token: frees the storage, releases the reference on the logon session. If the destroyed token's session loses its last token reference too, the session itself is destroyed and a logon-session-destroyed event is emitted via KMES. See Logon sessions.

A token's expiration field has no effect on the lifecycle in v0.20 — it is stored for future use but not enforced. A token lives until its references drop. Session revocation, when needed, is implemented by userspace (authd) walking /proc/*/token, identifying tokens with the offending auth_id, and killing the holding processes. This is documented under Inspecting security state.

Quick reference: which transitions change what #

TransitionIdentityPrivilegesGroupsIntegrityRefcount
ForkSameSameSameSame+1 (shared)
CLONE_THREADSameSameSameSameSame object
Exec (same integrity)SameSameSameSameSame object
Exec (NEW_PROCESS_MIN downgrade)SameSameSameLoweredNew token
Impersonation installDifferent (impersonation token)DifferentDifferentDifferent+1 on impersonation token
Impersonation revertBack to primaryBack to primaryBack to primaryBack to primary−1 on impersonation token
DuplicateTokenSameSameSameSameNew token with its own count
FilterTokenSameSubsetSubset (some deny-only, restricted_sids added)SameNew token
AdjustPrivilegesSameChanged (within rules)SameSameSame object
AdjustGroupsSameSameChanged (within rules)SameSame object
KACS_IOC_INSTALLSame token, different process———New attachment
Process exit————−1

Where to go next #

For what FilterToken's restricted variants actually do at access-check time, read Restricted and write-restricted tokens.

For the session a token belongs to and what happens when the last token of a session is released, read Logon sessions.

To drive these transitions from a shell — duplicate, restrict, adjust, install — read The token command.

Restricted and write-restricted tokens

Peios / Peios Security Fundamentals / Tokens

A restricted token is the kernel's narrow-the-identity primitive. It is a normal token with an extra list of SIDs attached — the restricted_sids list. During AccessCheck the kernel runs the DACL walk twice: once with the token's full identity, once with only the restricted SIDs. A bit is granted only if both passes grant it.

The effect is to put a ceiling on what the token can reach, expressed as "the rights this token would have if it were just those restricted SIDs". The identity (user, groups, privileges) is unchanged — but the kernel will only honour an ACE if the restricted-only view of the token would have honoured it independently.

Restricted tokens are the building block underneath several patterns: service hardening, sandbox processes, anti-malware quarantine, anything that says "this code should have less authority than the user it is running as".

The two-pass model #

flowchart TD
    A["AccessCheck"] -->|with full identity| B["Normal pass (user_sid + groups + ...)"]
    A -->|with restricted SIDs only| C["Restricted pass (restricted_sids only)"]
    B -->|granted_normal| D["Intersect"]
    C -->|granted_restricted| D
    D -->|granted = normal ∩ restricted| E["Final granted mask"]

When KACS evaluates AccessCheck for a restricted token, it runs through the same pipeline twice:

  1. Normal pass. The DACL walk uses the token's user_sid, all enabled groups, and all the usual rules. Produces a granted_normal mask.
  2. Restricted pass. The DACL walk runs again, but the only SIDs considered for matching are those in restricted_sids. The user_sid does not match. None of the regular groups match. Only the entries in restricted_sids count. Produces a granted_restricted mask.
  3. Intersection. The final granted mask is granted_normal & granted_restricted.

Both passes must agree to grant a right. If the normal pass would grant FILE_WRITE_DATA but the restricted SIDs do not appear in any allow ACE for that right, the bit is dropped.

The restricted SIDs are usually narrow on purpose. A common pattern is to put a single capability SID (for example internetClient) in the restricted list — the token now has authority only on objects whose DACLs explicitly grant access to that capability.

What is not affected #

A few things are explicitly not narrowed by the restricted pass:

  • Privileges. Privilege-granted access (SeBackup, SeRestore, SeTakeOwnership, SeSecurity) is restored after the intersection. A restricted token with SeBackup can still read any file the privilege would have granted.
  • The default DACL on new objects. A restricted token still creates new objects with its default DACL, derived from its full identity.
  • Reading the token's own state. Querying a restricted token does not require both passes; the token's self-SD governs that as normal.
  • MIC. Mandatory integrity is evaluated before the DACL walks. A restricted token cannot use the restricted-SID trick to escape integrity rules.

Privileges being orthogonal to the restricted pass is the most important of these. The restricted-token model narrows identity-based access, not capability-based access. If you want to drop privileges too, that is a separate operation — see "Creation" below.

Write-restricted: the common case #

A write-restricted token narrows only the write-category rights. Reads and execute come from the normal pass alone; only write-mapped bits go through the intersection.

The motivation: it is rare to want a sandbox that cannot read anything in the normal world. Sandboxed code usually needs to load shared libraries, read configuration, perhaps look up its own metadata. What it must not do is write — into the user's home directory, into system paths, into any file outside the small set the sandbox explicitly allows.

In a write-restricted token:

Right categoryWhere it comes from
Read (FILE_READ_DATA, FILE_LIST_DIRECTORY, etc.)Normal pass only.
Execute (FILE_EXECUTE, FILE_TRAVERSE)Normal pass only.
Write (FILE_WRITE_DATA, FILE_APPEND_DATA, WRITE_DAC, etc.)Intersection of both passes.

The "category" is determined by which generic right the bit maps to. FILE_WRITE_EA is in the write category; FILE_READ_EA is in the read category; and so on. The generic mapping for each object type defines the partition.

There is a quirk worth noting. When a token is write-restricted, the kernel also sets a user_deny_only flag on the token internally. This causes the token's own user_sid to match only deny ACEs in any pass, never allow ACEs. The motivation is to prevent a token from getting write access on an object simply because its user SID matches an allow ACE — the write-restricted intersection would otherwise be too easy to bypass by writing an ACE that names the user directly. With user_deny_only set, the user SID can still trigger denials but cannot grant.

This is a subtle interaction; most code that uses write-restricted tokens does not need to think about it, but if you are debugging an access denial on a write-restricted token and the user appears in the DACL with an allow ACE, the explanation is here.

Creation #

There is one path to creating a restricted token: KACS_IOC_RESTRICT on a token fd — the ioctl behind the FilterToken operation named elsewhere in this topic. The operation takes:

  • A list of privilege LUIDs to remove from the new token.
  • A list of group indices to mark SE_GROUP_USE_FOR_DENY_ONLY in the new token (set once, irreversible).
  • A list of restricting SIDs to put in the new token's restricted_sids.
  • An optional flag enabling write-restricted mode (which also sets user_deny_only).

The result is a new token fd. The source token is unchanged.

A typical sandbox launcher does something like this:

  1. Open its own primary token.
  2. Call KACS_IOC_RESTRICT to produce a restricted variant: remove every privilege except SeChangeNotifyPrivilege, mark unsafe groups deny-only, add the sandbox's allowed capability SIDs as the restricted SIDs, set the write-restricted flag.
  3. Fork.
  4. KACS_IOC_INSTALL the restricted token on the child.
  5. Exec the sandboxed binary.

The child now runs as the same user, but the user's group memberships are mostly invisible, the privileges are gone, and writes are confined to objects whose DACLs explicitly allow the sandbox's restricted SIDs.

Restricted tokens vs confinement #

Two things in Peios narrow what a token can reach: the restricted-token model on this page, and confinement (covered in Confinement). They look similar at a glance, but they exist for different audiences.

Restricted tokens are a tool for code. A program — a service, a sandbox launcher, an anti-malware engine — uses FilterToken or KACS_IOC_RESTRICT to narrow a token it already has, then runs sensitive work on the result. The decision is made in code, by the program itself, before it hands the restricted token to the constrained operation. Nothing outside the program needs to know about it, and nothing outside the program enforces it — the program is choosing to give itself less authority.

Confinement is a tool for policy. A sysadmin — or a service definition the sysadmin has chosen to deploy — declares that some component runs as a confined application with a specific confinement SID and an enumerated set of capabilities. The kernel enforces that policy whether or not the confined code is aware of it. Confinement is an administrative decision applied from outside the program, and the program cannot opt out of it.

That difference in audience is the reason behind the technical differences:

RestrictedConfinement
Who decidesThe program itself, in codeAdministrative policy, applied from outside
Typical callerA sandbox launcher, a hardened service, an anti-malware engineA service manager applying a service definition; a container runtime
Storage on the tokenrestricted_sids listconfinement_sid + confinement_capabilities
Where it fires in AccessCheckInside the DACL walk, identity-based intersectionAfter the DACL walk and privileges, absolute intersection
Bypassable with a privilege?Yes — privilege-granted bits surviveNo — confinement is absolute
Owner implicit rights still apply?YesNo
Write-only variant available?Yes (write-restricted)No

The technical asymmetry follows from the audience. A program restricting itself is trusting itself to use the primitive correctly — it can drop privileges if it wants to, leave them in if it doesn't, choose what its restricted SID set should be. Confinement is enforced against the code, so it has to be a harder line: privilege exercise and owner implicit rights are exactly the kinds of escape routes a confined application would otherwise reach for.

The two can be combined. A service that runs under a confinement policy and additionally restricts its own internal worker threads sets both. The kernel applies each layer; the final granted mask is the intersection of all of them.

Practical patterns #

A few patterns worth recognising:

  • Drop privileges only. Sometimes you want to remove dangerous privileges without restricting identity at all. KACS_IOC_RESTRICT with a privilege removal list, an empty deny-only list, and an empty restricted_sids list does this — the result is a token with reduced privileges and no restricted-SID intersection.
  • Write-restricted with the deny-only user trick. Set the write-restricted flag, leave restricted_sids containing only what you want the sandbox to be able to write to. The token's user SID can still match deny ACEs (so user-targeted denials still work) but cannot match allow ACEs on writes.
  • Capability-style sandbox. Put one or more capability SIDs (well-known or derived) in restricted_sids. The sandbox can then reach only objects whose DACLs explicitly grant access to those capabilities, plus whatever its user identity grants in the normal pass.
  • Anti-malware quarantine. Restrict to a narrow set of well-known SIDs (Everyone, Authenticated Users) and remove all privileges. The result is a token that can reach widely-shared resources but cannot exercise any system-level rights.

All of these are FilterToken / KACS_IOC_RESTRICT applied to the appropriate source token, with different inputs. The kernel does not distinguish between them; they are just patterns of use.

Where to go next #

For the linked Full/Limited token pair — the other derived-token pattern this topic covers — read Elevation and linked tokens.

For the policy-driven counterpart to restricted tokens, read Confinement.

To build a restricted token from a shell, read The token command.

Elevation and linked tokens

Peios / Peios Security Fundamentals / Tokens

Important

Not yet built in userspace. The kernel side of this page is complete and works as described — KACS_IOC_LINK_TOKENS, KACS_IOC_GET_LINKED_TOKEN, and the elevation_type field are all implemented. What does not happen yet is anything creating a pair. authd mints a single token carrying everything the principal is entitled to, and login runs the session on it; no interactive logon produces a Limited half, and there is no broker holding a Full one.

So on a current machine an administrator's shell holds their administrative privileges and High integrity for its whole life, and there is no elevation boundary. Read what follows as the model the system is built towards and the kernel already supports, not as a description of how your machine behaves today.

The blocker is not the token work. A Limited token marks BUILTIN\Administrators as USE_FOR_DENY_ONLY, and the security descriptor a stock image ships grants only LocalSystem and BUILTIN\Administrators — so a Limited shell could not list a directory. Per-subtree descriptors have to come first, otherwise the boundary would land on a machine nobody could use.

A linked token pair is two tokens for the same principal, one elevated (Full) and one not (Limited), associated with each other through their shared logon session. The point of the pair is to give a user a default identity that is not fully privileged, while keeping a second identity available for explicit elevation when the user actually wants to do administrative work.

If you have used UAC on a Windows desktop, this is the same model. The Limited token is what runs the user's shell and most of their software. The Full token is what runs the action they have just been prompted to authorise. The kernel does not decide when to switch — that is a user-space decision, prompted by some authority broker — but the kernel is responsible for keeping the pair linked, locating the partner on demand, and enforcing the rules around who is allowed to see what.

The model #

flowchart LR
    A["Logon session"] -->|associates| B["Full token (elevation_type = Full)"]
    A -->|associates| C["Limited token (elevation_type = Limited)"]
    B <-->|partner| C

Both tokens share the same logon session (auth_id). They have the same user SID, the same logon SID, the same created_at. What differs:

  • The Full token has whatever privileges and group memberships the principal is entitled to when running elevated — typically including BUILTIN\Administrators, SeBackup, SeRestore, etc.
  • The Limited token is a filtered version — privileges removed, sensitive groups marked USE_FOR_DENY_ONLY. It is the version the user runs in by default.

Each carries an elevation_type:

ValueMeaning
DefaultToken is not part of a linked pair. The vast majority of tokens.
FullThe elevated half of a pair.
LimitedThe non-elevated half of a pair.

A token's elevation_type is set when it joins a pair, never cleared. If the session is destroyed, the pair linkage is removed but the individual elevation_type values stay until the token objects themselves are freed.

Establishing a pair #

A pair is created by an authority broker — almost always authd, occasionally peinit — using:

KACS_IOC_LINK_TOKENS(elevated_fd, filtered_fd, session_id)

The kernel requires:

  • SeTcbPrivilege on the caller.
  • TOKEN_DUPLICATE on both token fds.
  • Neither token already linked.
  • Both tokens part of the same session (session_id).
  • The two tokens not the same token.

When the call succeeds, the kernel:

  1. Records the pair on the session.
  2. Sets the elevated token's elevation_type = Full.
  3. Sets the filtered token's elevation_type = Limited.

Both tokens continue to be valid as independent tokens. The pair linkage is additional state on the session, not a property that bundles the two into one object.

A typical flow during user login — none of which authd does yet, per the note at the top of this page:

  1. authd authenticates the user.
  2. authd creates the user's session.
  3. authd mints the Full token with all the user's entitled privileges and groups.
  4. authd FilterTokens the Full token down to the Limited version — privileges removed, admin groups deny-only.
  5. authd calls KACS_IOC_LINK_TOKENS to link the two.
  6. authd installs the Limited token as the primary of the user's first process.
  7. The Full token is kept available (via session state or an authority broker process) for later elevation requests.

The user's shell, file manager, web browser, and most of their applications now run on the Limited token. Whenever the user does something administrative, the authority broker — after whatever consent step is appropriate — fetches the Full token via KACS_IOC_GET_LINKED_TOKEN and installs it on the new process.

Looking up the partner #

KACS_IOC_GET_LINKED_TOKEN(token_fd) -> partner_fd

Given a token that is part of a pair, this ioctl returns a handle to the partner. The semantics depend on who is asking:

  • With SeTcbPrivilege, the caller gets a full handle on the partner token — TOKEN_ALL_ACCESS, the actual token object. This is what authority brokers use to perform an elevation: fetch the Full token's fd, install it on a child process.
  • Without SeTcbPrivilege, the caller gets a degraded handle — a freshly duplicated Identification-level clone of the partner, opened only with TOKEN_QUERY. The clone is enough to inspect the partner's identity ("am I currently the Limited token? what would the Full version contain?") but cannot be used for any access check and cannot be installed.

The asymmetry is deliberate. Anyone who can prove they hold one half of a pair is allowed to learn something about the other half — that is useful for diagnostics and for user-facing tools that want to display "you are running as Limited; admin rights are available". But actually wielding the elevated identity requires SeTcbPrivilege, the privilege held only by the authority broker that is supposed to gate elevation.

KACS_IOC_GET_LINKED_TOKEN fails with -ENOENT on a token whose elevation_type is Default, or on a token whose pair was destroyed when its session was destroyed.

What the link does not do #

A few clarifications on what linkage does not change:

  • It does not unify the two tokens. They remain independent objects. Adjustments to one have no effect on the other. Destroying one does not destroy the other.
  • It does not change access decisions. AccessCheck reads whichever token is currently in effect on the thread. The fact that a token has a partner is invisible to the access check — only the broker that calls KACS_IOC_GET_LINKED_TOKEN sees the pair.
  • It does not require either token to be installed. A pair can exist on a session that has not yet attached either of its tokens to any process. (Unusual but valid.)
  • It does not change auth_id. Both tokens still report the same session ID. The session is the level at which they are paired.

Teardown #

A pair is dissolved when the underlying logon session is destroyed. At that point:

  • The pair association is removed from the session.
  • Subsequent calls to KACS_IOC_GET_LINKED_TOKEN on either token return -ENOENT.
  • The individual tokens themselves continue to exist as long as their reference counts hold. Their elevation_type values are unchanged but no longer meaningful.

A session is destroyed when its last token reference drops (see Logon sessions). For a linked pair, that means: both tokens must lose all their attachments — every process running on them must exit, every fd open on them must be closed, every impersonation token derived from them must be reverted.

For the Limited token this happens naturally when the user logs out. For the Full token, the authority broker is responsible for releasing it when the session ends — typically by holding it in a process that exits when the user logs out.

Common patterns #

Default-Limited login. Every interactive login establishes a pair where the user's shell runs as Limited. The Full token is held by the authority broker.

Elevation on demand. When the user invokes an administrative action (a control panel, a sudo-equivalent), the broker prompts for consent, then uses KACS_IOC_GET_LINKED_TOKEN to fetch the Full token and KACS_IOC_INSTALL to install it on the privileged child process.

Service accounts (no pair). Services do not generally need elevation; their tokens are unpaired and elevation_type = Default. A service that needs occasional elevated work is a different pattern — usually IPC to an already-elevated service rather than a linked pair within the same service.

Diagnostics ("am I elevated?"). A process can call KACS_IOC_GET_LINKED_TOKEN on its own primary token (which it always has at least TOKEN_QUERY on). Without SeTcb it gets an Identification-level clone — enough to read elevation_type and groups on the partner and display "elevated rights available". The clone itself cannot be used for anything but inspection.

Where to go next #

For the session object that holds the pair together — and what happens to the pair when it dies — read Logon sessions.

To inspect a token's elevation type and its partner from a shell, read The token command.

The token command

Peios / Peios Security Fundamentals / Tokens

token is the command-line tool for working with tokens directly. It reads a token's contents, adjusts it, produces derived tokens, and drives impersonation — the low-level operations this topic describes, exposed at a shell.

token subcommand [target] [arguments]
$ token                       # one-line summary of your own token
$ token show --all             # every field of your own token
$ token privs --pid 4821       # the privileges on process 4821's token

token is a direct, debug-level tool. Day to day you do not inspect tokens by hand — the system does. token is for diagnosing an access problem, for understanding what identity a process is really running under, and for building and testing identity setups. Run with no subcommand, it prints a one-line summary of your own token.

Choosing which token #

Almost every subcommand operates on a token, and these flags choose which one. With none, the target is your own.

FlagTarget
--selfYour own token. The default.
--realYour primary token specifically, rather than the effective one — relevant when your thread is impersonating.
--pid PIDThe primary token of process PID.
--tid TIDThe impersonation token of thread TID (used with --pid).
--peer SOCK_FDThe peer's captured token on a connected socket — see Peer tokens.

Reading another process's token is itself access-controlled: it succeeds only with the right authority over that process.

Inspecting a token #

show #

token show prints a token's contents. It is the default — bare token is token show --short.

FlagEffect
--shortA one-line summary.
--allEvery query class — the fullest dump.

Field accessors #

Each of these prints one part of a token, for when you want just that piece:

SubcommandPrints
userThe user SID — who the token is.
ownerThe default owner SID.
groupThe primary group SID.
groupsThe group list.
privsThe privileges, with their enabled state.
capsThe capabilities.
claimsThe user and device claims.
integrityThe integrity level.
logonThe logon type and logon SID.
sourceWhat minted the token.
originThe originating session for a derived token.
statsToken statistics — IDs, timestamps, the modification counter.
default-daclThe token's default DACL.

query #

token query CLASS performs a raw read of a single named token-info class and prints the result as JSON — the lowest-level inspection route, for tooling.

Changing a token #

adjust #

token adjust mutates a token in place:

FormChanges
adjust privs NAME=STATE …Enable, disable, or remove privileges. STATE is enabled, disabled, or removed.
adjust groups IDX=STATE …Enable or disable groups by their list index.
adjust default --dacl SDDLReplace the token's default DACL. Also --owner-idx / --group-idx.
adjust session IDReplace the token's session id.

restrict #

token restrict produces a restricted token — a more limited variant of a token.

FlagEffect
--drop-privs MASK|NAMESPrivileges to drop.
--deny IDX,…Group indices to mark deny-only.
--restrict SID,…The restricting SIDs to apply.

duplicate #

token duplicate (alias dup) copies a token, optionally changing its --type (primary or impersonation), its impersonation --level, or its --access mask.

link and linked #

token link joins two tokens as an elevation pair — a full token and its filtered counterpart — given their file descriptors and a session id. token linked shows a token's elevation-linked counterpart, if it has one. See Elevation.

Impersonation #

SubcommandEffect
impersonateBegin impersonating the target token on the calling thread. With a trailing -- command …, run that command under the impersonating token.
revertDrop any active impersonation on the calling thread.

See Impersonation for the model these drive.

Creating tokens #

SubcommandEffect
create SPECCreate a token from a binary token-spec (SPEC is a file, or - for standard input).
install SPECCreate a token from a spec and install it as the caller's primary token.

Creating and installing tokens is a privileged operation, reserved for the components that legitimately mint identity.

Output options #

FlagEffect
--rawRender SIDs in raw S-1-… form only.
--labelRender SIDs as their labels where known, falling back to raw.
--jsonEmit JSON instead of human-readable output.

token and the inspection surfaces #

token is the convenient front-end. Underneath, it reads the same kernel surfaces and rules described in Inspecting tokens — that page covers the query mechanism, the access rules for reading another process's token, and what cannot be inspected.

Exit status #

CodeMeaning
0The operation succeeded.
1A usage error.
non-zeroThe operation failed — no such target, an access denial, or a bad spec.

Peios Learn — documentation for the Peios project.

Built with Trail.