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

Managing Local Principals

Single-page view · as markdown

Managing local principals

Peios / Peios Security Fundamentals / Managing Local Principals

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. A source is given a range of POSIX identifiers and counts inside it, and authd adds the base — so a source's principals land in its own range whatever the source sends, and the numbers below every range, uid 0 among them, belong to authd alone. lpsd starts at 1,000,000; a directory source would be given somewhere well clear of it.

That is why a uid on a Peios machine is larger than you might expect, and why moving a source's range is a configuration change rather than a migration: nothing a source stores was ever an absolute number.

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, and for a reason worth knowing: a source counts its POSIX identifiers relative to a range it is never told the base of, so only the authority can work out who a uid belongs to. See resolving names.

Where to start #

  • The local store — what lpsd keeps, 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 lps command — 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 / Managing Local Principals

lpsd keeps everything it knows in one file:

/var/state/lpsd/principals

It is read once at startup and held in memory. It changes only when an administrator changes it.

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, and a Unix ID. 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 Unix ID is their RID, and a group's is its RID. One object, one number — so a uid in a log tells you the RID without a lookup.

The numbers stored here are relative. lpsd knows nothing about where its range sits: 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.

That indirection is not bookkeeping. It is what stops a principal source reaching uid 0 or another source's numbers — authd reserves everything below the sources' ranges for its own, and refuses a relative number that runs past the end of a range rather than wrapping it. A source can only ever express numbers inside the range it was given.

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 same indirection is why getpwuid has to reach authd rather than lpsd: the arithmetic that made a number absolute happened in authd, and only authd can run it backwards. See resolving names.

Note

A - where a uid should be means authd has assigned this machine no range at all, and every principal here will sign in as nobody. The UnixIDBase value on this source is missing or unusable.

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 because this is where an administrator sets it, 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: written to a temporary file, flushed, then renamed over the old one.

A few hundred principals, rewritten when an administrator changes an account, 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.

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 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.

Creating accounts

Peios / Peios Security Fundamentals / Managing Local Principals

Creating an account #

Run lps add with no arguments and it asks for what it needs:

$ lps add
Name: alice
Full name [optional]: Alice Chen
Additional groups [comma-separated, optional]: Administrators
Primary group [the daemon's default]:
Home directory [the daemon's default]:
Shell [the daemon's default]:
Password for alice:
Again:

  name           alice
  full name      Alice Chen
  primary group  (the daemon's default)
  home           (the daemon's default)
  shell          (the daemon's default)
  groups         Administrators

Create this principal? [Y/n]:
created alice with RID 1002

An empty answer takes the default, and nothing is sent until you confirm. Or say it all on one line:

$ lps add alice --group Administrators

Anything you supply is not asked for, so either style works and they mix freely.

Groups are named however you like — Administrators, a local group's name, or a literal SID such as S-1-5-32-544. lpsd resolves it.

You need to be an administrator to do this. See the lps command for the full surface.

What a new account gets #

RIDThe next one. Never chosen, never reused.
uidThe RID, plus this machine's base — so RID 1000 signs in as 1001000.
Primary groupAuthenticated Users, unless you say otherwise.
Home/home/<name>
Shell/bin/sh

No per-user group is created. That Linux convention exists so a file's group ownership means something, and under KACS it means nothing — access is decided by the token, not by the mode bits. A group per user would be ceremony with a RID attached.

The home directory is not created either. lps records the path; nothing makes the directory. A principal whose home is missing still signs in, starting in /, and login says so.

Accounts with no password #

lps add <name> --no-password creates a principal who signs in without being asked for anything.

The sign-in is otherwise completely ordinary. lpsd still vouches for the principal, authd still mints the token and still applies this machine's privilege and integrity policy to it, and the session that results is indistinguishable from one reached by typing a password. Nothing is bypassed; there is simply nothing to collect.

It is a property of the account. Nothing ties it to a particular terminal, so the principal can be signed in at any logon prompt the machine offers — the console today, a remote one later. If what you want is "this console signs in automatically", a passwordless account is how Peios spells it, and the breadth is the cost.

An empty password is not the same thing and is refused. See the lps command.

Give the first account Administrators #

On a stock machine the tree-wide security descriptor grants LocalSystem and BUILTIN\Administrators, and nothing else.

An account without Administrators can traverse to a file it names exactly, and can execute it — but it cannot list a directory, because nothing bypasses that check. The symptom is a shell that appears to work until you type ls.

That is a limit of the descriptor the system ships with rather than anything about the account. Until per-subtree descriptors exist, an ordinary non-administrative account on a stock image is not comfortable to use.

Where the first account comes from #

A machine with no store provisions one at first boot, and it is empty — lpsd generates the domain, because a source with no domain can answer nothing, but it does not invent accounts.

So something has to create the first one, and there is a chicken-and-egg problem: lps talks to lpsd, so lpsd must already be running, which means this cannot be done by an early-boot script. Autorun scripts run before any service has started.

The answer is a oneshot service, ordered after lpsd, that runs lps like anything else would. On a development image that service exists and creates a known account. On a production image it does not, and an administrator creates the first account themselves.

On a development image #

Images built for development ship two things: a script that creates a known account, and a registry seed defining the oneshot service that runs it. If your image has them, it boots with an account already present, and lps list will show it.

On a live image that account has no credential at all, and it is an administrator. The script creates it with lps add peios --group Administrators --no-password, so anyone who reaches a logon prompt on that machine can become it — and the console signs in as it automatically, without asking anything.

That is deliberate rather than an oversight. A live ISO is an unauthenticated medium: anyone holding it can boot it and read everything on it, so a password would be a formality rather than a boundary, and one printed in the image at that. What it is not is a posture to carry anywhere else.

Give it a password the moment the machine becomes something you care about, which turns it into an ordinary account and stops the console signing in on its own:

$ lps password peios
New password for peios:
Again:
set the password for peios

Better still, build an image without the seed. See On an image without one below.

On an image without one #

lpsd starts, provisions an empty store, and logs that no principals exist and no logon can succeed until one is created. The login prompt will appear and nothing will satisfy it.

That is the correct behaviour rather than a fault, but it does mean you need a way in. Create the first account from a console session that is already SYSTEM — lps accepts LocalSystem as well as administrators, precisely for this case.

Disabling rather than deleting #

$ lps disable guest
disabled guest

A disabled account keeps its RID, its SID, and its group memberships. It simply cannot sign in. Re-enable it with lps enable.

Prefer this to lps remove. A removed principal's SID keeps appearing in the security descriptors of everything they owned, and because RIDs are never reused nothing will ever hold that SID again — the files become owned by a principal that no longer resolves.

You cannot lock yourself out #

lps refuses to remove the last enabled administrator, to disable them, or to take Administrators away from them.

$ lps disable jack
lps: jack is the only principal who can administer this machine; disabling them would leave no way back in

The check counts only enabled administrators, so a disabled standby account does not license removing the working one.

This guard exists because there is currently no offline repair. lps reaches the store only through lpsd, and lpsd only authenticates — so a machine with no enabled administrator has no way back short of editing its disk from another system.

Grouping people together #

For anything beyond the built-in groups, create one:

$ lps group create developers
created the group developers with RID 1001
$ lps group add alice developers
added alice to developers

A local group is a real object with its own SID and gid, so it can be named in a security descriptor and it shows up in getgroups. Deleting one is refused while anybody is still in it.

Changing your own password #

Not yet. lps password is an administrator resetting somebody else's.

Changing your own password will go over PGSS Logon rather than through lps, so that it works the same way whichever source holds your account — a domain principal will change their password exactly as a local one does. Until then, an administrator resets it for you.

Where to go next #

For the full command surface — every flag, the group and claim subcommands, and the exit statuses — read The lps command.

For what the store holds and why RIDs are never reused, read The local store.

The lps command

Peios / Peios Security Fundamentals / Managing Local Principals

lps administers the local principal store — this machine's own accounts and groups, held by lpsd.

lps subcommand [arguments]

It is a client and nothing more. It holds no state, opens no store, and has no privilege of its own: every command is a request over /run/lpsd/admin.sock that lpsd decides whether to honour. lpsd must be running.

You must be a member of BUILTIN\Administrators — enabled, not deny-only — or be LocalSystem.

Naming a group #

Anywhere lps takes a group, you may write it three ways:

lps group add jack Administrators              # a well-known name
lps group add jack BUILTIN\\Administrators      # the same, qualified
lps group add jack developers                  # a local group
lps group add jack S-1-5-32-544                # a literal SID

Names are matched case-insensitively, and lpsd resolves them, not lps. The tool carries no copy of the well-known table and does not know this machine's domain — a second copy of the machine's identity scheme would be free to disagree with the first.

A well-known name always wins over a local group of the same name, which is why you cannot create a local group called Administrators.

Inspecting #

lps list #

Every principal, with RID, uid, state, and how many groups each is in.

$ lps list
NAME     RID       UID  STATE     GROUPS
jack    1000   1001000  enabled   1
guest   1001   1001001  disabled  0

A - in the UID column means nothing numbers that principal — authd has assigned this machine no identifier range, and they will sign in as nobody. See the local store.

lps show <name> #

One principal in full.

$ lps show jack
name           jack
display name   Jack Palfrey
rid            1000
sid            S-1-5-21-2847362817-1094533892-3310298447-1000
uid            1001000
state          enabled
primary group  Authenticated Users [S-1-5-11]
home           /home/jack
shell          /bin/sh
groups         Administrators [S-1-5-32-544]
               developers [S-1-5-21-2847362817-1094533892-3310298447-1001] (gid 1001001)
claims         Department (string) = "Engineering"

Groups show a name where this machine knows one, and always show the SID. The two are kept side by side deliberately: the name is what you recognise, the SID is what a security descriptor actually holds, and the moment they disagree is exactly when you need to see both.

Only local groups show a gid. lpsd numbers the groups it owns and nothing else — Administrators and Authenticated Users are numbered by authd, from a table below every source's range, so lpsd genuinely does not know their gid and does not guess. To see the numbers a running session actually holds, read Groups: in /proc/self/status.

lps domain #

This machine's domain SID — the namespace every local principal and local group is numbered under.

$ lps domain
S-1-5-21-2847362817-1094533892-3310298447

Generated at first boot and unique to this machine.

Creating and removing principals #

lps add [name] [options] #

Creates a principal. Run with no arguments, it asks for what it needs:

$ lps add
Name: alice
Full name [optional]: Alice Chen
Additional groups [comma-separated, optional]: Administrators, developers
Primary group [the daemon's default]:
Home directory [the daemon's default]:
Shell [the daemon's default]:
Password for alice:
Again:

  name           alice
  full name      Alice Chen
  primary group  (the daemon's default)
  home           (the daemon's default)
  shell          (the daemon's default)
  groups         Administrators, developers

Create this principal? [Y/n]:
created alice with RID 1002

Any option you supply is not asked for, so scripted invocations keep working unchanged:

OptionEffect
--group <group>A membership. Repeatable.
--primary-group <group>The group that becomes the POSIX gid.
--home <path>Home directory. Must be absolute.
--shell <path>Login shell. Must be absolute.
--display-name <text>A human's name for a human to read.
--disabledCreate it unable to sign in.
--no-passwordCreate a principal that authenticates with no credential at all. See below.
--no-promptFail rather than ask for anything missing.

Nothing is sent until you confirm, so answering n creates nothing.

lps never prompts when standard input is not a terminal. A prompt down a pipe would consume the next line of whatever is driving the tool. See From a script below.

The RID is allocated by lpsd and reported back. You do not choose it, and the uid follows from it.

--no-password #

Creates a principal who signs in without being asked for anything. No password is collected, including in the interactive flow — an operator who has said the account needs no credential is not then asked to invent one.

$ lps add kiosk --group Administrators --no-password --no-prompt
created kiosk with RID 1003

This is a property of the account, not of a terminal. Nothing scopes it to the console: the principal can be signed in at any logon prompt the machine offers, now or later. That is the right posture for a live image, where the medium is unauthenticated anyway and anyone holding it can read everything on it. It is the wrong posture for almost anything else.

An empty password is not a way to spell this, and lps refuses one:

$ lps add alice
...
Password for alice:
Again:
lps: an empty password is not a password: pass --no-password to create a
principal that authenticates without one

The two produce accounts that behave differently — an empty password is still collected, still prompted for, and still has to be answered — so having both spellings would mean two things that look identical at creation and diverge at every sign-in. lpsd refuses an empty password too, so the rule holds however the request arrives.

lps remove <name> #

Deletes a principal.

The RID is not reclaimed. Files the principal owned keep naming a SID that now resolves to nobody, and nothing will ever hold that SID again. lps disable is usually what you wanted — see creating accounts.

Enabling and disabling #

lps enable <name> / lps disable <name> #

A disabled principal keeps everything except the ability to sign in.

Both are refused if they would leave the machine with no enabled administrator.

Passwords #

lps password <name> #

Sets a principal's password, prompting twice.

$ lps password jack
New password for jack:
Again:
set the password for jack

This is an administrator resetting somebody else's password. Changing your own will go over PGSS Logon instead, so that it works identically whichever source holds your account.

An empty password is refused here for the same reason it is refused at creation. Giving a passwordless principal a password works and makes them an ordinary account; there is currently no command that takes one away again, so a principal created with --no-password can gain a credential but not shed one.

From a script, lps reads a single line from standard input when it is not attached to a terminal, and does not ask for confirmation:

printf '%s\n' "$password" | lps add alice --group Administrators --no-prompt

Profiles #

lps set <name> [options] #

Changes part of a profile, leaving the rest alone.

$ lps set jack --shell /bin/bash --display-name "Jack Palfrey"
updated jack
OptionEffect
--home <path>Home directory. Absolute.
--shell <path>Login shell. Absolute.
--display-name <text>Empty clears it.
--primary-group <group>Which group projects to the POSIX gid.

None of this is identity — no security descriptor names a home directory, and the token carries none of it. It is what login needs in order to start a session, and it reaches login on the logon reply itself.

lps does not create the home directory. It records the path. A principal whose home does not exist still signs in, starting in /, and login says so.

The primary group need not be a membership: authd adds it to the token if it is missing, so the order of two commands does not matter.

Groups #

Local groups are objects with a name, a RID and a gid — unlike well-known groups, which exist on every Peios machine and are not stored here.

lps group list #

$ lps group list
NAME          RID       GID  MEMBERS  SID
developers   1001   1001001        2  S-1-5-21-2847362817-1094533892-3310298447-1001

MEMBERS counts principals in this store, which is the only count lpsd can answer for.

lps group create <name> / lps group delete <name> #

$ lps group create developers
created the group developers with RID 1001

Deleting is refused while anyone is still a member, or while the group is anybody's primary group. Either would leave a record pointing at something that no longer exists.

lps group add <name> <group> / lps group remove <name> <group> #

$ lps group add alice Administrators
added alice to Administrators

Removing BUILTIN\Administrators is refused if it would leave no enabled administrator.

Claims #

Claims are named, typed attributes carried on the token and read by conditional ACEs. They are how a security descriptor can say anyone in Engineering rather than naming principals one by one.

lps claim set <name> <claim> <type> [value]... #

$ lps claim set jack Department string Engineering
set the claim Department on jack
$ lps claim set jack Level int64 7
set the claim Level on jack
TypeValues
int64Signed integers
uint64Unsigned integers
booleantrue/yes/1 or false/no/0
stringText
sidSIDs, in S-1-… form
octetHexadecimal, even length

A claim may hold several values, and may hold none — lps claim set jack Department string empties it, which is different from removing it.

Claim names are matched case-insensitively, so setting department replaces Department.

lps claim remove <name> <claim> #

$ lps claim remove jack Department
removed the claim Department from jack

A claim reaches a principal at their next sign-in. A token's claims are fixed when it is minted, so setting one changes nothing for a session already running.

Exit status #

CodeMeaning
0Succeeded
1lpsd refused the request, or could not be reached
2The command line was wrong

The split lets a script distinguish "I called this incorrectly" from "it was refused".

Common failures #

cannot reach lpsd … it does not appear to be running — lpsd is not up. It exits if it cannot reach authd, or if the store will not read; check its logs before restarting it.

permission denied — you are not an administrator, or the socket's descriptor does not admit you. See the administrative socket.

there is no principal named … — names are case-insensitive, so this means the account genuinely is not there. lps list will show what is.

there is no group named … and it is not a SID — the name matched no well-known group and no local one, and does not parse as a SID. lps group list shows the local ones.

The administrative socket

Peios / Peios Security Fundamentals / Managing Local Principals

lpsd listens on:

/run/lpsd/admin.sock

lps is its only client today. If you are writing tooling against it, or diagnosing why a command was refused, this page is what you need.

Who may use it #

Two ways to satisfy the check, and lpsd reads the connecting process's token to decide:

  • membership of BUILTIN\Administrators, enabled; or
  • being LocalSystem.

LocalSystem is admitted deliberately rather than as a convenience. At first boot there is no administrator yet — that is the problem the bootstrap service exists to solve, and it runs as LocalSystem.

The word enabled is load-bearing. A group that is present in a token but marked deny-only contributes to denials and grants nothing, so lpsd requires the enabled attribute. A deliberately restricted token that merely mentions Administrators does not qualify.

What decides, and what does not #

The peer's token decides. lpsd asks the kernel who is on the other end of the connection. Nothing in any message contributes to that answer, and nothing a client sends could.

A KACS descriptor is defence in depth. lpsd stamps one on its runtime directory and socket, admitting LocalSystem and BUILTIN\Administrators.

That descriptor is load-bearing in a way worth knowing about: /run is seeded with a descriptor admitting LocalSystem alone, and everything created under it inherits that. Without lpsd stamping its own, an administrator running lps would be refused by KACS before a byte was exchanged.

The Unix mode decides nothing. KACS grants every managed process the capabilities that override the DAC check, so file modes do not gate anything on Peios. The socket's mode is permissive to say so rather than to imply a control that is not operating.

Tip

If you see permission denied from lps, it is a KACS refusal, not a Unix one. Check the descriptor on /run/lpsd/admin.sock with sd show, and check your token's groups with token.

Shape of the protocol #

One request per connection: lps connects, sends one request, reads one answer, and disconnects. There is no session and no state carried between commands.

That is partly the shape of the tool — a command does one thing and exits — and partly a bound on a real hazard. lpsd serves administrative requests on the same thread as logons, so a client that connected and then said nothing would stall every logon behind it. One request under a deadline bounds that.

The authorization check happens before anything is read, so an unauthorised connection costs a token read and a refusal rather than the full timeout. An open socket cannot be used to stall logons.

Durability #

A change is applied in memory, written to disk, and only then reported as done. If the write fails the change is rolled back and the command reports failure.

So a command that reports success has persisted. A command that reports failure changed nothing — not "possibly changed something", but nothing: lpsd keeps a snapshot and restores it.

The protocol itself #

The wire protocol is PLPS, sharing its codec and header layout with PGSS Logon (PGSS §2) and PSI (PSPU §2). It is not itself specified, because unlike those two it is one daemon's administrative interface rather than a contract anyone else implements.

If you are writing tooling, prefer driving lps and parsing its output over speaking the protocol directly. The command's surface is stable; the protocol's is not promised to be.

See also #

  • The lps command — the client this socket serves.
  • Managing local principals — the authority/source split behind the design.

Resolving names

Peios / Peios Security Fundamentals / Managing Local Principals

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 #

A principal source counts POSIX identifiers relative to a range it is never told the base of. authd adds the base — so jack, stored by lpsd as 1000, signs in as uid 1001000.

That arithmetic exists in exactly one place, and only that place can run it backwards. No source can answer "who is uid 1001000", because no source knows what 1001000 means. It is not that the authority is a convenient place to put this; it is the only party that can do it at all.

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.

Note

A machine with one source has nothing to order, and can ignore this entirely.

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:

AnswerMeans
FoundHere is the principal.
Not foundEvery source that could have answered was asked, and there is no such principal.
UnavailableA 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.

Tip

If names stop resolving and start reporting temporary failures, look for a principal source that is not running. authd logs which one it was waiting for.

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 BUILTIN\Administrators. A source holds the edges, so it can list 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, 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 #

There is no cache yet. Each lookup 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. What it means today is that listing a very large directory is slower than it will be.

Sources already declare what a cache 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.

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.

Peios Learn — documentation for the Peios project.

Built with Trail.