Authentication
Single-page view · as markdown
How authentication works
Peios / Peios Security Fundamentals / Authentication
Every sign-in on Peios is a conversation with an authority over a Unix socket at /run/logon.sock. The protocol is PGSS Logon, and it is a conformance requirement rather than an implementation detail: a system that does not offer it, at that path, with these semantics, is not Peios.
Knowing its shape is worth the few minutes it takes. It explains why a login prompt behaves the way it does, why a wrong password and an unknown account are indistinguishable, and what you would have to build to add a new way of signing in.
Three roles #
The authority listens on the logon socket. It decides whether a sign-in succeeds, and it is the only thing on the system that mints tokens. On a stock Peios system that is authd. There is at most one on a running system — two would make "who authenticated this?" unanswerable.
The client connects and speaks on the principal's behalf. login is a client. So is any greeter, remote access daemon or kiosk agent you might write. A client renders the prompts it is asked to render, returns the answers, and installs the token it is given. It is not trusted, and nothing it sends is taken as fact.
A principal source knows who exists and verifies credentials. It asserts an identity and can do nothing else — it cannot mint a token, grant a privilege or create a session, because the protocol it speaks has no message for any of those. lpsd is the local one. See principal sources.
The conversation #
A client opens a connection and sends one LogonStart, naming the principal, the kind of session it wants, and the credential types it is able to collect. The authority may then ask for credentials — as many rounds as the source needs — and each request carries prompts the client renders without interpreting. The conversation ends when the authority grants or denies.
The capability list is a statement about the client, not a demand. Declaring that you can collect a password does not mean one will be asked for; declaring that you can collect nothing says you are able to complete a sign-in that needs no interaction, and nothing else. An authority faced with that either completes the sign-in or denies it. That is the mechanism behind passwordless accounts and console autologon.
A connection carries exactly one conversation, so the connection is the conversation's identity. There is no correlation identifier to forge.
Changing your own password #
The logon socket carries one other kind of conversation: changing your own credential. It opens with a different message, which names nobody — the account is the one your token says you are — and it ends without a token, because a change mints nothing. Your current credential is asked for first, even though you are signed in. passwd is the client for it.
Because every principal needs to reach the socket for this, reaching it is not permission to originate a logon. The authority decides that per caller, from the caller's token.
The authority is not a process factory #
A grant carries a token as a file descriptor, a session identifier, and a profile. It carries nothing about what should happen next, because nothing about what happens next is the authority's concern.
The client installs the token itself and proceeds. login makes the token its own and then execs your shell, rather than the authority forking anything. This keeps the most privileged process on the system out of the business of launching programs: authd never learns about terminals, environments or session leadership, and stays small enough to audit.
It also means a client can obtain a token for a purpose the authority never anticipated.
Authentication and derivation are separate #
Two acts, deliberately distinguished.
Authentication establishes that the principal is who they claim. Its output is an identity and nothing more.
Derivation constructs the token — which SIDs it carries, which privileges, at what integrity level, with what projected identifiers. Its inputs are the authenticated identity and this machine's policy.
The separation is what stops an identity established elsewhere from carrying entitlements onto this machine with it. A directory can tell this machine that you are a member of Domain Admins; whether that membership carries SeLoadDriverPrivilege here is answered here, every time, by the authority applying local policy. An authority never accepts a token, a privilege set or an integrity level from another party, whatever its trust level.
See privileges for what that policy contains.
What the authority does not trust #
Nothing a client sends. Three consequences are worth knowing because you will see them from the outside:
Peer identity comes from the socket. The authority reads the connected peer's token from the kernel rather than believing anything in a message, because there is no field a client could put its identity in that it could not also lie in. SO_PEERCRED is not used: it returns the projected uid, and on Peios many principals project to the same number.
The logon type is a proposal. Only the caller knows whether a connection is an interactive shell or a batch command, so the caller says — and the authority constrains that against what the verified peer is permitted to request. See logon types.
The identifier is a claim until authentication succeeds, and tty and remote_host are never more than that. A client that lies about its remote host is not prevented from doing so by this protocol.
A wrong password and an unknown account look identical #
An authority never distinguishes an unknown principal from a bad credential — not by denial code, not by the text it returns, and not by timing. There is one denial for both.
lpsd holds to the timing half by verifying an unknown name against a decoy: a real verifier over a password nobody knows, so both outcomes cost one full derivation. Returning early for a name it did not recognise would make "does this account exist?" answerable by anyone who can time a sign-in.
Account existence is not a secret, and Peios does not pretend otherwise — it is answered plainly on a different socket, /run/ident.sock, which is how ls -l turns an owner into a name. What the logon socket refuses to do is let you learn it by guessing credentials. See resolving names.
What the audit trail records #
The caller is told only that authentication failed, but the audit trail may know more. Every sign-in leaves two events, one from each side of the conversation:
authd.logon.attempted, from the authority, once perLogonStart, however the logon ends. It names the originator: the principal of the program that asked, assource.token.sid. It also records the logon type asked for, the principal source asked, and the remote address when the originator gave an IP address. A success adds the principal signed in (subject.token.sid), the new session and the token. A failure addsoutcome.reason: the denial the originator was sent, such asauthentication-failed,logon-type-not-permittedoraccount-restricted.lpsd.credential.verified, from the local principal source, when it checks a credential. It says what the wire must not.outcome.reasonisno-such-account,wrong-credential,disabledornot-permitted, andobject.account.sidnames the account when one exists.
The name typed is never recorded, in either event. A name the authority could not match to an account is the caller's input, not an identity, and a record of it would turn the audit trail into a list of mistyped passwords and probed usernames. An account appears only by its SID.
authd.logon.attempted is essential, so the emission policy cannot switch it off. lpsd.credential.verified is standard and can be: a machine exposed to a network may be asked to check credentials far more often than it signs anybody in.
Where to start #
- The
logincommand — the terminal client, and how a live image signs in without being asked anything. - The
passwdcommand — changing your own password. - Logon sessions — the kernel object a successful sign-in creates.
- Authorities and principal sources — the accounts being signed in to.
The protocol is specified rather than merely documented: PGSS §2 is Logon, and PSPU §2 is PSI, the interface principal sources speak. Read those if you are writing a client or a source of your own.
Authorities and principal sources
Peios / Peios Security Fundamentals / Authentication
Signing in on Peios involves three separate things, and it is worth being able to name them before you administer any of them.
The authority is the process that mints tokens. On a stock Peios system that is authd. It listens on /run/logon.sock, it holds the privilege to create tokens, and it is the only thing on the system that does. When you type a password at a login prompt, login is talking to authd.
A principal source is a process that knows who exists. It verifies credentials and answers the question "who is this?" — and nothing else. It cannot mint a token, cannot grant a privilege, and cannot create a session, because the protocol it speaks gives it no way to say any of those things.
lpsd is the local principal source: the one that holds this machine's own accounts. It is where jack lives, and it is what you are administering when you run lps.
Why the split #
It would be simpler to have one daemon that both checks passwords and mints tokens. The split exists because those two jobs have very different risk profiles.
Checking a credential means parsing input that came from outside — from a login prompt, a network connection, a smartcard reader. That is the code most likely to have a defect in it. Minting a token is the most privileged operation on the system.
Putting them in one process means a defect in the first becomes a compromise of the second. So authd mints and cannot verify; lpsd verifies and cannot mint. A completely compromised lpsd can lie about the accounts it holds, and that is the whole of what it can do — it cannot elevate anyone, cannot forge a token, and cannot claim identities that are not its to claim.
The division runs through what a source is even able to say. A source states who someone is: their SID, their memberships, their POSIX identifiers, where their session starts. It never states how much this machine trusts them — privileges and integrity levels are authd's, decided from local policy, and there is no message with which a source could ask for one.
That local policy is a registry key, one record per principal, and it is where you go to change what an account may do as opposed to who it is: assigning privileges.
How they find each other #
lpsd connects to authd, not the other way round. That keeps authd — the process holding the token-minting privilege — free of any reason to open an outbound connection to anything.
At boot, lpsd starts after authd, connects to /run/psi.sock, registers itself, and only then reports itself ready. That ordering means anything that starts after lpsd — login, a greeter, a remote access daemon — finds a system that can genuinely authenticate, rather than one where the processes merely exist.
If you see lpsd failing to start, the usual causes are that authd is not running, or that this machine's configuration does not permit lpsd to register. The second is deliberate: a source must be explicitly allowed to assert identity, and an unconfigured machine permits none.
More than one source #
lpsd is the only source on a stock machine, but nothing about the design assumes it is alone. A directory-backed source can register alongside it, holding domain accounts while lpsd holds local ones, and authd routes each logon to whichever source owns the name.
Each source declares the SID namespace it is authoritative for, and authd confines it there. That is what stops a directory deciding who administers your machine, and equally what stops a local source handing out domain identities.
The same confinement applies to numbers. Sources assert relative POSIX identifiers; authd applies each source's assigned range and refuses out-of-range values. That keeps source-provided numbers clear of uid 0 and other sources' ranges. See Numeric Scope for the rebasing and bounds rules.
Seeing a principal, rather than a number #
Signing in is one half. The other is that everything on the system can turn an identifier back into a name — ls -l showing an owner, id showing a group.
That goes to authd too, on a socket of its own: the authority resolves effective POSIX identifiers and applies the machine's source-search order. See resolving names for what lookup answers mean, and Identity Lookup for the protocol boundary.
Where to start #
- The local store — what
lpsdkeeps, and where. - Resolving names — how a name, a SID or a number becomes a principal.
- Creating accounts — the first one, and every one after.
- The
lpscommand — the full reference.
The protocols themselves are specified rather than merely documented: PGSS §2 is Logon, and PSPU §2 is PSI, the interface sources speak. Read those if you are writing a principal source of your own.
The local store
Peios / Peios Security Fundamentals / Authentication
lpsd keeps everything it knows in one file:
/var/state/lpsd/principals
It is read once at startup and held in memory. It changes when an authorised account or group change is made, including self-service account changes.
What is in it #
The machine's domain SID. Every local principal is numbered under it, and it is generated — from the kernel's random number generator — the first time lpsd runs on a machine that has no store.
That last point matters more than it sounds. Two machines built from the same image do not share a domain, so the security descriptors written on one do not grant access to the other's users. An image cannot ship a domain, because the domain does not exist until first boot.
The domain is always of the form S-1-5-21-A-B-C. That shape is structural — the store holds three numbers under a fixed prefix, so it cannot express a domain of any other kind. It cannot claim S-1-5-32 (BUILTIN) or any well-known namespace, whatever is written into it.
The principals. For each: a relative identifier (RID), a Unix ID, the canonical spelling of the name, whether the account is enabled, a password verifier, the groups it belongs to, which of them is primary, a home directory, a shell, a display name, and any claims.
A principal's SID is the domain plus their RID. jack with RID 1000 in domain S-1-5-21-A-B-C is S-1-5-21-A-B-C-1000.
RIDs start at 1000 and are never reused. Not even after an account is deleted. That is deliberate and it is the property that makes deletion safe to offer at all: a reissued RID would give a new person the SID of an old one, silently inheriting every access the descriptors on this machine still grant them. It is the one identity mistake that cannot be undone by fixing the account afterwards.
The local groups. A group created here is an object: a RID from the same counter, a name, a Unix ID, and a description of what it is for, which anyone may read. Sharing the counter with principals is what stops a group ever colliding with a user's SID.
Well-known groups — BUILTIN\Administrators, Everyone, Authenticated Users — are not stored. Their SIDs are the same on every Peios machine, so a stored copy could only ever drift from the real one, and provisioning would freeze whatever the table said that day. lpsd resolves their names from a table in its own code.
Unix IDs #
Linux programs call getuid and getgroups and have never heard of a token, so every token carries POSIX numbers alongside the SIDs. Those numbers begin here.
A principal's stored Unix ID is their RID, and a group's is its RID. These numbers are relative: authd adds a base from the registry before the number reaches a token, so jack with RID 1000 stores 1000 and signs in as uid 1001000 when the base is 1,000,000.
The range is a boundary enforced by authd: an out-of-range relative ID is refused, so a source cannot number its principals as uid 0 or in another source's range. Numeric Scope specifies the rebasing and bounds checks.
It also means moving a range is a registry edit rather than a rewrite: nothing stored here was ever absolute.
lps list and lps show display the effective number, with the base already added, because that is the one an operator will see everywhere else. The authority communicates the range at registration for that purpose; lpsd must still assert relative identifiers.
At lookup time, getpwuid asks authd about the effective number; authd queries the relevant source in its relative namespace. See resolving names.
Profiles and claims #
The profile — home directory, shell, display name — is not identity. No security descriptor names a home directory and no token carries one. It is stored here alongside the account, and it travels to login on the logon reply so a session can start without a second lookup.
Claims are named, typed attributes that conditional ACEs read: a descriptor can grant access to anyone whose Department is Engineering without naming principals individually. They are fixed on a token when it is minted, so a claim set now takes effect at the principal's next sign-in.
What is not in it #
Passwords. What is stored is a verifier — an argon2id hash — and a verifier is deliberately not password-equivalent. You cannot authenticate with it, and it cannot be used to answer a challenge.
That is the property that makes stealing the store meaningfully weaker than knowing the passwords. It is also why Peios does not use challenge-response schemes: those require storing something a response can be recomputed from, which is exactly what this avoids.
Each verifier carries the cost parameters it was created with, so raising them applies to passwords set afterwards while existing accounts keep working.
Well-known group definitions. The store defines the groups this machine creates, and records memberships of everything else as SIDs. BUILTIN\Administrators exists on every Peios machine whether or not this store mentions it; lpsd only records that jack is in it.
Privileges and integrity levels. What a principal may do is not stored here at all. authd decides it, from local policy keyed on the SIDs a token ends up carrying — a registry key rather than this file, so that it stays readable by an administrator and this file can stay readable by nobody. A principal source says who someone is; it has no way to say how much this machine trusts them. See assigning privileges.
The descriptor #
The store file is owned by LocalSystem and its DACL grants LocalSystem and nothing else — not even BUILTIN\Administrators.
That is narrower than almost anything else on the system, and it is on purpose. This is the machine's password material, and the list of principals entitled to read it should be as close to empty as the system permits. An administrator who genuinely needs the file can take ownership, which is an act that leaves a trail; a read granted by the DACL does not.
It is also why lps talks to lpsd over a socket rather than editing the file. A tool that wrote the file would need that descriptor widened to whatever the tool runs as, undoing the one thing it exists to do.
A file, not a database #
The store is serialised whole and replaced atomically. Its temporary inode is created with a protected SYSTEM-only descriptor before the first byte is written, flushed, then renamed over the old one. Stale temporary files are unlinked and recreated exclusively; they are never truncated or followed. At startup lpsd establishes a protected SYSTEM-only inheritable descriptor on its state directory before loading or provisioning the store. The final file has the same SYSTEM-only access policy, independent of parent inheritance.
A few hundred principals, rewritten when an authorised account or group change is made, is not a workload that needs a write-ahead log. And because the swap is a rename, a reader sees the old file or the new one and there is no third outcome — which means there is no recovery code, and code that does not exist cannot be wrong.
The file carries a checksum. Not to catch a half-written file, which atomic replacement makes impossible, but because a corrupted store that decoded short would present as an account quietly no longer existing.
Its format, and upgrading #
The file also carries a format number. lpsd reads the format it writes and the one before it: a store written before groups had descriptions loads with every description empty, and is written in the new format the next time anything changes. Nothing has to be done to upgrade it.
Going back is another matter. An older lpsd refuses a store in a format newer than it knows, and does not start, rather than guess at what it cannot read. So once a newer lpsd has changed the store, an older one cannot be put back in its place.
Missing versus corrupt #
These are treated as opposite outcomes, and the distinction is the most important behaviour in this page.
No store at all means an unprovisioned machine. lpsd generates a domain, writes an empty store, and carries on.
A store that will not read, damaged or in a format this lpsd doesn't know, is fatal. lpsd refuses to start.
Collapsing the two — "cannot read it, so make a new one" — would turn a flipped bit into every account on the machine silently ceasing to exist, and then reappearing under a new domain with different SIDs, orphaning every security descriptor that named them. Refusing to start is loud, reversible, and leaves the evidence intact.
If lpsd reports that the store is corrupt, do not delete it. Take a copy first: it is the only record of your machine's identity.
Resolving names
Peios / Peios Security Fundamentals / Authentication
Every program that prints a name for a file's owner is asking the same question: who is this? On Peios that question goes to authd, over a socket of its own:
/run/ident.sock
Separate from /run/logon.sock, but the same standard — PGSS §2 defines both. What separates them is who calls: a handful of things ever originate a logon, where every process on the system resolves names. A filesystem walk is millions of lookups, and it must not be able to fill the queue an administrator needs in order to sign in.
Why the authority, and only the authority #
The protocol assigns this role to the authority: sources assert relative POSIX identifiers and answer numeric queries in their own relative namespace, while applications ask the authority about effective, machine-wide identifiers. The Identity Lookup contract defines that boundary; Numeric Scope explains how the authority applies a source's range even though the source knows it.
The same holds for names. A bare name may exist in more than one source, and which one wins is a property of the machine rather than of any source in it.
One answer, systemwide #
Whatever asks — a Peios tool, a Linux program through getpwnam, a future graphical console — the question goes to the same place and gets the same answer.
There is deliberately no way to arrange otherwise. glibc's identity databases are patched to reach the authority and nothing else, there is no /etc/passwd behind them, and PAM does not exist on Peios at all. Every route by which another system lets identity come from somewhere the authority cannot see has been closed rather than configured.
That is worth more than it sounds. If two components resolved names separately they could disagree, and a program acting on one principal's behalf while its access is checked against another is a confused-deputy bug rather than a cosmetic inconsistency. It also means a name resolves identically at a logon and at an ls -l, because the same code answers both.
The search order #
A name nobody qualified is resolved by asking sources in a configured order, stopping at the first that answers.
The order is a value on each source's allowlist entry:
Machine\Software\Authd\Sources\lpsd
SearchOrder REG_DWORD 1000
Lower is consulted first. Sources that share a value are ordered by name. A source that sets nothing resolves at 1000, which leaves room to place a new source either side of the ones already there without renumbering them.
It is deliberately explicit. Resolving in the order sources happened to register would let a slow disk change which principal a name refers to.
Warnings about shadowed names #
If more than one source holds the same bare name, only the first owner in the
search order answers the logon. After delivering a successful logon reply,
authd asks later sources whether they also own that name. These are identity
queries with no credentials or authentication assertion attached.
Each confirmed duplicate produces a warning in authd's service log, naming the entered name, its owning source and the later source. The warning does not change the token or select a different principal. Administrators should check whether an old local account is hiding a directory account, or whether a source order change has made a different SID win for a familiar name.
An absent, refusing or unavailable source makes the check inconclusive, not a failed logon. The success reply and normal logon audit record precede the check. All the later sources share one normal five-second lookup deadline; there is no extra query on a single-source machine or when the owning source is last. If the owning registration has disappeared or been replaced during authentication, authd skips the check rather than assuming its old position still holds. Absence of a warning therefore does not prove that a name is unique.
Bare in, qualified out #
You may look a principal up by a bare name. What comes back always carries the SID as well as the name.
So a tool can always tell which principal it got, compare two answers for identity, and record something unambiguous in a log. Ambiguity is permitted in what you ask; it is not permitted in what the authority answers.
Reserved characters #
A principal or group name may not contain @, \, /, : or ,, may not begin or end with a space, and must be printable ASCII.
Each of those is a separator somewhere a name ends up — a path, a passwd record, a group member list — and the damage is done by whatever reads it later, so it cannot be prevented at the point of display. @ and \ are reserved for qualified names (jack@local), which do not exist yet: reserving them now is what stops an account literally called jack@local colliding with the syntax the day it arrives.
The ASCII restriction defers confusables and normalisation rather than getting them wrong. jack spelled with a Cyrillic а renders identically and is a different principal, and the same name in NFC and NFD is two byte sequences a comparison calls two people. Relaxing this later is safe; tightening it later would mean renaming accounts.
Absent is not the same as missing #
Three answers, and the difference between the last two is the one worth knowing:
| Answer | Means |
|---|---|
| Found | Here is the principal. |
| Not found | Every source that could have answered was asked, and there is no such principal. |
| Unavailable | A source that could have answered did not. |
A source that is configured and not running does not simply drop out of the order. If lpsd has crashed, jack resolves to unavailable — not to a directory's jack further down the order, which would be a different principal with a different SID, and every security descriptor granting the local one would stop applying to the person signing in under that name.
It also matters because not found is safe to remember and unavailable is not. A cache told "no such user" during an outage would keep saying so long after the outage ended.
Groups you cannot list #
Ask who is in a local group and you get its members. Ask who is in Everyone and you get nothing — and that is the correct answer rather than a limitation.
Membership comes in three kinds:
Recorded. Local groups, and the BUILTIN groups: Administrators, Users and Guests. A source holds the edges, so it can list them, and authd gathers a BUILTIN group's members from every source that may record them. (Note that being well-known has nothing to do with it — Administrators is well-known and perfectly enumerable.)
Stapled. Everyone, Authenticated Users. Nothing records who is in them; authd adds them to every token it mints. Membership is a rule, not data, so there is no list to produce.
Session-scoped. Interactive, Remote Interactive, Network, Batch, Service. These are not properties of an account at all, but of a logon: jack is in Interactive at the console and not in it over the network. No static answer exists even in principle, which is why they have no POSIX group id either.
The asymmetry that falls out is the useful one. Which groups is this principal in is cheap and always works. Who is in this group is best-effort, and a source is entitled to decline it — listing a directory group can mean listing an entire organisation.
Every lookup is live #
The authority holds no cache. Each lookup it receives is a fresh round trip to the source that holds the answer.
That is a deliberate first step rather than an oversight: a cache is invisible on both sides of the wire, so adding one later changes nothing about how any of this behaves. Sources already declare what one will need — whether they can push a change notification, and how long an answer may be held — so the contract is in place before the thing that uses it.
One thing does cache, and only within a single command. Turning a SID into a name for display — the owner column of ls -l, and the same rendering everywhere else in the userland — remembers what it has already asked for the lifetime of that one process, so listing a large directory owned by a handful of principals costs a handful of lookups rather than one per file.
It is scoped that way on purpose. Two separate commands share nothing, so an account renamed between them shows its new name on the very next one, and there is no stale answer to invalidate because there is nothing that outlives the command. This is what a Linux system already does for getpwuid, so ls behaves the way its users expect.
Names that are fixed by specification rather than held by any source — Everyone, Local System, BUILTIN\Administrators and the rest — are answered from a table built into the renderer and never asked about at all. There is nothing a source could add to them, and asking would mean a large listing querying the authority thousands of times about Everyone, and printing something different depending on whether the authority happened to be running.
Name service switch lookups — getpwnam and friends — are not cached anywhere. Each is its own connection and its own round trip.
Asking from a program #
A Linux program asks through getpwnam and friends, and needs nothing more. A Rust program that wants the answer itself — a SID's name, or every principal there is — can use the libauthd-client crate in the authd repository. The name service switch module, ls and the other userland tools, Principals Manager and the permissions editor all ask through it.
It keeps the three answers apart: not found comes back as no principal, and a failure says what it was. Its transient says whether asking again could help, which is true when authd didn't answer in time or a source that could have answered didn't. Each question has a connection of its own, unless the program holds one for a walk through a long listing, a page at a time.
Where to go next #
For how Linux programs reach this resolution through getpwnam and friends, read Name service switch.
For where the local principals being resolved actually live, read The local store.