# 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](ssh-shell-proposal.md) 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 `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](#the-userspace-cleartext-terminalsession-factory)
  for the ownership rule.

Out of scope (with reasons recorded in
[Considered Alternatives](#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

```mermaid
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](certificates-and-tls-proposal.md) and
[Cryptography and Key Management](cryptography-and-key-management-proposal.md)
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:

```capnp
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 `TcpSocket`s 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.

### User authentication: mTLS path (recommended)

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 `login` → `caps` → `exit` 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](networking-proposal.md) 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](certificates-and-tls-proposal.md) for
  `TlsServerConfig`, `Certificate`, `CertificateChain`, `TrustStore`,
  `CertVerifier`, `CertificateStore.watch`, `Issuer`/ACME, algorithm
  policy, and CT/OCSP plumbing.
- [Cryptography and Key Management](cryptography-and-key-management-proposal.md)
  for sign-only `PrivateKey`, `KeyVault`, `SealPolicy`, and
  `EntropySource` (or a narrowed `TlsTransportCrypto` cap).
- [Shell](shell-proposal.md) 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](boot-to-shell-proposal.md) for `CredentialStore`,
  `SessionManager`, `AuthorityBroker`, and the `login`/`setup` flow
  the password fallback path reuses.
- [SSH Shell Gateway](ssh-shell-proposal.md) for the parallel
  `TerminalSession` factory requirement and the
  `TcpListenAuthority`/`RestrictedShellLauncher`/`SessionManager`
  conventions to mirror.
- [User Identity and Policy](user-identity-and-policy-proposal.md)
  for principal/account/session/profile semantics shared by
  password and mTLS paths.
- [Resource Accounting and Quotas](resource-accounting-proposal.md)
  for listener, socket, handshake-buffer, key-schedule, terminal,
  and shell-process bounds.
- [System Monitoring](system-monitoring-proposal.md) for audit
  record shape and retention boundaries.
- [Storage and Naming](storage-and-naming-proposal.md) 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](#considered-alternatives)).

## Grounding

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

- [`docs/proposals/networking-proposal.md`](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`](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`](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`](cryptography-and-key-management-proposal.md)
  for `KeyVault`, `SealPolicy`, sign-only `PrivateKey`, and
  `EntropySource` shape.
- [`docs/proposals/shell-proposal.md`](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`](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`](../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`](../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`](../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.
