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.
| Group | Fields | What they do |
|---|---|---|
| Identity | user_sid, groups, logon_sid, restricted_sids | Who the token represents. |
| Type and level | token_type, impersonation_level, integrity_level, mandatory_policy | What kind of token this is and how it may be used. |
| Defaults | owner_sid_index, primary_group_index, default_dacl | What the token contributes when new objects are created without an explicit security descriptor. |
| Session and provenance | auth_id, source, token_id, created_at, expiration, origin | Where the token came from and which authentication event it belongs to. |
| Privileges | privilege bitmask (present / enabled / used / removed states) | System-wide rights the token holds. |
| Confinement | confinement_sid, confinement_capabilities, confinement_exempt | Sandboxing constraints, when the token is for a confined application. |
| Claims | user_claims, device_claims, device_groups | Typed attributes for conditional ACE evaluation. |
| Projection | projected_uid, projected_gid, projected_supplementary_gids | Linux-compatibility identity numbers, derived from the rest of the token. |
| Audit policy | audit_policy bitmask | Per-token forced auditing flags. |
| Self SD | security_descriptor | The 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:
- Authentication succeeds, or a service is being launched.
- 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.
- It calls
kacs_create_tokenwith the resolved values. - The kernel returns a token file descriptor with
TOKEN_ALL_ACCESS. - 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.
| Right | Action it gates |
|---|---|
TOKEN_QUERY | Read token information — fields, groups, privileges, etc. |
TOKEN_DUPLICATE | Create an independent copy via DuplicateToken or FilterToken. |
TOKEN_IMPERSONATE | Install as a thread's impersonation token. |
TOKEN_ASSIGN_PRIMARY | Install as a process's primary token. |
TOKEN_ADJUST_PRIVILEGES | Enable, disable, or permanently remove privileges. |
TOKEN_ADJUST_GROUPS | Enable or disable groups. |
TOKEN_ADJUST_DEFAULT | Change the default DACL, owner index, or primary group index. |
TOKEN_ADJUST_SESSIONID | Change 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 case | Token type |
|---|---|
| A login session's identity | Primary |
| A server thread acting on behalf of a connected client | Impersonation |
| A service running with the identity authd assigned to it | Primary |
| A thread querying its caller's identity for an audit log entry | Impersonation (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:
| Level | What a server may do with this token |
|---|---|
| Anonymous | No identity. The token's user SID is the well-known Anonymous SID (S-1-5-7). The server learns nothing about the client. |
| Identification | The 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. |
| Impersonation | The server may act as the client for all local operations. This is the default, and the level most server-to-client code paths assume. |
| Delegation | Same 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:
| Value | Meaning |
|---|---|
| Default | The token is not part of a linked pair. Most tokens. |
| Full | The elevated half of a linked pair. |
| Limited | The 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.
| Class | Meaning | Examples |
|---|---|---|
| Fixed | Set 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. |
| Adjustable | Can 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-way | Can 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.
| Field | What it is |
|---|---|
user_sid | The primary identity of the token. The principal's SID. |
groups | An array of SID_AND_ATTRIBUTES — every group the principal is a member of, each with its current enabled/disabled state and other flags. |
logon_sid | The 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_sids | Secondary identity list for restricted tokens. Empty on unrestricted tokens. |
Type and level #
| Field | What it is |
|---|---|
token_type | Primary or Impersonation. |
impersonation_level | One of the four levels listed above (only meaningful for Impersonation tokens). |
integrity_level | Normally one of Untrusted / Low / Medium / High / System; technically any numeric S-1-16 level (see MIC). |
mandatory_policy | The 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.
| Field | What it is |
|---|---|
owner_sid_index | Index into [user_sid, groups[0..N-1]] selecting which SID becomes the default owner. |
primary_group_index | Same idea for the default primary group. |
default_dacl | The DACL applied to new objects when no explicit SD is supplied. |
Session and provenance #
| Field | What it is |
|---|---|
token_id | A unique LUID identifying this token instance. |
auth_id | The LUID of the logon session this token belongs to. |
source | An 8-character name plus a LUID identifying the component that minted the token (e.g. authd). |
created_at | Timestamp of original minting. Copied unchanged by DuplicateToken and FilterToken. |
expiration | Expiry timestamp. Set by authd. Informational only in v0.20 — not enforced by AccessCheck. |
origin | The originating logon session for derived tokens (S4U, network logon). |
modified_id | A 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 #
| Field | What it is |
|---|---|
confinement_sid | If non-null, the token is for a confined application; its identity for access checks is intersected with this SID. |
confinement_capabilities | The list of capability SIDs the confined application has declared. |
confinement_exempt | Escape hatch — if set, confinement is skipped entirely. |
isolation_boundary | Reserved for future use; not enforced in v0.20. |
See Confinement for the sandbox model.
Claims #
| Field | What it is |
|---|---|
user_claims | Typed attributes about the user, populated from the directory at authentication time. Used by @User.* references in conditional ACEs. |
device_claims | Same idea for the machine. Used by @Device.*. |
device_groups | The 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.
| Field | What it is |
|---|---|
projected_uid | The Linux UID the token's identity maps to (or 65534 if unmapped). |
projected_gid | Same for primary GID. |
projected_supplementary_gids | Same for supplementary GIDs. |
See Linux compatibility for what consumes these.
Audit policy #
| Field | What it is |
|---|---|
audit_policy | A 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 #
| Field | What it is |
|---|---|
security_descriptor | The token is itself a protected object; this is its own SD. Governs who may query, duplicate, impersonate, install, or adjust the token. |
Other #
| Field | What it is |
|---|---|
interactive_session_id | The interactive session number. Zero for services. Adjustable only with SeTcbPrivilege. |
elevation_type | Default / 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:
| Operation | Effect | Privilege required |
|---|---|---|
kacs_create_token | Mint a token from scratch using a wire-format specification. | SeCreateTokenPrivilege |
| DuplicateToken | Make 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 |
| FilterToken | Make 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:
- Validates the inputs (well-formed SIDs, no duplicate luids, all index references in range, etc.).
- Allocates a new token object with
refcount = 1. - Assigns a fresh
token_idLUID andmodified_id = 0. - Stamps the
created_attimestamp. - Injects the appropriate logon SID into the groups list (callers must not supply it).
- 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_INSTALLon a token fd makes the token the calling process's primary. RequiresTOKEN_ASSIGN_PRIMARYon the token andSeAssignPrimaryTokenPrivilegeon 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_IMPERSONATEon a token fd makes the token the calling thread's impersonation token. RequiresTOKEN_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_IMPERSONATEon 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 / ioctl | What it changes |
|---|---|
| AdjustPrivileges | Enable, disable, or permanently remove privileges. A reset-all sentinel restores defaults. |
| AdjustGroups | Enable or disable group entries. Cannot target mandatory groups, deny-only groups, the logon SID, or the user SID. |
| AdjustDefault | Modify the default DACL, owner index, or primary group index. |
kacs_set_sd on the token fd | Modify 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_openwithCREATEdisposition, 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 #
| Transition | Identity | Privileges | Groups | Integrity | Refcount |
|---|---|---|---|---|---|
| Fork | Same | Same | Same | Same | +1 (shared) |
| CLONE_THREAD | Same | Same | Same | Same | Same object |
| Exec (same integrity) | Same | Same | Same | Same | Same object |
| Exec (NEW_PROCESS_MIN downgrade) | Same | Same | Same | Lowered | New token |
| Impersonation install | Different (impersonation token) | Different | Different | Different | +1 on impersonation token |
| Impersonation revert | Back to primary | Back to primary | Back to primary | Back to primary | −1 on impersonation token |
| DuplicateToken | Same | Same | Same | Same | New token with its own count |
| FilterToken | Same | Subset | Subset (some deny-only, restricted_sids added) | Same | New token |
| AdjustPrivileges | Same | Changed (within rules) | Same | Same | Same object |
| AdjustGroups | Same | Same | Changed (within rules) | Same | Same object |
| KACS_IOC_INSTALL | Same 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:
- Normal pass. The DACL walk uses the token's
user_sid, all enabled groups, and all the usual rules. Produces agranted_normalmask. - Restricted pass. The DACL walk runs again, but the only SIDs considered for matching are those in
restricted_sids. Theuser_siddoes not match. None of the regulargroupsmatch. Only the entries inrestricted_sidscount. Produces agranted_restrictedmask. - 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 category | Where 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_ONLYin 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:
- Open its own primary token.
- Call
KACS_IOC_RESTRICTto produce a restricted variant: remove every privilege exceptSeChangeNotifyPrivilege, mark unsafe groups deny-only, add the sandbox's allowed capability SIDs as the restricted SIDs, set the write-restricted flag. - Fork.
KACS_IOC_INSTALLthe restricted token on the child.- 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:
| Restricted | Confinement | |
|---|---|---|
| Who decides | The program itself, in code | Administrative policy, applied from outside |
| Typical caller | A sandbox launcher, a hardened service, an anti-malware engine | A service manager applying a service definition; a container runtime |
| Storage on the token | restricted_sids list | confinement_sid + confinement_capabilities |
| Where it fires in AccessCheck | Inside the DACL walk, identity-based intersection | After the DACL walk and privileges, absolute intersection |
| Bypassable with a privilege? | Yes — privilege-granted bits survive | No — confinement is absolute |
| Owner implicit rights still apply? | Yes | No |
| 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_RESTRICTwith 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_sidscontaining 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
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:
| Value | Meaning |
|---|---|
| Default | Token is not part of a linked pair. The vast majority of tokens. |
| Full | The elevated half of a pair. |
| Limited | The 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:
SeTcbPrivilegeon the caller.TOKEN_DUPLICATEon 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:
- Records the pair on the session.
- Sets the elevated token's
elevation_type = Full. - 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:
- authd authenticates the user.
- authd creates the user's session.
- authd mints the Full token with all the user's entitled privileges and groups.
- authd FilterTokens the Full token down to the Limited version — privileges removed, admin groups deny-only.
- authd calls
KACS_IOC_LINK_TOKENSto link the two. - authd installs the Limited token as the primary of the user's first process.
- 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 withTOKEN_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_TOKENsees 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_TOKENon either token return-ENOENT. - The individual tokens themselves continue to exist as long as their reference counts hold. Their
elevation_typevalues 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.
| Flag | Target |
|---|---|
--self | Your own token. The default. |
--real | Your primary token specifically, rather than the effective one — relevant when your thread is impersonating. |
--pid PID | The primary token of process PID. |
--tid TID | The impersonation token of thread TID (used with --pid). |
--peer SOCK_FD | The 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.
| Flag | Effect |
|---|---|
--short | A one-line summary. |
--all | Every query class — the fullest dump. |
Field accessors #
Each of these prints one part of a token, for when you want just that piece:
| Subcommand | Prints |
|---|---|
user | The user SID — who the token is. |
owner | The default owner SID. |
group | The primary group SID. |
groups | The group list. |
privs | The privileges, with their enabled state. |
caps | The capabilities. |
claims | The user and device claims. |
integrity | The integrity level. |
logon | The logon type and logon SID. |
source | What minted the token. |
origin | The originating session for a derived token. |
stats | Token statistics — IDs, timestamps, the modification counter. |
default-dacl | The 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:
| Form | Changes |
|---|---|
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 SDDL | Replace the token's default DACL. Also --owner-idx / --group-idx. |
adjust session ID | Replace the token's session id. |
restrict #
token restrict produces a restricted token — a more limited variant of a token.
| Flag | Effect |
|---|---|
--drop-privs MASK|NAMES | Privileges 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 #
| Subcommand | Effect |
|---|---|
impersonate | Begin impersonating the target token on the calling thread. With a trailing -- command …, run that command under the impersonating token. |
revert | Drop any active impersonation on the calling thread. |
See Impersonation for the model these drive.
Creating tokens #
| Subcommand | Effect |
|---|---|
create SPEC | Create a token from a binary token-spec (SPEC is a file, or - for standard input). |
install SPEC | Create 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 #
| Flag | Effect |
|---|---|
--raw | Render SIDs in raw S-1-… form only. |
--label | Render SIDs as their labels where known, falling back to raw. |
--json | Emit 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 #
| Code | Meaning |
|---|---|
0 | The operation succeeded. |
1 | A usage error. |
| non-zero | The operation failed — no such target, an access denial, or a bad spec. |