sshd
sshd is the box’s front door: SSH, on ipd’s port 22. It authenticates a person’s key with the
steward’s answer, has keyd sign the key exchange with the host key it never holds, and carries
each session over its own channel, labelled with the session’s labels. It serves /dev/cons to
each session on that channel, and ssh approve@box, where only the steward talks. For file
transfers it only relays: the steward starts a transfer server per channel, holding the session’s
file binds and its audit connection, and nothing else. It uses sunset, an SSH library in no_std Rust without allocation.
Purpose
People reach the box only over SSH, so the one process that parses every byte from the network
before login is also the one that decides which channel a session’s output may reach. sshd keeps
both small: it holds no keys and decides no policy. Whose key a login used is the steward’s
question, the host key’s signature is keyd’s, and a channel’s labels come from the session the
steward started.
Interface
The core and its platforms
Status: built · tested (10)
- bench:sshd-host-tests
- bench:sshd-build
- bench:sshd-loopback-logins
- bench:sshd-loopback-r67
- bench:sshd-loopback-interrupt
- bench:sshd-loopback-independent
- bench:sshd-loopback-window-change
- bench:sshd-loopback-window-change-zero
- bench:sshd-loopback-env-refused
- bench:bench-ssh-loopback
sshd is a core and a platform. The core runs sunset over byte slices and makes every decision
this page states: the login name, the key checks, a channel’s labels, and what a channel may not
do. The platform is only what the core asks for, and is one trait:
- sign an exchange:
keyd’ssign_ssh_exchange, given the transcript’s parts (keyd); holds(key):keyd’s answer;- login: the steward’s answer, a session and its labels or a refusal (steward);
- a session’s console: its bytes each way, the window size, the interrupt, and its end;
- a refusal: the kind of each channel request the core refuses (
env,exec,subsystem, and anypty-req,shell,window-change,signalorbreakit does not pass on), as a name the core chooses; a labelled session’s shell without a pty is named apart,shell-without-pty, for the audit of R67. Never the request’s content: anenvname or value is the client’s bytes, and a log line built from it would be a line the client wrote.
The connection’s bytes are not in the trait: the core takes and gives slices, and the platform
moves them. On the box the platform is ipd’s listen scope, keyd’s ssh_host badge, the
steward and /dev/cons. On the build host it is a host tool, so the bench logs in with OpenSSH’s
ssh long before init and the steward exist
(against Redoubt’s sshd):
- Transport: standard input and output, one connection per process, as
sshstarts it for aProxyCommand; a log file of the server’s own lines. A client that hangs up, assshdoes after a refused login, ends the connection without an error. A write to a client that has gone can still fail (a broken pipe, aftersshrefuses the host key); the bench sends the server’s standard error to its log, so nothing of the server’s followsssh’s own last line. - Signer:
keyd’s own server code, in the same process, given one key inkeyd’s argument form,name,ssh_host,seed. So OpenSSH verifies a signature over the exchange hashkeydbuilt, end to end. The host tool tests the protocol, the login flow and the channel rules, not key separation: itskeydshares its memory, where on the box it is a separate process. - Logins: a fixed table. Principal
Plogs in with its test key fromtests/keys/, asP(no labels) orP+L(labels{P-L}); anything else is refused. - Console: a scripted line console, no shell:
echo,sleep,tty(whether the channel has a pty),labels(the channel’s labels),exit N, with;between commands. It logs each window change and interrupt it receives, and the platform logs each refusal by its name.
The key exchange is curve25519-sha256 only, the one keyd signs, with Ed25519 host and login
keys. sunset is used as published with one patch (patched
crates), which changes three things:
- The server’s host key signs outside it. Given a host key’s public half only,
sunsethands out the exchange’s parts (V_C,V_S,I_C,I_S,Q_C,Q_S,K) and waits for the signature, as its client already does for an SSH agent. It keeps a bounded copy of the peer’sKEXINITfor this, since after the first exchange onlysunsetsees one in the clear, and refuses a larger one (over 4 KiB). It still computes the hash itself for the session keys, and checks the signature against it before sending it, so a disagreement fails the exchange on the server; the client checks it too. - A client’s
window-change,signalandbreakrequests reach the core, and so does a pty’s starting size; publishedsunsetdrops the three requests on a server, and does not hand out the size. Its client gainsterm_signal, beside theterm_breakandterm_window_changeit has, so the patch’s tests send all three. The server gainssession_exit, which sends a session’s exit status, then its EOF, then its close, where publishedsunsetsends EOF and close only as echoes of the client’s. The server no longer echoes the client’s EOF, which is one direction only (RFC 4254): publishedsunsetwould end the session’s output there. A public key request gainssigned(), which tells a signed request from a query. - Its X25519 and Ed25519 verification use
ed25519-compact, the crate the loader andkeydalready link, in place of thedalekcrates. The box then has one implementation of each curve operation.
Under Miri
Status: built · partly tested: a recorded run, not a bench case
The vendored crates’ unsafe sits outside the ratchet, so Miri checks it instead, as for
ipd’s: nightly 2026-09-23 (rustc 1.100.0, Miri 0.1.0 of the same
nightly), Stacked Borrows, MIRIFLAGS=-Zmiri-disable-isolation (cmov’s property tests and
getrandom’s read the working directory or the system’s random source), and
RUSTFLAGS='--cfg aes_backend="soft" --cfg chacha20_backend="soft" --cfg poly1305_backend="soft" --cfg sha2_backend="soft"', so that each crate runs the software backend the box compiles. Each
crate’s own tests run in an unedited copy outside the tree, with the features the box builds it
with. Two of the box’s paths are not the ones Miri runs: on RISC-V, cmov makes its masks and
zeroize its optimisation barrier with inline assembly, which Miri does not interpret, so under
Miri both crates take their portable Rust paths. Those two assembly blocks are read
(vendor/README.md).
| Run | Result |
|---|---|
sshd: core (19 tests, each a whole SSH connection) | pass, in four groups, about 25 minutes in all |
aes 0.9.3 (zeroize) | 2 pass |
chacha20 0.10.2 (cipher, zeroize) | 5 pass |
| poly1305 0.9.1 | 6 pass |
| sha2 0.11.0 (no default features) | 14 pass; sha256_rand and sha512_rand, which hash a long generated stream, run too long under Miri to finish |
| inout 0.2.2 | 1 pass |
block-buffer 0.12.1 (zeroize) | 14 pass |
hybrid-array 0.4.15 (zeroize) | 70 pass |
| cmov 0.5.4 | 121 pass; its 73 property tests run too long under Miri to finish (7 in nine minutes) |
| zeroize 1.9.0 | 27 pass |
| subtle 2.6.1 (no default features) | 33 pass |
ascii 1.1.0 (the vendored copy: as published it does not compile on this nightly: a pattern binding named like an AsciiChar variant is now an error, which its patch fixes by renaming two bindings) | 94 pass; is_digit_strange_radixes fails natively too, since char::is_digit now panics on a radix below 2, and two doctests hit the same error |
| getrandom 0.4.3 | 16 pass, on its Linux backend; the box’s custom backend calls a function the program provides, which no test defines |
Sessions over SSH
Status: built · tested (30)
- bench:steward-ssh-resize
- bench:steward-ssh-two-principals
- bench:sshd-login-bound
- bench:sshd-refuses-slots
- host:redoubt-sshd::the_bound_is_twice_the_slowest_login_rounded_up
- host:redoubt-sshd::a_connection_not_logged_in_is_closed_at_the_bound_and_not_before
- host:redoubt-sshd::a_connection_logged_in_is_never_closed_by_the_bound
- host:redoubt-sshd::the_image_entry_holds_its_slots
- host:redoubt-sshd::every_manifests_slots_fit_ipds_parked_calls
- bench:steward-context-login
- host:redoubt-sshd::the_login_grammar
- host:redoubt-sshd::a_user_name_outside_the_grammar_reaches_the_steward_as_nobody
- bench:steward-ssh-idle
- bench:steward-vault-session
- bench:steward-vault-launch
- bench:steward-login-refused
- bench:steward-session-ends
- bench:steward-sub-budget-flood
- host:redoubt-sshd::a_read_waits_for_input_and_takes_what_was_typed
- host:redoubt-sshd::a_write_waits_for_room_and_the_channel_takes_it
- host:redoubt-sshd::an_accept_ipd_s_wait_ran_out_on_is_asked_again
- host:redoubt-sshd::a_data_call_refused_too_many_is_asked_again_a_bounded_number_of_times
- host:redoubt-sshd::an_ended_session_reads_the_end_and_cannot_write
- host:redoubt-sshd::the_console_is_one_file_carrying_the_channel_s_labels
- host:redoubt-sshd::consol_size_is_the_pty_s_and_a_resize_is_due_when_it_changes
- host:redoubt-sshd::a_login_s_two_consoles_and_only_the_steward_s_ends_the_channel
- host:redoubt-sshd::only_a_pty_channel_s_end_of_input_closes_the_terminal
- host:redoubt-sshd::a_slot_returns_when_the_client_hangs_up_first
- host:redoubt-sshd::a_slot_returns_when_the_server_ends_the_connection_first
- bench:steward-restart-ssh
- Listening.
sshdis the sole holder of anipdscope that listens on TCP port 22. Its accept, and each connection’s reads and writes, are asked again whenipdanswerstimeout(its wait ran out, ipd), and after 10 ms, up to 500 times in a row, when it answerstoo many(every in-flight call ofsshd‘s share taken: the accept, the slots’ parked reads and a driver’s waiting write). Any other failure of the accept endssshd(failure and restart), and of a connection’s call that connection only. - The host key.
sshdholdskeyd’sssh_hostroot badge, handed to it by the manifest, and askskeydto sign each key exchange;keydbuilds the exchange hash itself (keyd). The host key is never insshd’s memory, and the steward never holds its badge. - Login. A user name is
principal[+label][.context], in that order only, each part 1 to 64 bytes of[a-z0-9_-]starting with a letter (alice,alice+secrets,alice.work,alice+secrets.work);approvetakes no label and no context (contexts).sshdrejects a login key thatkeydholds (holds), then asks the steward whose key it is with the typed calllogin(principal, label, context, from, key, console, relay); the steward checks every part again and answers with a session, or refuses (steward).fromis the client’s address,a.b.c.d:port, whichsshdreads fromipd’s/tcp/N/remotefor the connection, or empty ifipddoes not say: only the steward’s notes of a context’s takeover show it.sshdparses the name only to split it and refuses nothing on it: a user name outside the grammar goes to the steward as the empty principal, which no manifest names, so every refusal after a verified signature takes the one path and says nothing about which names exist. Login keys are the person’s own and never live inkeyd(R35 (key separation)). - A key is tried twice. A client first asks whether a key would do, then sends a signature
with it.
sshdanswers the question withholdsalone: a keykeydholds is refused, any other may be tried. It asks the steward only once the signature has verified. So the steward never weighs a key whose private half the client has not shown, and a client that has not authenticated learns nothing about whose keys are whose. Passwords, keyboard-interactive andnoneauthentication are refused. - A pty session. A session gets one channel with a pty, on which
sshdserves its/dev/cons(consoled has the same protocol): input from the channel, output to it, and the window’s size and its changes. The channel’ssignalrequest (INT) andbreakrequest reach the session as its own interrupt key, the 0x1C byte (Ctrl+\), which no full-screen program can take (the shell): protocol messages, not signals, since nothing in Redoubt has signals. The core checks what arrives raw before the session sees it: any other signal is refused, a break’s length is not passed on, and a window size over 1,024 columns or rows reaches the session cut to 1,024. A zero means no size, as RFC 4254 says (a client whose input is not a terminal sends zeros): awindow-changecarrying one is refused, and a pty asked for with one starts at 80 by 24. On the console’s endpointsshdservesconsol:sizeanswers the window’s size (80 by 24 on a channel without a pty), and aresizeis parked until the window changes to another size, then answered with it; awindow-changeto the size it already has wakes nothing, and the session’s end answers every one still waiting, for the last time: after the end aconsolcall is refused (malformed), so a VM still running after its channel has ended stops asking rather than being answered at once for ever. A waitingresizeis one of the channel’s parked calls, in its caller’s share of the 4 a bucket holds, beside its waiting reads and writes: a client that parks more is refused at once (malformed), and what any number of waiters costssshdis that bucket’s fixed slots, not memory beyond them. The console’s parked completion call is answered at the end of each hold (multiplexed connections), so a session stays however long its channel is quiet. A program the session launches gets no connection to the console: its standard streams are pipes the session serves through itspiped, and what it writes reaches the channel only as the session draws it (native programs). - Two connections to a channel’s console. A login mints two, and sends both to the steward:
the steward keeps the first and hands the second to the context’s console relay
(consrelay), which serves the session’s own
/dev/consand forwards it here. Both read and write the channel, under its label check; onlyconsol’sendedthrough the steward’s ends it (console::Consoles), so neither the relay nor anything in the session can end its channel, and once the steward has, the relay’s reads find the end and its writes are refused. The channel’s skeleton is sized for this: two buckets,sshd’s own, through which it mints, and the session’s account; and two connections in the first, one login’s, since a channel has one login at a time. - Randomness. The program provides
getrandom’s__getrandom_v03_custom, the only sourcegetrandomhas on bare metal, and it writes all of the buffer it is given before it returnsOk:getrandomthen reads every byte as initialised (vendor/README.md). - State is per channel, and each channel carries its session’s labels (
alice@: none;alice+secrets@:{alice-secrets});sshdapplies the label check to them (R25 (the label check)). Channels are independent: one channel’s close, logout or VM death leaves every other channel,approve@box’s included, usable, and a channel learns nothing of another’s window size or waiting reads. - The one sink cleared for a label. A vault session’s output may reach its own channel, which
the steward opened for the label’s owner, and only a pty channel its owner authenticated: no
forwarding, no subsystems, no
execon a labelled channel. No other sink has an owner exemption (R67 (a channel keeps its labels)). - The box’s platform serves as many connections at once as its manifest entry’s
buckets=Nsays, each in a slot of two threads started once: a driver that runs the core, serves the channel’s/dev/consand makes the connection’s calls tokeyd, the steward andipd, and a reader that waits inipdfor the client’s bytes and hands each read to the driver. A call tokeydor the steward holds up only its own connection; a connection beyond the slots is closed when it arrives. A slot is free again once the reader’s last call for its connection is taken, whichever side ended the connection first.- The slots are the manifest’s.
initsizes N as it sizes every shared server’s count: at least the (account, label set)s the manifest declares, one channel for each at once, four in the image (init).sshdreads N with the serving library’s parser and refuses an entry it cannot hold, as every server does: the slots’ stacks must fit the entry’s budget, its pages less its heap cap and its first stack’s bound of 128 (serving). A slot’s two stacks are 48 pages for the driver (a whole login in one thread needs more than 88 KiB and fits 96 KiB; twice that) and 4 for the reader, and the steward’s watcher takes 4, so N slots take 52 N + 4 pages: the image’s four take 212 of the 512 its entry leaves (1,024 less the heap cap’s 384 and the 128), and nine would fit where ten would not. An entry that does not fit is left at once with a line saying the arithmetic, beforekeyd,ipdor the steward is asked, andinitrestarts it until its reboot rule. Each slot also keeps one read parked atipdbeside the listener’s accept, so N + 1 is at most the parked callsipd’s bucket forsshdallows, 5 by default, or a manifest’slimits=forsshd’s badge there (ipd): four slots and the accept are the default’s five.sshddoes not readipd’s entry, so that bound is held by a host test over every manifest in the tree, which parsesipd’s arguments asipddoes; a manifest with more domains raisesipd’s cap forsshdwith it. - Before login a slot is held for a bounded time. A connection that has not logged in
within the bound is closed, and the slot serves the next; the client is told nothing, as
every refusal before login is uniform. The bound is twice the slowest login the bench
records, rounded up to a second: from
ssh’s start to the shell’s prompt, one run of each case on each width,launch-systemtook 2.01 s (rv64) and 1.70 s (rv32), andsteward-ssh-two-principals2.00 s and 1.78 s for alice, then 2.41 s and 2.06 s for bob beside her running session; twice 2.41 s is 4.82 s, so the bound is 5 s. After login the connection is a channel, bounded by its session: a login past the context cap is refused and the connection ends (R82 (the context cap)), so no rule on how many connections one principal may hold is needed, and none is kept.
- The slots are the manifest’s.
- The steward’s end.
sshdkeeps onewatchcall at the steward, which the steward holds unanswered while it runs. When the steward ends, the call ends with it (Dead, R4b (a server dies), or an answer as it exits), and every session it ran is over:sshdends each of their channels with status 1 and sayssshd: session <id> ended: the steward is gone, and callswatchagain, which waits at the steward’s endpoint for its next instance (a second’s pause first if the call ended at once). A login, or a channel’s close, waits at most 30 seconds for the steward’s answer and is then refused, so a steward that is slow to come back holds no slot. - Ending. A channel closes when the steward lets it go: when its session ends or its VM
dies, or when a login takes its context over; the steward tells
sshdwithendedon its own connection, andsshdcloses the channel with status 0. A channel the client closes, or a pty channel whose input it ends once the session has read it all (the person closed the terminal), is reported to the steward (channel_closed), and in the second case closed with status 0; the steward detaches the context: the session runs on, and the next login of the context reattaches it (steward); a read on that channel finds no end of file until the channel ends, after the steward’s detach. On a channel without a pty (ssh alice@box < file) the end of input is the input’s own end: the session reads it as the end of the file, through its context’s relay, and the channel stays until the session ends or the client closes it.
sequenceDiagram
participant C as client
participant SH as sshd
participant KD as keyd
participant ST as steward
participant S as session
C->>SH: key exchange
SH->>KD: sign_ssh_exchange(transcript)
KD->>SH: signature
C->>SH: userauth alice+secrets, key K
SH->>KD: holds(K)
KD->>SH: no
SH->>ST: login(alice, secrets, K)
ST->>SH: session, labels {alice-secrets}
C->>SH: pty channel
SH->>S: /dev/cons on the labelled channel
Figure: an SSH login to a vault session.
The operations sshd sends the steward are in the steward’s table
(steward).
Measured by the bench from its ssh’s start to the prompt’s match (QEMU, release build, seconds
on the host’s clock; three runs each of launch-system at four harts and
steward-ssh-two-principals at one, at main of 2026-10-15), a login reaches its shell’s prompt about
1.9 s after ssh starts on either width: 1.65–2.08 s alone, and 2.1–2.3 s for a second
principal’s login beside the first’s running session, rv64 and rv32 within 5 % of each other. The
connection, the key exchange, the login, the steward’s carve and the VM’s start, to beamlet’s first
read of the shell from the boot pack, are the first part, 0.8 s alone and 1.15 s beside another
session; the shell’s load from that read to its prompt is the rest, 1.0–1.1 s, the larger stage on
both widths and in both cases. The console’s lines carry no host time, so the handshake and the
carve are not told apart within the first part. The bench prints each login under its case’s
result, so every train re-measures it (the test bench); no ceiling
is asserted until two trains agree within 20 %.
approve@box
Status: built · tested: bench:steward-approve-box, bench:steward-screen-inert, bench:steward-pending-cap, host:redoubt-steward-server::an_approval_channel_opens_on_the_approval_key_alone
ssh approve@box authenticates with the person’s own approval key, one of their own SSH keys that
the manifest lists for approval, and on that connection only the
steward talks: sshd relays the steward’s rendered requests and the person’s answers, and nothing
a session or agent sends reaches it (steward). The user
name approve names no principal, so sshd sends the key alone: it mints two connections to the
channel’s console, as for a login, and asks the steward with approval_opened(key, console, reader); the steward finds the principal from the key, opens the channel only for one of its
approval keys (a login key is bad_key, and sshd denies the login) and answers the channel’s
id, which serves as the session’s here; the close is approval_closed. The steward writes its
screens through the first connection and its reader for the channel reads the person’s lines
through the second, so the bytes each way are the steward’s and the person’s alone. The person
answers approve ID HASH with the hash the screen showed in full, or with the eight-character
code the screen shows beside it: a person cannot type 64 hex characters reliably, and the code
binds the approval to the screen the person saw as the full hash does, because the steward
expands it only from the last screen it wrote on that channel, and the screen is the steward’s
alone to write. A session’s
network scope never includes the box’s own addresses (ipd), so a
hijacked session cannot log in to approve@box over loopback
(R68 (only the steward on approve@box)). An address that
loops back to the box without being listed as the box’s own (a NAT hairpin) is refused only if the
manifest lists it; the backstop is that sshd refuses any login key keyd holds, so a hijacked
session that reaches approve@box that way still has no key to sign with.
Files in and out
Status: planned · M4 (files in and out)
File transfer is SFTP, for unlabelled sessions only (transfer has the operation table).
sshdrelays bytes only. On asubsystem sftprequest it asks the steward to start the transfer server for that session and pipes the channel to it, as it does/dev/cons.sshdnever parses SFTP.- The transfer server is a small native Rust program. The steward starts one per channel, in a
budget carved from the session’s, holding only the session’s file binds and one connection to
the steward’s audit path: no
/net, no/dev/cons, no powerbox, no budget or process handles. An SFTP-only connection gets a session budget as a login does, with the transfer server in place of the VM. - Confinement is by capability, not by path strings: the server holds namespace handles and
nothing else, so no path, however written, reaches anything outside them, and
..at a root stays at the root. - Unsupported operations fail visibly. Symlink, readlink and link get “operation unsupported”;
setting a size truncates, and mtime is set where
littlefsdstores it; mode, owner and group get “operation unsupported” rather than a silent no-op. - SCP is served only as SFTP. Current
scpclients use SFTP by default; legacyscp -O, which runsscpon the server, is refused, since there is noexecand no shell to run it. Old clients usesftporscp -s. - Vault sessions get no SFTP or SCP. A labelled channel has no subsystems (R67 (a channel keeps its labels)); vault input enters by an audited push and output leaves by declassification (steward).
- Every operation is audited: each open (reading and writing), each close with its byte count, remove, rename, mkdir, rmdir and setting attributes gets one record, signed as every audit record is (steward). The principal and labels in a record come from the badge the steward minted for the transfer server, never from the server’s own claim.
The attack tests: a path-escape suite (.., absolute paths, long and odd names) reaches only the
session’s binds; symlink, chmod and chown are refused; scp -O is refused; a subsystem request on a
vault channel is refused; every operation yields exactly one signed audit record with the right
principal; a second principal’s files are unreachable.
Open: auditing at littlefsd of the transfer server’s handles, needed only if the audit must survive
a compromised transfer server.
Authority
Status: built · partly tested: the transfer server request comes with M4 (files in and out) · tested: bench:steward-ssh-two-principals
sshdholds itsipdlisten scope for port 22,keyd’sssh_hostroot badge, a connection to the steward, and the/dev/consendpoints it serves to sessions.- It holds no private key and decides no login: it asks the steward whose key a login used, and asks the steward to start a transfer server for an SFTP request (files in and out). It never holds a session’s file binds itself.
- It is trusted across the labels of the channels it carries: with the steward it is the confinement check’s one named exception (init).
Security properties
R67 (a channel keeps its labels)
Status: built · tested: bench:sshd-loopback-r67, bench:steward-vault-session, bench:steward-vault-launch, host:redoubt-sshd::the_console_is_one_file_carrying_the_channel_s_labels
Each SSH channel carries its session’s labels, and a labelled session’s output reaches only its own
pty channel, authenticated by the label’s owner, with no forwarding, subsystem or exec. So vault
data leaves the box over SSH only to the person who owns the label, on the channel they opened.
R68 (only the steward on approve@box)
Status: built · partly tested: a route back to the box the manifest does not list is the stated gap · tested: bench:steward-approve-box, bench:steward-screen-inert
On an approve@box connection, authenticated with the person’s own approval key, every byte shown
comes from the steward and every answer goes to it; no session, agent or other channel can write to
it or open one from inside the box. A route back to the box that the manifest does not list as the
box’s own is the gap in the second half; sshd’s refusal of every key keyd holds is the backstop,
so such a session still cannot authenticate there with a keyd key.
Failure and restart
Status: built · tested: bench:sshd-restart-detaches, host:redoubt-sshd::a_refused_signature_fails_the_exchange
sshdcrashes: every SSH connection drops; the steward sees itswatchcall end and detaches every context, which runs on until a login reattaches it.initrestartssshd(init). The restarted instance is handed its predecessor’s badge atipd, and with it that one’s sockets, which outlive their owner until they are closed or aborted (ipd): its listener on port 22 would take every new connection and never accept it. So at its startsshdreads/tcpand aborts every socket the badge holds, then sendsTversion, which clunks every fid the predecessor left (serving), before it attaches and listens.keydfails a signature: the key exchange fails and the client sees a closed connection.
Residual risks
approve@sharessshdwith the most hostile input. Asunsetbug reached from any channel, before or after login, controls every channel and the approval screen, and a network flood delays approvals. A separatesshdinstance forapprove@, or the physical console, is planned for M6 (persist, install, share).sshdis trusted across labels. As one of the confinement check’s named mediators it carries every session’s channel; a bug in it reaches all of them.- The transfer audit is accountability, not a wall. A principal who compromises their own transfer server with crafted input holds only their session’s file capabilities, which they had already, and could suppress their own records; they could move data out unaudited through the pty anyway.
- An
ssh_hostbadge speaks as the box. A compromisedsshdcan complete key exchanges as the box for as long as it runs. sunsetcarries our patch. The patch is ours to read and keep: asunsetrelease that changes the code it touches needs it redone and read again.- A few unusual clients fail closed.
sunsethashes its own re-encoding of the peer’sKEXINIT, so a client whoseKEXINITdoes not re-encode to the same bytes fails the exchange; anded25519-compactrefuses a non-canonical X25519 public value. - Ed25519 signatures are checked cofactored, as the loader’s are (boot): a client’s signature can be made into another valid one for the same exchange, which authenticates nobody new.
- Agent forwarding is refused where no platform sees it.
sunsethas no agent code:auth-agent-req@openssh.comis left out of its request types, so it parses as an unknown request, whichsunsetrefuses before the core (with a failure only if the client wants a reply, andsshdoes not). Read from the vendored source; no case shows it, since nothing reaches the log. - The key exchange is not post-quantum. Traffic recorded now could be read by whoever later
breaks X25519. A post-quantum exchange (
mlkem768x25519-sha256) changes the transcriptkeydsigns. - Connections are first come, first served.
sshdserves its manifest’s count of connections at once, whoever opened them, and closes any beyond. A connection that never logs in holds a slot for at most the login bound, and a principal’s channels are bounded by its context cap (R82), so the slots a client can hold are bounded in time and in number; but within those bounds a principal’s open connections still keep another’s logins out until they end, and nothing favours a principal with no session over one with several.
Why
- Keys elsewhere. The process that parses pre-authentication bytes from the whole network is the
last place for a key;
keydsigns, andsshdasks. - The steward decides logins. Principals and their keys are the steward’s;
sshdasking keeps one place that knows them. - One cleared sink. A vault’s output must reach its owner somewhere; one sink, one kind of channel, the owner’s own authentication, and nothing else keeps the exemption as narrow as it can be.
sunset. An SSH implementation inno_stdRust with no allocation and nounsafe, by an author of dropbear, is small enough to read. Its core does no I/O, so the platform is a thin trait, and the host tool tests the same core the box runs.- A patch, not a fork. Carrying the published crate and one patch keeps what we changed
visible and small; its signer and channel-request changes are offered to
sunset’s author. - One curve implementation.
ed25519-compactis already read for the loader andkeyd, has no dependencies, and does X25519 as well; thedalekcrates would add about 30,000 lines for the same arithmetic. - Relay, never parse, transfers. An SFTP parser inside
sshdwould put a second protocol in the process that carries every channel; a transfer server per channel, holding only that session’s file binds and its audit connection, keeps a bug in it inside one session’s own files. Running SFTP in the session’s own VM would let the principal skip the audit records.