Logon Sessions
Single-page view · as markdown
Logon sessions
Peios / Peios Security Fundamentals / Logon Sessions
A logon session is the kernel object that records one authentication event. It exists from the moment a principal successfully signs in to the moment every token derived from that sign-in is released. Every token references exactly one session through its auth_id field, and the session ties together everything that came out of that sign-in: the primary tokens, the impersonation tokens derived from them, the linked Full/Limited pair when one is established.
Sessions exist to answer two questions cleanly:
- "Did all of these tokens come from the same sign-in?"
- "When this principal logs out, what should we tear down?"
The first matters for audit and for signing somebody out. The second matters because logging out has to do more than just close a shell — it has to end every process of the sign-in, drop linked-pair associations, fire the right events. Sessions are the unit at which those teardowns happen.
What a session contains #
A logon session is a small kernel object — fewer fields than a token, no privileges or groups of its own. Its job is to identify the event, not the principal.
| Field | What it is |
|---|---|
session_id | A unique LUID identifying the session. Same as the auth_id on every token belonging to the session. |
logon_type | What kind of sign-in produced this session — Interactive, Network, Service, Batch, etc. See Logon types. |
user_sid | The principal who signed in. The same SID appears in user_sid on every token of the session. |
auth_package | A string identifying the authentication mechanism (e.g. "Kerberos", "NTLM", "local"). Informational; not used by AccessCheck. |
created_at | Timestamp of the sign-in. |
logon_sid | A per-session SID derived from the session_id. See below. |
The session is the unit of identity provenance. If you want to answer "where did this token come from?", the path is: token → auth_id → session → logon_type, auth_package, created_at, user_sid. Everything an audit log needs to reconstruct a sign-in is reachable from the session.
The logon SID #
Every session has its own SID, called the logon SID:
S-1-5-5-X-Y
Where X and Y are the high and low 32 bits of the session's LUID. The logon SID is unique to the session — two different logins by the same user produce two different logon SIDs, because they have different session IDs.
Every token of the session carries its logon SID in two places: the dedicated logon_sid field, and as an entry in the groups list with the SE_GROUP_LOGON_ID flag set. Both views point at the same value. The flag exists so the access check can identify the logon SID without needing to know its specific value.
The logon SID is what lets you write an ACE that targets "the specific sign-in that did this" rather than "this user". Common uses:
- Per-session temporary objects. Grant access to the logon SID only. When the session ends, the SID becomes unreachable (no future token will ever carry it), so the object is effectively cleaned up at logout from an access-control perspective.
- Service-to-user isolation in shared sessions. Two services running for the same user but in different sessions can use the logon SID to keep their per-session state apart even though the user SIDs are identical.
- Audit anchoring. An audit event records the logon SID alongside the user SID. The two together let an investigator distinguish "this user did this" from "this specific sign-in did this".
The logon SID cannot be disabled (SE_GROUP_LOGON_ID is a mandatory-equivalent flag), and it cannot be removed from the token. It is intrinsic to the token.
Sessions and tokens #
A session can have many tokens. Every primary token of a process belonging to the session, every impersonation token derived from one of those primaries, every duplicated or filtered copy that shares the same auth_id — all of them are tokens of that session.
flowchart LR
A["Logon session, auth_id=42"] --> B["Primary token (user's shell)"]
A --> C["Primary token (child process)"]
A --> D["Impersonation token (service handling this user)"]
A --> E["Filtered token (restricted sandbox of B)"]
A --> F["Full token (linked pair)"]
A --> G["Limited token (linked pair)"]
The kernel keeps a reference count of how many tokens belong to a session. When the count drops to zero — every token of the session has been released — the session itself is destroyed and a kacs.session.destroyed event is emitted.
The implication is that destroying a session is not a primitive operation. There is no kacs_destroy_session syscall. You destroy a session by ensuring every token belonging to it is released, which usually means ending every process running on those tokens. Forced sign-out is implemented this way, in user space: asked by a person signing themselves out, or by an administrator signing somebody else out, authd ends every process whose primary token belongs to the session, and the kernel does the rest.
Bootstrap sessions #
Two sessions exist before authd is up, created by the kernel during boot:
| Session | ID | Purpose |
|---|---|---|
| SYSTEM session | 999 | The session of the SYSTEM token, attached to init and inherited by every process until authd assigns real tokens. |
| Anonymous session | 998 | The session of the Anonymous token, used as the user SID for Anonymous-level impersonation. |
Both are created by direct kernel initialisation — they do not go through kacs_create_logon_session. They are also never destroyed during a running system's lifetime: the SYSTEM token always exists somewhere, and the Anonymous token is a singleton.
You will see these IDs in audit logs and /sys/kernel/security/kacs/sessions listings. They are not bugs.
Sessions are not durable #
A session exists in the kernel only while it has active tokens. There is no on-disk session table. The session ID space is not stable across boots — after a reboot, the same user signing in again gets a different session ID, and a new logon SID.
This matters in two practical ways:
- You cannot reference a session across a reboot. An ACL entry that targets the logon SID of "the previous boot's session 5" is meaningless after the next boot. The SID format will look syntactically valid, but no token will ever match it.
- Per-session state should be in memory, not on disk. Anything intended to survive a single sign-in should be keyed on the user SID, not the logon SID.
Where to start #
If you want the catalog of logon types — what Interactive, Network, Service, Batch, NewCredentials, and the rest each mean — read Logon types.
If you want the creation, destruction, and forced sign-out mechanics — including the kacs.session.destroyed event and what authd does to sign somebody out — read Session lifecycle.
If you want to see which sessions are currently active on a running system, read Inspecting tokens, sessions, and processes.
To work with sessions from a shell — list them, see their processes, create and destroy them — read The logonse command.
Logon types
Peios / Peios Security Fundamentals / Logon Sessions
Every logon session carries a logon_type — a single number, set by authd at session creation, that classifies the nature of the sign-in. The type is informational from KACS's point of view: AccessCheck does not branch on it. But it is recorded — by name, such as interactive or network — in the kacs.session.destroyed event that closes the session, and it is what audit consumers and SIEM tools use to distinguish "the user logged in at the console" from "the user logged in over the network" from "a service started under this account".
There are seven types in v0.20.
The SSH assignments below follow the current PGSS SSH implementation contract, whose release qualification is tracked by PEI-67. They do not establish the behavior of older builds; the contract's coordinated-rebuild requirements still apply.
The seven types #
The numeric values are also catalogued in Other constants.
| Value | Name | What it means |
|---|---|---|
| 2 | Interactive | The user signed in at this machine's console or local desktop. |
| 3 | Network | The user authenticated to access network resources without an interactive sign-in. Typical for SMB, RPC, federated services. |
| 4 | Batch | A scheduled job. The principal is logged in to run a task at a specific time, not by a user actively present. |
| 5 | Service | A service started under a specific principal. Used for the long-lived service-account model. |
| 8 | NetworkCleartext | A network logon where the credential was transmitted in cleartext over the wire. The session is otherwise a Network session; the type distinction exists for audit. |
| 9 | NewCredentials | A session created to use different credentials when reaching out to remote resources, while keeping the local identity unchanged. The local thread continues to act as its primary token; outbound network requests carry the alternative credentials. |
| 10 | RemoteInteractive | The user signed in to a remote desktop or over SSH, including commands without a PTY and SFTP. The session is reached remotely rather than at hardware this machine owns. |
Values 1, 6, 7, and 11 onward are reserved and not used in v0.20.
When each type is used #
Interactive #
The type for local console and desktop sign-ins. SSH connections and remote desktop sessions use RemoteInteractive.
In audit logs, Interactive distinguishes a local interactive sign-in from a remote one. It is not a marker for all human activity: a person using SSH or a remote desktop has a RemoteInteractive session.
Network #
Used when a principal authenticates only to access a network resource — for example, a remote user mounting a file share. The session exists on the server, the user's identity is established there, but the user has no interactive presence on the machine. The session's tokens are typically short-lived (one connection's worth of work).
A Network session is what an inbound SMB connection produces, what an authenticated RPC call produces, what a federated service request produces. It is also what a host typically sees when a user reaches a service on it that is acting on the user's behalf.
Batch #
Scheduled tasks. The principal is "logged in" only in the sense that the system mints a token for the user so the task can run as them. There is no human presence. Once the task finishes, the session ends.
Batch sessions are useful for auditing scheduled work — distinguishing "the user typed this command" from "the user's scheduled task did this command".
Service #
The session under which a service runs. The service account signs in once at service start; the session lives for as long as the service runs. Most services on a Peios machine are Service-type sessions.
This is the type you will see most often in the session listing at /sys/kernel/security/kacs/sessions on a system without active interactive users — every running service contributes one Service-type session.
NetworkCleartext #
Used when the authentication protocol exposed the credential in cleartext on the wire. The semantic effect is identical to Network — it is a network sign-in producing a Network-style session — but the type difference exists so audit policy can flag the cleartext exposure.
You should rarely see this in normal operation. Its presence in an audit log is worth attention; it usually indicates that an older or misconfigured protocol is in use.
NewCredentials #
The unusual one. A NewCredentials session does not replace the calling thread's identity. Instead, it represents "I want to keep being myself locally but use these other credentials for outbound network calls". The thread's effective token retains the local user's SIDs and privileges for everything KACS evaluates locally, but any outbound credential-using request carries the alternative principal.
The pattern matters in environments where a user has local rights on one machine and different rights on another, and wants to keep both available without switching sessions.
RemoteInteractive #
A remote desktop or SSH session. Every SSH logon uses this type, including commands without a PTY and SFTP; it does not become Network or Batch when no terminal is requested.
GXWI requests this type when logging on through authd. The kernel and authd must both support value 10; an older authd refuses the request rather than substituting another type. After updating GXWI and authd, log off existing GXWI sessions and start new ones: reconnecting to an existing compositor retains that session's original token and logon type.
It is a separate type from Interactive rather than a flavour of it because the two differ in exactly one respect that an administrator may want to act on: whether the person is physically at the machine. Everything else about the session is the same, which is why a RemoteInteractive token carries the Interactive group SID as well as its own — see Well-known principals for what that pair lets you write.
Rebooting from a RemoteInteractive session needs SeRemoteShutdownPrivilege on top of SeShutdownPrivilege, as it does from a Network, NetworkCleartext or NewCredentials session. The session being remote is what counts, not whether it is graphical.
Where the type appears #
The logon type is stamped in three places you will care about:
- In the session itself, retrievable through
/sys/kernel/security/kacs/sessionsand the session inspection APIs. - In every token's session — via the token's
auth_idfield, which points to the session, which holds the type. - In audit events — every audit event that includes a subject also implicitly identifies the subject's session, and audit consumers can use that to retrieve the logon type for the event.
The type is not part of any token's SID list. There is no "Interactive" SID that gets added to a token's groups based on the logon type. The well-known SIDs like S-1-5-4 (Interactive) and S-1-5-2 (Network) are added to a token's groups by authd based on the logon type, but they are independent token attributes from that point on. A token with the Interactive group SID has it because authd put it there; the session's logon_type is the input that drove that decision.
This independence is intentional. A token can in principle carry the Interactive group without belonging to an Interactive-type session (rare, but legal). Audit and ACL evaluation use the group SID; provenance uses the session's type.
What the type does not affect #
AccessCheck does not read the logon type. The DACL walk, the MIC check, the PIP check, the restricted/confinement passes — none of them branch on logon_type.
If you want different access for "Interactive users" vs "Network users", you write ACEs that reference the well-known group SIDs (S-1-5-4, S-1-5-2, etc.). Those SIDs end up in the token because of the logon type, but the access check matches on the SID, not the type.
The type matters at the seams: when authd is making decisions about which groups to add to a token, when the audit subsystem is recording provenance, when a SIEM correlates events across sessions. Inside the access check itself, the type is invisible.
Where to go next #
For how a session is created, destroyed, and forcibly revoked, read Session lifecycle.
For the well-known group SIDs (S-1-5-4 Interactive, S-1-5-2 Network, and the rest) that authd derives from the logon type, read Well-known principals.
To see the logon type of each active session from a shell, read The logonse command.
Session lifecycle
Peios / Peios Security Fundamentals / Logon Sessions
A session's life is bracketed by two kernel events: a successful kacs_create_logon_session call that brings it into existence, and the implicit drop of its last token reference that destroys it. There is nothing in between — no resize, no rename, no kernel-side timeout. Sessions are simple objects whose lifecycle is driven entirely by the tokens attached to them.
This page covers the three phases that matter: creation, destruction, and forced sign-out (ending a session before the person signs out themselves, which authd does on request).
Creation #
A session is created by kacs_create_logon_session. The call requires SeTcbPrivilege, so in practice the only callers are authd (every interactive and network sign-in) and peinit (services launched during boot before authd is available).
The call takes a wire-format specification with three fields:
| Field | Meaning |
|---|---|
logon_type | The type — Interactive, Network, Service, Batch, etc. See Logon types. |
auth_package | A string identifying the authentication mechanism (informational). |
user_sid | The principal who signed in. |
The kernel:
- Validates the inputs (well-formed SID, recognised logon type, sane string lengths).
- Allocates a session object with a fresh
session_idLUID. - Stamps
created_at. - Derives the logon SID
S-1-5-5-X-Yfrom the session ID. - Initialises the session's token reference count at zero.
- Returns the new session ID to the caller.
The session now exists but has no tokens. It is in a transient state: any subsequent kacs_create_token call that references this session_id in its auth_id field bumps the count, and the session is "live".
authd's flow is "create session, then mint primary token referencing it". Between those two calls the session has no tokens, and a strict "destroy when refcount hits zero" rule would tear it down before authd's second call. So the kernel only destroys a session on a refcount that transitions from positive to zero, not on one that has been zero since creation. After a successful first attachment, normal destruction rules apply.
The consequence is that a session no token ever attaches to is never reaped. Nothing times it out. If authd's mint fails after it created the session, authd rolls the session back itself with kacs_destroy_empty_logon_session, which refuses any session that has a live token. An empty session left by some other caller stays in the listing until the machine restarts, or until a caller holding SeTcbPrivilege destroys it — logonse destroy does exactly that.
The boot sessions #
Two sessions exist without ever passing through kacs_create_logon_session:
| Session | ID | Created by |
|---|---|---|
| SYSTEM session | 999 | Direct kernel init |
| Anonymous session | 998 | Direct kernel init |
Both are constructed in early boot, before any process exists. The SYSTEM session is attached to the kernel's bootstrap SYSTEM token, which init inherits and which propagates through every process until authd assigns real tokens. The Anonymous session backs the well-known Anonymous token used by Anonymous-level impersonation.
Neither is ever destroyed during the running system's lifetime. They are reference-counted like any other session, but their references never drop to zero — the SYSTEM token always has at least one attachment somewhere, and the Anonymous token is a kernel-internal singleton.
Destruction #
A session is destroyed when its token reference count, having been positive, drops to zero. The kernel:
- Removes the session from the session table.
- Tears down any linked-pair association it was holding (see Elevation and linked tokens).
- Emits a
kacs.session.destroyedevent through KMES.
The event carries enough information for consumers to clean up downstream state:
| Field | What it is |
|---|---|
object.session.id | The destroyed session's ID. |
object.session.user.sid | The principal. |
object.session.logon-type | The type, by name: interactive, network, batch, service, network-cleartext, new-credentials or remote-interactive. |
object.session.auth-package | The auth package string. |
object.session.logon-time | The session's creation time, in nanoseconds. |
The consumers are audit pipelines, accounting tools and session-aware services. authd is not one of them: it keeps no per-session state — no tickets, no cached credentials, no handle on the tokens it mints — so there is nothing of its own for it to release when a session goes. An authority that did hold session-scoped state would subscribe here to learn when to drop it.
The event is fire-and-forget — there is no acknowledgement, no retry, no replay. A consumer that misses an event misses it.
What ends a session #
A session ends only when every reference to it drops. The references are:
- Every primary token attached to a process with
auth_id = session_id. - Every impersonation token currently installed on any thread.
- Every token fd open on a process's behalf.
- The linked-pair association, while it exists.
Practically, this means a session ends when:
- Every process running on a token of the session has exited.
- Every thread that was impersonating a token of the session has reverted or exited.
- Every fd held on a token of the session has been closed.
- The linked pair, if any, has been dissolved.
The fourth condition is satisfied automatically when the session reaches refcount zero — the kernel dissolves the pair as part of destruction. The first three are user-space's responsibility.
For an ordinary logout, this happens naturally: the user's shell exits, its child processes exit, and anything that was handed a descriptor for one of the session's tokens closes it. Once all of those happen, the kernel sees refcount zero, fires the event, frees the session. (authd holds nothing: it closes its descriptor for a token the moment it has handed the token over.)
Forced sign-out: there is no kernel call #
There is no syscall to forcibly end a session. No kacs_destroy_session, no kacs_kill_session. The session model is reference-counted, and the only way to end one is to ensure every reference drops. (The one destroy syscall that exists, kacs_destroy_empty_logon_session, is a rollback primitive for empty sessions only — it refuses with -EBUSY any session that still has live tokens. This is what logonse destroy wraps.)
Forced sign-out — an administrator deciding that a person should not be signed in any more, or a person signing themselves out everywhere — is therefore a userspace operation, and authd performs it. A program that signs somebody out — a task manager, a command-line tool — asks with a SessionEnd request on /run/logon.sock (PGSS §2.22) and does none of the work itself. authd:
- Checks that the caller may. Anyone may end their own session, when they are a person signed in as themselves. Ending anyone else's needs the right granted by the
SessionEndSecuritydescriptor onMachine\Generic\Authn\Policy— by default SYSTEM and Administrators have it. Nobody may end SYSTEM's session (999), Anonymous's (998), or a service's: a service is stopped through the service manager. - Finds every process whose primary token belongs to the session: it walks
/proc, opens each process by pidfd, and readsauth_idfrom that process's primary token. A thread that is only impersonating a token of the session — a service answering the person's request — is not in the session, and is left alone. - Sends each
SIGTERMthrough its pidfd, so the signal reaches the process whose token was read even if its pid has since been reused. It waits up to five seconds for them to exit, then sendsSIGKILLto what is left. - Walks again, because a process may have forked during the grace period, and repeats — at most three rounds.
- Answers with how many processes it ended and how many still hold the session, and records the request, who made it and the result as an
authd.session.endedevent.
Once the last process exits, the session reaches refcount zero, the event fires, and the person is signed out.
What the audit trail shows #
A forced sign-out leaves two records, from two components:
authd.session.ended, written by authd when it has finished the rounds. It names who asked (subject.token.sid), the session (object.session.id), whose session it was (object.session.user.sid) and its logon type.outcome.successis false, withoutcome.reasonprocesses-remaining, when something still held the session after the last round. It is an essential event, so the emission policy cannot switch it off.kacs.session.destroyed, written by the kernel when the last reference drops. It cannot say who asked: only authd knows that.
A refused request is not an authd event. Whether a caller may end another principal's session is an access check against the SessionEndSecurity descriptor, and KACS records that decision as kacs.audit.access.checked, with object.kind authd-session-end, when the descriptor's SACL asks it to. To audit refused sign-outs, give the descriptor a failure-audit ACE, such as S:(AU;FA;0x1;;;WD). Ending one's own session needs no grant, so it runs no check. Neither does a request for SYSTEM's, Anonymous's or a service's session, which is always refused.
A program that ends the session it is itself running in is ended with it. authd therefore answers such a request first, saying how many processes it found, and does the work afterwards.
What this cannot reach:
- A token descriptor held outside the session. Token descriptors can be passed between processes; a process outside the session holding one keeps the session alive after every process in it has gone. authd walks processes, not descriptors, so it neither finds nor counts these.
- A thread impersonating a token of the session, by design (step 2). The session lasts until that thread reverts.
- A process that has exited but not been reaped. It holds its token until its parent reaps it, and counts as remaining until then.
There are two reasons the kernel does not provide a direct revoke:
- No graceful path. A "kill the session" syscall would need to choose between killing every process holding any of its tokens (loss of work, possible data corruption) and merely refusing future operations, which leaves running processes with stale identity. User-space can stage the teardown — send SIGTERM, wait, escalate — in a way the kernel cannot.
- Reference-counted identity is the simpler model. The rule "a session exists if and only if a token exists referencing it" has one fewer transition than "a session exists if and only if its tokens exist AND no revocation has been requested". Fewer transitions, fewer corner cases.
The cost is that revocation is observable: a thread can detect that its session is about to die (its parent process getting a TERM) before the kernel sees the session as gone. Anything that wants to enforce immediate revocation needs to design around that — typically by minimising the work a thread can do between receiving a signal and actually exiting.
Inspecting active sessions #
The kernel exposes the active session list at /sys/kernel/security/kacs/sessions. The file is a text listing, one session per line:
logon_session_id=1042 user_sid=<hex-encoded SID> logon_type=2 auth_package=<hex-encoded name> created_at=...
Lines are stable in their leading fields; new fields may be appended in a future version, so consumers must ignore unknown trailing fields. The file's own SD grants read to BUILTIN\Administrators and SYSTEM only.
This is the canonical way to enumerate sessions. Other tools — eventd consumers, who-equivalents, session monitors — can read it directly or through helpers that wrap it. See Inspecting tokens, sessions, and processes.
Where to go next #
For the token-side half of the same story — the references whose rise and fall drive a session's life — read Token lifecycle.
To list, create, and destroy sessions from a shell, read The logonse command.