Skip to content

fix: accept-new host key mode is implemented as NoCheck, implement real TOFU verification and known_hosts recording #239

Description

@inureyes

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

  1. Add a TOFU variant to ServerCheckMethod (for example AcceptNewKnownHostsFile), returned by get_check_method for AcceptNew.
  2. 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.
  3. 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.
  4. Fix strict Yes mode: a missing known_hosts file must behave as an empty file (unknown hosts rejected), not as NoCheck.
  5. Distinguish KeyChanged from other errors in all check methods so Yes mode also reports changed keys clearly instead of the generic "ServerCheckFailed".
  6. 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.
  7. 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

  • With 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.
  • Non-standard ports are recorded and matched using the [host]:port format.
  • With yes: unknown host is rejected even when known_hosts is missing; changed key produces the specific changed-key error, not a generic failure.
  • With no: behavior unchanged (no verification, nothing written).
  • TOFU works identically for direct, jump-chain (first hop, intermediate hops, destination), and SFTP paths.
  • Help text, code comments, and actual behavior agree.
  • Unit tests cover the three modes using a temp known_hosts file and generated ed25519 keys, including the KeyChanged path and the concurrent-append dedup.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions