Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

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 uid become 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

  • user is 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.
  • A session is a live policy context 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.

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

Session

A session is a live context derived from a principal plus authentication and policy state. Sessions carry freshness, expiry, auth strength, quota profile, audit identity, and default cap-bundle selection.

enum AuthStrength {
  none @0;
  localPresence @1;
  password @2;
  hardwareKey @3;
  multiParty @4;
}

struct SessionInfo {
  sessionId @0 :Data;
  principal @1 :PrincipalInfo;
  authStrength @2 :AuthStrength;
  createdAtMs @3 :UInt64;
  expiresAtMs @4 :UInt64;
  profile @5 :Text;
}

interface UserSession {
  info @0 () -> (info :SessionInfo);
  defaultCaps @1 (profile :Text) -> (caps :List(GrantedCap));
  auditContext @2 () -> (sessionId :Data, principalId :Data);
  logout @3 () -> ();
}

interface SessionManager {
  login @0 (method :Text, proof :Data) -> (session :UserSession);
  guest @1 () -> (session :UserSession);
  anonymous @2 (purpose :Text) -> (session :UserSession);
}

GrantedCap should be the same transport-level result-cap concept used by ProcessSpawner, not a parallel authority encoding.

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.

Session Startup Flow

flowchart TD
    Input[Login, guest, or anonymous request]
    Auth[Authentication or guest policy]
    Session[UserSession cap]
    Broker[AuthorityBroker / PolicyEngine]
    Bundle[Scoped cap bundle]
    Shell[Native, agent, or POSIX shell]
    Audit[AuditLog]

    Input --> Auth
    Auth --> 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 or Console
self            self/session introspection
status          read-only SystemStatus
logs            read-only LogReader scoped to this user/session
home            Directory or Namespace scoped to user data
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.

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 home and terminal caps.
  • 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. Instead, each client connection carries one or more of:

  • a badge on the endpoint cap identifying the client/session relation,
  • a UserSession or SessionContext cap,
  • a scoped object cap such as Directory, Namespace, LogWriter, or ApprovalClient,
  • a quota donation for server-side state.

The service uses these values to select scoped storage, enforce per-client limits, emit audit records, and return narrowed caps. 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.

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.

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.

POSIX Compatibility

POSIX user concepts are compatibility metadata, not authority.

  • uid, gid, user names, groups, $HOME, /etc/passwd, chmod, and chown live in libcapos-posix, a filesystem service, or a profile service.
  • open("/home/alice/file") succeeds only if the process has a Directory or Namespace cap that resolves that synthetic path.
  • setuid cannot 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:

  • developer can receive a launcher for development tools and read-only service logs.
  • net-operator can request a leased ServiceSupervisor(net-stack).
  • storage-admin can 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);
}

Roles do not bypass capabilities. They only let a broker decide whether it may mint or return particular scoped caps.

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.

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:

  1. Cedar for runtime authorization. Cedar’s request shape is already close to capOS: principal, action, resource, and context. It supports RBAC and ABAC in one policy set, has schema validation, and has a Rust implementation. That makes it the best fit for AuthorityBroker and MacBroker service prototypes.

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

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

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

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 Secret cap labeled high cannot be transferred to a workload labeled low,
  • a LogReader for 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.

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-proposal.md for the dedicated abstract-automaton and proof track.

References:

Composition Order

When policies compose, use this order:

  1. Mandatory policy defines the maximum possible authority.
  2. RBAC selects coarse eligibility and default bundles.
  3. ABAC narrows the decision for context, freshness, object attributes, and requested duration.
  4. The broker returns specific capabilities or denies the request.
  5. 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 session metadata caps:

  • guest() for local guest sessions,
  • anonymous(purpose) for ephemeral unauthenticated callers,
  • later login(method, proof) for authenticated users.

The first implementation can be boot-config backed. It does not need a persistent account database. UserSession should describe the principal, session ID, 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.

Safer first split:

SessionManager -> UserSession metadata cap
AuthorityBroker(session, profile) -> 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.

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 can wait for persistent storage and administrative tooling.

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:

  • RestrictedLauncher wraps ProcessSpawner.
  • ScopedNamespace wraps a broader namespace/store.
  • ScopedLogReader filters by session ID or service subtree.
  • LeasedSupervisor wraps a broader supervisor with expiry and target binding.
  • ApplicationManifestUpdater rejects kernel/device/service-manager grants.
  • LabelledEndpoint enforces 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. Before real authentication exists, support guest, anonymous, and localPresence only.

Revocation, Audit, and Quotas

User/session policy depends on the Stage 6 authority graph work:

  • badge metadata lets shared services distinguish session/client relations,
  • resource ledgers and session quotas prevent denial-of-service through session creation,
  • CAP_OP_RELEASE and process-exit cleanup reclaim local hold edges,
  • epoch revocation lets a broker invalidate leased or compromised caps,
  • audit logs record the cap grant and release lifecycle.

Audit should record identity and policy metadata, but it should not contain secrets, raw authentication proofs, or broad environment dumps.

Implementation Plan

  1. Document the model. Keep user identity out of the kernel architecture and link this proposal from the shell, service, storage, and roadmap docs.

  2. Session-aware native shell profile. Treat the shell proposal’s minimal daily cap set as a session bundle. Add self/session introspection and scoped logs/home caps once the underlying services exist.

  3. Authority broker and audit log. Add ActionPlan, CapRequest, ApprovalClient, leased grant records, and an append-only audit path. Start with RBAC-style profile bundles and explicit local authentication.

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

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

  6. Guest and anonymous demos. Show a guest shell with terminal, tmp, and restricted launcher, and show an anonymous workload with strict quotas and no persistent storage.

  7. POSIX profile adapter. Provide synthetic uid/gid, $HOME, /etc/passwd, and cwd behavior from a session profile and granted namespace caps.

  8. 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?
  • Should UserSession.defaultCaps() return actual caps or a plan that must be executed by ProcessSpawner?
  • 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 identities map into local principals?
  • Should the first broker prototype embed Cedar directly, or use a simpler hand-written evaluator until the policy surface stabilizes?