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

Confinement

Single-page view · as markdown

Confinement

Peios / Peios Security Fundamentals / Confinement

Confinement is the sandbox model in Peios. A confined application is one whose token carries a confinement identity — a single SID identifying which package or sandbox the application belongs to — and an enumerated list of capability SIDs naming what the sandbox is allowed to reach. The kernel enforces this confinement whether or not the code knows about it: the application cannot opt out, cannot disable it, cannot exercise a privilege to escape it.

Confinement is the answer to a specific question: how do you run code you do not fully trust on a system whose other resources you do trust? The answer is to give the code a token with a confinement identity and capabilities that name only what it should reach. The kernel then performs an absolute intersection: even if the code somehow obtains broader access through normal means, the confinement layer strips it back to what the capabilities permit.

Who confinement is for #

Confinement is administratively applied. A sysadmin deploys a service or installs an application under a confinement policy — the service definition declares the confinement SID and capabilities; the kernel enforces them. The confined code does not write the policy and cannot read it back to change it. From the application's perspective, confinement is just "the kernel will not let me reach X".

This is the key difference from restricted tokens. Restricted tokens are self-imposed — a program calls FilterToken to narrow its own authority before launching a sensitive operation. Confinement is externally imposed — the policy was decided before the application started and cannot be modified by the application.

The two coexist. A confined service can also internally restrict its own tokens for specific worker threads. The kernel applies both layers independently.

The three fields on the token #

Confinement state lives in three fields on the token:

FieldMeaning
confinement_sidThe package or sandbox identity. If null, the token is not confined and the confinement pass is a no-op. If non-null, the token is confined and the kernel will enforce.
confinement_capabilitiesAn array of capability SIDs declaring what the confined application is allowed to reach. Used during the confinement intersection — entries here are the SIDs that match ACEs on objects the confined application is permitted to touch.
confinement_exemptA boolean escape hatch. When true, the confinement pass is skipped entirely. Set very rarely, only for code that legitimately needs to step outside its own confinement (a confined application's privileged helper, in some narrow patterns).

All three are set at token creation and cannot be changed at runtime. authd is responsible for putting the right values on a confined application's token; once the token is minted, the fields are immutable.

The kernel imposes no special rule on ALL_APPLICATION_PACKAGES (S-1-15-2-1) at token creation: its presence in confinement_capabilities is what selects normal confinement mode, and its absence selects strict mode. The kernel neither synthesises it nor rejects tokens that carry it — authd decides which capabilities a package's token receives. See Capabilities and modes.

The model in one paragraph #

A confined token's effective access is the intersection of:

  1. What the DACL plus privileges would grant the token's full identity.
  2. What the DACL would grant a "fresh" caller whose entire identity is the confinement SID plus the declared capability SIDs.

In other words: the confined application has whatever rights its real identity has, and its confinement identity has, on every object it touches. If either side is missing, the right is dropped. The confinement layer is what makes the second condition real — without it, the token's full identity would be all that the access check considered.

The full mechanics of the intersection — when it fires, what it intersects against, what bypasses and what does not — are in The confinement pass. The capability matching specifically (how the SIDs in confinement_capabilities are compared against ACEs in DACLs) is in Capabilities and modes.

Why privileges do not bypass confinement #

The thing that makes confinement different from every other narrowing layer is its treatment of privileges. Restricted tokens preserve privilege-granted bits through the intersection. Confinement does not. A confined token with SeBackupPrivilege enabled and BACKUP_INTENT set still has its read access narrowed by the confinement intersection — the privilege grant survives steps 4 and 9 of the pipeline, then gets stripped at step 11.

The reason is the audience. A program that restricts itself is trusted to use privileges responsibly — it knows what it is doing because it wrote the restriction. A confined application is not trusted in the same way; the confinement policy came from outside, and a privilege that bypassed it would be an escape hatch for the confined code.

Confinement says: this code may run as whoever, with whatever privileges authd granted, but it cannot reach beyond the capabilities the administrator declared. Privileges are not the lever for breaking out.

What confinement is not #

A few things worth clarifying:

  • Confinement is not a privilege-removal mechanism. A confined token can still carry privileges. The privileges fire normally in their kernel-standalone uses (a confined application with SeShutdown can shut down the system if nothing else gates the call). What confinement narrows is AccessCheck-influencing privileges specifically — the ones that grant bits via AccessCheck. The non-AccessCheck privileges work as usual.
  • Confinement is not a process-level firewall. It is a token-level intersection that runs during AccessCheck. It does not block network calls, ptrace, signals, or any other kernel surface that does not go through AccessCheck. Other layers (PIP, the process SD, kernel-standalone privilege checks) handle those.
  • Confinement is not container isolation. It runs alongside the application's normal access surface, not inside a separate namespace. A confined application sees the same filesystem, the same registry, the same processes as everyone else — it just cannot exercise rights to most of them. Container-style namespace isolation is a separate concern.
  • Confinement is not opt-in for the code. The code does not call a "please confine me" syscall. The token arrives confined; the kernel enforces.

When to use confinement vs other layers #

A quick map of when each narrowing layer is the right tool:

Want thisUse this
A long-running service to run with less authority than its account would imply, set by administrative policyConfinement
A program to internally drop authority for a specific sensitive operation, set by codeRestricted token
Centrally-defined organisational policy applied across many objectsCAAP
Prevent untrusted binaries from interfering with trusted onesPIP
Block writes from lower-integrity to higher-integrityMIC

Confinement is for applications and services that need to be sandboxed by administrative decision. The capability model — "this application can reach the network, but not the filesystem outside its own data" — is what confinement expresses well.

Where to start #

If you want the capability model — how confinement capabilities match ACEs, the well-known and derived capability SIDs, and the difference between normal and strict modes — read Capabilities and modes.

If you want the mechanics of the confinement intersection — when it fires in the access pipeline, what does and does not bypass it, the role of confinement_exempt, and the isolation-boundary reservation — read The confinement pass.

If you want to understand the canonical Peios pattern for managing service access at scale — putting capability SIDs into a token's normal groups rather than (or in addition to) confinement_capabilities — read Positive confinement. The pattern is what most non-trivial deployments use, and the name is more misleading than the concept itself.

Capabilities and modes

Peios / Peios Security Fundamentals / Confinement

A confined application's reach is expressed as a list of capability SIDs on its token. Each capability is a named permission to use a kind of resource: network access, removable storage, the certificate store. The kernel matches the capability list against ACEs in the DACLs of objects the application touches; an ACE granting access to a capability the confined application has declared lets the access through, while objects whose DACLs do not mention any declared capability are unreachable.

This page covers the capability SID format, the well-known capabilities, how custom capabilities are derived from names, and the two confinement modes (normal and strict).

What a capability SID looks like #

All capability SIDs sit under the S-1-15-3-* namespace. There are two flavours:

  • Well-known capabilities have small numeric sub-authorities (S-1-15-3-1, S-1-15-3-2, etc.) assigned to specific named permissions.
  • Derived capabilities have eight sub-authorities computed from the SHA-256 hash of the capability name. The same name always produces the same SID. The hash is split into eight little-endian 32-bit values and appended to S-1-15-3-.

The format itself does not distinguish the two. They are both S-1-15-3-...-shape SIDs; the access check matches them as plain SIDs.

The well-known capabilities #

SIDCapability
S-1-15-3-1internetClient — outbound internet access.
S-1-15-3-2internetClientServer — inbound and outbound internet.
S-1-15-3-3privateNetworkClientServer — LAN / private network access.
S-1-15-3-8enterpriseAuthentication — domain credential access.
S-1-15-3-9sharedUserCertificates — certificate store access.
S-1-15-3-10removableStorage — removable media access.

The SIDs at positions 4 through 7 are reserved and not used in v0.20. The well-known capability set is deliberately small — every additional well-known capability has to be defined globally and means the same thing on every Peios system. Capabilities specific to one application or one environment use the derived form.

The way these SIDs become meaningful is the same as any other SID: they appear in ACEs on objects whose DACLs grant access to them. The system's network stack, for example, has SDs on its endpoints that grant access to internetClient; a confined application carrying internetClient as a capability can reach those endpoints; one without it cannot. The capability SID is the link between the policy on the resource and the declaration on the token.

Derived capabilities #

For capabilities specific to an application or a deployment, the SID is derived from a string:

S-1-15-3-{h0}-{h1}-{h2}-{h3}-{h4}-{h5}-{h6}-{h7}

The eight 32-bit values come from SHA-256(name), split into eight 32-bit little-endian integers in order. The same name always produces the same SID. Two different names (even names that differ in a single character) produce different SIDs with overwhelming probability — SHA-256's collision resistance does the work.

A derived capability is meaningful when:

  1. The application's manifest declares the capability by name, so authd can compute the SID and put it on the token.
  2. The resources the capability protects have DACLs containing ACEs that grant rights to the same SID.

Both ends use the same derivation, so they meet at the same SID. The string name is human-readable convention; the kernel only ever sees the derived SID.

Derived capabilities are how an application-vendor or system integrator extends the model without coordinating with the OS. An application that needs access to a vendor-specific resource declares a vendor-specific capability name; the vendor's installer arranges for the resource's DACL to grant that capability; the two sides agree implicitly through the SHA-256 derivation.

Confinement modes — normal vs strict #

The well-known SID ALL_APPLICATION_PACKAGES (S-1-15-2-1) and the related ALL_RESTRICTED_APPLICATION_PACKAGES (S-1-15-2-2) define the two confinement modes. These are not capabilities themselves — they are matchers used in ACEs to grant access to all confined applications, or to confined applications in strict mode.

Confinement modeALL_APPLICATION_PACKAGES matches the token?ALL_RESTRICTED_APPLICATION_PACKAGES matches?
NormalYesYes
StrictNoYes

The difference: an ACE granting rights to ALL_APPLICATION_PACKAGES lets every confined application reach the object in normal mode, but not strict-mode applications. An ACE granting rights to ALL_RESTRICTED_APPLICATION_PACKAGES lets confined applications in both modes through.

The mode is a property of the token at creation. It is encoded by whether ALL_APPLICATION_PACKAGES appears as one of the token's confinement_capabilities:

  • If ALL_APPLICATION_PACKAGES (S-1-15-2-1) is present in the token's confinement capabilities → normal mode.
  • If it is absent → strict mode.

There is no separate mode flag and no rejection rule — the mode is purely the presence or absence of the SID, and the kernel never synthesises or strips it.

The practical effect of strict mode: a strict-mode application can reach only objects whose DACLs explicitly grant access to its capabilities or to ALL_RESTRICTED_APPLICATION_PACKAGES. The much larger set of objects that grant to ALL_APPLICATION_PACKAGES are off-limits.

This is what "strict" means. Most operating-system resources whose policy grants broad access to confined applications use ALL_APPLICATION_PACKAGES; a strict application is choosing to opt out of those broad grants and rely only on its specific named capabilities.

When to choose strict over normal: when the application's threat model says it should not be able to reach resources whose authors granted broad access without specifically intending to include it. The trade-off is operational — strict applications often need their own specific capability grants on every resource they need, which is more work.

Capability matching during the confinement pass #

When the confinement pass (pipeline step 11) re-walks the DACL against the confinement identity, the matching rule is:

  • The token's confinement_sid matches an ACE whose SID is the same.
  • Each entry in confinement_capabilities matches an ACE whose SID is the same.
  • ALL_APPLICATION_PACKAGES matches an ACE on that SID only if the token's capabilities include it (i.e. only in normal mode).
  • ALL_RESTRICTED_APPLICATION_PACKAGES matches an ACE on that SID always, in both modes.
  • Group attributes on the token's capabilities are ignored. The capability list is presence-based — what matters is whether a SID appears, not what its enabled/disabled state is.

The matching is bare SID equality. The capability identity does not carry a privilege bitmask, group membership semantics, or anything else. It is just a SID that names a kind of resource.

The full mechanics of the pass — what gets intersected, what bypasses — are in The confinement pass.

Capability declaration vs grant #

A small but important distinction: declaring a capability is not the same as having access to the resources it names.

A token's confinement_capabilities is the list of capabilities the confined application is allowed to use. Whether it actually gets access to a specific object still depends on the object's DACL — the capability SID has to appear in an allow ACE.

If a token declares internetClient but no network resource has an ACE granting internetClient rights, the capability declaration achieves nothing. Conversely, if a token does not declare internetClient but some objects have ACEs granting access to that SID, the confined application still cannot reach them — the capability has to be on the token and the ACE has to grant access to it.

The model is conjunctive: both the token-side declaration and the resource-side grant have to agree. Capabilities are a vocabulary; the DACLs are the actual permissions. Without both halves the access does not happen.

This page describes the declaration half — the appearance of capability SIDs in confinement_capabilities, where they are consumed by the confinement pass. Capability SIDs can also appear in a token's groups list, where they are consumed by the ordinary DACL walk and act as positive grants rather than confinement constraints. That convention — positive confinement — is how most non-trivial Peios deployments express service access. The capability vocabulary is the same; the placement on the token decides which mechanism uses it.

Capabilities and other narrowing layers #

Capabilities live in the confinement pass. They do not appear in:

  • The normal DACL walk (step 8). The DACL walk uses the token's user_sid and groups, not its confinement_capabilities. Capabilities are invisible to step 8.
  • The restricted-token pass (step 10). Restricted tokens narrow against restricted_sids, not against capabilities. A restricted-and-confined token gets both intersections.
  • CAAP evaluation (step 12). Central access policies do not match capabilities specifically; they match the token's normal SIDs.

The capability SID is only used in step 11. If you are writing an ACE that grants access to a capability, that ACE will only be relevant when a confined token is the caller. For a non-confined token, the ACE is just an entry with a SID nothing in the token matches.

Practical pattern: granting access to a capability #

A typical SD on a resource intended to be reachable by confined applications might look like:

  • An ACE granting BUILTIN\Administrators GENERIC_ALL. (Administrative full control.)
  • An ACE granting SYSTEM GENERIC_ALL. (System full control.)
  • An ACE granting Authenticated Users GENERIC_READ. (Standard authenticated read.)
  • An ACE granting internetClient FILE_READ_DATA | FILE_WRITE_DATA. (Confined applications with the internetClient capability can use it.)

A non-confined token reaches the resource through its normal identity (Authenticated Users, administrative groups, etc.). A confined token additionally needs the capability ACE to match a SID in its confinement_capabilities for the confinement pass to leave the access intact. The two sides — normal identity for the DACL walk, capability for the confinement pass — both have to grant.

The presence of capability ACEs on system resources is what makes confined applications usable in practice. Without them, every confined application would be locked out of everything that did not specifically know about it.

Where to go next #

For the mechanics of how these capabilities are matched at access-check time — what fires, what gets intersected, and what is preserved — read The confinement pass.

For the other use of the same capability SIDs — placed in groups as positive grants — read Positive confinement.

For the wider catalog of system-defined SIDs these capabilities sit alongside, read Well-known principals.

The confinement pass

Peios / Peios Security Fundamentals / Confinement

The confinement pass is the access-check step that enforces confinement policy. It fires at step 11 of the pipeline, after the DACL walk and the restricted-token pass have produced their results. The kernel runs the DACL one more time against the confinement identity — confinement_sid plus confinement_capabilities — and intersects the result with what the rest of the pipeline has so far granted.

The intersection is absolute. Any bit not present in both the running grant and the confinement-only grant is dropped. This is what makes confinement different from the restricted-token pass: there is no "privileges are restored after the intersection" step. What confinement removes stays removed.

This page covers the mechanics of the pass — when it fires, what the secondary walk does, what bypasses it, and the role of confinement_exempt and isolation_boundary.

When the pass fires #

The confinement pass fires when:

  • The token's confinement_sid is non-null, and
  • The token's confinement_exempt flag is false.

If either is false, the pass is a no-op and the running grant passes through unchanged.

The pass is per access check. There is no caching of "this token always loses these bits" — the kernel runs the secondary walk every time, because the DACL on the object being accessed determines what the confinement identity would have been granted, and that varies per object.

What the secondary walk does #

flowchart LR
    A["Running grant after step 10 (DACL + privileges + restricted intersection)"] --> X["Confinement intersect"]
    B["DACL"] --> Y["Walk against confinement identity"]
    Y --> X
    X --> R["Running grant after step 11"]

The kernel runs a second DACL walk against the same DACL the normal walk ran against. But the matching identity is different:

ElementUsed in secondary walk
User SIDConfinement SID, in place of the token's user_sid.
GroupsConfinement capabilities (confinement_capabilities list), in place of the token's groups. Group attributes (enabled/disabled) are ignored — presence-based matching only.
Restricted SIDsIgnored. The restricted-token pass already ran in step 10.
Owner SIDThe object's owner SID, but owner implicit rights are not applied. A confined caller who happens to own the object does not get `READ_CONTROL
Mandatory labelThe MIC pre-decisions from step 5 are not re-evaluated. They were applied before the DACL walk; they remain applied.

In strict mode (the token does not carry ALL_APPLICATION_PACKAGES), an ACE granting rights to ALL_APPLICATION_PACKAGES does not match. In normal mode it does. ALL_RESTRICTED_APPLICATION_PACKAGES matches in both modes.

The walk produces a granted_confinement mask. The pipeline then computes:

granted = granted & granted_confinement

Any bit not in both sides drops out. No restoration step follows.

What confinement does not preserve #

The intersection drops bits regardless of where they came from. Specifically:

  • Privilege-granted bits are dropped. Bits granted by SeBackupPrivilege, SeRestorePrivilege, SeSecurityPrivilege, or SeTakeOwnershipPrivilege in steps 4 or 9 are subject to the intersection. If the confinement DACL would not have granted them, they are lost. This is the major distinction from the restricted-token pass.
  • Owner implicit rights are dropped if the confinement identity is not the owner. Step 8 granted the owner READ_CONTROL | WRITE_DAC. The confinement secondary walk does not apply owner implicit rights to a non-matching identity — the confinement SID is what is being walked, and it is not the owner. So the owner's implicit rights are lost.

The owner-implicit-rights case is the surprising one. A confined application running as a user who owns an object cannot read or modify the SD of that object unless the DACL specifically grants the confinement identity those rights. The fact that the user is the owner does not help — the confined identity is what counts at step 11.

What confinement does preserve #

A handful of decisions are not re-evaluated in step 11:

  • MIC decisions from step 5. Whatever MIC pre-decided as denied at step 5 stays decided. Confinement does not undo MIC; it adds on top.
  • PIP decisions from step 5. Same.
  • The token's identity for non-AccessCheck purposes. Confinement does not change user_sid or groups for any kernel API other than AccessCheck. A confined process still appears as its original user in getpwuid-style queries, in /proc/<pid>/status, and in any other identity-display surface.

The intersection is purely an AccessCheck mechanism. It does not change who the process is; it changes what AccessCheck lets the process do.

confinement_exempt #

The confinement_exempt flag on a token is the escape hatch. When true, step 11 is skipped entirely: the running grant from step 10 passes through unchanged. The token is still confined in the sense that confinement_sid is set — it just is not enforced.

The flag is set very rarely. The intended use case: a privileged helper that runs alongside a confined application and needs to step outside the confinement for specific operations. Both halves share the user identity; the helper has confinement_exempt set so it can reach resources the confined main application cannot.

confinement_exempt is set at token creation by authd and cannot be changed at runtime. Like every other token field, the flag is immutable once minted.

The flag does not affect the rest of the pipeline. Steps 0 through 10 run normally on a confinement_exempt token. The flag short-circuits only step 11. PIP, MIC, the DACL walk, restricted-token narrowing, and CAAP all still apply.

isolation_boundary #

The token has a fourth confinement-related field: isolation_boundary. It is reserved in v0.20 — the kernel reads it but does not enforce it. The semantic the field is reserved for: an additional layer on top of confinement where objects outside the boundary are made invisible rather than just denied. A confined application with isolation_boundary set would see only objects whose policy granted to its boundary; everything else would appear not to exist (rather than appearing and being denied).

The distinction matters in two cases:

  • Enumeration. A confined application listing the contents of a directory should see only objects in its boundary, not "denied" entries for objects outside.
  • Existence checks. A confined application calling stat on a path outside its boundary should get "no such file" rather than "permission denied".

The full mechanics — what "outside the boundary" means, how it interacts with the FACS handle model, how object enumeration is filtered — are reserved for a future version. In v0.20, isolation_boundary is a no-op; the field is on the token for forward compatibility.

For now, treat it as unused. Tokens that need invisibility-instead-of-denial semantics will need an updated kernel to enforce them.

Composition with other narrowing layers #

A confined token can also be restricted, and can also be accessing a CAAP-bound object. All three narrowing layers (restricted at step 10, confinement at step 11, CAAP at step 12) fire in order. Each is a strict intersection. The final granted mask is the conjunction of:

  1. Whatever the DACL walk + privileges produced (steps 4–9).
  2. Whatever the restricted-token walk would have produced (step 10, if restricted_sids is non-empty).
  3. Whatever the confinement walk would have produced (step 11, this page).
  4. Whatever every applicable CAAP rule's effective DACL would have produced (step 12).

For each step that is active, the running grant is narrowed by the intersection. For each that is not, the grant passes through.

The order matters in one subtle way: privileges are restored after the restricted-token pass but not after the confinement pass. A bit that survived step 10 because privileges restored it can still be dropped at step 11 if confinement does not grant it. The privilege rescue is partial.

See Narrowing layers for the composition rules across all three intersections.

A worked example #

A service is deployed under confinement. Its token has:

  • user_sid = jellyfin_user_SID
  • groups = [jellyfin_user_SID, BUILTIN\Users, Authenticated Users, Everyone]
  • confinement_sid = S-1-15-2-<jellyfin-package-hash> (the package identity — a hash-derived SID, not a well-known one)
  • confinement_capabilities = [S-1-15-3-1 (internetClient), S-1-15-3-10 (removableStorage), S-1-15-2-1 (ALL_APPLICATION_PACKAGES — normal mode)]
  • privileges = [SeChangeNotifyPrivilege, SeCreateSymbolicLinkPrivilege] (default-grant set; nothing else)

The service tries to open /var/state/services/jellyfin/library.db for reading. The file's SD:

  • Owner: jellyfin_user_SID
  • DACL:
    • ACE 1: ACCESS_ALLOWED Authenticated Users GENERIC_READ
    • ACE 2: ACCESS_ALLOWED ALL_APPLICATION_PACKAGES GENERIC_READ

The access check:

  1. Steps 0–4: no impersonation issue, SD valid, generic mapping expands GENERIC_READ to FILE_READ_DATA | FILE_READ_ATTRIBUTES | FILE_READ_EA | READ_CONTROL, no privileges applicable.
  2. Step 5: no MIC label (default Medium / NO_WRITE_UP — does not block read), no PIP label. Nothing pre-decided.
  3. Step 6: virtual group injection. OWNER RIGHTS is added (the token owns the file). But the object's DACL has no OWNER RIGHTS ACE, so the implicit READ_CONTROL | WRITE_DAC will be granted at step 8.
  4. Step 8: owner implicit grants READ_CONTROL | WRITE_DAC. The DACL walk grants FILE_READ_DATA et al. via ACE 1 (Authenticated Users match). The grant is comprehensive.
  5. Step 10: restricted-token pass skipped (no restricted_sids).
  6. Step 11: confinement pass. The secondary walk runs against the confinement identity.
    • The confinement SID matches no ACE (the SD has no ACE on the package SID).
    • The capability internetClient matches no ACE.
    • The capability removableStorage matches no ACE.
    • The capability ALL_APPLICATION_PACKAGES matches ACE 2, granting GENERIC_READ-expanded bits.
    • Owner implicit rights are not applied (the confinement identity is not the user_sid).
    • Result: granted_confinement = {FILE_READ_DATA, FILE_READ_ATTRIBUTES, FILE_READ_EA, READ_CONTROL, SYNCHRONIZE}.
    • The intersection with the running grant: the running grant had owner-implicit READ_CONTROL | WRITE_DAC plus the user-granted read bits. The intersection keeps the read bits and READ_CONTROL but drops WRITE_DAC (which the confinement identity would not have been granted).
  7. Step 12: no CAAP.
  8. Result: the service can read the file but cannot modify its DACL even though it owns the file. The owner implicit grant is gone because confinement does not preserve it.

This is the expected behaviour. The service runs as the user who owns its library file but does not get owner-style authority on the file because the confinement layer specifically removed it.

Where to go next #

For the convention that turns the same capability SIDs into positive grants — the canonical pattern for service access at scale — read Positive confinement.

For how the confinement intersection composes with the restricted-token and CAAP intersections, read Narrowing layers.

Positive confinement

Peios / Peios Security Fundamentals / Confinement

Positive confinement is a convention, not a kernel feature. It is the practice of taking capability SIDs — the same S-1-15-3-* SIDs that name a confinement capability — and placing them in a token's normal groups list rather than its confinement_capabilities list. The kernel does not distinguish capability SIDs from any other SIDs in the groups list; they participate in the ordinary DACL walk like every other group. The result is that the capability acts as a positive grant — "this token has this capability, and any ACE that grants rights to this capability grants those rights to this token" — rather than as a confinement constraint.

The name is awkward. "Positive confinement" is not about confinement at all in the access-narrowing sense. It is named after the kind of SID involved (capabilities, which come from the confinement model), with "positive" marking the opposite effect — grant rather than restrict. Once you have the convention in mind it makes sense; on first encounter the name does not help. Reading it as "positive use of capability SIDs" is the cleanest paraphrase.

This page covers what the convention is, why it exists, and how it composes with standard confinement.

A capability SID is just a SID #

The capability SIDs documented in Capabilities and modes — S-1-15-3-1 (internetClient), S-1-15-3-10 (removableStorage), and the derived capabilities produced from SHA-256 of capability names — are normal Peios SIDs. They follow the SID format; they compare with byte equality; they appear in ACEs like any other SID; they can be present on a token in any of the SID-bearing fields.

The kernel does not have a "capability SID" type tag. The format does not distinguish them from other SIDs. What makes a capability SID a capability is the namespace convention (S-1-15-3-*) and the way administrators choose to use it, not any special handling.

So when you see a capability SID in a token, the question is not "is this a capability". The question is which field on the token is it sitting in. The answer is what determines how the access check uses it.

Placement determines effect #

A token has several SID-bearing fields. Two of them are commonly the home of capability SIDs:

FieldWhat happens to a capability SID placed here
confinement_capabilitiesThe capability is consumed by the confinement pass at pipeline step 11. The capability matches ACEs only inside the confinement intersection — its role is to narrow what the confined token can reach.
groupsThe capability is consumed by the normal DACL walk at pipeline step 8. The capability matches ACEs in the ordinary first-writer-wins evaluation — its role is to grant the token whatever rights the ACE specifies.

Standard confinement uses the first column. Positive confinement uses the second. The same SID, the same DACL, but a very different result.

A token can carry the same capability SID in both fields, or in either, or in neither. The kernel does not check for consistency between the two — the fields are independent.

There are also other SID-bearing fields on the token (the restricted_sids list, for example). Capability SIDs are not commonly placed there today, but nothing in the format prevents it; if a future convention emerges, the kernel will treat the SID like any other in that field. The model is open in this respect: a capability SID is reachable by every mechanism that walks any SID list on the token.

The two effects, compared #

A worked-through contrast on the same DACL.

The DACL on a resource contains one ACE:

ACCESS_ALLOWED  S-1-15-3-1 (internetClient)  GENERIC_READ

Case 1: token has internetClient in confinement_capabilities.

The token's normal identity (user_sid, groups) does not match the ACE, so step 8's DACL walk grants nothing. The token reaches step 11 with an empty grant. The confinement intersection re-walks the DACL against the confinement identity — and the capability list — and finds the ACE matches. The intersection produces a grant of GENERIC_READ-mapped bits. But the running grant from step 8 was empty, so the intersection yields empty too.

Result: no access. The capability said "the confined application is permitted to use this resource if its own identity reaches it", and the token's own identity didn't.

Case 2: token has internetClient in groups.

The token's groups include the capability SID. The normal DACL walk at step 8 matches the ACE against the group SID and grants GENERIC_READ-mapped bits. The confinement pass — if even active on this token — re-walks the DACL against confinement_sid and confinement_capabilities; if the token is not confined, step 11 is a no-op.

Result: access granted. The capability acted as a normal group membership — "the token is a member of the internetClient-bearing group, the DACL grants access to that group, the token gets access".

Case 3: token has internetClient in both.

Step 8 grants because the group SID matches. Step 11 (if confined) re-walks against the confinement identity and finds the same match — the capability is in confinement_capabilities, the ACE grants to the capability, the intersection passes. The grant survives.

Result: access granted, with the confinement intersection satisfied. This is the pattern when a token is genuinely confined and needs to reach a specific capability-gated resource through the confinement layer.

The kernel did the same thing in all three cases — it walked the SIDs it had in the fields it had them in, against the same DACL. The different outcomes are entirely a function of placement.

Why this convention exists #

In a deployment with a handful of services and a few resources, the obvious model is to give each service a dedicated user account and write ACEs naming that user. jellyfin reads /var/state/services/jellyfin/, owned by jellyfin, with a DACL granting jellyfin full access. Add a new service, create a new user, write new ACEs.

At any kind of scale this breaks down. A dozen services each touching a few dozen shared resources is hundreds of ACE entries to maintain, and every new service requires editing the DACL of every resource it touches. Service accounts become a sprawl of nobody-style entries that exist only to appear in ACL lists.

Positive confinement is the alternative pattern. Instead of:

  • A jellyfin user, mentioned in every media-file DACL.
  • A transmission user, mentioned in every download-directory DACL.
  • A nextcloud user, mentioned in every shared-storage DACL.

You define capabilities — semantic units of access — and grant ACEs to those:

  • A media-library-read capability. Every media file's DACL grants GENERIC_READ to this capability.
  • A download-write capability. Every download directory's DACL grants GENERIC_WRITE to this capability.
  • A shared-storage capability. Every shared-storage object's DACL grants to this capability.

Then each service is given the capabilities it needs as entries in its token's groups. Adding a new media-handling service does not require modifying every media file's DACL — the DACL already grants media-library-read; the new service just needs the capability on its token.

The administrative model becomes: services consume capabilities; resources grant capabilities; the directory maintains which services have which capabilities. ACLs on shared resources are stable. Adding a service is a token-policy change, not a DACL-rewriting exercise.

This is what positive confinement is for. Most non-trivial Peios deployments use it. Dedicated service accounts continue to exist for cases where the per-service identity is genuinely meaningful (a service whose data should be exclusively its own), but for shared resources reached by multiple services, capability-style positive grants are the canonical pattern.

Positive confinement does not bypass confinement #

This is the most important rule about composition and it is worth saying directly: positive confinement does not bypass confinement. If a token is confined, putting a capability SID in the token's groups does not exempt the resulting access from the confinement pass at step 11. The DACL walk at step 8 may grant rights through the capability appearing in groups, but the confinement intersection still runs, and if the confinement identity (confinement_sid plus confinement_capabilities) does not match the same ACE, the grant is dropped.

The kernel does not look at the SID and reason about its "intent". It runs each pipeline step with the inputs that step uses. Step 8 uses the token's groups. Step 11 uses the token's confinement_capabilities. A capability SID present in one but not the other satisfies one step but not the other, and an intersection that fails at any active step removes the rights.

Practically: on a confined token, positive confinement and standard confinement are complementary, not alternatives. To reach an object via a capability, both halves need to be in place:

  • The capability SID in groups, so the normal DACL walk at step 8 grants access.
  • The capability SID also in confinement_capabilities, so the confinement intersection at step 11 preserves the grant.

Either half on its own grants the confined token nothing. Putting the capability only in groups produces a step-8 grant that step 11 strips. Putting it only in confinement_capabilities keeps step 11 happy but step 8 grants nothing for the intersection to preserve.

The pattern, then, on a confined service's token: the same capability SID appears in both places. The DACL grants to the capability; the token bears it twice; both checks pass.

For tokens that are not confined — which is many of them, especially in standalone or smaller deployments — only the groups half matters. The confinement pass is a no-op for an unconfined token; the normal DACL walk is the only check that runs against the capability. This is the "positive confinement without confinement" case: the capability SIDs are used as ordinary groups for access management, and the confinement layer never enters the picture.

What this is not #

A few clarifications, because the name encourages misreading:

  • It is not a bypass of confinement. Placing a capability SID in a confined token's groups does not exempt the resulting access from step 11. The confinement pass still fires; the grant from the DACL walk is still subject to the intersection. See "Positive confinement does not bypass confinement" above.
  • It is not a separate access-check pass. There is no "positive confinement evaluator" in the kernel. The DACL walk at step 8 finds the capability SID like it finds any other group SID. No new mechanism, no new code path, no special handling.
  • It is not the same as confinement. A token with capability SIDs only in groups (no confinement_sid) is not confined. The confinement pass is a no-op. The token has the broad access its identity grants; the capabilities sit alongside as additional group memberships.
  • It is not opt-in for the resource. A resource whose DACL grants access to a capability does not need to know whether the caller is using the capability positively or as a confinement entry. The DACL says "this SID gets these rights"; whoever has the SID in a relevant field gets the access.
  • It is not a kernel feature you can disable. Because there is no feature flag — only a convention about where you put capability SIDs on tokens — there is nothing to turn off. Sites that prefer dedicated service accounts simply do not use capability SIDs in groups.

A worked deployment #

A Peios machine runs three services that all read the user's music library: jellyfin, mpd, and a future streaming-bridge. The administrative pattern is positive confinement.

The DACL on /data/media/music/ (and every file under it):

  • ACCESS_ALLOWED SYSTEM GENERIC_ALL
  • ACCESS_ALLOWED BUILTIN\Administrators GENERIC_ALL
  • ACCESS_ALLOWED music-library-read GENERIC_READ (the capability SID, derived from "music-library-read" via SHA-256)

The directory policy in authd configures each service's token to include the music-library-read capability SID in its groups:

  • jellyfin's token: groups = [jellyfin_user_SID, music-library-read, network-server, ...]
  • mpd's token: groups = [mpd_user_SID, music-library-read, audio-output, ...]
  • streaming-bridge's token: groups = [streaming-bridge_user_SID, music-library-read, network-server, ...]

Each service is a different user identity. Each service has the capability through its groups. Each service can read the music library.

The administrator adds a fourth music-handling service. The new service's token policy includes music-library-read in groups. The DACL on /data/media/music/ is not touched. The service starts reading the library immediately.

If the deployment additionally confines the services for sandboxing, the same capability SID also appears in each service's confinement_capabilities. The DACL ACE for music-library-read is unchanged. The token now has the SID in two places, and both passes find it. Access still works.

This is the canonical scale pattern. Capabilities are the vocabulary of access; tokens consume capabilities; resources grant capabilities; administrative policy decides who gets what. No DACL rewrites when new services arrive; no proliferation of per-service users in every shared-resource ACL.

Where to go next #

For the intersection that still applies when a confined token carries capabilities in groups, read The confinement pass.

For the ordinary DACL walk that consumes capability SIDs placed in groups, read DACL evaluation.

Peios Learn — documentation for the Peios project.

Built with Trail.