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

Security

Single-page view · as markdown

Access control on keys

Peios / Using Peios / Security

Every registry key is a secured object. It carries a security descriptor — owner, DACL, and optionally a SACL — exactly like a file, a process, or a token. And the registry does not invent its own access logic: opening a key runs the same AccessCheck pipeline that governs every other protected object in Peios, against the same tokens and the same SD format. If you understand access control for files, you already understand most of it for the registry.

Access control on keys, in one sentence #

Every key carries a security descriptor, and access to it is decided by the same AccessCheck that governs everything else — evaluated once when you open the key, then cached on the handle for the life of that handle.

Security is per key, not per value #

A security descriptor lives on the key. Values do not have their own — they inherit their key's access control. "Who can read this value, who can change it" is answered by the SD on the key that contains it, and there is no finer-grained permission than that. If two values need different access rules, they belong in different keys.

The handle model #

Access is checked at open, not on every operation. When you open a key for some set of rights, AccessCheck runs once; if it grants them, you get a key handle (a file descriptor) with a granted access mask baked in. Every later operation through that handle is a cheap bitmask check against the cached mask — is this operation's required right in what I was granted? — not a fresh AccessCheck.

The consequence is the same check-at-open rule that files and central access policies follow: changing a key's SD affects future opens, not handles that are already open. An administrator who tightens a key's DACL does not retract access from a service that already has the key open — the recourse is to make the service reopen (typically by restarting it). The handle is a snapshot of the decision made at open time.

The registry-specific rights #

A key's DACL is written in terms of rights specific to keys:

RightGates
KEY_QUERY_VALUEReading values.
KEY_SET_VALUEWriting and deleting values.
KEY_CREATE_SUB_KEYCreating child keys.
KEY_ENUMERATE_SUB_KEYSListing child keys.
KEY_NOTIFYArming a watch.
KEY_CREATE_LINKCreating a link key (privileged).
DELETEDeleting the key, or hiding it in a layer.
READ_CONTROLReading the SD and key metadata.
WRITE_DAC / WRITE_OWNERChanging the DACL / the owner.
ACCESS_SYSTEM_SECURITYReading or changing the SACL (audit policy).

The usual convenience bundles apply — KEY_READ (query, enumerate, notify, read the SD), KEY_WRITE (set values, create subkeys), and KEY_ALL_ACCESS.

Ordinary access — reading a value, writing one, creating or listing subkeys — is decided entirely by the key's SD through AccessCheck. No privilege grants ordinary registry access. A few privileges appear only where a write means more than storage: establishing a layer that outranks others (policy — see Layers), creating a link, or bulk backup and restore. Those are special operations, covered where they arise; everyday access is the SD's job alone.

No traversal check #

One surprise sets the registry apart from a filesystem: only the SD on the key you open is checked. The keys above it on the path are not. A process can open Machine\System\Services\Jellyfin with no access whatsoever to Machine\System\Services or Machine\System. There is no "execute/traverse" right that you must hold on every ancestor the way a filesystem demands. Access is decided at the destination, full stop.

This matters when you reason about exposure: locking down a parent key does not lock down what is beneath it. If a subtree must be protected, the protection has to be on the keys that hold the data, not merely on a key somewhere above them.

Where a key's SD comes from #

A key's SD is computed once, at creation, by inheriting from its parent — the same eager, static inheritance files use. It is then a complete value stored on the key; the parent is not consulted again at access time. Changing a parent's SD does not ripple down to children that already exist — re-applying inheritance to an existing subtree is a deliberate administrative walk, not something that happens on its own.

The chain has to start somewhere, and it starts at the hive roots, whose SDs are the seeds for everything below:

Hive rootDefault access
Machine\SYSTEM and Administrators: full control. Authenticated Users: read. (All inheritable.)
Users\<SID>\That user, SYSTEM, and Administrators: full control.

Subsystems that need something tighter than "Authenticated Users can read" set an explicit SD on their own subtree root at creation, overriding the inherited default for everything beneath it.

Watching requires permission to read #

Arming a watch needs KEY_NOTIFY, so a process cannot monitor a key it could not otherwise observe. One deliberate asymmetry: a subtree watcher learns that a descendant key was created or deleted — structural facts — without holding any access to that descendant. It learns that something appeared; it must still open it (and pass AccessCheck) to read what is inside. Structure visibility is intentionally weaker than content visibility.

The sharp edge: security is not layered #

This is where the registry's two big ideas meet, and the result surprises people. Layers revert cleanly — delete a layer and its values fall away. A security change does not.

A key's SD is a direct property of the key object. It is not tagged with a layer and it is not a write in the per-value contest. So when you change a key's DACL — tightening access, say — you are mutating the key itself, permanently. If a role layer existed at the time and is later uninstalled, the values it set revert, but the security change you made stays exactly where it is.

The reasoning is deliberate: security is operational state, not configuration overlay. Imagine the alternative — an administrator locks down a sensitive key while some unrelated role happens to be installed, and then removing that role silently reopens the key. Configuration is the kind of thing you want to revert in bundles; an access decision is not. So the registry keeps them on different tracks: values and key existence are layered and revertible; ownership and permissions are mutations on the object that outlive any layer.

The short version: layers revert what the system is configured to do; they never revert who is allowed to do it.

Where to go next #

If you want to see how a process reacts to a change instead of polling for it — and how watch events report effective-state changes with the layering invisible — read Watching for changes.

If you want the kernel/store split underneath all of this — who actually holds the SDs, and the trust boundary that follows — read LCS and sources.

For the broader access model these rights plug into — the AccessCheck pipeline itself — read Access decisions.

Default security descriptors

Peios / Using Peios / Security

Some software stamps security descriptors onto objects it creates at runtime — a freshly mounted filesystem root, a state file, a spool directory. Each of those descriptors is a policy decision: who can enumerate this directory, who can read this file, what everything created beneath this root will inherit. Decisions like that are configuration, and in Peios configuration has one home. The SdDefaults convention is the standard place a component keeps these descriptors, so that an operator can inspect them, change them, and trust that every component publishes them the same way.

Default security descriptors, in one sentence #

A component keeps each security descriptor it stamps at runtime as a named SDDL value under Machine\Software\<Software>\SdDefaults\, with a compiled-in copy as the fallback — a value that is present and valid overrides the compiled default; a value that is invalid is ignored, loudly.

The convention #

Machine\Software\<Software>\SdDefaults\<SD Name>
  • Machine\Software\<Software> is the component's own configuration key — the same key the rest of its settings live under.
  • SdDefaults is the literal subkey name.
  • <SD Name> names the object the descriptor protects: SpoolDirectory, StateFile, Run. One value per descriptor, stored as a string containing SDDL.

A fictional spooler that creates its spool directory at startup would publish:

Machine\Software\ExampleSpooler\SdDefaults\SpoolDirectory
    REG_SZ  "O:SYG:SYD:(A;OICI;GA;;;SY)(A;OICI;GA;;;BA)"

The defaults are per component, deliberately. A single shared tree of default descriptors would need every SD name to be unique across every application ever written — a namespacing promise nobody can keep. Scoping the names under the component that reads them dissolves the problem, and it keeps discovery unsurprising: a component's descriptors live where the rest of its configuration lives.

Compiled default, registry override #

The registry value is an override, never the only copy. Every component that follows the convention carries a compiled-in default for each named descriptor, and resolves the one to use at the point where it stamps it:

State of SdDefaults\<SD Name>Descriptor in force
AbsentThe compiled-in default. This is the normal state — the key need not exist at all.
Present, valid SDDLThe registry value.
Present, invalidThe compiled-in default. The stored value is ignored, and an event records the key, the rejected value, and the descriptor actually in force.

This is the registry's general reject-or-keep rule applied to descriptors, and here it is doing its most important work. A security descriptor that fails to parse is never repaired, never approximated, and never replaced with something broader: the descriptor in force is always one that was compiled in and reviewed. A typo in an SdDefaults value costs you your customisation, not your system.

When a change takes effect #

A default descriptor is read when the component stamps it — typically when the object is created. Two consequences follow:

  • Changing a value affects objects stamped after the change. It does not rewrite objects that already exist.
  • Where the stamped object is a directory whose descriptor carries inheritable ACEs, everything created beneath it derives its descriptor at creation time (inheritance is computed once, when the child is born). Changing the default later does not re-derive existing children.

So the honest answer to "when does my change apply?" varies by descriptor — next boot, next mount, next time the object is recreated — and the component's regman documentation is where that answer lives. Every <SD Name> a component publishes has a regman entry, and its Applies line states exactly this.

The values are access policy — protect them #

Whoever can write a component's SdDefaults values decides what access the component will grant on the objects it creates. These values are not settings about security; they are the security. A component's SdDefaults key therefore carries a tight security descriptor of its own — writable by Administrators and SYSTEM, no wider — which the registry enforces like any other key. The arrangement is self-hosting: the store that holds the descriptors is protected by the same mechanism the descriptors configure.

What the convention does not cover #

Bootstrap seeding. The very first descriptors of a boot — the seed stamped onto a fresh root before any registry source is attached — are compiled into the tools that write them, and are not customisable through the registry. There is no registry to read at that point in boot; that is not a gap in the convention but the reason it has a floor.

Kernel fallbacks. The descriptors the kernel synthesises when a mount policy has no template, and the default DACL applied when a created object has no parent to inherit from, are fixed. They are the floor under a missing policy, not configuration.

Files installed by packages. A package payload entry gets its descriptor by inheritance from its destination directory, or from a declaration in the package manifest — see PSPU §5.20. SdDefaults governs what software stamps at runtime, not what the package manager installs.

For component authors #

If your component stamps descriptors at runtime, follow the convention:

  1. Ship a compiled default for every named descriptor. The registry value is an override; your component must work, with reviewed policy, on a system where the SdDefaults key has never been created.
  2. Name each value for the object it protects, not for its content — SpoolDirectory, not SystemOnlyOici.
  3. Resolve with reject-or-keep. An unparseable value is ignored in favour of the compiled default, and the rejection is recorded in an event naming the key, the rejected value, and the descriptor in force. Never substitute anything broader than the compiled default.
  4. Document every name in regman, including an Applies line that says when a change is picked up.

Where to go next #

For the doctrine behind reject-or-keep — why the registry stores values it does not validate, and why readers keep their last known-good — read Configuration, not storage.

For what an SDDL string actually says — owner, DACL, ACEs, and the inheritance flags that make one descriptor govern a whole tree — start at Security descriptors and Inheritance.

For protecting the SdDefaults key itself, read Access control on keys.

Peios Learn — documentation for the Peios project.

Built with Trail.