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 current boot path is still a systems bring-up path. It starts fixed services, proves kernel and userspace invariants, and exits cleanly. That is useful for validation, but it is not an operating environment.
The first interactive milestone should be 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.
- Offer a browser-hosted text terminal later in the same milestone family, with WebAuthn/passkey authentication.
- 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
In scope:
- Serial/local text console login and first-boot credential setup.
- Native text shell as the post-login workload.
- Minimal
SessionManager,CredentialStore,AuthorityBroker, andAuditLogpieces needed to launch that shell with an explicit CapSet. - Password verifier records stored with a memory-hard password hash.
- Passkey registration and authentication for a web text shell.
- A passkey-only account path that does not require creating a password first.
- Local recovery/setup policy for machines with no credential records.
Out of scope:
- Graphical shell, desktop session, compositor, GUI app launcher, clipboard, or remote desktop.
- POSIX
/bin/login, PAM,sudo,su, or Unixuid/gidsemantics. - 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 and web authentication 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.
- 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 boot-to-shell path is a userspace service graph started by init after
the manifest executor milestone is complete:
flowchart TD
Kernel[kernel starts init only]
Init[init manifest executor]
Boot[BootPackage]
Cred[CredentialStore]
Session[SessionManager]
Broker[AuthorityBroker]
Audit[AuditLog]
Console[ConsoleLogin]
Web[WebShellGateway]
Launcher[RestrictedShellLauncher]
Shell[Native text shell]
Kernel --> Init
Init --> Boot
Init --> Cred
Init --> Session
Init --> Broker
Init --> Audit
Init --> Console
Init --> Web
Console --> Session
Web --> Session
Session --> Broker
Broker --> Launcher
Launcher --> Shell
Cred --> Session
Audit --> Session
Audit --> Broker
init owns broad boot authority long enough to start the authentication and
session services. It should not spawn the interactive shell directly with broad
boot caps. The broker returns a narrow shell bundle such as:
terminal TerminalSession or Console
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.
Console Login
The local console path has three states.
Password Configured
If CredentialStore has an enabled console password verifier for the selected
principal or profile, ConsoleLogin prompts for the password before launching
the shell.
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, ConsoleLogin records an audit event and applies bounded
backoff. 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, the console does not launch an ordinary shell. It enters setup mode.
Setup mode can:
- create the first console password verifier,
- enroll a first passkey for the web text shell,
- create both credentials,
- choose an explicit local guest or development profile if the manifest permits it.
For normal images, the setup flow must create at least one usable credential or leave the machine without an ordinary interactive shell. 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. 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.
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
The web shell in this milestone 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:
- Browser requests a login challenge.
WebShellGatewayasksSessionManagerorCredentialStorefor a bounded, random challenge tied to the relying-party ID and intended principal.- Browser calls the platform authenticator.
- Gateway verifies the WebAuthn assertion, origin, challenge, credential ID, public-key signature, user-presence/user-verification flags, and sign-count behavior.
SessionManagermints aUserSessionwith auth strengthhardwareKey.AuthorityBrokerreturns the shell bundle for that session/profile.RestrictedShellLauncherstarts 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.
Required Interfaces
These are ordinary capabilities, not kernel modes.
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,
- issue and consume bounded WebAuthn challenges,
- rotate or disable credentials through an authenticated admin path.
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.
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.
WebShellGateway
Terminates the browser terminal session, handles passkey challenge/response, 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.
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, or complete environment dumps.
Prerequisites
Boot-to-shell should not be selected before these pieces are credible:
- Default boot uses init-owned manifest execution; the kernel starts only
initwith fixed bootstrap authority. initcan start long-lived services and not just short smoke binaries.ProcessSpawnercan launch the shell and login services with exact grants.- A terminal input path exists. Current
Consoleis output-oriented; login needs line input, paste boundaries later, and cancellation behavior. - The native text shell exists as a
capos-rtbinary withcaps,inspect,call,spawn,wait,release, and basic error display. - Secure randomness exists for salts, session IDs, WebAuthn challenges, and setup tokens.
- There is at least boot-config-backed credential storage. Durable credential storage can come later, but the first implementation must be honest about whether credentials survive reboot.
- Minimal
SessionManager,AuthorityBroker, andAuditLogservices 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.
Milestone Definition
The “Boot to Shell” milestone is complete when:
make run-shellor the default boot path reaches a text login/setup prompt.- 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.
- 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.
- Audit output records setup/auth/session/broker/shell-launch events without leaking secrets.
- The web text shell can authenticate with a registered passkey and launch the same native text shell profile.
- A passkey-only account can be enrolled through local setup authority or an explicit bootstrap credential, with no password verifier present.
- Graphical shell work is not part of the acceptance criteria.
Implementation Plan
-
Text console substrate. Add
TerminalSessionor extend the console service enough for authenticated line input, echo control, paste/framing markers later, and cancellation. -
Native shell binary. Land the shell proposal’s minimal REPL over
capos-rt: list CapSet entries, inspect metadata, callConsole, spawn a boot-package binary, wait, release, and print typed errors. -
Credential store prototype. Add boot-config-backed credential records and Argon2id verification. If Argon2id is too heavy for the first kernel/userspace environment, use a host-generated verifier in the manifest only as a temporary gate and keep the milestone open until in-system verification is real.
-
Console setup/login. Implement the configured-password path and no-password setup path. The setup code should create credential records through
CredentialStore, not write ad hoc config in the shell process. -
Minimal session and broker. Create
UserSessionmetadata and a static policy broker that returns a narrow shell bundle. Add a manifest-gated local guest bundle and keep anonymous bundles separate from ordinary shell login. Prove the shell cannot obtain broad boot authority by default. -
Audit and failure policy. Add audit records and bounded attempt backoff. Verify logs do not contain raw passwords, verifier bytes, passkey private data, or challenge secrets.
-
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.
-
Durability and recovery. Move credential 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, rawProcessSpawner,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?
- Should the first credential store be manifest-backed, RAM-backed, or wait for the first storage service?
- 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?