Problem / Background
The default host key checking mode accept-new performs no host key verification at all and never records keys. The CLI default is --strict-host-key-checking accept-new (src/cli/bssh.rs:170-175) and the help text promises: "accept-new - Accept new hosts, reject changed keys (recommended)".
The implementation does not do that. get_check_method in src/ssh/known_hosts.rs:55-86 maps StrictHostKeyChecking::AcceptNew to ServerCheckMethod::NoCheck, and check_server_key in src/ssh/tokio_client/connection.rs:529-530 then accepts any server key unconditionally. Comments in known_hosts.rs say "async-ssh2-tokio doesn't support TOFU mode directly"; that is stale, bssh talks to russh directly now and fully controls check_server_key. Nothing is ever written to ~/.ssh/known_hosts in this mode, and changed keys are never rejected.
A second defect: in Yes (strict) mode with a missing known_hosts file, get_check_method downgrades to NoCheck with only a warning (src/ssh/known_hosts.rs:36-45). Strict mode should treat a missing file as an empty known_hosts (reject unknown hosts), never disable verification.
A third defect: check_server_key collapses every error from russh::keys::check_known_hosts_path into the generic Error::ServerCheckFailed (src/ssh/tokio_client/connection.rs:543-563), so a changed key (russh::keys::Error::KeyChanged) is indistinguishable from an unknown host or an unreadable file. TOFU requires that distinction.
Impact
- Security: the documented and recommended default gives zero MITM protection, contrary to what the help text claims.
- UX: because bssh never populates known_hosts, a later OpenSSH connection to the same hosts prompts "The authenticity of host ... can't be established" even for hosts used daily through bssh. This caused a real diagnostic confusion where an unrelated bssh connection failure was misattributed to host key state.
Verified Implementation Facts
Checked against russh 0.62.1, the version pinned in Cargo.toml:
russh::keys::check_known_hosts_path(host, port, key, path) and check_known_hosts exist. Match semantics: Ok(true) known and matching, Ok(false) unknown host, Err(russh::keys::Error::KeyChanged { line }) key mismatch.
- russh 0.62.1 exposes NO
learn_known_hosts helper (verified against docs.rs), so bssh must implement the known_hosts append itself.
Proposed Solution
- Add a TOFU variant to
ServerCheckMethod (for example AcceptNewKnownHostsFile), returned by get_check_method for AcceptNew.
- In
check_server_key for that variant:
Ok(true): accept.
Ok(false) (unknown host): append an OpenSSH-format entry to ~/.ssh/known_hosts (the server key is an ssh_key::PublicKey re-export; to_openssh() produces the key text), print an OpenSSH-style notice to stderr ("Permanently added '' () to the list of known hosts."), and accept. Create ~/.ssh with mode 0700 and the file with 0600 if missing. Use the [host]:port entry format for non-22 ports so the entry round-trips through check_known_hosts_path.
Err(KeyChanged { line }): reject with a loud, OpenSSH-style warning including the SHA256 fingerprint of the offered key, the conflicting file and line, and a remediation hint. Do not overwrite the entry.
- Concurrency: bssh connects to many nodes in parallel, so concurrent first-time connects can race the append. Serialize known_hosts writes behind a process-wide mutex and re-check the file after acquiring it to avoid duplicate entries.
- Fix strict
Yes mode: a missing known_hosts file must behave as an empty file (unknown hosts rejected), not as NoCheck.
- Distinguish
KeyChanged from other errors in all check methods so Yes mode also reports changed keys clearly instead of the generic "ServerCheckFailed".
- Remove the now-redundant
strict_mode match at src/jump/chain/tunnel.rs:217-220 (get_check_method already handles No), and delete the stale async-ssh2-tokio comments in src/ssh/known_hosts.rs.
- Verify the behavior applies to every connection path, since all of them flow through
get_check_method and check_server_key: direct connections, the first jump hop (src/jump/chain.rs:406), intermediate hops (src/jump/chain/tunnel.rs:115), the final destination through the tunnel (src/jump/chain/tunnel.rs:219, the handler receives the destination hostname at tunnel.rs:232, so TOFU works through jump chains), and SFTP file transfer (src/ssh/client/file_transfer.rs:658).
Acceptance Criteria
Problem / Background
The default host key checking mode
accept-newperforms no host key verification at all and never records keys. The CLI default is--strict-host-key-checking accept-new(src/cli/bssh.rs:170-175) and the help text promises: "accept-new - Accept new hosts, reject changed keys (recommended)".The implementation does not do that.
get_check_methodinsrc/ssh/known_hosts.rs:55-86mapsStrictHostKeyChecking::AcceptNewtoServerCheckMethod::NoCheck, andcheck_server_keyinsrc/ssh/tokio_client/connection.rs:529-530then accepts any server key unconditionally. Comments inknown_hosts.rssay "async-ssh2-tokio doesn't support TOFU mode directly"; that is stale, bssh talks to russh directly now and fully controlscheck_server_key. Nothing is ever written to~/.ssh/known_hostsin this mode, and changed keys are never rejected.A second defect: in
Yes(strict) mode with a missing known_hosts file,get_check_methoddowngrades toNoCheckwith only a warning (src/ssh/known_hosts.rs:36-45). Strict mode should treat a missing file as an empty known_hosts (reject unknown hosts), never disable verification.A third defect:
check_server_keycollapses every error fromrussh::keys::check_known_hosts_pathinto the genericError::ServerCheckFailed(src/ssh/tokio_client/connection.rs:543-563), so a changed key (russh::keys::Error::KeyChanged) is indistinguishable from an unknown host or an unreadable file. TOFU requires that distinction.Impact
Verified Implementation Facts
Checked against russh 0.62.1, the version pinned in Cargo.toml:
russh::keys::check_known_hosts_path(host, port, key, path)andcheck_known_hostsexist. Match semantics:Ok(true)known and matching,Ok(false)unknown host,Err(russh::keys::Error::KeyChanged { line })key mismatch.learn_known_hostshelper (verified against docs.rs), so bssh must implement the known_hosts append itself.Proposed Solution
ServerCheckMethod(for exampleAcceptNewKnownHostsFile), returned byget_check_methodforAcceptNew.check_server_keyfor that variant:Ok(true): accept.Ok(false)(unknown host): append an OpenSSH-format entry to~/.ssh/known_hosts(the server key is anssh_key::PublicKeyre-export;to_openssh()produces the key text), print an OpenSSH-style notice to stderr ("Permanently added '' () to the list of known hosts."), and accept. Create~/.sshwith mode 0700 and the file with 0600 if missing. Use the[host]:portentry format for non-22 ports so the entry round-trips throughcheck_known_hosts_path.Err(KeyChanged { line }): reject with a loud, OpenSSH-style warning including the SHA256 fingerprint of the offered key, the conflicting file and line, and a remediation hint. Do not overwrite the entry.Yesmode: a missing known_hosts file must behave as an empty file (unknown hosts rejected), not asNoCheck.KeyChangedfrom other errors in all check methods soYesmode also reports changed keys clearly instead of the generic "ServerCheckFailed".strict_modematch atsrc/jump/chain/tunnel.rs:217-220(get_check_methodalready handlesNo), and delete the stale async-ssh2-tokio comments insrc/ssh/known_hosts.rs.get_check_methodandcheck_server_key: direct connections, the first jump hop (src/jump/chain.rs:406), intermediate hops (src/jump/chain/tunnel.rs:115), the final destination through the tunnel (src/jump/chain/tunnel.rs:219, the handler receives the destination hostname attunnel.rs:232, so TOFU works through jump chains), and SFTP file transfer (src/ssh/client/file_transfer.rs:658).Acceptance Criteria
accept-new(default): first connection to an unknown host succeeds and appends exactly one known_hosts entry (including under parallel multi-node connects to the same new host); a subsequent connection where the server presents a different key fails with the changed-key warning.[host]:portformat.yes: unknown host is rejected even when known_hosts is missing; changed key produces the specific changed-key error, not a generic failure.no: behavior unchanged (no verification, nothing written).