mkit-cli 0.4.0

The mkit command-line tool: a content-addressed VCS with native attestation support
Documentation
//! Opt-in, high-fidelity SSH e2e against REAL `ssh(1)` + `sshd`.
//!
//! This is the strongest proof that the issue #389 trust-pinning options
//! actually reach `ssh(1)`: it pins a `known_hosts` entry and asserts a
//! WRONG pin under `StrictHostKeyChecking=yes` causes the connection to
//! be REJECTED by real OpenSSH — something no shim can fake.
//!
//! It is `#[ignore]`d by default and additionally gated behind the
//! `MKIT_SSH_E2E_REAL=1` environment variable, so CI without an sshd
//! stays green. Run it explicitly:
//!
//! ```sh
//! MKIT_SSH_E2E_REAL=1 cargo test -p mkit-cli --test ssh_e2e_real -- --ignored --nocapture
//! ```
//!
//! ## What a full implementation needs (left as a documented skeleton)
//!
//! Wiring a hermetic `sshd` is environment-fragile (privilege-separation
//! dirs, host-key generation, a free port, an `authorized_keys` with a
//! `command="mkit serve <path>"` forced command, and a `known_hosts`
//! pinned to the generated host key). Rather than ship something that
//! flakes per-runner, this file documents the exact recipe and asserts
//! the security-relevant behaviour at the boundary we CAN drive
//! deterministically. To complete it for a given environment:
//!
//! 1. `ssh-keygen -t ed25519` a CLIENT key; authorize it in the test
//!    sshd's `authorized_keys` with a forced command:
//!    `command="<abs>/mkit serve <served-repo-path>",no-pty <pubkey>`.
//! 2. `ssh-keygen -t ed25519 -f host_ed25519` a HOST key; start
//!    `/usr/sbin/sshd -f <test_sshd_config> -p <port> -h host_ed25519`
//!    bound to 127.0.0.1, `StrictModes no`.
//! 3. Write a CORRECT `known_hosts`:
//!    `[127.0.0.1]:<port> ssh-ed25519 <host_pubkey>`.
//! 4. `mkit config ssh.user_known_hosts_file <correct known_hosts>`,
//!    `ssh.identity_file <client key>`,
//!    `ssh.strict_host_key_checking yes`.
//!    Then `mkit clone mkit+ssh://<user>@127.0.0.1:<port><served-repo>`
//!    MUST succeed.
//! 5. Repeat with a `known_hosts` pinned to a WRONG host key; the clone
//!    MUST fail (OpenSSH refuses the host-key mismatch under
//!    `StrictHostKeyChecking=yes`). That failure is the proof the option
//!    reached real `ssh(1)`.
//!
//! ## Containerized completion of steps 2-5 (below, `#[ignore]`d on Docker)
//!
//! `real_ssh_strict_host_key_checking_rejects_wrong_pin_via_container`
//! completes steps 2, 3, and 5 above (the host-key-pinning security claim
//! itself) using a real `sshd` in a `lscr.io/linuxserver/openssh-server`
//! Docker container instead of a hand-provisioned host daemon — the
//! container sidesteps exactly the "environment-fragile... would flake
//! per-runner" concern above, since the daemon's provisioning lives in a
//! pinned, pre-built image rather than on the host. It does NOT cover
//! steps 1 and 4 (a forced `mkit serve` command and a real `mkit clone`
//! through it) — that would additionally need an `mkit` binary built for
//! the container's platform and wired into `authorized_keys`, which is a
//! separable, larger piece of work left for later rather than bundled in
//! here.

mod common;

use common::require_env_flag;
use std::process::Command;

/// Minimal liveness check: confirm the host actually has the `ssh` and
/// `sshd` binaries this suite would need. We assert presence rather than
/// standing up a full daemon here (see module docs) so the test fails
/// LOUDLY with an actionable message instead of silently passing if the
/// environment is missing the tooling.
#[test]
#[ignore = "requires real ssh + sshd; gate with MKIT_SSH_E2E_REAL=1"]
fn real_ssh_strict_host_key_checking_rejects_wrong_pin() {
    // Loud skip (#505 PR 2/5): opting in is required to run this suite at
    // all. Under `MKIT_TEST_STRICT`, a job that expects to drive the
    // real-ssh e2e must set `MKIT_SSH_E2E_REAL=1` itself — leaving it
    // unset there is a CI config bug, not a routine skip.
    if !require_env_flag("MKIT_SSH_E2E_REAL") {
        return;
    }

    // Precondition check: the tooling must exist. This stays a hard
    // `assert!` (not a loud-skip helper) — once an operator opts in via
    // `MKIT_SSH_E2E_REAL=1` they've asked for the real suite to run, so a
    // missing `ssh` binary is a failure, not something to skip past.
    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"
    );

    // The full daemon stand-up is intentionally left as the documented
    // skeleton above: it is environment-specific (sshd path, privilege
    // separation, free port) and would flake across runners. The
    // hermetic shim test in `ssh_e2e.rs` already proves the
    // Config -> SshOptions -> spawned-program wiring deterministically;
    // this file pins the recipe for a real-ssh confirmation when an
    // operator opts in.
    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."
    );
}

/// Runs a single `ssh` attempt against `port` using `known_hosts`, with a
/// short connect timeout. Not retried — callers that need to ride out
/// container-readiness races use [`wait_for_real_sshd_ready`] first, so a
/// failure here is a real host-key/auth outcome, not a timing artifact.
#[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")
}

/// The container's own "sshd is listening" log line fires before the
/// daemon reliably completes a real SSH handshake (observed directly:
/// without this probe, the very next connection attempt intermittently
/// fails with a connection-level error, not a host-key/auth failure).
/// Poll with a host-key-accepting, `BatchMode` probe (so it can never
/// hang on a password prompt) until it succeeds, then stop — the actual
/// correct/wrong-host-key assertions run exactly once each afterward, so
/// a failure there is trustworthy rather than papered over by a retry.
#[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");
}

/// Completes this file's documented recipe (steps 2, 3, and 5) against a
/// REAL `sshd` running in a `lscr.io/linuxserver/openssh-server` Docker
/// container: a generated client key is authorized via the image's
/// `PUBLIC_KEY` env var, the container's own generated host key is read
/// back from its startup log, and a correct vs. a deliberately wrong
/// `known_hosts` pin are each tried under `StrictHostKeyChecking=yes`.
/// Only the second (wrong pin) is expected to fail — proof the pin
/// reaches and is honored by real OpenSSH, not a shim.
///
/// `#[ignore]`d: requires a running Docker daemon. Run explicitly:
/// `cargo test -p mkit-cli --test ssh_e2e_real -- --ignored --nocapture`.
#[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(" ");

    // Correct pin: real ssh(1) must accept it.
    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)
    );

    // Wrong pin: real ssh(1) must reject it under StrictHostKeyChecking=yes.
    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"
    );
}