Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 46 additions & 16 deletions crates/openshell-cli/src/ssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ struct SshSessionConfig {
sandbox_id: String,
gateway_url: String,
token: String,
host_key_fingerprint: String,
}

async fn ssh_session_config(
Expand Down Expand Up @@ -127,22 +128,51 @@ async fn ssh_session_config(
sandbox_id: session.sandbox_id.clone(),
gateway_url,
token: session.token,
host_key_fingerprint: session.host_key_fingerprint,
})
}

fn ssh_base_command(proxy_command: &str) -> Command {
fn ssh_base_command(proxy_command: &str, host_key_fingerprint: &str) -> Command {
let mut command = Command::new("ssh");
command
.arg("-o")
.arg(format!("ProxyCommand={proxy_command}"))
.arg("-o")
.arg("StrictHostKeyChecking=no")
.arg("-o")
.arg("UserKnownHostsFile=/dev/null")
.arg("-o")
.arg("GlobalKnownHostsFile=/dev/null")
.arg("-o")
.arg("LogLevel=ERROR");
.arg(format!("ProxyCommand={proxy_command}"));
if host_key_fingerprint.is_empty() {
command
.arg("-o")
.arg("StrictHostKeyChecking=no")
.arg("-o")
.arg("UserKnownHostsFile=/dev/null")
.arg("-o")
.arg("GlobalKnownHostsFile=/dev/null");
} else {
// Write a temporary known_hosts with the gateway-provided fingerprint
// and enable strict host key checking to detect MITM attacks.
let known_hosts = format!("[sandbox]:2222 {host_key_fingerprint}");
let applied = (|| -> Option<()> {
let dir = tempfile::tempdir().ok()?;
let path = dir.into_path().join("known_hosts");
std::fs::write(&path, &known_hosts).ok()?;
command
.arg("-o")
.arg("StrictHostKeyChecking=yes")
.arg("-o")
.arg(format!("UserKnownHostsFile={}", path.display()))
.arg("-o")
.arg("GlobalKnownHostsFile=/dev/null");
Some(())
})();
if applied.is_none() {
command
.arg("-o")
.arg("StrictHostKeyChecking=no")
.arg("-o")
.arg("UserKnownHostsFile=/dev/null")
.arg("-o")
.arg("GlobalKnownHostsFile=/dev/null");
}
}
command.arg("-o").arg("LogLevel=ERROR");
command
}

Expand Down Expand Up @@ -236,7 +266,7 @@ async fn sandbox_connect_with_mode(
) -> Result<()> {
let session = ssh_session_config(server, name, tls).await?;

let mut command = ssh_base_command(&session.proxy_command);
let mut command = ssh_base_command(&session.proxy_command, &session.host_key_fingerprint);
command
.arg("-tt")
.arg("-o")
Expand Down Expand Up @@ -315,7 +345,7 @@ pub async fn sandbox_forward(

let session = ssh_session_config(server, name, tls).await?;

let mut command = TokioCommand::from(ssh_base_command(&session.proxy_command));
let mut command = TokioCommand::from(ssh_base_command(&session.proxy_command, &session.host_key_fingerprint));
command
.arg("-N")
.arg("-o")
Expand Down Expand Up @@ -395,7 +425,7 @@ async fn sandbox_exec_with_mode(
}

let session = ssh_session_config(server, name, tls).await?;
let mut ssh = ssh_base_command(&session.proxy_command);
let mut ssh = ssh_base_command(&session.proxy_command, &session.host_key_fingerprint);

if tty {
ssh.arg("-tt")
Expand Down Expand Up @@ -465,7 +495,7 @@ pub async fn sandbox_sync_up_files(

let session = ssh_session_config(server, name, tls).await?;

let mut ssh = ssh_base_command(&session.proxy_command);
let mut ssh = ssh_base_command(&session.proxy_command, &session.host_key_fingerprint);
ssh.arg("-T")
.arg("-o")
.arg("RequestTTY=no")
Expand Down Expand Up @@ -534,7 +564,7 @@ pub async fn sandbox_sync_up(
) -> Result<()> {
let session = ssh_session_config(server, name, tls).await?;

let mut ssh = ssh_base_command(&session.proxy_command);
let mut ssh = ssh_base_command(&session.proxy_command, &session.host_key_fingerprint);
ssh.arg("-T")
.arg("-o")
.arg("RequestTTY=no")
Expand Down Expand Up @@ -627,7 +657,7 @@ pub async fn sandbox_sync_down(
),
);

let mut ssh = ssh_base_command(&session.proxy_command);
let mut ssh = ssh_base_command(&session.proxy_command, &session.host_key_fingerprint);
ssh.arg("-T")
.arg("-o")
.arg("RequestTTY=no")
Expand Down
1 change: 1 addition & 0 deletions crates/openshell-tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ miette = { workspace = true }
owo-colors = { workspace = true }
serde = { workspace = true }
tracing = { workspace = true }
tempfile = "3"
url = { workspace = true }

[lints]
Expand Down
69 changes: 47 additions & 22 deletions crates/openshell-tui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,44 @@ async fn fetch_sandbox_detail(app: &mut App) {
// Shell connect (suspend TUI, launch SSH, resume)
// ---------------------------------------------------------------------------

/// Apply SSH host key verification arguments to an SSH command.
///
/// When the gateway provides a host key fingerprint, a temporary `known_hosts` file
/// is written and strict checking is enabled. Otherwise, host key checking is
/// disabled (current behavior until the gateway populates the fingerprint field).
fn apply_host_key_args(cmd: &mut std::process::Command, host_key_fingerprint: &str) {
if host_key_fingerprint.is_empty() {
cmd.arg("-o")
.arg("StrictHostKeyChecking=no")
.arg("-o")
.arg("UserKnownHostsFile=/dev/null")
.arg("-o")
.arg("GlobalKnownHostsFile=/dev/null");
} else {
let known_hosts = format!("[sandbox]:2222 {host_key_fingerprint}");
let applied = (|| -> Option<()> {
let dir = tempfile::tempdir().ok()?;
let path = dir.into_path().join("known_hosts");
std::fs::write(&path, &known_hosts).ok()?;
cmd.arg("-o")
.arg("StrictHostKeyChecking=yes")
.arg("-o")
.arg(format!("UserKnownHostsFile={}", path.display()))
.arg("-o")
.arg("GlobalKnownHostsFile=/dev/null");
Some(())
})();
if applied.is_none() {
cmd.arg("-o")
.arg("StrictHostKeyChecking=no")
.arg("-o")
.arg("UserKnownHostsFile=/dev/null")
.arg("-o")
.arg("GlobalKnownHostsFile=/dev/null");
}
}
}

/// Suspend the TUI, launch an interactive SSH shell to the sandbox, resume on exit.
///
/// This replicates the `openshell sandbox connect` flow but uses `Command::status()`
Expand Down Expand Up @@ -863,13 +901,9 @@ async fn handle_shell_connect(
let mut command = std::process::Command::new("ssh");
command
.arg("-o")
.arg(format!("ProxyCommand={proxy_command}"))
.arg("-o")
.arg("StrictHostKeyChecking=no")
.arg("-o")
.arg("UserKnownHostsFile=/dev/null")
.arg("-o")
.arg("GlobalKnownHostsFile=/dev/null")
.arg(format!("ProxyCommand={proxy_command}"));
apply_host_key_args(&mut command, &session.host_key_fingerprint);
command
.arg("-o")
.arg("LogLevel=ERROR")
.arg("-tt")
Expand Down Expand Up @@ -1012,14 +1046,9 @@ async fn handle_exec_command(
.join(" ");
let mut ssh = std::process::Command::new("ssh");
ssh.arg("-o")
.arg(format!("ProxyCommand={proxy_command}"))
.arg("-o")
.arg("StrictHostKeyChecking=no")
.arg("-o")
.arg("UserKnownHostsFile=/dev/null")
.arg("-o")
.arg("GlobalKnownHostsFile=/dev/null")
.arg("-o")
.arg(format!("ProxyCommand={proxy_command}"));
apply_host_key_args(&mut ssh, &session.host_key_fingerprint);
ssh.arg("-o")
.arg("LogLevel=ERROR")
.arg("-tt")
.arg("-o")
Expand Down Expand Up @@ -1434,13 +1463,9 @@ async fn start_port_forwards(
let mut command = std::process::Command::new("ssh");
command
.arg("-o")
.arg(format!("ProxyCommand={proxy_command}"))
.arg("-o")
.arg("StrictHostKeyChecking=no")
.arg("-o")
.arg("UserKnownHostsFile=/dev/null")
.arg("-o")
.arg("GlobalKnownHostsFile=/dev/null")
.arg(format!("ProxyCommand={proxy_command}"));
apply_host_key_args(&mut command, &session.host_key_fingerprint);
command
.arg("-o")
.arg("LogLevel=ERROR")
.arg("-o")
Expand Down
Loading