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: Boot to Shell

How capOS should move from “boot runs smokes and halts” to an authenticated, text-only interactive shell without weakening the capability model.

Problem

The old boot path was a systems bring-up path that started fixed services, proved kernel and userspace invariants, and exited cleanly. The completed local console milestone added interactive login/setup and shell behavior; the later init-owned default manifest moved that shell behind standalone init. The remaining problem space is remote/web login, stronger credential policy, and richer shell/session behavior without reintroducing ambient authority.

The first interactive milestone was deliberately modest:

  • Boot QEMU or a local machine to a text console login/setup prompt.
  • Start a native capability shell after local authentication or first-boot setup.
  • Keep browser-hosted text terminal, WebAuthn/passkeys, and remote enrollment as later work in the same proposal family after the local console path works.
  • Keep graphical shells, desktop UI, window systems, and app launchers as a later tier.

The risk is that “make it interactive” tends to smuggle ambient authority back into the system. A login prompt must not become a kernel uid, a web terminal must not become an unaudited remote root shell, and first-boot setup must not be a first-remote-client-wins race.

Scope

The completed local-console milestone covered:

  • Serial/local text console login and first-boot credential setup.
  • Native text shell as the post-login workload.
  • Minimal SessionManager, CredentialStore, AuthorityBroker, and AuditLog pieces needed to launch that shell with an explicit CapSet.
  • Password verifier records stored with a memory-hard password hash.
  • Local recovery/setup policy for machines with no credential records.

Later in the same proposal family:

  • Passkey registration and authentication for a web text shell.
  • A passkey-only account path that does not require creating a password first.
  • Federated login via OpenID Connect (OIDC) identity providers — device code on the local/serial console, authorization code + PKCE on the web text shell. See OIDC and OAuth2.

Out of scope:

  • Graphical shell, desktop session, compositor, GUI app launcher, clipboard, or remote desktop.
  • POSIX /bin/login, PAM, sudo, su, or Unix uid/gid semantics.
  • Password reset by policy fiat. Recovery is a separate authenticated setup or operator action.
  • Making authentication proofs visible to the shell, agent, logs, or ordinary application processes.

Design Principles

  • Authentication creates a UserSession; capabilities remain the authority.
  • The shell is an ordinary process launched with a broker-issued CapSet.
  • Console authentication, web authentication, and federated OIDC login feed the same session model.
  • Passwords are verified against versioned password-verifier records; raw passwords are never stored, logged, or passed to the shell.
  • Passkeys store public credential material only; private keys stay in the authenticator.
  • OIDC ID tokens are verified against a pinned OidcIdentityProvider; the raw token never reaches the shell or audit stream as bytes.
  • First-boot setup requires local setup authority or an explicitly configured bootstrap credential. Remote first-come setup is not acceptable.
  • A missing credential store does not imply an unlocked system.
  • Guest and anonymous sessions are explicit policy profiles, not fallbacks for missing credentials.
  • Development images may have an explicit insecure profile, but that must be visible in the manifest and serial output.

Architecture

The original local console boot-to-shell proof collapsed the authentication service and interactive shell into a single userspace process. Focused shell-led smokes still boot capos-shell directly as initConfig.init with a narrow bootstrap CapSet (see Service Architecture). The default system.cue path now runs capos-shell as an init-started service through standalone init (Service Architecture), but the shell-side authority model is the same: it mints its own anonymous UserSession and only upgrades after a password login:

flowchart TD
    Kernel[kernel starts one init]
    Init[standalone init or focused shell init]
    Shell[capos-shell]
    Cred[CredentialStore]
    Session[SessionManager]
    Broker[AuthorityBroker]
    Audit[AuditLog]
    Term[TerminalSession]
    Web[WebShellGateway]
    Launcher[RestrictedShellLauncher]

    Kernel --> Init
    Init --> Shell
    Shell --> Term
    Shell --> Cred
    Shell --> Session
    Shell --> Broker
    Shell --> Audit
    Session --> Broker
    Broker --> Launcher
    Cred --> Session
    Audit --> Session
    Audit --> Broker
    Web -. "future" .-> Session

The shell keeps the authority-holding caps needed for its session boundary (terminal, creds, sessions, audit, broker) because the current interactive substrate has not split login, shell, and approval into separate services. It does not hand those caps to any child it spawns; spawn grants go through the broker-issued RestrictedLauncher whose allowlist depends on the current session’s profile (empty for anonymous, full interactive shell set for operator, and empty or narrowly policy-selected for guest). The launcher itself is the Service Architecture ProcessSpawner cap wrapped behind broker-enforced policy, so a shell child cannot widen its CapSet at spawn time. The broker returns a narrow shell bundle such as:

terminal        TerminalSession
self            UserSession metadata
status          read-only SystemStatus
logs            scoped LogReader
home            scoped Namespace or temporary Namespace
launcher        RestrictedLauncher
approval        ApprovalClient

Early builds can omit storage-backed home and use a temporary namespace. They still should not hand the shell broad BootPackage, ProcessSpawner, FrameAllocator, raw device, or global service-supervisor authority by default.

First Terminal Boundary

The first interactive console boundary should be a session-scoped TerminalSession, not a widened boot Console cap and not a raw byte-stream cap handed directly to login or shell processes.

Console stays the early-boot and panic-path output surface. The component that owns the underlying local console transport, line discipline, edit buffer, and later web-terminal framing can be called ConsoleTerminal or TerminalMux; the external authority boundary is the same either way:

  • only the terminal service owns raw console transport state and line buffers,
  • the shell process receives the foreground TerminalSession cap and drives pre-auth password/setup input through it with per-call echo = hidden,
  • shell children do not inherit the terminal unless the shell names it in a spawn plan.

A later web-shell or federated-login service that needs a separate authentication front-end will still get its own TerminalSession and its own broker-issued bundle; it does not widen authority on the local console shell. The shell-side framing of this split — terminal-host process versus shell process, with the terminal owning raw console state and the shell owning the post-auth command loop — lives in Shell.

The first interface should stay line-oriented:

enum LineEcho {
  visible @0;
  hidden @1;
}

enum LineStatus {
  submitted @0;
  cancelled @1;
  closed @2;
}

struct LineRequest {
  prompt @0 :Text;
  maxBytes @1 :UInt32;
  echo @2 :LineEcho;
  allowEmpty @3 :Bool;
}

interface TerminalSession {
  write @0 (data :Data) -> ();
  writeLine @1 (text :Text) -> ();
  readLine @2 (request :LineRequest) -> (status :LineStatus, line :Data);
}

That shape fixes the first boot-to-shell boundary:

  • readLine returns one bounded line or a structured cancelled/closed result. The service owns the temporary edit buffer and scrubs it after completion or cancellation.
  • Echo policy is per call. Password entry uses echo = hidden; the shell never toggles a terminal-global echo bit that could leak into later prompts.
  • The terminal service enforces a hard implementation ceiling even if a caller asks for a larger maxBytes. ConsoleLogin and setup flows should request smaller bounds than the shell’s ordinary command reader.
  • Cancellation is line-scoped. Operator abort input returns cancelled and the caller receives no partial secret buffer.
  • The first milestone does not need raw byte reads, terminal history replay, multi-reader fan-out, or shell-visible secret-state. Paste framing, resize, and richer terminal controls can extend TerminalSession later.

This keeps password/setup entry inside ConsoleLogin and terminal services. The broker, audit log, shell, and shell children only see the outcomes they need: session metadata, policy results, and a terminal handle for post-auth interactive work.

Console Login

The local console path now runs entirely inside capos-shell, so “login” is a shell command rather than a separate pre-shell process. The shell always boots with an anonymous session; authentication is an explicit user action. The three states below describe what the login and setup commands see, not a boot-time mode selector. The in-shell command surface and the login / setup / caps / inspect command behavior live in Shell and Shell; this proposal describes only the session/credential/broker authority side of the same flow. The make run-login smoke covers the password path, and make run-shell covers the anonymous-only path.

Password Configured

If CredentialStore has an enabled console password verifier for the selected principal or profile, login prompts for the password, verifies it through CredentialStore, mints an operator UserSession via SessionManager.login, asks the broker for the operator shell bundle, and swaps the in-shell session and launcher in place.

The verifier record should be versioned:

PasswordVerifier {
  algorithm: "argon2id"
  params: { memoryKiB, iterations, parallelism, outputLen }
  salt: random bytes
  hash: verifier bytes
  createdAtMs
  credentialId
  principalId
}

Argon2id is the default target because it is memory-hard and widely reviewed. The record must include parameters so stronger settings can be introduced without invalidating older records. A deployment may add a TPM- or secret-store-backed pepper later, but the design must not depend on a pepper being present.

On failed attempts, the shell records an audit event and applies bounded backoff before re-prompting within the same login invocation. The backoff state is not a security boundary by itself, because local attackers may reboot; the password hash strength still matters.

No Console Password

If no console password verifier exists, login reports that setup is required. The user must run setup to create the first verifier. The make run-login-setup smoke drives the first-boot path: no verifier exists, login refuses, setup mints the first volatile verifier through the manifest operator seed principal, and the shell then upgrades to an operator session.

Setup mode can:

  • create the first console password verifier,
  • enroll a first passkey for the web text shell (future),
  • create both credentials (future).

Until a credential is created, the shell stays in the anonymous session: it can exercise caps, inspect, session, and help, but the broker-issued anonymous launcher has an empty allowlist, so the shell cannot spawn children or escalate authority. This matches the operator expectation: no configured password means “setup required”, not “open console”.

Passkey-Only Deployment

Passkey-only should be possible without creating a password. It still needs a bootstrap authority path.

Acceptable first-passkey bootstrap paths:

  • local console setup enrolls the first passkey and then never creates a password verifier,
  • the manifest or cloud metadata includes a predeclared passkey public credential for an operator principal,
  • the console prints a short-lived setup challenge that a web enrollment flow must redeem before registering the first passkey.

Unacceptable path:

  • the first remote browser to reach the web endpoint becomes administrator because no password exists.

If a machine is passkey-only, the local console can still expose setup, recovery, guest, or diagnostic profiles according to policy. It should not silently become an unauthenticated administrator shell.

Guest and Anonymous Profiles

The user-identity proposal distinguishes authenticated, guest, anonymous, and pseudonymous sessions (see User Identity and Policy for the full taxonomy and User Identity and Policy for the underlying session structure). Boot-to-shell should consume that model directly.

Authenticated password login creates a human or operator UserSession with auth strength password. Authenticated passkey login normally creates a human, operator, or pseudonymous UserSession with auth strength hardwareKey. Neither proof is authority by itself; both feed the broker. Default password-authenticated local operator sessions do not expire by fixed wall-clock timestamp; their normal lifecycle is explicit logout, terminal/connection/process-tree close, or administrator revocation. A manifest can still opt into a hard operator lifetime for focused proofs or deployment policy.

Guest is the only unauthenticated profile that belongs on the local interactive console by default. It is a deliberate SessionManager.guest() path with a local interactive affordance, weak or no authentication, short expiry, tight quotas, no durable home unless policy grants one, and a bundle such as:

terminal        TerminalSession
self            guest UserSession metadata
tmp             temporary Namespace
launcher        RestrictedLauncher(allowed = ["help", "settings"])
logs            scoped LogReader for this guest session

Guest should not receive ApprovalClient for administrative actions unless a named policy grants it. If no console password exists, setup may offer a guest session only when the manifest explicitly enables a guest profile. Otherwise the operator must create a credential or leave the ordinary shell unavailable.

Anonymous is different. It is usually remote or programmatic, has a random ephemeral principal ID, receives a smaller cap bundle than guest, and has no elevation path except “authenticate” or “create account”. It is not the console fallback for missing credentials, and it should not be counted as “booted to shell” unless the product goal is an explicitly anonymous demo.

If the web gateway later supports anonymous access, it should be a purpose-scoped workload or very restricted text terminal with no durable home, strict quotas, short expiry, and audit keyed by network context plus ephemeral session ID. It must not share the passkey setup path, because passkey-only bootstrap is a credential-enrollment flow, not anonymous access.

An empty CapSet remains the “Unprivileged Stranger” case. It is useful for attack-surface demonstration, but it is not a session profile and not a shell login mode.

Web Text Shell and Passkeys

This is later work in the same proposal family, not part of the current local-console acceptance gate. The web shell is a browser-hosted terminal transport, not a graphical shell. It should display the same native text shell protocol through a terminal UI and should launch the same kind of session bundle as the local console path.

Required pieces:

  • network stack and HTTP/WebSocket or equivalent streaming transport,
  • TLS or a deployment mode acceptable to browsers for WebAuthn,
  • stable relying-party ID and origin policy,
  • random challenge generation,
  • passkey credential storage,
  • user-verification policy,
  • audit and rate limiting.

Passkey credential records should store public material:

PasskeyCredential {
  credentialId
  principalId
  publicKey
  relyingPartyId
  userHandle
  signCount
  transports
  userVerificationRequired
  createdAtMs
}

The authentication flow is:

  1. Browser requests a login challenge.
  2. WebShellGateway asks SessionManager or CredentialStore for a bounded, random challenge tied to the relying-party ID and intended principal.
  3. Browser calls the platform authenticator.
  4. Gateway verifies the WebAuthn assertion, origin, challenge, credential ID, public-key signature, user-presence/user-verification flags, and sign-count behavior.
  5. SessionManager mints a UserSession with auth strength hardwareKey.
  6. AuthorityBroker returns the shell bundle for that session/profile.
  7. RestrictedShellLauncher starts the native text shell connected to the web terminal stream.

Registration requires an existing authenticated session, local setup authority, or an explicit bootstrap path. Passwordless registration is allowed; unauthenticated remote registration is not.

Remote Session Clients

The same authentication and broker model also serves non-shell remote clients. A host app – CLI, native GUI, Tauri backend, webapp gateway, or service client – should not have to start a terminal shell just to call typed services. After password, public-key, OIDC, passkey, mTLS, guest, anonymous, or service/workload admission succeeds under policy, SessionManager mints a UserSession and AuthorityBroker returns a remote-client bundle. The client then sees a remote CapSet view whose entries are Cap’n Proto RPC object references, not local capOS cap slots.

This keeps boot/login policy unified:

  • authentication proofs are consumed by trusted session/admission services;
  • the broker chooses the CapSet for the selected profile;
  • shells, web terminals, agents, and non-shell remote clients are different consumers of session bundles;
  • password auth is one adapter, not the remote protocol shape.

The detailed remote-client design lives in Remote Session CapSet Clients.

Federated Login (OIDC)

OIDC is the third authentication path alongside password and passkey. It lets capOS accept identity from a corporate IdP (Azure AD, Google Workspace, Okta, Keycloak, Dex, GitHub) without capOS storing or managing primary user credentials. The schemas, grant types, JWKS handling, and token lifecycle live in OIDC and OAuth2; this section describes only the integration surface.

Console (device code)

Serial consoles have no browser. The login path is RFC 8628 device authorization:

  1. ConsoleLogin calls OAuthClient.startDeviceCode on an IdP that the manifest has configured as acceptable for console login.
  2. TerminalSession.writeLine prints the verification URL and user code; the user completes the flow on a separate device.
  3. ConsoleLogin polls pollDeviceCode at the advertised interval, honoring slow_down. Expiry is a hard fail.
  4. On granted, ConsoleLogin passes the resulting IdToken cap to SessionManager.login(method = "oidc", proof = idTokenRef).
  5. SessionManager calls OidcIdentityProvider.verifyIdToken with the client’s IdTokenPolicy, receives IdTokenClaims, derives PrincipalInfo.id = hash(iss, sub), derives authStrength from acr/amr, and mints a UserSession.
  6. The broker returns the same shell bundle as for other login methods; no OIDC-specific authority flows into the shell.

Failed verification uses the same generic failure text and bounded backoff as password login. The manifest controls which IdPs the console accepts and which subject patterns are allowed to log in; unlike password/passkey paths, OIDC login does not implicitly treat “any valid token from any configured IdP” as authority — a permitted- subject allow-list is required.

Web text shell (authorization code + PKCE)

WebShellGateway offers OIDC alongside WebAuthn. The gateway drives OAuthClient.startAuthCode, redirects the browser to the IdP, and consumes the returned code through completeAuthCode. PKCE is mandatory; state and nonce are generated from EntropySource. The gateway validates redirect URI exactly, requires TLS, and enforces IdTokenPolicy.nonceMustMatch.

Identity provider trust

CredentialStore gains IdP trust records alongside password verifiers and passkey public credentials:

IdpTrustRecord {
  recordId
  issuer                 # canonical URL
  clientRegistrations    # allowed OAuthClient records for this IdP
  jwks                   # snapshot or discovery URL + pinned TLS roots
  allowedAlgorithms
  allowedAcr / allowedAmr
  subjectAllowList       # e.g. principals matching sub/email/groups
  clockSkewSeconds
  authStrengthMap        # acr/amr -> AuthStrength (X.1254 LoA)
  createdAtMs
}

Records are public material (IdP URLs, JWKS, policy). Like passkey records, they can be bootstrapped from the manifest or cloud metadata, with a bounded RAM overlay for admin-managed records until durable storage exists. CredentialStore.verify stays a secret- preserving boundary; OIDC verification that rejects a token returns only denied with a generic failure class.

Federated principal bootstrap

For a fresh image with no local password, OIDC login can create the first UserSession when the manifest explicitly predeclares:

  • one or more trusted issuers,
  • a subject allow-list or group/claim predicate,
  • the principal identities those subjects map to.

This is the OIDC analog of the manifest-declared passkey bootstrap path: the authority comes from the manifest trust root, not from “the first caller who presents a token wins.” Without predeclared trust, OIDC login cannot be the only path to an administrative session on a fresh image — setup mode applies.

Scope of tokens

Access tokens issued alongside the ID token belong to the OAuth service. Neither the shell nor the broker ever receives raw token bytes. If the broker needs to delegate outbound authority to the session (e.g. “read from our corporate storage API”), it returns a wrapper cap holding an AccessToken cap, not a bearer string.

Refresh and session duration

SessionManager holds the RefreshToken cap associated with a federated session when the IdP issues one and the scope includes offline_access (or the IdP’s equivalent). Token refresh is a privileged operation scoped to SessionManager and audited; the shell cannot refresh its own session token. On logout or session expiry, SessionManager releases the refresh token and optionally calls the IdP’s revocation endpoint.

Required Interfaces

These are ordinary capabilities, not kernel modes.

EntropySource

Owns the only approved path for fresh auth/session secrets in the first implementation.

Responsibilities:

  • provide unpredictable bytes for password salts, session IDs, setup tokens, and later WebAuthn challenges,
  • fail closed when secure randomness is unavailable instead of returning predictable bytes,
  • keep raw entropy authority out of shells and ordinary workloads.

Only CredentialStore, SessionManager, later WebShellGateway, and a future SshGateway or narrower SSH transport-crypto service should hold it. ConsoleLogin, the shell, and spawned workloads should never mint their own session IDs, salts, setup tokens, SSH key-exchange material, or challenges.

CredentialStore

Owns credential verifier records and challenge state.

Responsibilities:

  • list whether setup is required without exposing hashes,
  • create password verifier records from setup authority,
  • verify password attempts without returning the password or verifier bytes,
  • register passkey public credentials,
  • store trusted OIDC identity-provider records (issuer, JWKS or pinned discovery URL, allowed audiences, subject allow-list, acr/amrAuthStrength mapping) so SessionManager can consume OidcIdentityProvider caps bound to deployment policy,
  • issue and consume bounded WebAuthn challenges,
  • rotate or disable credentials through an authenticated admin path.
  • load bootstrap verifier/public-credential and IdP-trust records from manifest or cloud bootstrap config and maintain a bounded RAM overlay until durable storage exists.

SessionManager

Creates UserSession metadata after successful authentication, explicit local guest policy, purpose-scoped anonymous policy, or setup policy. It should record auth method, auth strength, freshness, expiry, profile, and audit context. It should not hand out broad system caps directly. Boot-to-shell uses authenticated sessions and optional local guest sessions for ordinary interactive shells; anonymous sessions are narrower remote/programmatic contexts unless a manifest explicitly defines an anonymous demo terminal. Session IDs come from EntropySource; if fresh randomness is unavailable, authenticated login and token-bearing setup flows fail closed instead of reusing predictable IDs. The end-to-end mint/promote sequence and the account-store boundary it consumes are User Identity and Policy; the shell-side immutable-per-process invocation context that consumes the minted session lives in Shell and is proven by make run-session-context. The make run-local-users smoke covers the manifest-seeded local operator path that backs the password-login flow.

AuthorityBroker

Maps a session/profile to a narrow CapSet. Early policy can be static and manifest-backed. The important constraint is that the broker returns capabilities, not roles or strings that downstream services treat as authority.

ConsoleLogin

Consumes TerminalSession, CredentialStore, SessionManager, broker access, and a restricted shell launcher. It never receives broad boot-package or device authority unless a recovery profile explicitly grants it. It owns pre-auth password/setup entry and must not forward raw password bytes, setup tokens, or partial secret input into the shell, broker, or audit service.

On the current local-console substrate ConsoleLogin is not a separate process. Its responsibilities are folded into capos-shell, which owns the pre-auth TerminalSession, drives password/setup prompts, invokes CredentialStore/SessionManager/broker, and promotes its own session in place. The authority rules above still apply: the same process must not leak password bytes, setup tokens, or broker secrets into spawned children. A future web-shell or federated-login front-end can re-introduce a separate ConsoleLogin-shaped service that mints sessions for a distinct shell process.

WebShellGateway

Terminates the browser terminal session, handles passkey challenge/response, drives the OAuth authorization code + PKCE flow for federated login, and connects the authenticated session to the shell process. It should not own general administrative caps. It should ask the broker for the same narrow shell bundle as any other session.

SshGateway

Terminates SSH transport for CLI remote shell access, verifies host/user key protocol state, maps accepted SSH public keys to sessions, and connects the authenticated session to the shell process through an SSH-backed TerminalSession. It should not own general administrative caps, raw KeyVault administration, port-forward authority, or broad process-spawn authority. It should ask the broker for the same narrow shell bundle as any other session. The detailed transport and key-custody model is in SSH Shell Gateway. The initial schema names the supporting authority surfaces TcpListenAuthority, SshHostKey, AuthorizedKeyStore, SshTerminalFactory, and RestrictedShellLauncher; the development host-key path now exists only as an explicitly labeled non-production QEMU proof. Bounded QEMU proofs now cover configured authorized-key lookup, fixture public-key session minting, restricted shell launch, and a plain-TCP terminal-host handoff, while real SSH signing, encrypted transport, packet/channel handling, and the final OpenSSH harness remain later gates.

OAuthClient and OidcIdentityProvider

Supplied by the OAuth service (OIDC and OAuth2). ConsoleLogin holds an OAuthClient cap configured for device-code grants against the manifest-declared IdPs, and an OidcIdentityProvider cap for ID-token verification. WebShellGateway holds analogous caps configured for authorization code + PKCE. Neither service retains access tokens in long-lived session state — refresh tokens live inside SessionManager, bound to the UserSession lifecycle.

AuditLog

Records setup entry, credential creation, failed attempts, successful session creation, broker decisions, shell launch, credential disablement, and logout. Audit entries must not include passwords, password hashes, passkey private material, bearer tokens, complete environment dumps, or full terminal lines. Correlate auth/session events with opaque record IDs and policy/result codes, not with secret-bearing payloads.

First Security Substrate

Before local setup/login code lands, the first implementation should fix these rules:

  • Entropy source: CredentialStore and SessionManager receive an EntropySource cap. Password salts, session IDs, setup tokens, and later passkey challenges come only from it. If secure randomness is unavailable, credential creation, authenticated session creation, setup-token issuance, and passkey enrollment fail closed. The only remaining boot path is an explicit manifest-gated guest or development profile.
  • Credential backing: CredentialStore is initialized from manifest or cloud-bootstrap verifier/public-credential records plus a bounded RAM overlay for setup-created credentials and disable/rotate state. Until a real storage service exists, any setup-created credential and any disable/rotate action recorded only in that overlay is volatile and both the console UX and audit records must say so. The manifest may carry verifier or public-credential material, not raw passwords or reusable setup tokens.
  • Bounded setup-token/challenge state: CredentialStore owns one bounded table for setup tokens and later WebAuthn challenges. Each record is bound to a purpose, principal/profile, opaque record ID, secret bytes, created/expiry times, and consumed bit. The first redemption attempt consumes the record whether the attempt succeeds or fails, so replay always fails closed and retry requires a newly minted token or challenge. Records are scrubbed on consume or expiry.
  • Auth failure policy: CredentialStore.verify returns only success, denied, or unavailable. ConsoleLogin prints generic failure text and enforces bounded backoff without revealing whether a principal exists, which field mismatched, or whether a verifier came from bootstrap config or the RAM overlay. Permanent lockout is out of scope for the first milestone; bounded delay plus audit is required.
  • Audit and redaction: AuditLog records structured auth/session events with result codes, profile, auth method, reason classes, and opaque credential/token record IDs. Principal/session IDs appear only after successful authentication or when referring to an already minted session; a failed pre-auth attempt logs only a terminal-local event ID plus generic failure class. It must never log raw passwords, verifier bytes, salts, setup-token/challenge secrets, passkey private material, or full terminal lines. When setup creates a volatile credential or RAM-only disable state, the audit event records volatile = true rather than any secret-bearing payload.

Prerequisites

Boot-to-shell should not be selected before these pieces are credible:

  • Default boot uses init-owned manifest execution; the kernel starts only init with fixed bootstrap authority.
  • init can start long-lived services and not just short smoke binaries.
  • ProcessSpawner can launch the shell and login services with exact grants.
  • A TerminalSession path exists. Current Console stays output-oriented; login and shell work should use bounded line input with per-call echo mode and structured cancellation instead of raw console reads.
  • The native text shell exists as a capos-rt binary with caps, inspect, call, spawn, wait, release, and basic error display.
  • EntropySource exists for salts, session IDs, setup tokens, and later WebAuthn challenges, and auth/setup flows fail closed if it is unavailable.
  • There is at least bootstrap verifier/public-credential backing plus a bounded RAM overlay. Durable credential storage can come later, but the first implementation must be honest about whether created credentials survive reboot.
  • Minimal SessionManager, AuthorityBroker, and AuditLog services exist.
  • A restricted launcher or broker wrapper prevents the shell from receiving broad init authority.
  • Web text shell requires networking, HTTP/WebSocket or equivalent, TLS/origin handling, and WebAuthn verification. It can lag local console boot-to-shell. TLS configuration, server certificates, ACME issuance, OCSP stapling, and CT policy are defined in Certificates and TLS; WebAuthn attestation certificate verification uses the CertVerifier from that proposal against a FIDO MDS trust store.
  • Federated OIDC login requires outbound TLS to the IdP discovery and JWKS endpoints, an OAuth client service, and manifest-declared IdP trust records. It depends on networking and the interfaces in OIDC and OAuth2. Device code can land with the local console path once networking exists; authorization code + PKCE lands with the web text shell.

Completed Local Milestone Definition

The local-console boot-to-shell milestone completed when:

  • make run-shell or the default boot path reaches a text login/setup prompt. The focused proofs are make run-terminal for the bounded line-discipline TerminalSession surface, make run-credential for the password-verifier store, make run-login for the password-login path, make run-login-setup for the first-boot setup path, make run-local-users for the manifest-seeded local-operator path, and make run-shell for the anonymous-only path.
  • With a configured password verifier, the console refuses the shell on a bad password and launches it on the correct password.
  • With no console password verifier, the console enters setup mode and requires creating a credential or selecting an explicitly configured local guest or development policy before launching a normal shell.
  • If secure randomness is unavailable, setup and authenticated login fail closed; only explicitly enabled guest or development profiles may continue.
  • Guest console sessions, when enabled, are created through SessionManager.guest() and receive only terminal/tmp/restricted-launcher style caps with no administrative approval path by default.
  • Anonymous sessions are not used as the missing-password console fallback and are not accepted as proof that the ordinary boot-to-shell milestone works.
  • The shell starts with a broker-issued CapSet and can prove at least one typed capability call plus one exact-grant child spawn through a granted launcher or other explicitly scoped spawn authority.
  • ConsoleLogin drops its TerminalSession once the shell starts, and a shell-spawned child without an explicit terminal grant cannot use the terminal.
  • Audit output records setup/auth/session/broker/shell-launch events without leaking secrets.
  • Web text shell, passkey-only enrollment, and remote setup remain later work in this proposal family after the local console path exists.
  • Graphical shell work is not part of the acceptance criteria.

Implementation Plan

  1. Text console substrate. TerminalSession is the first interactive console boundary. Keep Console output-only; terminal services own bounded line buffers, per-call echo mode, and cancellation behavior.

  2. Native shell binary. The shell proposal’s minimal REPL over capos-rt lists CapSet entries, inspect metadata, call granted capabilities including TerminalSession, use a granted restricted launcher or other scoped spawn authority for exact-grant child launch, wait, release, and print typed errors. The ordinary shell profile must not depend on BootPackage or broad ProcessSpawner authority.

  3. Credential store prototype. Manifest/cloud-bootstrap-backed verifier and public-credential records, a bounded RAM overlay for setup-created credentials, EntropySource integration for salts/session IDs/tokens, and Argon2id verification anchor the local path. Host-generated verifier inputs are bootstrap configuration, not acceptance evidence for future credential work.

  4. Console setup/login. The configured-password path and no-password setup path are implemented. Setup creates verifier state through CredentialStore, not ad hoc shell process config. The local password path now prompts for username> before hidden password>, routes SessionManager.login through an account/principal selector plus proof/source metadata, verifies only selected accounts that own console-password, and migrates the existing seeded console password to an explicit default operator account without creating username-enumeration terminal differences. Durable account-local verifier records remain future storage-backed work.

  5. Minimal session and broker. UserSession metadata and the policy broker return a narrow shell bundle. Anonymous bundles stay separate from ordinary shell login, and QEMU proofs show the shell cannot obtain broad boot authority by default.

  6. Audit and failure policy. Generic auth failure handling, bounded attempt backoff, hidden password entry, and redacted audit records are part of the completed local path. Future passkey/setup-token challenge state must preserve the same no-secret logging rule.

  7. Web text shell gateway. After networking and a terminal transport exist, add WebAuthn registration and authentication for the browser-hosted terminal. Support passkey-only enrollment through local setup or explicit bootstrap authority.

  8. Federated OIDC login. Add OAuthClient/OidcIdentityProvider integration to ConsoleLogin (device code) and WebShellGateway (auth code + PKCE). Extend CredentialStore with IdP trust records. Map acr/amr claims to AuthStrength. Require a manifest-declared subject allow-list for administrative sessions.

  9. Durability and recovery. Move credential and IdP-trust records from boot config or RAM into a storage-backed service once storage exists. Define recovery as a credential-admin operation, not an implicit bypass.

Security Notes

  • Password hashing belongs in userspace auth services, not the kernel fast path.
  • WebAuthn challenge state must be single-use and bounded by expiry.
  • The web gateway must validate origin and relying-party ID; otherwise passkey authentication is meaningless.
  • Setup tokens are credentials. They must be short-lived, single-use, audited, and hidden from ordinary process output.
  • Credential records are sensitive even though they are not raw secrets; avoid printing them in debug logs.
  • The shell and any agent running inside it must treat logs, terminal input, files, web pages, and service output as untrusted data.

Non-Goals

  • No graphical shell in this milestone.
  • No passwordless remote first-use takeover.
  • No kernel uid, gid, root, or login mode.
  • No default shell access to broad BootPackage, raw ProcessSpawner, DeviceManager, raw storage, or global supervisor caps.
  • No authentication proof passed through command-line arguments, environment variables, shell variables, audit records, or agent prompts.

Open Questions

  • Which Argon2id parameters fit the early userspace memory budget while still resisting offline guessing?
  • How should durable storage merge bootstrap verifier records with the first RAM overlay once a storage-backed credential service exists?
  • How should local console setup prove physical presence on cloud VMs where serial console access may itself be remote?
  • What is the first acceptable TLS/origin story for QEMU and local development WebAuthn testing?
  • Should passkey-only machines keep a disabled console password slot for later recovery, or should recovery be entirely credential-admin/passkey based?