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

Impersonation

Single-page view · as markdown

Impersonation

Peios / Peios Security Fundamentals / Impersonation

Impersonation is the mechanism that lets one thread temporarily act as another principal. The thread installs a second token — an impersonation token — and from that moment until it reverts, every access decision on that thread reads the impersonation token instead of the thread's primary. Other threads in the same process are unaffected. The primary token is preserved untouched and resumes its role the instant the thread reverts.

Impersonation exists because of one stubborn fact: most code that handles user requests does not run as the user. A storage service runs as a service account, a database runs as postgres, the registry daemon runs as loregd. When a real user asks one of these services to do something, the work has to be evaluated against the user's rights, not the service's. Impersonation is how that swap happens cleanly.

The two-token model #

A thread can have two tokens at once:

flowchart LR
    A["Thread"] -->|primary token, always| B["Primary token, the service's identity"]
    A -->|impersonation token, while impersonating| C["Impersonation token, the client's identity"]

While the impersonation token is installed, every AccessCheck on that thread reads it. The primary is invisible until revert. Other threads in the same process still read their own primary tokens — impersonation is strictly per-thread.

Two consequences worth knowing up front:

  • The process's identity does not change. Other processes inspecting this one still see the primary token as the process's identity. Tools like ps, /proc/<pid>/token, and audit events that record the process identity all report the primary. Only the thread's own view is overridden.
  • The PSB does not change. A thread impersonating a higher-trust token does not get higher process integrity protection. PIP is a property of the binary running, not of the impersonation token. See Process integrity protection for the why.

A canonical server flow #

The simplest case — one OS thread, one request, start to finish — follows a flow like this:

flowchart LR
    A["Client connects"] --> B["Server accepts"]
    B --> C["Server calls kacs_impersonate_peer(fd)"]
    C --> D["Server does the work"]
    D --> E["Server calls kacs_revert()"]
    E --> F["Server handles next request"]
  1. Client connects. Before connect, the client may have called kacs_set_impersonation_level on the socket to bound how far its identity may travel (see Impersonation levels).
  2. Server accepts. The kernel captures the client's identity onto the socket at connect time.
  3. Server impersonates. A call to kacs_impersonate_peer(fd) installs the captured identity onto the calling thread, at the effective level determined by the two-gate model.
  4. Server does the work. Every access check on this thread now runs against the client's identity. File opens succeed if the client could open the file; registry reads succeed if the client could read; and so on.
  5. Server reverts. kacs_revert() drops the impersonation token. The thread is back to its primary identity for the next request.

A server that handles many concurrent clients impersonates separately per thread. A server that handles requests serially impersonates and reverts in a loop. Either way, the impersonation is always a bracketed operation — install, do work, revert.

Just-in-time impersonation #

The canonical flow above assumes one OS thread is dedicated to one request from start to finish. That model fits a thread-per-connection server, but it does not fit most modern application runtimes. In Go, a goroutine can be scheduled onto any of the runtime's worker threads, and may move between them at any await point. In Rust's async runtimes, in Java's virtual threads, in Node's event loop — wherever you have M:N scheduling, the "impersonate at the top of the request, revert at the bottom" pattern is unsafe. The impersonation is installed on whichever OS thread happened to be running when the call was made; the request may continue on a different OS thread an instant later.

Most real Peios applications use just-in-time impersonation instead: capture the client's identity as a token fd at request start, hold the fd in the request context, and install it on the current OS thread only for the brief moment of the access-requiring action, reverting immediately after.

flowchart LR
    A["Accept connection"] --> B["Capture peer fd"]
    B --> C["Service-identity work"]
    C --> D["Install token, one action, revert"]
    D --> E["More service-identity work"]
    E --> F["Install token, one action, revert"]
    F --> G["..."]
    G --> H["Close token fd"]

The sequence:

  1. At accept, the server calls kacs_open_peer_token(fd) to capture the client's identity as a token fd. The fd is stored in the request context — request struct, goroutine-local, whatever the runtime provides.
  2. Most of the request is processed as the service's own identity. Decoding, dispatching, internal bookkeeping, response framing — none of these need the client.
  3. When the code reaches the one operation that requires the client — opening a user-controlled file, reading the user's home directory, anything where the access decision must be the client's, not the server's — it installs the impersonation token on the current OS thread (KACS_IOC_IMPERSONATE), does the single operation, and calls kacs_revert immediately.
  4. The request continues at service identity. Further per-request impersonations follow the same install-do-revert pattern.
  5. At end of request, the captured token fd is closed.

This pattern has two real advantages over the canonical flow:

  • It is safe under multiplexed threading. The impersonation is installed and reverted within a tight, synchronous block of code that does not yield to the runtime, so the runtime cannot move the work to another OS thread mid-impersonation. The token fd in the request context travels with the request regardless of which thread is currently executing it; only the install-do-revert window actually pins a thread.
  • It minimises the time spent impersonated. Bugs where the wrong code runs as the wrong identity — a logging call between impersonate and the real work, an error path that forgets to revert, an unrelated background task pre-empting on the same thread — are bounded by the same tight block. Most of the request is unambiguously the service's identity; only the access-requiring call is the client's.

The cost is a small amount of boilerplate at every impersonation point. The performance cost is negligible: install and revert are pure in-kernel operations with no allocation and no IPC.

If you are writing a new service for Peios in a modern runtime, this is the pattern to start from. The canonical flow above is the simple-case reference; just-in-time is the realistic default.

Why not just run the server as the user? #

The reason impersonation exists rather than "spawn one server process per user" is the cost. Long-lived services that handle many users — a file server, a database, the registry daemon — would otherwise need one process per concurrent session. Impersonation lets a single long-lived service process handle requests for arbitrary users by swapping identities per thread, paying only the cost of an in-kernel token install per request.

The trade-off is that the server has to be careful. Code paths that touch user-controlled state are expected to be running impersonated; code paths that touch service-internal state (its own configuration, its own logs) are not. Mixing these up is the most common bug category in services written for this model: a request handler that opens a user-controlled path while still running as the service account, or a maintenance routine that touches the service's own state while still impersonating the last client.

The pattern that minimises this is to make impersonation the default state of a request thread and revert only at well-defined boundaries (logging, internal bookkeeping, between requests).

What impersonation does not do #

A few things impersonation looks like at a glance but is not:

  • It does not invent new authority. The impersonation token carries the client's privileges, integrity (capped at the server's own — see The two-gate model), groups, and SIDs intact, and the impersonating thread can exercise any of them on the client's behalf. What impersonation does not do is grant more than the client had, raise integrity above the server's ceiling, or change the binary's PIP.
  • It does not give the server arbitrary identity. The server can only assume identities the kernel has handed it — through a captured peer token, or through a token fd the server already possessed by some other path. There is no API to fabricate an impersonation token from a SID string.
  • It does not grant inherent access. Impersonating a user is not a master key to the user's data. The user's home directory, registry keys, and in-flight tokens are reachable if and only if the user themselves has access — the access check still runs to decide. Impersonation is the mechanism for evaluating that check as the user; it is not the access itself.
  • It does not survive exec. A thread that execs while impersonating has its impersonation token released before the new program runs. Impersonation is intra-program state; the new binary cannot inherit it.

Where to start #

If you want to understand the four impersonation levels — Anonymous, Identification, Impersonation, Delegation — and what each one permits, read Impersonation levels.

If you want the rules that decide what level a server actually ends up with — the identity gate, the integrity ceiling, and the silent downgrade behaviour — read The two-gate model.

If you want the concrete mechanics — peer token capture from sockets, the difference between kacs_impersonate_peer and kacs_open_peer_token, and the explicit-fd variant — read Peer tokens and capture.

Impersonation levels

Peios / Peios Security Fundamentals / Impersonation

Every impersonation token carries an impersonation level — one of four values, ordered from least to most permissive. The level is the answer to "how much may a server do with this identity?". Each level admits all the operations of the levels below it and additionally allows specific extra things.

There is one rule worth pinning before the catalog: the level is set by the client, not the server. A client calls kacs_set_impersonation_level on the socket before connect() to declare the maximum level a server may use. The kernel records the level on the socket. When the server later impersonates the peer, the level cannot exceed what the client set. A server cannot ask for a higher level; it can only end up at the level the client granted or lower.

The four levels #

The numeric values are also catalogued in Other constants.

ValueNameWhat a server may do
0AnonymousNothing identity-related. The impersonation token has the Anonymous SID as its user; the server learns nothing about the actual client.
1IdentificationInspect the client's identity. The server may read the client's SIDs, groups, integrity level, and privileges — but the token must not be used for AccessCheck. Any access check using an Identification-level token is denied immediately, no matter what the DACL says.
2ImpersonationAct as the client for all local operations. This is the default and the level most server-to-client code paths assume.
3DelegationIdentical to Impersonation locally, plus the client's credentials may be forwarded to a remote machine over Kerberos. The kernel only records that Delegation was granted; the actual forwarding is authd's responsibility.

The default — what a socket carries if the client never sets a level — is Impersonation. This default exists because the vast majority of clients want their server to be able to act on their behalf; making the no-action case the conservative one would push every client into boilerplate.

Anonymous #

The least useful level, and not as common as you might expect. An Anonymous impersonation token has:

  • A user SID of S-1-5-7 (the well-known Anonymous SID).
  • The Everyone group, but not Authenticated Users.
  • Untrusted integrity.
  • No privileges.

A server impersonating an Anonymous client effectively has no identity at all. It can act, but every access check is evaluated as Anonymous — which usually fails because almost every interesting object's DACL grants nothing to Everyone-but-not-Authenticated-Users.

You will mostly see Anonymous in two cases:

  • A client explicitly opted out of identity disclosure. Some protocols want the server to act on a request without knowing who issued it. The client calls kacs_set_impersonation_level(ANONYMOUS) on its socket; the server impersonates and gets Anonymous.
  • A connection that was never authenticated. A network peer that connected without going through any auth step is, from the server's point of view, Anonymous. The token reflects this honestly.

Identification #

The most misleading level. Identification gives the server enough of the client's token to inspect identity but explicitly forbids using it for access decisions. AccessCheck with an Identification-level token returns ACCESS_DENIED immediately, before reading the DACL.

The intended use is for servers that need to know who is asking — for audit logging, for routing decisions, for displaying a username — without actually wanting to act as them. A web server that logs which user made each request but always reads files as itself wants Identification.

The pitfall: a server that thinks it is doing Impersonation but the client set the level to Identification finds every file open returning ACCESS_DENIED for reasons that look nothing like an identity problem. The DACL is fine; the user has rights to the file; the server's primary identity has rights to the file. Yet every access fails. The cause is almost always that the client connected at Identification level and the server is supposed to be inspecting, not acting.

The fix is at the client end. If your server has to act on behalf of the user, the client must connect at Impersonation level (or accept the default — which is already Impersonation).

There is one specific construction that always produces an Identification-level token: KACS_IOC_GET_LINKED_TOKEN called without SeTcbPrivilege returns an Identification-level clone of the partner token. The clone is for inspection only — it cannot be installed and would fail every access check if it were. See Elevation and linked tokens.

Impersonation #

The level you will use almost all of the time. An Impersonation-level token can act as the client for any local operation:

  • Open files as the client.
  • Read and modify registry keys as the client.
  • Connect to other local services as the client (and have those services impersonate the same client — see "Identity cascading" in Peer tokens and capture).
  • Be the subject of any AccessCheck call.

What an Impersonation-level token cannot do is cross the machine boundary with the client's identity. If the impersonating service makes a network request that requires authentication, that request goes out using the service's credentials, not the client's. The client's identity stays on this machine.

That last point is the dividing line between Impersonation and Delegation.

Delegation #

Delegation extends Impersonation to remote calls. A server at Delegation level can act as the client locally and forward the client's identity to a remote machine over Kerberos. The remote service receives a token that represents the original client, not the intermediary.

The mechanism is entirely in authd. KACS records that the impersonation token was granted Delegation; the kernel does nothing further. When the impersonating server makes an outbound network request, authd consults the token's level. If it sees Delegation, authd performs the Kerberos credential forwarding (constrained or unconstrained, depending on the domain policy). If it sees only Impersonation, authd authenticates the outbound request as the server's own identity.

The split — KACS records the flag, authd acts on it — keeps the kernel out of the Kerberos business. It also means Delegation is meaningful only on domain-joined machines where authd has Kerberos configured. On a standalone machine, Delegation is recorded but has no effect, since there are no remote services to forward to.

What sets the level #

Three things determine the level a server actually ends up with:

  1. The client's request, set on the socket before connect via kacs_set_impersonation_level. Default is Impersonation if the client says nothing.
  2. The two-gate model, applied at impersonation time: the identity gate and the integrity ceiling. Either can cause the granted level to be silently downgraded. See The two-gate model.
  3. The transport. Some Unix-socket variants — SOCK_DGRAM, socketpair, pre-existing pipes — do not carry a peer token. Servers using these transports cannot use kacs_impersonate_peer and must use the explicit-fd ioctl instead. See Peer tokens and capture.

The first sets the maximum. The second can lower it. The third is about the mechanism for getting the token at all.

Inspecting the granted level #

A server that has just impersonated can read its own effective token to check what level it actually got. Reading the impersonation_level field of the effective token (via KACS_IOC_QUERY on the thread's token fd) returns the level in effect.

This is worth doing at runtime if your code branches on level. The "client asked for Impersonation, but the integrity ceiling silently downgraded to Identification" case is silent by design — the impersonation call succeeds and the work proceeds, but every access check on the downgraded token will fail. Code that wants to detect the downgrade before doing access-requiring work should query the level first and skip work for which the level is insufficient.

Where to go next #

For how the kernel decides the level a server actually ends up with — and why the downgrade is silent — read The two-gate model.

For how a server gets hold of a client's identity in the first place, read Peer tokens and capture.

The two-gate model

Peios / Peios Security Fundamentals / Impersonation

When a server impersonates a client, it does not automatically get the level the client granted on the socket. The kernel runs two independent gates first. Each gate can lower the effective level. The granted level is the minimum the two gates permit.

The two gates are:

  1. The identity gate — is the server allowed to impersonate this particular user's identity?
  2. The integrity ceiling — may the impersonation token's integrity level exceed the server's own?

Each gate is checked independently. Failing either one is silent: the impersonation call still succeeds, the level is silently capped, and the server only finds out by inspecting the resulting token. The kernel deliberately does not raise an error, because the requested level is the maximum — any lower level is a valid outcome.

There is one case where the kernel refuses outright with -EPERM rather than downgrading. That case is covered below.

Gate 1: identity #

The identity gate asks: does the server have authority to impersonate this specific user? It has two ways to pass:

  • The server's primary token has the same user SID as the client. A process can always impersonate its own user, regardless of privileges.
  • The server's primary token holds SeImpersonatePrivilege. The privilege explicitly grants the right to assume any user's identity.

If neither is true, the identity gate fails. The token still gets installed, but the effective level is capped at Identification — the server can inspect the client's identity but cannot use the token for any access check.

The split is deliberate. A service whose job is handling user requests — a file share, a database, the registry daemon — holds SeImpersonatePrivilege and can impersonate any user. A process running as user X can act as user X without any special privilege. Code that holds neither — a random user-mode process trying to install some other user's token — is restricted to inspection only.

SeImpersonatePrivilege is one of the few privileges that authd grants liberally to services in the role-policy assignment. Almost every service that handles user requests has it. The privilege is what distinguishes "code authorised to act as users" from "code that happened to receive a token fd".

Gate 2: integrity ceiling #

The integrity ceiling asks: does the impersonation token's integrity level exceed the server's own primary token's integrity level?

If it does — the client has High integrity, the server has Medium — the effective impersonation level is silently capped at Identification. The token still installs, and the client's integrity label may be preserved as inert identity metadata, but the token cannot pass any access check.

The motivation is concrete. Without the ceiling, a Medium-integrity service that captured a High-integrity client's token would suddenly be operating at High integrity for the duration of the request. That breaks the integrity model — a process's effective ceiling should be bounded by what it would have had on its own.

The ceiling is always enforced, and it caps the level, not the token's integrity label. SeImpersonatePrivilege does not bypass it. There is no privilege that does. A service that needs to act on behalf of a High-integrity client must itself run at High integrity.

Composition: the minimum permitted #

The two gates are independent. Each one decides what level it permits:

Identity gate resultIntegrity ceiling resultGranted level
Pass at full levelToken's integrity is at or below server'sThe level the client granted on the socket.
Pass at full levelToken's integrity exceeds server'sIdentification. The ceiling downgrades the level; the token installs but no AccessCheck will pass.
Fail (no same-SID, no SeImpersonate)Token's integrity is at or below server'sIdentification. Token installs, server can inspect, but no AccessCheck will pass.
FailToken's integrity exceeds server'sIdentification. Both gates independently cap to the same level.

The granted level is the minimum the two gates permit. Neither gate fails the operation; both can downgrade it. The downgrade is silent.

The silent downgrade #

This is the consequence worth memorising: impersonation never errors on policy failure. It silently downgrades.

A server that calls kacs_impersonate_peer(fd) and gets a successful return does not know whether the resulting token has the level it expected. Both an Impersonation-level token and an Identification-level token can come out of the same call, with no error in either case. The only way to tell is to query the token after install.

Code that wants to detect a downgrade has to do something like:

  1. Call kacs_impersonate_peer(fd) (or the explicit-fd variant).
  2. Open the thread's effective token.
  3. Read impersonation_level via KACS_IOC_QUERY.
  4. Branch on the result: proceed if Impersonation or Delegation, log-and-skip if Identification or Anonymous.

The reason for the silent design is that the gates are bounded by policy and the policy is the operator's decision. A service may legitimately want to handle requests from clients regardless of whether it can fully impersonate them — recording who asked for what even when it cannot act as them is useful. Making the downgrade an error would force every server to either bypass the failure or refuse to handle the request, neither of which is the right default.

The one hard-deny case #

There is exactly one situation where impersonation fails with an error rather than downgrading: a restricted token attempting to impersonate an unrestricted token of the same user.

The kernel refuses with -EPERM. The impersonation does not happen, the thread does not get a new token, the call returns an error.

The reason is sandbox-escape prevention. A process running on a restricted token — a sandbox process, an anti-malware quarantine — is supposed to have narrowed identity. If that process could impersonate an unrestricted version of the same user, the restriction would be trivially escapable: capture any token fd belonging to the same user (which the sandbox could obtain via socket peer capture or other means), impersonate, and suddenly the sandbox is running with full user identity.

The hard-deny applies specifically to the same-user case. A restricted process can still impersonate a different user's token, subject to the normal gates. It just cannot use impersonation as a way to "promote" itself back to the unrestricted version of its own identity.

This is the only impersonation case that errors. Every other policy failure is silent.

Double impersonation #

A thread that is already impersonating can install a second impersonation token. The kernel handles this by silently reverting the first impersonation before installing the second — there is no nesting.

The gates are evaluated against the primary token, not the current impersonation. Whatever the thread was previously impersonating does not affect what it can impersonate next. The primary's identity and privileges (notably SeImpersonatePrivilege) are what the kernel reads to decide both gates.

When the gates do not apply #

The gates fire on impersonation install. They do not fire on:

  • Reverting. kacs_revert always succeeds and does not consult the gates.
  • Reading a token. kacs_open_peer_token or kacs_open_thread_token returns a token fd without impersonating; the gates only run when the token is actually installed on a thread.
  • Process token operations. Opening another process's primary token (kacs_open_process_token) is governed by the process SD and PIP dominance, not the impersonation gates.

The gates are specifically about installing a foreign identity on a thread. Reading information about a token is a different operation governed by different checks.

The principle behind the model #

The two-gate model is the kernel's enforcement of one rule: a thread can never end up at a higher effective trust level than it could have reached on its own.

The identity gate makes sure the impersonation has authority behind it: either the same user, or the explicit SeImpersonatePrivilege that says "this service handles users by design". The integrity ceiling makes sure the impersonation cannot raise the server's effective integrity. Together they bound impersonation to what the server's own primary token would have permitted.

Everything else is a consequence. The silent downgrade exists because the requested level is a maximum, not a promise. The restricted-token hard-deny exists because that one case is the only way a same-user impersonation could increase authority rather than decrease it. The PIP exclusion (covered in Process integrity protection) is the same rule applied to the binary's trust label: PIP is a property of the binary, not of an identity, and impersonation does not change which binary is running.

Where to go next #

For the capture mechanism the gates run against — how a client's identity gets onto a socket and into the server's hands — read Peer tokens and capture.

For the restricted tokens behind the one hard-deny case, read Restricted and write-restricted tokens.

Peer tokens and capture

Peios / Peios Security Fundamentals / Impersonation

When a client connects to a server over a connected Unix socket, the kernel captures the client's identity onto the socket at connect time. The server can later impersonate that identity by calling kacs_impersonate_peer(fd) on the connection — no further negotiation, no credential exchange, no token transfer in the message stream. The token is held by the kernel and tagged to the socket from the moment of connect.

This is the primary way servers acquire impersonation tokens. Almost every local IPC in Peios — registry access, service-to-service calls, user-to-daemon requests — goes through it. The model is simple enough that most services need to know only one syscall (kacs_impersonate_peer) and one cleanup call (kacs_revert).

This page covers the capture mechanism, the two different syscalls for using a captured peer token, the transports that do and do not carry peer tokens, and how identity cascades when a service that is itself impersonating connects to a third service.

Capture at connect time #

flowchart LR
    A["Client thread, primary token T_C"] -->|"kacs_set_impersonation_level (optional)"| B["Socket"]
    A -->|connect| C["Server socket"]
    B -.->|captured at connect| D["Peer token on server side, derived from T_C at the client-requested level"]

When the client calls connect() on a Unix stream or seqpacket socket, the kernel:

  1. Reads the client's effective token (impersonation if set, primary otherwise).
  2. Reads the impersonation level the client requested on the socket (default Impersonation).
  3. Constructs a peer token derived from the client's token, at the requested level.
  4. Attaches the peer token to the server-side socket.

The peer token is held by the kernel for the life of the socket. The client's identity travels into the kernel at connect, not at impersonate. By the time the server calls kacs_impersonate_peer, the work of capturing identity is already done.

This timing matters in one specific way: the captured identity is the client's identity at the moment of connect. If the client changes identity afterwards (impersonates a third party, drops privileges, anything), the change does not affect the peer token on the existing connection. The peer token is a snapshot. Servers that want to track changing client identity across a long-lived connection need to renegotiate (typically by having the client reconnect).

Two ways to use a peer token #

The kernel exposes two operations on a peer token:

  • kacs_impersonate_peer(fd) — the combined operation. Captures the peer's identity from the socket and installs it on the calling thread, at the level the two-gate model permits. The most common path.
  • kacs_open_peer_token(fd) — the inspect-and-store operation. Returns a token fd to the peer's identity without installing it. The fd carries TOKEN_QUERY | TOKEN_IMPERSONATE access — enough to read the token and to install it later via KACS_IOC_IMPERSONATE, but not enough to duplicate or adjust it.

The two operations exist because servers have different needs:

  • Direct request handling. A request thread that wants to impersonate, do work, and revert all in one tight synchronous block calls kacs_impersonate_peer and then kacs_revert. There is no reason to hold the token fd around.
  • Just-in-time impersonation. A server that wants to keep the client's identity available across a request but only install it at the moment of each access-requiring action — the just-in-time pattern covered on the overview page — calls kacs_open_peer_token to capture the token fd at request start, stores it in the request context, and installs it on the current OS thread (KACS_IOC_IMPERSONATE) just before each action that needs the client's identity, reverting immediately after. This is the right pattern for any server using a multiplexed runtime (Go, Rust async, Java virtual threads, Node), and is the recommended default for new services.
  • Deferred work across threads. A request handler that hands off work to a thread pool cannot reasonably keep its own thread impersonating while another thread does the work — impersonation is per-thread. The same kacs_open_peer_token mechanism lets it capture the token fd, pass it to the worker, and have the worker install it before doing the access-requiring operation.
  • Inspection without action. A logging or audit thread that wants to record the client's identity without doing any work as them calls kacs_open_peer_token, queries the token via KACS_IOC_QUERY, and closes the fd. No impersonation is ever installed.

The "capture once, install per action" pattern is the main reason for separating the two operations. It is what makes safe impersonation possible under M:N threading and what bounds the time a thread is actually impersonated to the smallest window that does the work.

kacs_revert and the explicit-fd variant #

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

There is also KACS_IOC_IMPERSONATE — an ioctl on a token fd, not on a socket. This is the explicit-fd variant. The caller passes the fd of a token they have obtained by some other means (DuplicateToken, kacs_open_peer_token, etc.) and the kernel installs it on the calling thread, running the two-gate model as usual.

kacs_impersonate_peer is essentially kacs_open_peer_token followed by KACS_IOC_IMPERSONATE, fused for the common case where you do not need the token fd to outlive the impersonate-and-revert cycle.

Transports that carry peer tokens #

Peer token capture works on Unix sockets that have a real connect step. Specifically:

TransportPeer token?
SOCK_STREAM Unix socketYes. Captured at connect.
SOCK_SEQPACKET Unix socketYes. Captured at connect.
SOCK_DGRAM Unix socketNo. No connect step; no point at which to capture.
socketpair(2)No. The two ends share an origin; there is no "client" and "server".
Pipes (pipe(2), named FIFOs)No. No socket framework.
TCP socketsNo. Peer is potentially remote; KACS does not capture network identities.

Servers using a transport that does not carry a peer token cannot use kacs_impersonate_peer. They have to obtain a token fd by some other path and use KACS_IOC_IMPERSONATE to install it. Common patterns:

  • Token fd passed over the connection. A datagram protocol can include a token fd in an SCM_RIGHTS message; the receiver gets the fd and impersonates from it. The token fd has whatever access the sender opened it with, bounded by what the sender held.
  • Out-of-band capture. A service that knows the peer's PID can call kacs_open_process_token(pidfd) to get the peer's primary token (subject to the process SD and PIP dominance checks). Less common; usually only the loopback-like patterns inside the TCB work this way.

The lack of peer tokens on datagram and pipe transports is intentional, not an oversight. Those transports are fundamentally connection-less or pre-existing; capturing a "peer" identity that may not exist is ill-defined.

Identity cascading #

A service that is impersonating client A may itself need to call a third service. When it does, the connection it opens carries A's identity, not the service's own.

flowchart LR
    A["Client A"] -->|connect| B["Server S1, impersonating A"]
    B -->|connect to S2 while impersonating| C["Server S2"]
    C -.->|peer token sees A, not S1| D["S2 can impersonate A"]

The mechanism: the impersonation token is the thread's effective token, and the effective token is what the kernel reads when capturing peer identity at connect. So when S1, while impersonating A, calls connect() on a socket to S2, the kernel captures A's identity onto S2's side of the socket. S2 can then kacs_impersonate_peer and get A.

This cascading is automatic and is what makes the local-IPC ecosystem work cleanly. A user makes one request to a service, and that service makes downstream requests to other services on the user's behalf — registry, file system, audit logging — and each of those downstream services sees the user as the peer. No explicit token forwarding is required.

The cascading is bounded by the impersonation level the original client granted. If A connected at Impersonation level, S1 captures A at Impersonation; when S1 connects to S2, the captured identity on S2's side is bounded by what S1's effective token currently is — which is A at Impersonation. The level does not get re-extended by cascading.

The same mechanism reaches the network boundary only at Delegation. If A connected at Delegation level, the captured identity on cascaded local connections is still A at Delegation, and outbound network calls from S1 carry A's Kerberos credentials (via authd; KACS only records the flag). Without Delegation, network calls from S1 go out as S1, not as A.

What the server has to do #

There are two server-side shapes, depending on which pattern (canonical synchronous or just-in-time) the server is using. Both start at the same point — the kernel has captured the client's peer token onto the socket at connect time — and diverge after accept.

Canonical synchronous flow (one OS thread, one request, start to finish):

  1. Accept the connection. Standard accept() on the Unix socket. The kernel has already captured the client's peer token onto this socket.
  2. Impersonate the peer. Call kacs_impersonate_peer(fd). The kernel runs the two-gate model and installs the resulting token.
  3. (Optional) Inspect the granted level. Read the thread's effective token's impersonation_level. If it is below what you expected, the client's request, the identity gate, or the integrity ceiling has lowered it.
  4. Do the work. Every access check on this thread is now against the impersonation token.
  5. Revert. Call kacs_revert(). The thread is back to its primary identity.
  6. Handle the next request. Either on the same connection (loop back to step 2 if the client may re-impersonate per request) or on the next connection.

Just-in-time flow (the right shape for any modern runtime — Go, Rust async, etc. — and the recommended default):

  1. Accept the connection. As above.
  2. Capture the peer token fd. Call kacs_open_peer_token(fd) and store the returned token fd in the request context. The fd carries TOKEN_QUERY | TOKEN_IMPERSONATE.
  3. (Optional) Inspect the captured token. Query the captured fd to verify the identity and level before doing any work.
  4. Do most of the request as the service. Decoding, dispatch, internal bookkeeping, response framing — all run as the service's own identity.
  5. For each access-requiring action, install the captured token on the current OS thread (KACS_IOC_IMPERSONATE on the stored fd), do the single action, call kacs_revert() immediately. Keep the impersonation window as tight as possible.
  6. Close the captured fd at end of request.

A server that handles concurrent connections per thread runs whichever flow it uses in each thread, in parallel. The impersonation state is strictly per-thread; nothing shared.

Common mistakes #

A few patterns that bite first-time service authors:

  • Forgetting to revert. A thread that finishes a request without calling kacs_revert continues to impersonate the previous client. The next request handled on that thread uses the wrong identity. Make kacs_revert part of your between-requests cleanup, unconditionally.
  • Reverting too early. A thread that reverts before all of the work is done — perhaps because an inner function is doing cleanup that runs as the service identity — will fail access checks on the parts that needed to remain impersonated. Keep the impersonation scope wide enough to cover the whole user-facing operation.
  • Holding a peer token fd across exec. Token fds are file descriptors and survive exec by default (unless O_CLOEXEC was set). But the impersonation state of the thread does not survive exec — it is reverted automatically. If your service execs another binary, the new binary starts with no impersonation, even if the old code was impersonating.
  • Assuming the level you asked for is the level you got. The two-gate model can silently downgrade. Code that branches on level should query, not assume.

Where to go next #

For what the impersonated identity is checked against once the server starts acting as the client, read Security descriptors.

To drive impersonation from a shell — impersonate a peer, revert, inspect the effective token — read The token command.

Peios Learn — documentation for the Peios project.

Built with Trail.