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, andAuditLogpieces 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 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, 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
TerminalSessioncap and drives pre-auth password/setup input through it with per-callecho = 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:
readLinereturns one bounded line or a structuredcancelled/closedresult. 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.ConsoleLoginand setup flows should request smaller bounds than the shell’s ordinary command reader. - Cancellation is line-scoped. Operator abort input returns
cancelledand 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
TerminalSessionlater.
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:
- 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.
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:
ConsoleLogincallsOAuthClient.startDeviceCodeon an IdP that the manifest has configured as acceptable for console login.TerminalSession.writeLineprints the verification URL and user code; the user completes the flow on a separate device.ConsoleLoginpollspollDeviceCodeat the advertisedinterval, honoringslow_down. Expiry is a hard fail.- On
granted,ConsoleLoginpasses the resultingIdTokencap toSessionManager.login(method = "oidc", proof = idTokenRef). SessionManagercallsOidcIdentityProvider.verifyIdTokenwith the client’sIdTokenPolicy, receivesIdTokenClaims, derivesPrincipalInfo.id = hash(iss, sub), derivesauthStrengthfromacr/amr, and mints aUserSession.- 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/amr→AuthStrengthmapping) soSessionManagercan consumeOidcIdentityProvidercaps 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:
CredentialStoreandSessionManagerreceive anEntropySourcecap. 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:
CredentialStoreis 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:
CredentialStoreowns 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.verifyreturns onlysuccess,denied, orunavailable.ConsoleLoginprints 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:
AuditLogrecords 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 recordsvolatile = truerather 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
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
TerminalSessionpath exists. CurrentConsolestays 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-rtbinary withcaps,inspect,call,spawn,wait,release, and basic error display. EntropySourceexists 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, 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.
TLS configuration, server certificates, ACME issuance, OCSP stapling, and
CT policy are defined in
Certificates and TLS;
WebAuthn attestation certificate verification uses the
CertVerifierfrom 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-shellor the default boot path reaches a text login/setup prompt. The focused proofs aremake run-terminalfor the bounded line-disciplineTerminalSessionsurface,make run-credentialfor the password-verifier store,make run-loginfor the password-login path,make run-login-setupfor the first-boot setup path,make run-local-usersfor the manifest-seeded local-operator path, andmake run-shellfor 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.
ConsoleLogindrops itsTerminalSessiononce 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
-
Text console substrate.
TerminalSessionis the first interactive console boundary. KeepConsoleoutput-only; terminal services own bounded line buffers, per-call echo mode, and cancellation behavior. -
Native shell binary. The shell proposal’s minimal REPL over
capos-rtlists CapSet entries, inspect metadata, call granted capabilities includingTerminalSession, 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 onBootPackageor broadProcessSpawnerauthority. -
Credential store prototype. Manifest/cloud-bootstrap-backed verifier and public-credential records, a bounded RAM overlay for setup-created credentials,
EntropySourceintegration 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. -
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 forusername>before hiddenpassword>, routesSessionManager.loginthrough an account/principal selector plus proof/source metadata, verifies only selected accounts that ownconsole-password, and migrates the existing seeded console password to an explicit defaultoperatoraccount without creating username-enumeration terminal differences. Durable account-local verifier records remain future storage-backed work. -
Minimal session and broker.
UserSessionmetadata 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. -
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.
-
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.
-
Federated OIDC login. Add
OAuthClient/OidcIdentityProviderintegration toConsoleLogin(device code) andWebShellGateway(auth code + PKCE). ExtendCredentialStorewith IdP trust records. Mapacr/amrclaims toAuthStrength. Require a manifest-declared subject allow-list for administrative sessions. -
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, 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?
- 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?