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: Telnet over TLS Shell

A production remote-shell path that wraps the existing Telnet TerminalSession handoff in TLS 1.3, intended as a peer to the SSH Shell Gateway rather than a subordinate or research-only intermediate. The two paths target different operational profiles and share the same underlying capability surface.

Why Both, And Why Not Just SSH

The networking proposal and docs/status.md correctly call out plaintext Telnet on 127.0.0.1:2323 as a loopback-only research demo and name the SSH Shell Gateway as the production remote-shell successor to that demo. That comparison is between SSH and plaintext Telnet, not between SSH and TLS-wrapped Telnet. Once TLS is in the picture the operational tradeoffs change, and capOS has good reasons to expose both paths in production.

AspectSSH Shell GatewayTelnet over TLS
TransportBespoke SSH-2 binary packet protocol; the gateway parses KEXINIT, channel-open, requests, etc.TLS 1.3 record layer plus a Telnet IAC stream consumed by whichever TerminalSession implementation receives the bytes (kernel SocketTerminalSession for plaintext TCP, the cleartext-pair factory for TLS), through a shared state-machine module.
Protocol surface inside capOSWhole SSH message set must be parsed and reviewed even when only one channel is allowed; algorithm-policy parser; channel/forwarding/agent/X11/subsystem reject paths.TLS handshake (rustls or equivalent) + the shared Telnet IAC state machine already implemented for the plaintext path. The gateway itself is mostly handshake plumbing and does not parse IAC.
User authPublic-key built into the protocol (SshHostKey, AuthorizedKeyStore, SessionManager.sshPublicKey). Password optional and gated.Two paths: passwords through the local CredentialStore flow, or mTLS client certificates verified against a TrustStore and mapped through SessionManager.tlsClientCert.
Identity modelSSH key fingerprints, principal records keyed off public-key bytes, custom rotation/audit story.X.509 with subjects/SANs and the project’s existing certs/TLS proposal: ACME issuance, CT, OCSP, CertificateStore.watch, mTLS, pinning, name constraints. Rotation and revocation share infrastructure with everything else TLS.
Ecosystem leverageNew SSH-only operational track: authorized_keys, host-key custody, fingerprint pinning, key rotation tooling.Reuses the PKI/ACME track that capOS already needs for cloud KMS HTTPS, the web-shell gateway, mTLS between services, monitoring egress, OIDC, and audit.
Client populationOpenSSH and friends; familiar to operators.Standard TLS-capable telnet clients (telnets:// on port 992) and openssl s_client piping into telnet’s IAC discipline; clients are scarcer than for SSH but tooling exists.
ComposabilityOne protocol; one auth model.Transport identity (server cert, optional client cert) is orthogonal to user auth; deployments can layer mTLS-issued client identity and a passkey/OIDC step-up if they want, without inventing protocol extensions.
Best fitOperator CLI access where SSH client tooling and public-key login are the right defaults.Workload-to-workload terminal access between services that already speak TLS, deployments that prefer corporate-CA client certs for identity, browser/web bridges that already terminate TLS, and any environment where minimising new protocol surface is a security goal.

The two paths are complementary. A production capOS deployment can expose both, neither, or just one, depending on its operational stack. This proposal defines the TLS path so the certs/TLS, key-management, and PKI proposals have a real ingress consumer and so the project does not implicitly couple “remote shell” to “OpenSSH-shaped operator workflow.”

Scope

The first milestone is deliberately narrow but production-shaped from day one — there is no separate “demo-only” gate after which the proposal pivots to a different cert custody story:

  • TLS 1.3 only. Implicit-TLS variant: TLS handshake first, then a normal Telnet byte stream over the established TLS record layer. IANA-registered port 992 (“telnets”) is the default; deployments pick their own port via the manifest.
  • One interactive TerminalSession per connection.
  • Server-side TLS always; mTLS client certificates as the recommended user-auth path, with passwords through CredentialStore as the fallback for deployments that have not provisioned client certs yet.
  • Algorithm policy is a single reviewed set: one ECDHE group (x25519), one signature algorithm pair (ed25519 leaf, ed25519 or ecdsa_p256 root acceptable), one AEAD cipher suite (TLS_CHACHA20_POLY1305_SHA256 or TLS_AES_128_GCM_SHA256). No downgrade negotiation surface, no TLS 1.2 fallback.
  • Cert and key custody routes through the cert proposal’s KeyVault/CertificateStore/CertVerifier/TrustStore caps from the start. The QEMU smoke uses a manifest-seeded development leaf exactly the same way ACME issuance would: through KeyVault import and CertificateStore.put. There is no separate dev-only signing surface that production has to retire.
  • Telnet IAC handling is owned by whichever TerminalSession implementation receives the byte stream — the kernel SocketTerminalSession for the plaintext-TCP path, the cleartext- byte-pair TerminalSession factory for the TLS path — both sharing a single IAC state machine module so the byte rules do not fork. The TLS gateway terminates TLS, hands a cleartext byte pair to the factory, and itself parses no IAC. See The Userspace-Cleartext TerminalSession Factory for the ownership rule.

Out of scope (with reasons recorded in Considered Alternatives where the question is liable to come up again):

  • STARTTLS-via-Telnet-options upgrade.
  • TLS 1.2, RSA key exchange, non-AEAD ciphers, compression, externally provided session-ticket keys, multi-cipher policy.
  • SSH-style channel multiplexing, port forwarding, agent forwarding, X11, subsystem requests.
  • In-kernel TLS termination.

Components

flowchart TD
    Client[telnets:// client / openssl s_client] -->|TCP + TLS 1.3| Gateway[telnet-tls-gateway]
    Gateway --> Listen[TcpListenAuthority badge 992]
    Gateway --> TlsCfg[TlsServerConfig cap]
    TlsCfg --> Key[PrivateKey sign-only]
    TlsCfg --> ChainSrc[CertificateStore.watch]
    TlsCfg --> Verifier[Optional CertVerifier + TrustStore for mTLS]
    Verifier --> ClientTrust[TrustStore for client CA]
    Gateway --> Sessions[SessionManager]
    Gateway --> Broker[AuthorityBroker]
    Gateway --> Launcher[RestrictedShellLauncher]
    Gateway --> Audit[AuditLog]
    Gateway --> Factory[TerminalSessionFromByteStream]

    Factory --> Term[Cleartext-backed TerminalSession]
    Launcher --> Shell[capos-shell]
    Term --> Shell
    Sessions --> Broker
    Broker --> Bundle[Scoped shell bundle]
    Bundle --> Shell

The shape mirrors the SSH proposal one-for-one. Only the transport authority changes.

telnet-tls-gateway is the only network-facing component. It owns:

  • The TCP listener, acquired through a manifest-declared TcpListenAuthority whose badge is the configured TLS port (992 in the default manifest).
  • TLS 1.3 server-side handshake and record layer, composed from a TlsServerConfig cap. The gateway never sees the raw PrivateKey; the TLS config encapsulates sign-only key authority, the certificate-chain source, the optional client-cert verifier, and the algorithm policy.
  • The cleartext byte pair (read half + write half) produced by the TLS layer, immediately handed to TerminalSessionFromByteStream after the handshake completes. The gateway implements no Telnet, echo, line-discipline, or terminal logic.

TlsServerConfig, TrustStore, CertVerifier, CertificateStore.watch, KeyVault, SealPolicy, EntropySource are not defined here. They are the caps the Certificates and TLS and Cryptography and Key Management proposals already specify.

RestrictedShellLauncher and the broker/session/credential plumbing are unchanged from the plaintext demo and the SSH proposal. The spawned capos-shell receives only terminal, child-local stdio, and the broker-issued shell bundle (session, creds, sessions, audit, broker, optional shell_config). It does not see TLS, certificate, trust-store, key, listener, raw socket, or gateway-protocol authority.

The Userspace-Cleartext TerminalSession Factory

The plaintext demo terminates the byte stream inside the kernel: TcpSocket.intoTerminalSession consumes a connected accepted socket and returns a move-only TerminalSession. The kernel SocketTerminalSession owns line discipline, password echo policy, and IAC filtering.

That model does not extend to TLS. TLS termination must be userspace — adding rustls to the kernel would be a substantial expansion of the in-kernel networking surface, exactly the expansion the networking proposal’s “trust-boundary debt” paragraph forbids. The kernel TCP path was acceptable because the bytes already crossed that boundary; TLS records do not.

This means TLS-backed remote shells need a different TerminalSession construction surface that consumes a userspace-owned bidirectional cleartext byte pair. Sketch:

interface ByteStreamPair {
    inbound  @0 () -> (rx :ByteStream);
    outbound @1 () -> (tx :ByteStream);
    closeHint @2 () -> (hint :CloseHint);
}

interface TerminalSessionFromByteStream {
    wrap @0 (pair :ByteStreamPair, options :TerminalLineOptions)
        -> (term :TerminalSession);
}

Line discipline (cooked vs raw, password echo policy, paste handling, CRLF state) belongs inside the implementation of wrap, not in the gateway. The implementation must:

  • Preserve the same LineEcho::Hidden semantics the kernel SocketTerminalSession enforces today, including the fix history captured in the recent Telnet IAC handoff commits.
  • Keep the spawned shell’s view of TerminalSession byte-identical to what TcpSocket.intoTerminalSession produces. The shell must not need to care about the transport.
  • Treat partial reads, partial writes, peer close, and TLS close_notify as ordinary TerminalSession close events, not transport-specific errors leaking to the shell.
  • Own Telnet IAC handling for the cleartext byte pair. Over TCP, IAC ownership lives inside kernel/src/cap/network.rs::SocketTerminalSession because the kernel owns the byte stream. Over TLS there is no TcpSocketHandle for the kernel to filter; the cleartext bytes reach the terminal only through this factory. IAC parsing therefore must move into the factory’s implementation rather than being refused. The IAC state machine itself (option negotiation, suppress- go-ahead, echo policy, the recent NUL-prefixed-password and staircase-output fixes) becomes a shared module the kernel SocketTerminalSession and the cleartext-pair factory both call into; neither owns IAC end-to-end alone, and neither version forks the byte rules. The kernel keeps responsibility for IAC over a raw TcpSocket exactly as today; the factory takes responsibility for IAC over a userspace cleartext byte pair.

This factory is also what the SSH Shell Gateway needs: SSH channel-backed terminals are not connected TcpSockets either. This proposal therefore defines the surface in a transport-neutral way. Whichever of SSH or Telnet-over-TLS lands first will deliver TerminalSessionFromByteStream, and the other reuses it.

Authority Model

The gateway receives only the capabilities required for its job:

  • TcpListenAuthority whose badge is the configured TLS port. Mints exactly one TcpListener for that port and nothing else; raw NetworkManager.createTcpListener is not granted.
  • TlsServerConfig for TLS server-side handshake. Not the underlying PrivateKey, KeyVault, CertificateStore administrative surface, or TrustStore mutation.
  • EntropySource, or a narrowed TlsTransportCrypto cap that owns entropy and exposes only TLS handshake, record-layer, rekey, and random-padding operations. Random material for handshake nonces, key derivation, and record nonces never comes from ambient process state.
  • TerminalSessionFromByteStream for the cleartext-backed terminal.
  • SessionManager to mint a session at handoff: anonymous in the password path, tlsClientCert in the mTLS path.
  • AuthorityBroker to request the normal shell bundle profile.
  • RestrictedShellLauncher to spawn capos-shell with the supplied session and the reviewed pass-through grants only.
  • AuditLog append authority for connection, handshake outcome, authentication outcome, shell launch, and teardown records. Audit records carry stable reason codes; they do not carry private key material, certificate private parts, raw entropy, decrypted password bytes, or terminal content.

It explicitly does not receive:

  • Raw NetworkManager, raw TcpListener factories beyond the configured port, outbound connectTcp, or any UDP/ICMP authority.
  • Raw PrivateKey access, KeyVault administration, key generation, key export, or certificate issuance.
  • CertificateStore mutation or trust-store administration. The gateway consumes a TrustStore for client-cert verification; it cannot add or remove anchors.
  • Broad ProcessSpawner authority. Shell launch goes through RestrictedShellLauncher only.
  • CredentialStore authority, and no parsing, logging, audit, or storage authority for credential bytes. The gateway necessarily has plaintext password bytes in its TLS-record and cleartext-pair buffers while a record is being consumed (see the password fallback section’s TCB note); it does not run CredentialStore verification, does not interpret those bytes as credentials, and does not retain them. capos-shell handles login exactly as on the local console.
  • Any kernel-internal or system-wide TerminalSession factory beyond the cleartext-byte-pair construction surface.

The spawned shell does not gain TLS, certificate, trust-store, key, network, listener, raw socket, or gateway-protocol authority. The boundary the plaintext demo proves with caps is preserved verbatim.

Authentication

Server identity

Server identity is asserted through the leaf certificate carried by TlsServerConfig. Custody routes through the cert/key proposals from the start:

  • The leaf private key is a KeyVault-backed sign-only PrivateKey cap under an explicit SealPolicy allowing only TLS server-side signing.
  • The leaf chain is produced through whichever issuance path the deployment uses: ACME for internet-facing endpoints, manifest-issued for development and air-gapped, internal-CA-issued for corporate fleets. The cert proposal’s Issuer/Acme interfaces are the source of truth.
  • Rotation lands through CertificateStore.watch. TlsServerConfig re-derives its leaf for the next handshake; existing TLS sessions finish on the old chain. No gateway restart, no SIGHUP, no filesystem signaling.

The QEMU development manifest seeds a leaf and key through the same cap surface — the cert is imported into KeyVault and CertificateStore, not exposed through a parallel “dev-only” signing cap. Smoke harnesses pin the development leaf by SHA-256 SPKI; deploys pin or trust through their normal trust path.

The recommended production user-auth path is mTLS:

  1. TlsServerConfig.clientVerifier returns a CertVerifier plus a TrustStore of acceptable client CAs, scoped to the deployment.
  2. The TLS handshake requires a client certificate. The gateway verifies it through CertVerifier.verifyChain against the client-CA TrustStore, with name constraints, EKU (clientAuth), and revocation status enforced by the verifier policy.
  3. On success, the gateway hands the verified leaf to SessionManager.tlsClientCert (a new mint path mirroring sshPublicKey). The session manager maps subject/SAN/fingerprint to a principal record and allowed shell profiles, and mints a UserSession with tlsClientCert authentication strength.
  4. AuthorityBroker issues the shell bundle for the matched profile; RestrictedShellLauncher spawns capos-shell with that bundle and the cleartext-backed terminal.

The session manager’s mapping is intentionally explicit. A verified client cert proves “this private key signed this handshake,” not “this is user X.” Mapping subject/SAN to a principal is a separate authorization step that lives in SessionManager, exactly as AuthorizedKeyStore does for SSH public keys. Anonymous holders of a trusted cert do not silently become privileged accounts.

mTLS user auth fails closed without ever reaching the shell. The failure path is staged so transport verification and authorization stay distinct, mirroring how SSH AuthorizedKeyStore and SessionManager.sshPublicKey separate “key signature is valid” from “key maps to a principal”:

  • A client cert that fails the TLS trust path — untrusted issuer, expired, revoked, signature invalid, name constraint violation, missing clientAuth EKU — ends with a TLS handshake alert. No authorization step runs and SessionManager.tlsClientCert is never called.
  • A client cert that successfully verifies through the configured CertVerifier but maps to no principal record causes a SessionManager.tlsClientCert deny with a sanitized audit reason code, before any shell launch. Verified-but-unmapped certs are an authorization failure, not a transport failure, and must not be collapsed into the TLS alert above.
  • A profile mismatch between the requested shell bundle and the mapped principal’s allowed profiles causes an AuthorityBroker deny, again before launch.

User authentication: password fallback

Deployments that have not yet provisioned client certs use the existing local-shell path:

  1. The TLS handshake completes with no client certificate (or with a client cert that the deployment has explicitly marked “transport-only”), and the gateway mints an anonymous session.

  2. RestrictedShellLauncher spawns capos-shell, which prints login: and runs login/setup against CredentialStore with the same generic-failure / bounded-backoff / audit policy used on the local UART console.

  3. Password bytes are LineEcho::Hidden input through the terminal session. The gateway implements no Telnet, line-discipline, or credential parsing of plaintext beyond moving bytes between the TLS record layer and the cleartext byte pair, and never logs password bytes or includes them in audit records or proof transcripts.

    Plaintext password bytes do exist in gateway-mapped TLS record-layer buffers and in the cleartext byte pair while the record is being consumed; that is unavoidable for any in-process TLS terminator and must be acknowledged honestly. The gateway is therefore part of the password-fallback TCB, comparable to the way the kernel SocketTerminalSession is part of the plaintext demo’s TCB today. The mTLS path is preferred precisely because it does not put password bytes on the wire or through the gateway in the first place.

This is weaker than mTLS but the trust boundary is no larger than the local console: the kernel TCB plus one terminator-shaped component (the gateway here, SocketTerminalSession for the local UART or plaintext Telnet demo). It exists so deployments can ship Telnet-over-TLS before completing client-cert provisioning, not as a recommended end state.

Step-up paths (future)

Deployments may want to combine transport-level identity (mTLS) with an additional human factor (passkey, OIDC, TOTP). Step-up is the shell’s responsibility, not the gateway’s: capos-shell gains a stepUp command in a separate proposal, the gateway does not short-circuit it. Treating mTLS plus passkey as orthogonal layers is one of the reasons this path exists alongside SSH at all.

Considered Alternatives

STARTTLS via Telnet options

Rejected. Three reasons, in decreasing order of weight:

  1. No mainline client support. Generic Telnet+STARTTLS has no IETF-standardised binding. RFC 2941 (Telnet Authentication Option) and RFC 2946 (Telnet Data Encryption Option) are generic frameworks; the only concrete TLS binding lives in TN3270E (mainframe terminal emulators such as x3270, IBM Personal Communications, and Vista TN3270). BSD/netkit telnet — the standard Linux client and the one capOS already harnesses — does not speak it. GNU inetutils telnet, the Microsoft Windows telnet client, and PuTTY do not speak it. Targeting STARTTLS would commit capOS to a TN3270E-shaped client population it has no reason to address, while excluding the implicit-TLS clients that do exist (telnets://, openssl s_client, modern TLS-capable telnet implementations).
  2. Pre-handshake plaintext window. STARTTLS requires plaintext IAC option exchange before TLS. That window leaks client identity, supports active downgrade attacks (server claims STARTTLS support is unavailable, expecting cleartext fallback), and complicates audit (where does “I refused to start TLS” log, and how does the server distinguish a legitimate non-TLS client from a downgrade attempt?).
  3. Breaks the kernel-IAC ownership boundary. The post-7a155f4 design moved IAC handling into kernel SocketTerminalSession precisely so the userspace gateway never has to do a pre-handoff recv. STARTTLS forces the gateway back into pre-handoff IAC parsing, complete with its own state machine to detect the STARTTLS option and decide whether to invoke TLS. That is more userspace networking code, not less.

If a future deployment specifically needs TN3270E-style STARTTLS for mainframe interoperation, it is a separate proposal with its own authority model — not a generalisation of this one.

In-kernel TLS termination

Rejected. The networking proposal’s “trust-boundary debt” paragraph explicitly forbids expanding kernel-side networking surface for its own sake. TLS termination is large, well-served by rustls in userspace, and gains nothing by living in the kernel.

A single “remote shell” proposal covering both SSH and TLS

Rejected. The two paths share a TerminalSession factory and the broker/session/launcher plumbing, but their transport, key custody, client population, and user-auth ergonomics differ enough that collapsing them produces a worse design document. They are described separately, sized separately, and can be implemented and audited independently.

Implementation Slices

Slices land in this order. None is a single opaque commit. Slice 1 is shared with the SSH gateway and may be delivered by either project.

  1. Userspace cleartext-byte-pair TerminalSession factory. Define ByteStreamPair, TerminalSessionFromByteStream.wrap, and TerminalLineOptions. Implement against a plaintext userspace byte pair first, with no TLS in the loop. Factor the line-discipline + IAC state machine out of kernel/src/cap/network.rs::SocketTerminalSession into a shared module the kernel TCP path and the cleartext-pair factory both call into; both paths must produce byte-identical output for echo policy, hidden password, CRLF state, IAC option negotiation, and peer close. The kernel SocketTerminalSession keeps owning IAC for the raw TcpSocket path; the factory owns IAC for the cleartext-pair path. Either project (this proposal or the SSH gateway) may deliver this slice; both projects depend on it.

    No SSH or TLS terminal transport slice should proceed past fixture work until this factory exists, IAC/line discipline is factored, hidden password behavior is byte-identical to the raw TCP terminal, and repeated close/reconnect proofs pass.

  2. TlsServerConfig consumption with development leaf. Wire a manifest-seeded leaf into KeyVault and CertificateStore, compose TlsServerConfig with the reviewed algorithm policy, and add make run-telnet-tls-config proving the cap signs handshake transcripts, refuses non-allow-listed algorithms, and never exposes private key bytes in proof logs. The dev path uses the same caps as production; only the issuance source differs (manifest import vs ACME / internal CA).

  3. telnet-tls-gateway service, password path. Boot the userspace gateway against a scoped TcpListenAuthority for port 992, terminate one host-loopback TLS 1.3 handshake with openssl s_client, write the cleartext byte pair into the factory, and run a logincapsexit transcript through the existing CredentialStore flow. Prove the “Service Liveness” rule with repeated connections.

  4. mTLS user auth. Add SessionManager.tlsClientCert, define AuthorizedTlsClient records (subject/SAN/fingerprint → principal/profile mapping), wire TlsServerConfig.clientVerifier, and prove the four staged states with the trust-path/authorize distinction the mTLS auth section already requires: trust-path failure such as untrusted issuer, expired, revoked, signature invalid, name-constraint violation, or missing clientAuth EKU (TLS handshake alert, no SessionManager call); verified-but- unmapped cert (SessionManager.tlsClientCert deny pre-launch with sanitized audit reason); verified+mapped cert with profile mismatch (AuthorityBroker deny); accepted cert (UserSession with tlsClientCert strength reaches the shell, caps confirms boundary).

  5. Production custody path. Replace the manifest-seeded leaf with an ACME-issued or internal-CA-issued chain through the cert proposal’s Issuer interface. Prove rotation through CertificateStore.watch lands without restart and without breaking in-flight sessions.

  6. system-telnet-tls.cue, make run-telnet-tls, and the host harness. Default the manifest to mTLS-required with a fallback passwordOnly knob, add cleanup proofs for client disconnect, server close_notify, and shell exit, and update the topic indexes, sidebar, and WORKPLAN.md when the slice lands.

Each slice keeps the kernel networking surface untouched. New TLS state lives in the userspace gateway; new line-discipline state, if any, stays inside the TerminalSession factory’s implementation.

Resource And Teardown Rules

The gateway must enforce fixed per-connection bounds and fail closed when they are exceeded. Disconnect, TCP close, TLS close_notify, failed handshake, terminal-factory error, shell exit, and gateway shutdown must all release the same resources:

  • accepted socket,
  • TLS connection state (handshake buffers, key schedule, record-layer buffers),
  • cleartext byte pair,
  • TerminalSession object,
  • spawned shell handle,
  • broker-issued grants,
  • audit correlation record.

Shell exit closes the cleartext byte pair, which closes the TLS layer, which closes the TCP socket. Client disconnect or TLS close_notify closes the TLS layer, which closes the byte pair, which the shell observes as a normal TerminalSession close. There is no privileged “tear down everything” path that bypasses the byte-pair lifecycle.

The accept loop applies the same shape as the post-7a155f4 plaintext gateway: per-connection failures (handshake error, factory error, launch error, shell wait error) are log-and-continue events; setup-time failures (listener creation, broker bootstrap, TLS config acquisition) and accept itself remain fail-closed. The “Service Liveness” review rule applies verbatim.

Threat Model And Honest Limits

What Telnet-over-TLS gives, with TLS 1.3 + AEAD + ECDHE + deployment-issued or pinned-development leaf:

  • Confidentiality and integrity against passive and active network observers.
  • Forward secrecy of session bytes after the connection ends.
  • Per-session randomness (replay protection) from the TLS handshake.
  • Server identity assertion as good as the deployment’s trust path: ACME-issued public chain, corporate-CA chain, or SPKI pinning in the QEMU smoke.
  • With mTLS: cryptographic client identity tied to PKI, with rotation and revocation on the same operational track as the rest of the deployment’s TLS estate.

What it does not give:

  • SSH-style channel multiplexing, exec, port forwarding, agent forwarding, subsystem requests. These are explicit non-goals; if they are needed, the SSH gateway is the right path.
  • Resistance against an attacker who can replace the deployment’s trust path on the client side. SPKI pinning in the harness mitigates this for the QEMU smoke; deployments must use a real trust anchor.
  • Stronger user auth than the deployment provisioned. mTLS without principal mapping is just transport; password fallback without step-up is just CredentialStore. The gateway does not synthesise authority it was not given.

This proposal does not claim Telnet-over-TLS is “as secure as SSH” or “more secure than SSH.” It is a different protocol with a different operational profile and a smaller surface to review. Whether that profile suits a given deployment is an operational decision, not a default.

Dependencies

  • Networking for the existing TcpListenAuthority, kernel-side IAC handling in SocketTerminalSession, the host-loopback exposure rule, and the trust-boundary-debt paragraph this proposal must not extend.
  • Certificates and TLS for TlsServerConfig, Certificate, CertificateChain, TrustStore, CertVerifier, CertificateStore.watch, Issuer/ACME, algorithm policy, and CT/OCSP plumbing.
  • Cryptography and Key Management for sign-only PrivateKey, KeyVault, SealPolicy, and EntropySource (or a narrowed TlsTransportCrypto cap).
  • Shell for the TerminalSession boundary and the rule that remote text transports do not turn the shell into a raw byte-stream consumer.
  • Boot to Shell for CredentialStore, SessionManager, AuthorityBroker, and the login/setup flow the password fallback path reuses.
  • SSH Shell Gateway for the parallel TerminalSession factory requirement and the TcpListenAuthority/RestrictedShellLauncher/SessionManager conventions to mirror.
  • User Identity and Policy for principal/account/session/profile semantics shared by password and mTLS paths.
  • Resource Accounting and Quotas for listener, socket, handshake-buffer, key-schedule, terminal, and shell-process bounds.
  • System Monitoring for audit record shape and retention boundaries.
  • Storage and Naming for the capability-native storage path that production leaf certs and client-cert principal records become durable through.

External standards grounding:

  • IANA Service Name and Transport Protocol Port Number Registry — telnets on TCP/992 (the implicit-TLS variant the default manifest binds).
  • RFC 8446 (TLS 1.3). Older TLS RFCs are listed only to document why they are explicitly out of scope.
  • RFC 854/855/856/857/858 (Telnet, option negotiation, binary, suppress-go-ahead, echo) for the upper protocol the kernel IAC filter already implements.
  • RFC 5280 (X.509 PKI) and RFC 8555 (ACME) for the certificate chain and issuance paths.
  • RFC 2941 / RFC 2946 cited only as the explicitly-rejected STARTTLS-style alternative (see Considered Alternatives).

Grounding

In-tree project docs read or re-read while shaping this proposal:

  • docs/proposals/networking-proposal.md for the Phase A/B/C boundaries, the TcpListenAuthority shape, the kernel-side IAC filter, the post-7a155f4 IAC handoff fix, and the trust-boundary-debt rule against expanding kernel networking surface.
  • docs/proposals/ssh-shell-proposal.md for the RestrictedShellLauncher / SessionManager / AuthorityBroker / scoped-listener pattern and the staged transport-verify-then-authorize separation that the mTLS path now mirrors.
  • docs/proposals/certificates-and-tls-proposal.md for TlsServerConfig, CertVerifier, TrustStore, CertificateStore.watch, Issuer/ACME, and the rotation-without- restart rule the production-custody slice depends on.
  • docs/proposals/cryptography-and-key-management-proposal.md for KeyVault, SealPolicy, sign-only PrivateKey, and EntropySource shape.
  • docs/proposals/shell-proposal.md for the TerminalSession boundary and the rule that remote text transports do not become raw ByteStream/StdIO substitutes.
  • docs/proposals/boot-to-shell-proposal.md for the login/setup flow the password fallback reuses and the CredentialStore failure/backoff/audit policy.
  • REVIEW.md for the Service Liveness rule applied to the gateway accept loop, the design-grounding requirement that produced this section, and the proposal-doc shape (status header, last-reviewed timestamp with timezone, relative links).

docs/research/ files read for prior-art grounding:

  • docs/research/genode.md for the session-factory precedent: clients receive narrowed sessions from authority-bearing components rather than holding a broad factory themselves. The TerminalSessionFromByteStream / gateway split follows that pattern, exactly as the SSH proposal does.
  • docs/research/pingora.md for the listener / TLS-termination / service split that informs keeping the TcpListener, the TLS terminator, and the application-shaped shell-launch authority on separate caps. The TCB-acknowledgement paragraph in the password-fallback section is grounded in this separation: TLS termination puts plaintext in the terminator’s memory by construction, and the right answer is to size and bound the terminator, not to claim it never sees the bytes.
  • docs/research/plan9-inferno.md for the Plan 9 cpu remote-shell precedent: a CPU server is reached over a connection-oriented transport (originally TCP, with TLS/SSL added later), the client authenticates through 9P’s pluggable Tauth/Rauth auth-fid mechanism, and only after authentication does the client Tattach and run an interactive shell. Inferno’s certificate-based authentication model is the same shape with X.509 instead of Kerberos. The relevance here is structural: remote-CLI access can be built around connection- oriented authenticated transports with verification and authorization as separate stages, exactly the split this proposal uses for mTLS plus SessionManager.tlsClientCert. capOS does not adopt Plan 9’s namespace-as-authority model — that is the wrong primitive for a Cap’n Proto-typed system — but the staged authenticate-then-attach pattern validates the design.

No other docs/research/ file is directly applicable: the seL4, Zircon, EROS/CapROS/Coyotos, LLVM, capnp/OS error handling, IX-on-capOS hosting, and out-of-kernel scheduling reports do not address remote-shell transport choice or PKI integration in ways that change this proposal.

Non-Goals

  • Replacing or subordinating the SSH Shell Gateway. The two are peer production paths.
  • Telnet-over-TLS as a research-only or demo-only path. Production custody (KeyVault + CertificateStore.watch + ACME / internal CA) is the target shape from slice 5; the manifest-seeded development leaf is a stepping stone, not a parallel architecture.
  • STARTTLS via Telnet options.
  • TLS 1.2 or any cipher-policy negotiation surface that allows downgrade.
  • Adding rustls, in-kernel TLS, or any new in-kernel networking parser beyond what SocketTerminalSession already owns.
  • SSH-style channel multiplexing, exec, port forwarding, agent forwarding, X11, subsystems.
  • Treating a verified client cert as authority. Authority comes from the principal mapping in SessionManager and the bundle issued by AuthorityBroker, exactly as for SSH public keys.