Summary
Implement real SSH host key verification. The current client accepts every server key, which is equivalent to disabling StrictHostKeyChecking and leaves users open to MITM attacks.
Current implementation check
src/ssh/client/mod.rs implements ClientHandler::check_server_key as a TODO that logs skipping server key verification and returns Ok(true).
Client::connect only passes the socket address to russh_connect; the handler does not currently know the configured host, port, or known-hosts path.
russh 0.60.2 exposes russh::keys::known_hosts::{check_known_hosts, check_known_hosts_path, learn_known_hosts}.
- Config currently has
[ssh] host, port, user, key_path, password, and umask; there is no host-key policy field.
- Existing SSH e2e tests connect to
127.0.0.1:2222 with a throwaway Docker service and no known-hosts setup.
Proposed config and behavior
Add an explicit host key policy with secure defaults:
[ssh]
host = "cse.unsw.edu.au"
user = "z5555555"
host_key_checking = "strict" # strict | accept-new | insecure
# known_hosts = "~/.ssh/known_hosts" # optional override for tests/custom environments
Policy semantics:
strict: require a matching known-hosts entry. Unknown or changed keys fail.
accept-new: trust-on-first-use for unknown hosts by recording the key, but fail if a known key changed.
insecure: accept any key, log a warning, and document this as test/debug-only.
Usage examples:
# Recommended: pre-seed known_hosts with OpenSSH, then run normally
ssh-keyscan -p 22 cse.unsw.edu.au >> ~/.ssh/known_hosts
biwa run hostname
# First-use convenience for a private test host
BIWA_SSH_HOST_KEY_CHECKING=accept-new biwa run hostname
# CI-only/test-only escape hatch
BIWA_SSH_HOST_KEY_CHECKING=insecure biwa run --skip-sync true
Implementation plan
- Add
HostKeyChecking enum and optional known_hosts path to SshConfig, including env vars, schema defaults, docs, and config template output.
- Expand
ClientHandler to store the original configured host, port, host-key policy, and known-hosts path.
- Change
Client::connect to accept those host-key settings. Keep DNS resolution for socket addresses, but verify against the configured host string, not each resolved IP, unless an IP was explicitly configured.
- Implement
check_server_key:
strict: call check_known_hosts_path or check_known_hosts and return Ok(true) only on match.
accept-new: first call check; if no key matches and no changed-key error occurred, call learn_known_hosts_path then accept.
insecure: warn once and return Ok(true).
- Convert known-hosts errors into actionable messages that distinguish unknown host, changed key, unreadable file, and invalid known-hosts line.
- Make tests deterministic by letting e2e set
BIWA_SSH_KNOWN_HOSTS to a temp file and either pre-seed it or use accept-new.
- Run
mise run render:schema and mise run render:usage if CLI/config output changes.
Checks
- Unit tests for policy parsing/defaults and env override behavior.
- Unit tests for known-hosts verification using temporary files: exact match, unknown host, changed key, hashed known-hosts entries if supported by
russh, non-22 [host]:port, and custom known-hosts path.
- E2E test with
strict and a pre-seeded Docker host key.
- E2E test with
accept-new creating a known-hosts entry in a temp file.
- E2E test that a mismatched known-hosts entry fails before authentication.
- Run at minimum:
cargo test host_key, cargo test --test ssh_e2e_run, mise run render:schema, and LINT=true mise run check.
Docs updates
- Add a Host Key Verification section to
docs/src/ssh-key-setup.md.
- Update
docs/src/configuration.md for ssh.host_key_checking and ssh.known_hosts.
- Mention how to pre-seed with
ssh-keyscan and how CI/test environments should use a temp known-hosts file.
Acceptance criteria
- Default behavior no longer accepts arbitrary host keys.
- Unknown and changed host-key failures are clear and actionable.
- CI e2e tests do not depend on the developer's real
~/.ssh/known_hosts.
- The insecure escape hatch exists only behind explicit configuration or env var and emits a warning.
Summary
Implement real SSH host key verification. The current client accepts every server key, which is equivalent to disabling
StrictHostKeyCheckingand leaves users open to MITM attacks.Current implementation check
src/ssh/client/mod.rsimplementsClientHandler::check_server_keyas a TODO that logsskipping server key verificationand returnsOk(true).Client::connectonly passes the socket address torussh_connect; the handler does not currently know the configured host, port, or known-hosts path.russh 0.60.2exposesrussh::keys::known_hosts::{check_known_hosts, check_known_hosts_path, learn_known_hosts}.[ssh] host,port,user,key_path,password, andumask; there is no host-key policy field.127.0.0.1:2222with a throwaway Docker service and no known-hosts setup.Proposed config and behavior
Add an explicit host key policy with secure defaults:
Policy semantics:
strict: require a matching known-hosts entry. Unknown or changed keys fail.accept-new: trust-on-first-use for unknown hosts by recording the key, but fail if a known key changed.insecure: accept any key, log a warning, and document this as test/debug-only.Usage examples:
Implementation plan
HostKeyCheckingenum and optionalknown_hostspath toSshConfig, including env vars, schema defaults, docs, and config template output.ClientHandlerto store the original configured host, port, host-key policy, and known-hosts path.Client::connectto accept those host-key settings. Keep DNS resolution for socket addresses, but verify against the configured host string, not each resolved IP, unless an IP was explicitly configured.check_server_key:strict: callcheck_known_hosts_pathorcheck_known_hostsand returnOk(true)only on match.accept-new: first call check; if no key matches and no changed-key error occurred, calllearn_known_hosts_paththen accept.insecure: warn once and returnOk(true).BIWA_SSH_KNOWN_HOSTSto a temp file and either pre-seed it or useaccept-new.mise run render:schemaandmise run render:usageif CLI/config output changes.Checks
russh, non-22[host]:port, and custom known-hosts path.strictand a pre-seeded Docker host key.accept-newcreating a known-hosts entry in a temp file.cargo test host_key,cargo test --test ssh_e2e_run,mise run render:schema, andLINT=true mise run check.Docs updates
docs/src/ssh-key-setup.md.docs/src/configuration.mdforssh.host_key_checkingandssh.known_hosts.ssh-keyscanand how CI/test environments should use a temp known-hosts file.Acceptance criteria
~/.ssh/known_hosts.