Skip to content

Implement strict host key checking for SSH client (check_server_key) #409

@risu729

Description

@risu729

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

  1. Add HostKeyChecking enum and optional known_hosts path to SshConfig, including env vars, schema defaults, docs, and config template output.
  2. Expand ClientHandler to store the original configured host, port, host-key policy, and known-hosts path.
  3. 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.
  4. 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).
  5. Convert known-hosts errors into actionable messages that distinguish unknown host, changed key, unreadable file, and invalid known-hosts line.
  6. Make tests deterministic by letting e2e set BIWA_SSH_KNOWN_HOSTS to a temp file and either pre-seed it or use accept-new.
  7. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions