Proposal: User Identity, Sessions, and Policy
How capOS should represent human users, service identities, guests, anonymous callers, and policy systems without reintroducing Unix-style ambient authority.
Status: partially implemented. The current tree has entropy-backed
UserSession metadata for anonymous, operator, and guest profiles; a bootstrap
CredentialStore; shell-driven login, setup, and guest profile changes;
AuthorityBroker.shellBundle returning broker-issued launcher, copied session,
SystemInfo, and operator-scoped service endpoint caps; and manifest seed
records for local operator/guest proofs. Guest shell bundles are manifest-gated
and receive no default service endpoints. Endpoint calls now keep subject
details private by default and disclose only requested-and-allowed fields from
cap-held service/broker disclosure scope. The broader proposal remains target
design for durable account storage, external identity bindings, session
logout/revocation/renewal lifecycle, quota-backed profiles, ABAC/MAC policy
engines, and POSIX compatibility metadata.
Problem
capOS has processes, address spaces, capability tables, object identities,
badges, quotas, and transfer rules. It deliberately does not have global
paths, ambient file descriptors, a privileged root bit, or Unix uid/gid
authorization in the kernel.
Interactive operation still needs a way to answer practical questions:
- Who is using this shell session?
- Which caps should a normal daily session receive?
- How does a service distinguish Alice, Bob, a service account, a guest, and an anonymous network caller?
- How do RBAC, ABAC, and mandatory policy fit a capability system?
- How does POSIX compatibility expose users without letting
uidbecome authority?
The answer should keep the enforcement model simple: capabilities are the authority. Identity and policy decide which capabilities get minted, granted, attenuated, leased, revoked, and audited.
Design Principles
useris not a kernel primitive.uid,gid, role, and label values do not authorize kernel operations.- A process is authorized only by capabilities in its table.
- Authentication proves or selects a principal; it does not itself grant authority.
- An account is a durable local record for a principal; it is not a running subject.
- A session is a live policy context with selected policy and resource profiles that receives a cap bundle.
- A workload is a process or supervision subtree launched with explicit caps.
- POSIX user concepts are compatibility metadata over scoped caps.
- Guest and anonymous access are explicit policy profiles, not missing policy.
- External roles, groups, claims, and local roles are broker inputs, not authority after the corresponding caps are absent.
Concepts
Principal
A principal is a durable or deliberately ephemeral identity known to auth and policy services. It is useful for policy decisions, ownership metadata, audit records, and user-facing display. It is not a kernel subject.
Examples:
- human account
- operator account
- service account
- cloud instance or deployment identity
- guest profile
- anonymous caller
- pseudonymous key-bound identity
The schema excerpt below is proposal-level shape. Where the interfaces already
exist in schema/capos.capnp, the ordinals shown here must match the checked-in
schema; future methods must be assigned from the next free ordinal when the
schema is actually extended.
enum PrincipalKind {
human @0;
operator @1;
service @2;
guest @3;
anonymous @4;
pseudonymous @5;
}
struct PrincipalInfo {
id @0 :Data; # Stable opaque ID, or random ephemeral ID.
kind @1 :PrincipalKind;
displayName @2 :Text;
}
PrincipalInfo is intentionally descriptive. Possessing a serialized
PrincipalInfo value must not grant authority.
Federated authentication uses a canonical external subject key:
hash(providerKind, issuer, tenant, subject). For OIDC, issuer is iss,
subject is sub, and tenant is the normalized tenant or configured empty
tenant. sub alone is not unique across IdPs and must not be used directly.
Admission policy either maps that external key to an existing local principal
through an ExternalIdentityBinding or admits it as a pseudonymous principal
under an explicit policy/resource profile pair. PrincipalKind covers the resolved
local principal through human / operator / service / pseudonymous
depending on deployment intent; a federated service account is service, a
federated human is human, and a federated ephemeral identity with no stable
person behind it is pseudonymous. The OIDC integration details live in
OIDC and OAuth2.
User
user is a user-facing category for a principal/session that represents a
human or human-adjacent actor. It is not a kernel object, not a UID, and not
an authority source. Use principal, account, session, or workload
when one of those narrower concepts is meant.
Account
An account is a durable local record for a principal. It binds credential references, status, roles, attributes, storage roots, quotas, and default policy/resource profile names. Some principals deliberately have no account: anonymous callers, some guests, and some one-shot external sessions.
Accounts do not run and do not hold capabilities. Session creation reads an account record, manifest seed record, or external admission binding, then asks a trusted broker to mint the actual CapSet for a live session or workload.
Profile
A profile is a named policy template. It contains no authority by itself.
- A policy profile selects roles, ABAC defaults, allowed bundle fragments, approval paths, label defaults, and external admission constraints.
- A resource profile selects storage, memory, CPU share, process/thread/cap limits, IPC limits, log volume, network posture, and launcher posture.
Use plain profile only when prose intentionally covers both policy and
resource profiles.
Session
A session is a live context derived from a principal plus authentication and policy state. Sessions carry freshness, expiry, auth strength, audit identity, and selected policy and resource profiles. The selected profiles influence which caps a broker may mint and which quotas wrappers apply; the profiles are not usable authority.
AuthStrength aligns with ITU-T X.1254 Entity authentication assurance
framework (= ISO/IEC 29115) level-of-assurance tiers. X.1254 defines
LoA 1 (little or no confidence) through LoA 4 (very high confidence) as a
composite of identity-proofing strength, credential strength, and
authentication-protocol strength. capOS uses the same tiers so that
policy decisions can be expressed as “require LoA ≥ 3 for
ServiceSupervisor(net-stack)” without inventing parallel terminology.
# ITU-T X.1254 / ISO/IEC 29115 level-of-assurance tiers.
# `loa0` covers "no assertion" (`anonymous` sessions) and sits below
# the X.1254 lattice; the standard numbers LoA 1-4 only.
enum AuthStrength {
loa0 @0; # no authentication; anonymous
loa1 @1; # little/no confidence; self-asserted identity
loa2 @2; # some confidence; single-factor, e.g. password
loa3 @3; # high confidence; multi-factor, hardware-backed key
loa4 @4; # very high confidence; multi-factor with tamper-resistant
# hardware and in-person or equivalent identity proofing
}
struct SessionInfo {
sessionId @0 :Data;
principal @1 :PrincipalInfo;
authStrength @2 :AuthStrength;
createdAtMs @3 :UInt64;
expiresAtMs @4 :UInt64;
policyProfile @5 :ProfileSummary;
resourceProfile @6 :ProfileSummary;
# Multi-party / delegated / federated session context. Populated when
# the session was minted through an AuthorityBroker approval flow or a
# federated IdP rather than direct interactive login.
delegationChain @7 :List(Data); # opaque session/IdP IDs
}
struct ProfileSummary {
id @0 :Data;
displayName @1 :Text;
versionId @2 :Data;
epoch @3 :UInt64;
}
struct CapabilityResultHandle {
brokerId @0 :Data;
grantId @1 :Data;
interfaceId @2 :UInt64;
issuedAtMs @3 :UInt64;
expiresAtMs @4 :UInt64;
}
interface UserSession {
info @0 () -> (info :SessionInfo);
auditContext @1 () -> (sessionId :Data, principalId :Data);
logout @2 () -> ();
# Future result/grant metadata methods must use fresh ordinals; they are
# intentionally not assigned in this proposal sketch.
}
interface SessionManager {
login @0 (
method :Text,
selector :LoginSelector,
proof :Data,
source :LoginSourceMetadata
) -> (sessionIndex :UInt16);
guest @1 () -> (sessionIndex :UInt16);
anonymous @2 () -> (sessionIndex :UInt16);
sshPublicKey @3 (
username :Text,
algorithm :Text,
publicKey :Data,
authBytes :Data,
signature :Data,
sourceAddr :Data
) -> (sessionIndex :UInt16);
# Future renewal must use the next free ordinal in the checked-in schema,
# currently @4, not @3.
}
When brokers return granted caps, GrantedCap should be the same
transport-level result-cap concept used by ProcessSpawner, not a parallel
authority encoding.
UserSession is the live session/profile summary surface, not the account
database and not the process invocation subject itself. In the session-bound
invocation model, the immutable kernel-installed SessionContext on the
process is the invocation context; kernel/src/session_context.rs owns that
state and the spawn-time inheritance/broker-selection rules described in
Service Architecture. A
UserSession cap may expose stable session metadata, profile summaries, audit
context, expiry, and opaque handles for cap-broker results that have already
been minted. It can also be used as trusted broker/session-manager input to
spawn a child with a matching SessionContext, but copying a UserSession
into an existing process cannot install a second session or relabel future
calls. These handles are
non-bearer metadata for audit and UI display: they cannot be redeemed into
caps unless the caller also holds the separate broker, approval, or launcher
authority required for the grant. UserSession must not expose mutable account
records, credential records, role bindings, storage-root records, policy
document bodies, or redeemable grant tokens. Fresh cap bundles come from
AuthorityBroker or a launcher/supervisor that consumes the session context;
the session cap itself is not a general account-store reader and is not the
ordinary authority-vending path.
Session Lifecycle And Renewal
The expiresAtMs field is not sufficient by itself. The target model treats a
session as a revocable lease with explicit state:
live | logged_out | revoked | expired | recovery_only
The immutable process SessionContext identifies the subject selected at
spawn (see Service Architecture
for the kernel-owned spawn-time installation and the
make run-session-context proof). It should point at, or be paired with,
trusted session-manager liveness state that can change without relabeling the
process:
SessionLivenessCell {
sessionId
sessionEpoch
state
notBeforeMs
notAfterMs
policyEpoch
resourceProfileEpoch
auditRecordId
}
The liveness cell answers whether ordinary invocation may continue. Grant leases answer whether a particular broker-issued bundle or elevated cap remains valid. Object/facet epochs answer whether the target live object generation has been revoked or replaced. These checks compose; none of them is a substitute for capability possession.
For local password-authenticated shells, fixed short wall-clock expiry should not be the only interactive policy. A sane default is that the session remains live until explicit logout, terminal/connection close, owner shell or supervisor subtree exit, administrator revocation, account disablement, policy version invalidation, or a configured idle/hard maximum. Guest, anonymous, remote, federated, and elevated sessions may use much shorter leases.
Renewal must be a narrow session-manager or broker path. The exact Cap’n Proto
signature is future schema work; with the current checked-in SessionManager
ordinal map, the first renewal method would be assigned @4 unless another
schema change lands first:
interface SessionManager {
renew @nextFree (
session :UserSession,
proof :Data,
requestedDurationMs :UInt64
) -> (session :UserSession);
}
renew may extend the same liveness cell or mint a successor session in the
same audit family, depending on policy. It must check account status, auth
freshness, session state, policy/resource profile epochs, requested duration,
absolute maximum lifetime, and explicit revocation state. It must not make all
old grants fresh. When policy needs a new decision, the broker returns fresh
grant leases and wrapper caps; stale ordinary grants remain stale or are
explicitly revoked.
Only named recovery methods should work after expiry: logout, renew, recovery, and narrowly scoped self-diagnostic status. Explicit revocation should block ordinary renewal unless a separately audited recovery policy says otherwise. Owner-shell exit and gateway disconnect should call logout for sessions they own, then process-exit cleanup releases local hold edges.
Workload
A workload is a process or supervision subtree started from a session, service, or supervisor. Workloads may carry session metadata for audit and policy, but they do not run “as” a user in the Unix sense. They run with a CapSet.
Common workload shapes:
- interactive native shell
- agent shell
- POSIX shell compatibility session
- user-facing application
- per-user service instance
- shared service handling many user sessions
- service account process
Capability
A capability remains the actual authority. A process can only use what is in its local capability table. Policy services can choose to mint, attenuate, lease, transfer, or revoke capabilities, but they do not create a second authorization channel.
Account and Admission Sources
capOS should have three account and admission sources. All three feed policy; none of them bypass the capability graph.
- Manifest seed accounts. Immutable or append-only bootstrap records in the boot package. These create first local operators, recovery identities, service identities, emergency guest policy, and initial policy bundles. Seed data must be sufficient to boot, recover, unlock storage, and create or repair the local account store. It must not become the ordinary mutable account database.
- Local account store. Mutable Store/Namespace-backed records for accounts, credentials, roles, attributes, quotas, policy profiles, resource profiles, and storage roots. After initialization, disk state is authoritative for ordinary local accounts, with explicit versioning, rollback detection, and recovery import/export.
- External identity admission and bindings. OIDC, passkey, cloud, deployment, or certificate-backed principals mapped to named policy/resource profiles or existing local accounts. External claims are normalized ABAC inputs and may select a binding; they do not grant local authority by themselves.
Account Store Boundary
Mutable account state belongs in a separate account-store schema and service
slice, not in the session schema. The identity/session schema should contain
PrincipalInfo, SessionInfo, profile summaries, audit context, and opaque
broker result handles. The account-store slice owns durable account records,
credential references, local role bindings, external identity bindings,
profile bodies and versions, storage-root references, recovery/import records,
and mutation/audit metadata.
The account-store service should expose typed reads for trusted policy
services and compare-and-set mutation methods for administrative tooling.
SessionManager reads account-store records only while creating or refreshing
a session, then returns a UserSession summary. AuthorityBroker uses that
summary plus account-store/profile lookups to mint caps. Ordinary workloads
must not learn more than the scoped session/profile metadata and caps they
were explicitly granted.
Initial records should stay cap-shaped:
struct AccountRecord {
recordId @0 :Data;
principalId @1 :Data;
kind @2 :PrincipalKind;
displayName @3 :Text;
status @4 :AccountStatus;
credentialRefs @5 :List(Data);
roles @6 :List(Text);
attributes @7 :List(Attribute);
resourceProfile @8 :ProfileRef;
policyProfile @9 :ProfileRef;
homeRoot @10 :StorageRootRef;
createdAtMs @11 :UInt64;
updatedAtMs @12 :UInt64;
schemaVersion @13 :UInt32;
storeEpoch @14 :UInt64;
recordVersion @15 :UInt64;
policyEpoch @16 :UInt64;
previousHash @17 :Data;
contentHash @18 :Data;
}
struct ProfileRef {
profileId @0 :Data;
versionId @1 :Data;
epoch @2 :UInt64;
}
struct StorageRootRef {
storageServiceId @0 :Data;
rootObjectId @1 :Data;
rootKind @2 :StorageRootKind;
schemaVersion @3 :UInt32;
rootVersion @4 :Data;
}
enum StorageRootKind {
namespace @0;
}
enum AccountStatus {
active @0;
disabled @1;
locked @2;
recoveryOnly @3;
}
struct ResourceProfile {
profileId @0 :Data;
versionId @1 :Data;
epoch @2 :UInt64;
homeQuotaBytes @3 :UInt64;
tempQuotaBytes @4 :UInt64;
processLimit @5 :UInt32;
threadLimit @6 :UInt32;
capLimit @7 :UInt32;
memoryCommitLimitBytes @8 :UInt64;
frameGrantLimitPages @9 :UInt64;
endpointQueueLimit @10 :UInt32;
inFlightCallLimit @11 :UInt32;
retired12 @12 :UInt32; # was pending IPC submission quota; do not reuse
ringScratchLimitBytes @13 :UInt64;
logQuotaBytesPerWindow @14 :UInt64;
networkProfile @15 :Text;
cpuBudgetUsPerWindow @16 :UInt64;
cpuWindowUs @17 :UInt64;
timerWaiterLimit @18 :UInt32;
launcherProfile @19 :Text;
}
struct ExternalIdentityBinding {
bindingId @0 :Data;
provider @1 :Text;
subjectHash @2 :Data; # hash(provider kind, issuer, tenant, subject)
principalId @3 :Data;
tenant @4 :Text;
acceptedClaims @5 :List(Text);
expiresAtMs @6 :UInt64;
policyProfile @7 :ProfileRef;
resourceProfile @8 :ProfileRef;
schemaVersion @9 :UInt32;
storeEpoch @10 :UInt64;
recordVersion @11 :UInt64;
policyEpoch @12 :UInt64;
previousHash @13 :Data;
contentHash @14 :Data;
}
homeRoot is a persistent reference that the account/storage broker resolves
into a live Namespace capability at session-bundle time. It is not a path,
not a raw Directory, and not itself a capability. Compatibility Directory
views are projections returned only when a workload needs file-like APIs.
Manifest seed records and local account records may name roles and profiles,
but the resulting authority is still the CapSet returned by
AuthorityBroker. A disabled or locked account can authenticate only to
explicit recovery flows allowed by its account state and current policy.
Stable ID Formats
Names are display and lookup hints only. They must not be treated as authority or as stable cross-store identity. All durable IDs used for account-store joins should be opaque binary values with a declared version and fixed length:
- Local principals:
principalIdis a 32-byte opaque random value minted by the local account store or imported from a trusted recovery record. User names, display names, POSIX names, and email addresses are attributes, not identifiers. - Account records:
recordIdis a 32-byte opaque record identity. It may equalprincipalIdonly if the store permanently enforces one account record per local principal; otherwise it must be separate. - External bindings:
subjectHashis a 32-byte hash over canonical provider kind, issuer, tenant, and external subject.bindingIdis a 32-byte opaque or content-derived ID over the normalized binding tuple plus the local principal ID. Provider display names and group strings are not authority. - Policy and resource profiles:
profileIdis a 32-byte opaque profile identity.versionIdis a 32-byte content hash of the canonical profile body, schema version, parent version if any, and effective constraints. Profile display names such asoperatororguest-shellare aliases. - Policy versions: policy bundles use a 32-byte
versionIdplus a monotonically increasingpolicyEpoch. Brokers refuse grants when the session/profile summary names a stale epoch. - Storage roots:
storageServiceId,rootObjectId, androotVersionare storage-service-owned opaque binary identifiers. A storage root is never a path or user name; the storage broker resolves it into a liveNamespaceonly after current policy permits the grant.
Version, Rollback, and CAS Rules
Disk-backed account-store records must be rejected unless their integrity and
freshness checks pass. The minimum record header is schemaVersion,
storeEpoch, recordVersion, policyEpoch, previousHash, and
contentHash. schemaVersion selects the decoder and migration policy.
storeEpoch is a monotonic store-wide epoch advanced for every accepted
mutation batch. recordVersion is monotonic per record. policyEpoch binds
the record to the policy/profile generation used to evaluate it.
previousHash chains the prior accepted canonical record bytes when a previous
record exists, and contentHash covers the canonical bytes excluding the
hash field itself.
Mutations use compare-and-set semantics:
update(recordId, expectedStoreEpoch, expectedRecordVersion, expectedHash, patch)
-> accepted(newStoreEpoch, newRecordVersion, newHash)
-> stale(currentStoreEpoch, currentRecordVersion, currentHash)
-> denied(reason)
Administrative tools must submit the last observed epoch, version, and hash. The store accepts an update only when those values match the current durable record and the new record validates against the active schema and policy epoch. Replayed records, older store epochs, lower or equal record versions, hash-chain breaks, unknown schema versions, profile versions not recognized by the active policy bundle, and missing rollback metadata are fail-closed denials. A failed check may leave the account disabled for ordinary login while allowing only explicit recovery identities to inspect or repair it.
The account store should persist a signed or sealed store checkpoint that
records the latest storeEpoch, account-store installation ID, accepted
policy epoch, and root hash. If the checkpoint says a later epoch existed than
the records currently on disk, the store is in recovery mode and must not let
disk account records override manifest seed data or widen authority.
Recovery Import and Seed Repair
Manifest seed data is the recovery source when the local account store is missing, unreadable, or rollback-damaged. Recovery records should include first-operator or break-glass principal IDs, recovery credential references, profile refs, storage-root repair refs, import/export record IDs, allowed repair operations, expiry or quorum requirements, and audit requirements. Recovery identities are not normal operators: their default session bundle is limited to inspecting account-store state, exporting/importing records, disabling stale bindings, and applying exact-target repairs.
Import from seed or offline export is additive and conservative:
- preserve local
principalId,recordId, profile IDs, storage-root refs, and externalbindingIdvalues when their hashes and epochs validate; - import missing seed operators, service identities, recovery identities, and minimum guest/anonymous profiles needed to boot and repair the system;
- disable, not delete, external bindings whose provider, tenant, subject hash, policy epoch, or profile version cannot be validated;
- never auto-map a new external subject to a broader local role or profile than the signed seed/import record names;
- never widen caps, quotas, storage roots, roles, or approval paths as a side effect of recovery import;
- emit audit records for import start, source identity, records accepted, records preserved, records disabled, denials, and the final store epoch.
If audit storage is unavailable, recovery may continue only into a bounded emergency mode whose transcript is written to the best available append-only sink and whose repaired accounts remain disabled for ordinary login until an auditable store checkpoint is committed.
Session Startup Flow
flowchart TD
Input[Login, guest, or anonymous request]
Auth[Authentication or guest policy]
Source[Manifest seed, account store, or external binding]
Session[UserSession cap]
Broker[AuthorityBroker / PolicyEngine]
Bundle[Scoped cap bundle]
Shell[Native, agent, or POSIX shell]
Audit[AuditLog]
Input --> Auth
Auth --> Source
Source --> Session
Session --> Broker
Broker --> Bundle
Bundle --> Shell
Broker --> Audit
Shell --> Audit
The shell proposal’s minimal daily cap set is a session bundle:
terminal TerminalSession
self self/session introspection
status read-only SystemStatus
logs read-only LogReader scoped to this principal/session
home Directory or Namespace scoped to account storage
launcher restricted launcher for approved user applications
approval ApprovalClient
The shell still cannot mint additional authority. It can ask
ApprovalClient for a plan-specific grant, and a trusted broker can return
a narrow leased capability if policy and authentication allow it.
The terminal cap is the session-scoped foreground TerminalSession, not the
boot debug Console; login hands that terminal into the shell bundle only
after authentication or explicit guest/setup policy succeeds. The concrete
default-boot login/setup flow that consumes this bundle is documented in
Boot to Shell, and the shell-side
contract for receiving and inspecting it lives in
Shell.
Detailed decomposition for manifest-seeded accounts, disk-backed account storage, default resource bundles, local roles, RBAC, ABAC, MAC/MIC labels, POSIX profile metadata, and external identity bindings lives in Local Users, Storage, and Policy.
Multi-User Workloads
capOS should support two normal multi-user patterns.
Per-Session Subtree
The session owns a shell or supervisor subtree. Every child process receives an explicit CapSet assembled from the session bundle plus workflow-specific grants.
Example:
- Alice’s shell receives
home = Namespace("/users/alice"). - Bob’s shell receives
home = Namespace("/users/bob"). - The same editor binary launched from each shell receives different
homeandterminalcaps. - The editor cannot cross from Alice’s namespace into Bob’s unless a broker deliberately grants a sharing cap.
This is the right default for interactive applications and POSIX shells.
Shared Service With Per-Client Session Authority
A server process may handle many users in one address space. It should not infer authority from a caller’s self-reported user name, principal ID, role name, or endpoint label. Instead, a trusted issuer binds the subject before the service accepts it:
- authentication or admission creates a live
SessionContext; - a spawned process receives exactly one immutable session context, installed
at spawn time by
kernel/src/session_context.rs(see Service Architecture); AuthorityBrokergrants service roots or narrower facets for that session;- endpoint calls expose privacy-preserving caller-session metadata by default;
- subject details are disclosed only when the method/call explicitly requests disclosure and a broker/service-granted disclosure scope allows the named fields;
- quota donations or accounting caps may accompany service grants when server-side state needs explicit resource backing.
The service uses the caller session reference, disclosed subject facts, and service-local records to select scoped storage, enforce per-client limits, emit audit records, and return narrowed caps. Endpoint badges are not a normal identity mechanism; any remaining badge-shaped kernel field should be treated as internal endpoint transport state during the migration. This is the right shape for HTTP services, databases, log services, terminals, and shared daemons.
Service Accounts
Service identities are principals too. They are usually non-interactive and receive caps from init, a supervisor, or a deployment manifest rather than from a human login flow.
Service-account policy should be explicit:
- which binary or measured package may use the identity,
- which supervisor may spawn it,
- which caps are in its base bundle,
- which caps it may request from a broker,
- which audit stream records its activity.
Service account records may be manifest seeded or stored in the local account store, but their sessions should receive no terminal and no interactive bundle. They launch as workloads with measured binary, supervisor, service name, network/IPC, log, state namespace, and key-use constraints.
Anonymous, Guest, and Pseudonymous Access
These are distinct profiles.
Empty Cap Set
An untrusted ELF with an empty CapSet is not a user session. It is the
roadmap’s “Unprivileged Stranger”: code with no useful authority. It can
terminate itself and interact with the capability transport, but it cannot
reach a resource because it has no caps. The visible proof was achieved by
commit d4016ab at 2026-04-22 16:35 UTC.
Anonymous
Anonymous means unauthenticated and usually remote or programmatic. It should receive a random ephemeral principal ID and a very small cap bundle.
Typical properties:
- no durable home namespace by default,
- strict CPU, memory, outstanding-call, and log quotas,
- short session expiry,
- no elevation path except “authenticate” or “create account”,
- audit records keyed by ephemeral session ID and network/service context.
Guest
Guest means an interactive local profile with weak or no authentication.
Typical properties:
- terminal/UI access,
- temporary namespace,
- optional ephemeral home reset on logout,
- restricted launcher,
- no administrative approval path unless policy grants one explicitly,
- clearer user-facing affordance than anonymous.
Pseudonymous
Pseudonymous means durable identity without necessarily naming a human. A public key, passkey, service token, or cloud identity can select the same principal across sessions. This can receive persistent storage and quotas while still remaining separate from a verified human account.
External pseudonymous sessions require explicit admission configuration. A binding either maps the external subject to an existing local account or allows auto-creation of a tenant-scoped account with named policy and resource profiles. Durable storage is granted only through that local principal mapping and a broker-minted storage cap.
POSIX Compatibility
POSIX user concepts are compatibility metadata, not authority.
uid,gid, user names, groups,$HOME,/etc/passwd,chmod, andchownlive inlibcapos-posix, a filesystem service, or a profile service.open("/home/alice/file")succeeds only if the process has aDirectoryorNamespacecap that resolves that synthetic path.setuidcannot grant new caps. At most it asks a compatibility broker to replace the process’s POSIX profile or launch a new process with a different cap bundle.- POSIX ownership bits may influence one filesystem service’s policy, but they cannot authorize access to caps outside that service.
This lets existing programs inspect plausible user metadata without making Unix permission bits the capOS security model.
Policy Models
RBAC, ABAC, and mandatory access control fit capOS as grant-time and
mint-time policy. They should mostly live in ordinary userspace services:
AuthorityBroker, PolicyEngine, SessionManager, RoleDirectory,
LabelAuthority, AuditLog, and service-specific attenuators.
The kernel should keep enforcing capability ownership, generation, transfer rules, revocation epochs, resource ledgers, and process isolation. It should not evaluate roles, attributes, or label lattices on every capability call.
RBAC
Role-based access control maps principals or sessions to named role sets. Roles select cap bundles and approval eligibility.
Examples:
developercan receive a launcher for development tools and read-only service logs.net-operatorcan request a leasedServiceSupervisor(net-stack).storage-admincan request repair caps for selected storage volumes.
Implementation shape:
interface RoleDirectory {
rolesFor @0 (principal :Data) -> (roles :List(Text));
}
interface AuthorityBroker {
request @0 (
session :UserSession,
plan :ActionPlan,
requestedCaps :List(CapRequest),
durationMs :UInt64
) -> (grant :ApprovalGrant);
# Mint an ApprovalInbox for the bound session. The broker policy
# decides whether the requesting session is allowed to triage
# approvals and which entries are visible (own requests only,
# role-scoped queue, multi-party reviewer queue).
inbox @1 (
session :UserSession
) -> (inbox :ApprovalInbox);
}
The detailed ActionPlan, ActionStep, CapRequest, GrantedCap,
ApprovalInbox, ApprovalEntry, and ApprovalListener schemas live
in Shell under
Approval and Authentication. The broker is the single producer for
both ApprovalGrant (the requester-side handle) and ApprovalInbox
(the decider-side handle); they meet only at the broker, never on a
shared transport channel.
Roles do not bypass capabilities. They only let a broker decide whether it may mint or return particular scoped caps.
The role/attribute/decision split matches the ITU-T X.812 Access control framework (= ISO/IEC 10181-3) decomposition into ADF (access-control decision function) and AEF (access-control enforcement function). In capOS terms:
- The AEF is the
CapObject::calldispatch plus wrapper caps: the enforcement point that cannot be bypassed because it is the only path to the underlying object. - The ADF is the
PolicyEngine/AuthorityBroker: it evaluates a decision request and returns a capability (or refuses) rather than returning a boolean that downstream code might ignore.
The ADF/AEF split is why capOS can make PolicyDecision a
cap-minting input rather than a per-call allow/deny flag — the
enforcement point is already structural (you need a cap to reach the
object) and the decision point returns the cap.
Remote Client Bundles
Remote programmatic and GUI clients consume the same identity and policy model as shells, but they need a different bundle shape. A remote host app may authenticate with password, public key, OIDC, passkey/WebAuthn, mTLS, guest/anonymous admission, or a service/workload credential. After admission, the broker returns a remote-client bundle whose entries are exported as Cap’n Proto RPC object references by a per-session gateway worker.
Those references are live capability proxies, not bearer tokens and not local
cap-table metadata. A remote bundle may include session, systemInfo, and
specific service caps such as chat, paperclips, or command surfaces. It
should not inherit terminal, launcher, broad storage, raw network, key-vault,
credential-store, or process-spawn authority merely because an operator shell
profile would receive some of those caps. The detailed transport and lifetime
rules live in
Remote Session CapSet Clients.
ABAC
Attribute-based access control evaluates a richer decision context:
- subject attributes: principal kind, roles, auth strength, session age, device posture, locality,
- action attributes: requested method, target service, destructive flag, requested duration,
- object attributes: service name, namespace prefix, data class, owner principal, sensitivity,
- environment attributes: time, boot mode, recovery mode, network location, cloud instance metadata, quorum state.
ABAC is useful for contextual narrowing:
- allow log read only for the caller’s session unless break-glass policy is active,
- issue
ServiceSupervisor(net-stack)only with fresh hardware-key auth, - grant
Namespace("/shared/project")read-write only during a maintenance window, - deny network caps to guest sessions.
ABAC decisions should return capabilities, wrappers, or denials. They should not create hidden ambient checks downstream.
OAuth2 scopes and OIDC claims (acr, amr, groups, tenant-specific
fields) are ABAC inputs. The broker consumes them alongside session
freshness, object attributes, and environment state to pick a cap
bundle or decline. They never authorize capability calls directly,
and they do not create a downstream check outside the broker’s
decision path. See
OIDC and OAuth2.
ABAC Policy Engine Choices
Do not invent a policy language first. The capOS-native interface should be small and capability-shaped, while the broker implementation can start with a mainstream engine behind that interface.
Recommended order:
-
Cedar for runtime authorization. Cedar’s request shape is already close to capOS:
principal,action,resource, andcontext. It supports RBAC and ABAC in one policy set, has schema validation, and has a Rust implementation. That makes it the best fit forAuthorityBrokerandMacBrokerservice prototypes. -
OPA/Rego for host-side and deployment policy. OPA is widely used for cloud, Kubernetes, infrastructure-as-code, and admission-control style checks. It is useful for validating manifests, cloud metadata deltas, package/deployment policies, and CI rules. The Wasm compilation path is worth tracking for later capOS-side execution, but OPA should not be the first low-level runtime dependency.
-
Casbin for quick prototypes only. Casbin is useful for simple RBAC/ABAC experiments and has Rust bindings, but its model/matcher style is less attractive as a long-term capOS policy substrate than Cedar’s schema-validated authorization model.
-
XACML for interoperability and compliance, not native policy. XACML remains the classic enterprise ABAC standard. It is useful as a conceptual reference or import/export target, but it is too heavy and XML-centric to be the native capOS policy language.
The capOS service boundary should hide the selected engine:
interface PolicyEngine {
decide @0 (request :PolicyRequest) -> (decision :PolicyDecision);
}
struct PolicyRequest {
principal @0 :PrincipalInfo;
action @1 :Text;
resource @2 :ResourceRef;
context @3 :List(Attribute);
}
struct PolicyDecision {
allowed @0 :Bool;
reason @1 :Text;
leaseMs @2 :UInt64;
constraints @3 :List(Attribute);
}
PolicyDecision is still not authority. It is input to a broker that returns
actual caps, wrapper caps, leased caps, or denial.
References:
- Cedar policy language docs: https://docs.cedarpolicy.com/
- Amazon Verified Permissions concepts: https://docs.aws.amazon.com/verifiedpermissions/latest/userguide/terminology.html
- Open Policy Agent docs: https://www.openpolicyagent.org/docs
- Casbin supported models: https://www.casbin.org/docs/supported-models
- OASIS XACML technical committee: https://www.oasis-open.org/committees/xacml/
- ITU-T Rec. X.812 (11/95) — Information technology - Open Systems Interconnection - Security frameworks for open systems: Access control framework. ADF/AEF terminology.
- ITU-T Rec. X.741 (10/95) — Systems Management: Objects and attributes for access control. Concrete managed-object attributes for ACLs, ACIs, default access, and access-decision inputs.
Mandatory Access Control
Mandatory access control is non-bypassable policy set by the system owner or deployment, not discretionary sharing by ordinary users. In capOS, MAC should be implemented as mandatory constraints on cap minting, attenuation, transfer, and service wrappers.
Examples:
- a
Secretcap labeledhighcannot be transferred to a workload labeledlow, - a
LogReaderfor security logs cannot be granted to a guest session even if an application asks, - a recovery shell can inspect storage read-only but cannot write without a separate exact-target repair cap,
- cloud user-data can add application services but cannot grant
FrameAllocator,DeviceManager, or raw networking authority.
Implementation components:
enum Sensitivity {
public @0;
internal @1;
confidential @2;
secret @3;
}
struct SecurityLabel {
domain @0 :Text;
sensitivity @1 :Sensitivity;
compartments @2 :List(Text);
}
interface LabelAuthority {
labelOfPrincipal @0 (principal :Data) -> (label :SecurityLabel);
labelOfObject @1 (object :Data) -> (label :SecurityLabel);
canTransfer @2 (
from :SecurityLabel,
to :SecurityLabel,
capInterface :UInt64
) -> (allowed :Bool, reason :Text);
}
For ordinary services, MAC can be enforced by brokers and wrapper caps. For high-assurance boundaries, the remaining question is whether transfer labels need kernel-visible hold-edge metadata. That should be added only for a concrete mandatory policy that cannot be enforced by controlling all grant paths through trusted services.
The attribute model borrows from ITU-T X.741, which enumerates the
managed-object attributes a directory-based access-control system
tracks: ACL entries, access-control information (ACI), default access,
initiator ACI, target ACI, and access-decision outcome. X.741 targets
the X.500 directory, so the schema does not port directly, but the
attribute taxonomy is a good completeness check for what
LabelAuthority and PolicyEngine requests should expose to a
decision engine.
GOST-Style MAC and MIC
Russian GOST framing is stricter than the generic “MAC means labels” summary. The relevant standards split at least two policies that capOS should keep separate:
-
Mandatory access control for confidentiality. ГОСТ Р 59383-2021 describes mandatory access control as classification labels on resources and clearances for subjects. ГОСТ Р 59453.1-2021 goes further: a formal model that includes users, subjects, objects, containers, access levels, confidentiality levels, subject-control relations, and information flows. The safety goal is preventing unauthorized flow from an object at a higher or incomparable confidentiality level to a lower one.
-
Mandatory integrity control for integrity. ГОСТ Р 59453.1-2021 treats this separately from confidentiality MAC. The integrity model constrains subject integrity levels, object/container integrity levels, subject-control relationships, and information flows so lower-integrity subjects cannot control or corrupt higher-integrity subjects.
For capOS, this should map to labels on sessions, objects, wrapper caps, and eventually hold edges:
struct ConfidentialityLabel {
level @0 :Text; # e.g. public, internal, secret.
compartments @1 :List(Text);
}
struct IntegrityLabel {
level @0 :Text; # ordered by deployment policy.
domains @1 :List(Text);
}
struct MandatoryLabel {
confidentiality @0 :ConfidentialityLabel;
integrity @1 :IntegrityLabel;
}
Capability methods need a declared flow class. capOS cannot rely on generic
read and write syscalls:
- read-like:
File.read,Secret.read,LogReader.read; - write-like:
File.write,Namespace.bind,ManifestUpdater.apply; - control-like:
ProcessSpawner.spawn,ServiceSupervisor.restart; - transfer-like:
CAP_OP_CALL,CAP_OP_RETURN, and result-cap insertion when they carry caps or data across labeled domains.
Initial rules can be expressed as broker/wrapper checks:
read data-bearing cap:
subject.clearance dominates object.classification
write data-bearing cap:
target.classification dominates source.classification
# no write down
control process or supervisor:
controlling subject is same label, or is an explicitly trusted subject
integrity write/control:
writer.integrity >= target.integrity
This is not enough for a GOST-style formal claim, because uncontrolled cap transfer can bypass the broker. A higher-assurance design needs:
- kernel object identity for every labeled object,
- label metadata on kernel objects or per-process hold edges,
- transfer-time checks for copy, move, result caps, and endpoint delivery,
- explicit trusted-subject/declassifier caps,
- an audit trail for every label-changing or declassifying operation,
- a formal state model covering users, subjects, objects, containers, access rights, accesses, and memory/time information flows.
The proposal therefore has two levels:
- Pragmatic capOS MAC/MIC: userspace brokers and wrapper caps enforce labels on grants and method calls.
- GOST-style MAC/MIC: a formal information-flow model plus kernel-visible labels/hold-edge constraints for transfers that cannot be forced through trusted wrappers. See Formal MAC/MIC for the dedicated abstract-automaton and proof track.
References:
- ГОСТ Р 59383-2021, access-control foundations: https://lepton.ru/GOST/Data/752/75200.pdf
- ГОСТ Р 59453.1-2021, formal access-control model: https://meganorm.ru/Data/750/75046.pdf
Composition Order
When policies compose, use this order:
- Mandatory policy defines the maximum possible authority.
- RBAC selects coarse eligibility and default bundles.
- ABAC narrows the decision for context, freshness, object attributes, and requested duration.
- The broker returns specific capabilities or denies the request.
- Audit records the plan, decision, grant, use, release, and revocation.
The composition result is still a CapSet, leased cap, wrapper cap, or denial.
Service Architecture
The policy stack should be decomposed into ordinary capOS services. Init or a trusted supervisor grants broad authority only to the small services that need to mint narrower caps.
SessionManager
Creates and manages session metadata/control caps:
guest()for local guest sessions,anonymous(purpose)for ephemeral unauthenticated callers,login(method, proof)for authenticated users,renew(session, proof, requestedDurationMs)for narrow continuation or recovery when policy allows it,logout(session)through theUserSessioncontrol cap.
The first implementation can be manifest-seed backed. It does not need a
persistent account database, but its seed records must use the same principal,
account, policy-profile, and resource-profile vocabulary as the later local
account store. UserSession should describe the principal, session ID, policy
profile, resource profile, auth strength, expiry, and audit context. It should
not be a general-purpose authority vending machine unless it was itself minted
as a narrow wrapper around a fixed cap bundle. Session IDs should come from the
same dedicated entropy source that the bootstrap login/setup flow in
Boot to Shell uses for credential
salts and setup tokens; if fresh randomness is unavailable, authenticated
session creation should fail closed instead of recycling predictable IDs.
SessionManager should own the mutable liveness cell for sessions it mints. The
kernel-installed process SessionContext (owned by
kernel/src/session_context.rs; see
Service Architecture) remains
immutable; renewal changes the cell or produces a successor session, not a new
subject label inside the same process. This is the mechanism that makes
long-running shells usable without treating fixed short wall-clock expiry as
the only safety boundary.
Safer first split:
SessionManager -> UserSession metadata cap
AuthorityBroker(session, policyProfile, resourceProfile) -> base cap bundle
Supervisor/Launcher -> spawn shell with that bundle
AuthorityBroker
The broker owns or receives powerful caps from init/supervisors and returns narrow caps after RBAC, ABAC, and mandatory checks.
Examples:
- broad
ProcessSpawner->RestrictedLauncher(allowed = ["shell", "editor"]), - broad
NamespaceRoot->Namespace("/users/alice"), - broad
ServiceSupervisor->LeasedSupervisor("net-stack", expires = 60s), - broad
BootPackage->BinaryProvider(allowed = ["shell", "editor"]).
The broker is the normal policy decision and cap minting point.
AuditLog
Append-only audit interface. Initially this can write to serial or a bounded log buffer; later it should be Store-backed.
Record at least:
- session creation,
- cap request,
- policy input summary,
- policy decision,
- cap grant,
- cap release or revocation,
- denial,
- declassification or relabel operation.
Audit entries must not contain raw auth proofs, private keys, bearer tokens, or broad environment dumps. For auth/session flows, the initial backend should record opaque credential/token record IDs, volatility flags, and policy/result codes rather than secret-bearing payloads. Failed pre-auth attempts should log only a terminal-local event ID and generic failure class; do not emit principal-identifying fields to the serial-backed path before authentication actually succeeds.
RoleDirectory
Role lookup should start static and boot-config backed:
guest -> guest-shell
alice -> developer
ops -> net-operator
net-stack -> service:network
This is enough for early RBAC bundles. Dynamic role assignment moves into the local account store once persistent storage and administrative tooling exist. Provider groups are not imported as roles automatically; a binding rule may map a provider group to a local role only for a named provider/tenant, expiry, and policy version.
LabelAuthority
Owns the label lattice and dominance checks. In the pragmatic phase, it is a userspace dependency of brokers and wrappers. In a GOST-style phase, the same lattice needs a kernel-visible representation for transfer checks.
Wrapper Caps
Wrappers are the main mechanism. Prefer them over per-call ACL checks in a central service:
RestrictedLauncherwrapsProcessSpawner.ScopedNamespacewraps a broader namespace/store.ScopedLogReaderfilters by session ID or service subtree.LeasedSupervisorwraps a broader supervisor with expiry and target binding.ApplicationManifestUpdaterrejects kernel/device/service-manager grants.LabelledEndpointenforces declared data-flow and control-flow constraints.
This keeps authority visible in the capability graph.
Bootstrap Sequence
Early boot can be static:
init
-> starts AuditLog
-> starts SessionManager
-> starts AuthorityBroker with broad caps
-> asks broker for a system, guest, or operator shell bundle
-> spawns shell through a restricted launcher
Before durable storage exists, policy config comes from BootPackage /
manifest config. Early authentication may still use bootstrap verifier or
public-credential records plus guest/anonymous/local-presence profiles, but it
must keep fresh-entropy requirements fail-closed and treat any RAM-only
credential or disable-state changes as volatile.
Revocation, Audit, and Quotas
User/session policy depends on the Stage 6 authority graph work:
- one-session-per-process plus privacy-preserving endpoint caller-session metadata lets shared services distinguish session/client relations; receiver selectors are only routing metadata,
- mutable session liveness cells distinguish live, logged-out, revoked, expired, and recovery-only sessions without relabeling running processes,
- resource ledgers and session quotas prevent denial-of-service through session creation,
CAP_OP_RELEASEand process-exit cleanup reclaim local hold edges,- epoch revocation lets a broker invalidate leased or compromised caps,
- renewal mints or refreshes session/grant leases under policy; it must not revive stale ordinary grants by accident,
- audit logs record the cap grant and release lifecycle.
The cross-cutting quota model lives in Resource Accounting and Quotas. Account and session resource profiles are templates; brokers, supervisors, and resource owners translate them into concrete ledgers and wrapper caps.
Audit should record identity and policy metadata, but it should not contain secrets, raw authentication proofs, or broad environment dumps.
Implementation Plan
-
Document the model. Keep user identity out of the kernel architecture, publish the principal/user/account/profile/session/role/workload vocabulary, and link this proposal from the shell, service, storage, and roadmap docs.
-
Manifest-seeded account and profile schema. Define boot-package seed records for first operators, recovery identities, service identities, guest policy, policy profiles, resource profiles, and initial role bindings. Validate that seed data names policy inputs only and does not grant ordinary accounts privileged kernel caps directly.
-
Session-aware native shell profile. Treat the shell proposal’s minimal daily cap set as a session bundle. Add
self/sessionintrospection and scopedlogs/homecaps once the underlying services exist. -
Authority broker and audit log. Add
ActionPlan,ActionStep,CapRequest,ApprovalClient,ApprovalInbox,ApprovalEntry, leased grant records, and an append-only audit path. The shell-proposal Approval and Authentication section defines the schemas; the broker is the single producer for both the requester-sideApprovalGrantand the decider-sideApprovalInbox. Start with RBAC-style policy/resource profile bundles and explicit local authentication. -
Local account store and external bindings. Add a Store/Namespace-backed
AccountStorefor account records, credential references, role bindings, external identity bindings, policy versions, resource profiles, and storage-root references. Include version and rollback checks before treating disk-backed account mutation as durable. -
ABAC policy engine. Extend the broker decision with session freshness, auth strength, object attributes, requested duration, and environment state. Prefer Cedar for the runtime broker interface; use OPA/Rego for host-side manifest and deployment checks. Keep decisions visible in audit records.
-
Mandatory policy labels. Add pragmatic labels to policy-managed services and wrappers. Keep confidentiality and integrity separate. Defer kernel-visible labels until a specific MAC/MIC policy cannot be enforced by trusted grant paths.
-
Guest and anonymous demos. Show a guest shell with
terminal,tmp, and restrictedlauncher, and show an anonymous workload with strict quotas and no persistent storage. -
POSIX profile adapter. Provide synthetic
uid/gid,$HOME,/etc/passwd, and cwd behavior from session policy/resource profiles and granted namespace caps. -
GOST-style formalization checkpoint. If capOS later claims GOST-style MAC/MIC, write the abstract state model before implementation: users, subjects, objects, containers, access rights, accesses, labels, control relations, and information flows. Then decide which labels must become kernel-visible.
Non-Goals
- No kernel
uid/gid. - No ambient
root. - No global login namespace in the kernel.
- No authorization from serialized identity structs.
- No model-visible authentication secrets.
- No POSIX permission bits as system-wide authority.
- No per-call role/attribute/label interpreter in the kernel fast path.
- No claim of GOST-style MAC/MIC until the formal model and transfer enforcement story exist.
Open Questions
- Which session interfaces are needed before persistent storage exists?
- Which audit store is acceptable before durable storage and replay exist?
- Which MAC policies, if any, justify kernel-visible hold-edge labels?
- How should remote capnp-rpc or future OCapN/CapTP-style identities map into local principals? Transport identity, locator hints, and routing metadata are not local user/session identity by themselves; remote peers should enter through broker/session policy rather than raw protocol fields. See Cloudflare, Cap’n Proto, Workers RPC, and Cap’n Web and Spritely, OCapN, and CapTP.
- Should the first broker prototype embed Cedar directly, or use a simpler hand-written evaluator until the policy surface stabilizes?