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.
| Aspect | SSH Shell Gateway | Telnet over TLS |
|---|---|---|
| Transport | Bespoke 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 capOS | Whole 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 auth | Public-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 model | SSH 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 leverage | New 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 population | OpenSSH 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. |
| Composability | One 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 fit | Operator 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
TerminalSessionper connection. - Server-side TLS always; mTLS client certificates as the recommended
user-auth path, with passwords through
CredentialStoreas 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 (ed25519leaf,ed25519orecdsa_p256root acceptable), one AEAD cipher suite (TLS_CHACHA20_POLY1305_SHA256orTLS_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/TrustStorecaps from the start. The QEMU smoke uses a manifest-seeded development leaf exactly the same way ACME issuance would: throughKeyVaultimport andCertificateStore.put. There is no separate dev-only signing surface that production has to retire. - Telnet IAC handling is owned by whichever
TerminalSessionimplementation receives the byte stream — the kernelSocketTerminalSessionfor the plaintext-TCP path, the cleartext- byte-pairTerminalSessionfactory 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-CleartextTerminalSessionFactory 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
TcpListenAuthoritywhose badge is the configured TLS port (992 in the default manifest). - TLS 1.3 server-side handshake and record layer, composed from a
TlsServerConfigcap. The gateway never sees the rawPrivateKey; 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
TerminalSessionFromByteStreamafter 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::Hiddensemantics the kernelSocketTerminalSessionenforces today, including the fix history captured in the recent Telnet IAC handoff commits. - Keep the spawned shell’s view of
TerminalSessionbyte-identical to whatTcpSocket.intoTerminalSessionproduces. The shell must not need to care about the transport. - Treat partial reads, partial writes, peer close, and TLS
close_notifyas ordinaryTerminalSessionclose 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::SocketTerminalSessionbecause the kernel owns the byte stream. Over TLS there is noTcpSocketHandlefor 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 kernelSocketTerminalSessionand 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 rawTcpSocketexactly 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:
TcpListenAuthoritywhose badge is the configured TLS port. Mints exactly oneTcpListenerfor that port and nothing else; rawNetworkManager.createTcpListeneris not granted.TlsServerConfigfor TLS server-side handshake. Not the underlyingPrivateKey,KeyVault,CertificateStoreadministrative surface, orTrustStoremutation.EntropySource, or a narrowedTlsTransportCryptocap 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.TerminalSessionFromByteStreamfor the cleartext-backed terminal.SessionManagerto mint a session at handoff: anonymous in the password path,tlsClientCertin the mTLS path.AuthorityBrokerto request the normal shell bundle profile.RestrictedShellLauncherto spawncapos-shellwith the supplied session and the reviewed pass-through grants only.AuditLogappend 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, rawTcpListenerfactories beyond the configured port, outboundconnectTcp, or any UDP/ICMP authority. - Raw
PrivateKeyaccess,KeyVaultadministration, key generation, key export, or certificate issuance. CertificateStoremutation or trust-store administration. The gateway consumes aTrustStorefor client-cert verification; it cannot add or remove anchors.- Broad
ProcessSpawnerauthority. Shell launch goes throughRestrictedShellLauncheronly. CredentialStoreauthority, 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 runCredentialStoreverification, does not interpret those bytes as credentials, and does not retain them.capos-shellhandlesloginexactly as on the local console.- Any kernel-internal or system-wide
TerminalSessionfactory 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-onlyPrivateKeycap under an explicitSealPolicyallowing 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/Acmeinterfaces are the source of truth. - Rotation lands through
CertificateStore.watch.TlsServerConfigre-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.
User authentication: mTLS path (recommended)
The recommended production user-auth path is mTLS:
TlsServerConfig.clientVerifierreturns aCertVerifierplus aTrustStoreof acceptable client CAs, scoped to the deployment.- The TLS handshake requires a client certificate. The gateway
verifies it through
CertVerifier.verifyChainagainst the client-CATrustStore, with name constraints, EKU (clientAuth), and revocation status enforced by the verifier policy. - On success, the gateway hands the verified leaf to
SessionManager.tlsClientCert(a new mint path mirroringsshPublicKey). The session manager maps subject/SAN/fingerprint to a principal record and allowed shell profiles, and mints aUserSessionwithtlsClientCertauthentication strength. AuthorityBrokerissues the shell bundle for the matched profile;RestrictedShellLauncherspawnscapos-shellwith 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
clientAuthEKU — ends with a TLS handshake alert. No authorization step runs andSessionManager.tlsClientCertis never called. - A client cert that successfully verifies through the configured
CertVerifierbut maps to no principal record causes aSessionManager.tlsClientCertdeny 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
AuthorityBrokerdeny, again before launch.
User authentication: password fallback
Deployments that have not yet provisioned client certs use the existing local-shell path:
-
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.
-
RestrictedShellLauncherspawnscapos-shell, which printslogin:and runslogin/setupagainstCredentialStorewith the same generic-failure / bounded-backoff / audit policy used on the local UART console. -
Password bytes are
LineEcho::Hiddeninput 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
SocketTerminalSessionis 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:
- 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). - 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?).
- Breaks the kernel-IAC ownership boundary. The post-
7a155f4design moved IAC handling into kernelSocketTerminalSessionprecisely 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.
-
Userspace cleartext-byte-pair
TerminalSessionfactory. DefineByteStreamPair,TerminalSessionFromByteStream.wrap, andTerminalLineOptions. Implement against a plaintext userspace byte pair first, with no TLS in the loop. Factor the line-discipline + IAC state machine out ofkernel/src/cap/network.rs::SocketTerminalSessioninto 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 kernelSocketTerminalSessionkeeps owning IAC for the rawTcpSocketpath; 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.
-
TlsServerConfigconsumption with development leaf. Wire a manifest-seeded leaf intoKeyVaultandCertificateStore, composeTlsServerConfigwith the reviewed algorithm policy, and addmake run-telnet-tls-configproving 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). -
telnet-tls-gatewayservice, password path. Boot the userspace gateway against a scopedTcpListenAuthorityfor port 992, terminate one host-loopback TLS 1.3 handshake withopenssl s_client, write the cleartext byte pair into the factory, and run alogin→caps→exittranscript through the existingCredentialStoreflow. Prove the “Service Liveness” rule with repeated connections. -
mTLS user auth. Add
SessionManager.tlsClientCert, defineAuthorizedTlsClientrecords (subject/SAN/fingerprint → principal/profile mapping), wireTlsServerConfig.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 missingclientAuthEKU (TLS handshake alert, noSessionManagercall); verified-but- unmapped cert (SessionManager.tlsClientCertdeny pre-launch with sanitized audit reason); verified+mapped cert with profile mismatch (AuthorityBrokerdeny); accepted cert (UserSessionwithtlsClientCertstrength reaches the shell,capsconfirms boundary). -
Production custody path. Replace the manifest-seeded leaf with an ACME-issued or internal-CA-issued chain through the cert proposal’s
Issuerinterface. Prove rotation throughCertificateStore.watchlands without restart and without breaking in-flight sessions. -
system-telnet-tls.cue,make run-telnet-tls, and the host harness. Default the manifest to mTLS-required with a fallbackpasswordOnlyknob, add cleanup proofs for client disconnect, serverclose_notify, and shell exit, and update the topic indexes, sidebar, andWORKPLAN.mdwhen 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,
TerminalSessionobject,- 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 inSocketTerminalSession, 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, andEntropySource(or a narrowedTlsTransportCryptocap). - Shell for the
TerminalSessionboundary 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 thelogin/setupflow the password fallback path reuses. - SSH Shell Gateway for the parallel
TerminalSessionfactory requirement and theTcpListenAuthority/RestrictedShellLauncher/SessionManagerconventions 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 —
telnetson 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.mdfor the Phase A/B/C boundaries, theTcpListenAuthorityshape, the kernel-side IAC filter, the post-7a155f4IAC handoff fix, and the trust-boundary-debt rule against expanding kernel networking surface.docs/proposals/ssh-shell-proposal.mdfor theRestrictedShellLauncher/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.mdforTlsServerConfig,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.mdforKeyVault,SealPolicy, sign-onlyPrivateKey, andEntropySourceshape.docs/proposals/shell-proposal.mdfor theTerminalSessionboundary and the rule that remote text transports do not become rawByteStream/StdIOsubstitutes.docs/proposals/boot-to-shell-proposal.mdfor thelogin/setupflow the password fallback reuses and theCredentialStorefailure/backoff/audit policy.REVIEW.mdfor 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.mdfor the session-factory precedent: clients receive narrowed sessions from authority-bearing components rather than holding a broad factory themselves. TheTerminalSessionFromByteStream/ gateway split follows that pattern, exactly as the SSH proposal does.docs/research/pingora.mdfor the listener / TLS-termination / service split that informs keeping theTcpListener, 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.mdfor the Plan 9cpuremote-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 pluggableTauth/Rauthauth-fid mechanism, and only after authentication does the clientTattachand 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 plusSessionManager.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
SocketTerminalSessionalready 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
SessionManagerand the bundle issued byAuthorityBroker, exactly as for SSH public keys.