mod common;
use common::require_env_flag;
use std::process::Command;
#[test]
#[ignore = "requires real ssh + sshd; gate with MKIT_SSH_E2E_REAL=1"]
fn real_ssh_strict_host_key_checking_rejects_wrong_pin() {
if !require_env_flag("MKIT_SSH_E2E_REAL") {
return;
}
let ssh_ok = Command::new("ssh").arg("-V").output().is_ok();
assert!(
ssh_ok,
"MKIT_SSH_E2E_REAL=1 but `ssh` is not runnable on PATH"
);
eprintln!(
"ssh_e2e_real: tooling present. Full sshd stand-up is operator-driven; \
follow the recipe in this file's module docs to assert wrong-pin rejection."
);
}
#[cfg(test)]
fn ssh_attempt(
known_hosts: &std::path::Path,
client_key: &std::path::Path,
port: u16,
) -> std::process::Output {
Command::new("ssh")
.args([
"-F",
"/dev/null",
"-o",
&format!("UserKnownHostsFile={}", known_hosts.display()),
"-o",
"StrictHostKeyChecking=yes",
"-o",
"ConnectTimeout=2",
"-i",
])
.arg(client_key)
.args(["-p", &port.to_string(), "testuser@127.0.0.1", "echo", "ok"])
.output()
.expect("ssh must be runnable")
}
#[cfg(test)]
fn wait_for_real_sshd_ready(client_key: &std::path::Path, port: u16) {
let scratch_known_hosts = std::env::temp_dir().join(format!("mkit-ssh-e2e-readiness-{port}"));
let _ = std::fs::remove_file(&scratch_known_hosts);
for _ in 0..30 {
let out = Command::new("ssh")
.args([
"-F",
"/dev/null",
"-o",
&format!("UserKnownHostsFile={}", scratch_known_hosts.display()),
"-o",
"StrictHostKeyChecking=accept-new",
"-o",
"BatchMode=yes",
"-o",
"ConnectTimeout=2",
"-i",
])
.arg(client_key)
.args(["-p", &port.to_string(), "testuser@127.0.0.1", "true"])
.output()
.expect("ssh must be runnable");
if out.status.success() {
return;
}
std::thread::sleep(std::time::Duration::from_millis(400));
}
panic!("containerized sshd never became ready to accept a real SSH handshake");
}
#[test]
#[ignore = "requires a running Docker daemon"]
fn real_ssh_strict_host_key_checking_rejects_wrong_pin_via_container() {
use testcontainers::core::{IntoContainerPort, WaitFor};
use testcontainers::runners::SyncRunner;
use testcontainers::{GenericImage, ImageExt};
let tmp = tempfile::tempdir().expect("tempdir");
let client_key = tmp.path().join("client_ed25519");
let keygen = Command::new("ssh-keygen")
.args(["-t", "ed25519", "-N", ""])
.arg("-f")
.arg(&client_key)
.arg("-q")
.status()
.expect("ssh-keygen must be runnable");
assert!(keygen.success(), "ssh-keygen failed");
let client_pub =
std::fs::read_to_string(client_key.with_extension("pub")).expect("client pubkey written");
let image = GenericImage::new("lscr.io/linuxserver/openssh-server", "latest")
.with_exposed_port(2222.tcp())
.with_wait_for(WaitFor::message_on_stdout("sshd is listening on port 2222"))
.with_env_var("PUBLIC_KEY", client_pub.trim())
.with_env_var("USER_NAME", "testuser")
.with_env_var("PASSWORD_ACCESS", "false");
let container = image.start().expect("container starts");
let port = container
.get_host_port_ipv4(2222)
.expect("port 2222 is mapped");
wait_for_real_sshd_ready(&client_key, port);
let logs = container.stdout_to_vec().expect("container logs readable");
let logs_str = String::from_utf8_lossy(&logs);
let hostkey_line = logs_str
.lines()
.find(|l| l.starts_with("ssh-ed25519"))
.expect("container logs its generated ssh-ed25519 host key on startup")
.split_whitespace()
.take(2)
.collect::<Vec<_>>()
.join(" ");
let known_hosts_ok = tmp.path().join("known_hosts_ok");
std::fs::write(
&known_hosts_ok,
format!("[127.0.0.1]:{port} {hostkey_line}\n"),
)
.expect("write known_hosts_ok");
let ok = ssh_attempt(&known_hosts_ok, &client_key, port);
assert!(
ok.status.success(),
"real ssh(1) rejected the CORRECT host-key pin:\nstderr: {}",
String::from_utf8_lossy(&ok.stderr)
);
let known_hosts_bad = tmp.path().join("known_hosts_bad");
std::fs::write(
&known_hosts_bad,
format!(
"[127.0.0.1]:{port} ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBOGUSBOGUSBOGUSBOGUSBOGUSBOGUSBOGUSBOG=\n"
),
)
.expect("write known_hosts_bad");
let bad = ssh_attempt(&known_hosts_bad, &client_key, port);
assert!(
!bad.status.success(),
"real ssh(1) accepted a WRONG host-key pin under StrictHostKeyChecking=yes — \
the trust-pinning option is not reaching OpenSSH"
);
}