|
@@ -39,14 +39,22 @@ async fn ssh_session_with_timeout(
|
|
|
inactivity_timeout,
|
|
inactivity_timeout,
|
|
|
..Default::default()
|
|
..Default::default()
|
|
|
};
|
|
};
|
|
|
- let expected_key = Arc::new(std::sync::Mutex::new(node.host_key.clone()));
|
|
|
|
|
|
|
|
|
|
- if expected_key.lock().unwrap().is_none() {
|
|
|
|
|
- warn!(
|
|
|
|
|
- "No host_key set for {} — skipping verification (insecure)",
|
|
|
|
|
- node.name
|
|
|
|
|
|
|
+ // Refuse to connect without a pinned host key — a missing `host_key`
|
|
|
|
|
+ // would otherwise skip verification entirely (MITM vector). Run
|
|
|
|
|
+ // `sm121 init-host-keys` to discover and pin keys, or set `host_key`
|
|
|
|
|
+ // manually in config.toml. (`fetch_host_key` / init-host-keys uses a
|
|
|
|
|
+ // separate capturing handler and is intentionally exempt.)
|
|
|
|
|
+ if node.host_key.is_none() {
|
|
|
|
|
+ anyhow::bail!(
|
|
|
|
|
+ "No host_key set for node '{}' ({}). Refusing to connect without \
|
|
|
|
|
+ host-key verification. Run `sm121 init-host-keys --cluster <name>` \
|
|
|
|
|
+ to discover and pin host keys, or set `host_key` manually in config.toml.",
|
|
|
|
|
+ node.name,
|
|
|
|
|
+ node.ssh_ip
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
+ let expected_key = Arc::new(std::sync::Mutex::new(node.host_key.clone()));
|
|
|
|
|
|
|
|
let handler = VerifyingHandler { expected_key };
|
|
let handler = VerifyingHandler { expected_key };
|
|
|
let mut session = tokio::time::timeout(
|
|
let mut session = tokio::time::timeout(
|
|
@@ -239,8 +247,19 @@ pub async fn fetch_host_key(config: &Config, node: &Node) -> Result<String> {
|
|
|
let handler = CaptureHandler {
|
|
let handler = CaptureHandler {
|
|
|
captured: captured_clone,
|
|
captured: captured_clone,
|
|
|
};
|
|
};
|
|
|
- let mut session =
|
|
|
|
|
- russh::client::connect(Arc::new(ssh_config), (node.ssh_ip.as_str(), 22), handler).await?;
|
|
|
|
|
|
|
+ // Apply the same connect timeout as `ssh_session_with_timeout` so an
|
|
|
|
|
+ // unreachable node fails fast instead of hanging `health --fingerprint`.
|
|
|
|
|
+ let mut session = tokio::time::timeout(
|
|
|
|
|
+ CONNECT_TIMEOUT,
|
|
|
|
|
+ russh::client::connect(Arc::new(ssh_config), (node.ssh_ip.as_str(), 22), handler),
|
|
|
|
|
+ )
|
|
|
|
|
+ .await
|
|
|
|
|
+ .context(format!(
|
|
|
|
|
+ "SSH connect to {}:{} timed out after {}s",
|
|
|
|
|
+ node.ssh_ip,
|
|
|
|
|
+ 22,
|
|
|
|
|
+ CONNECT_TIMEOUT.as_secs()
|
|
|
|
|
+ ))??;
|
|
|
|
|
|
|
|
session
|
|
session
|
|
|
.authenticate_publickey(
|
|
.authenticate_publickey(
|
|
@@ -259,6 +278,20 @@ pub async fn fetch_host_key(config: &Config, node: &Node) -> Result<String> {
|
|
|
Ok(fp)
|
|
Ok(fp)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+pub(crate) fn shq(s: &str) -> String {
|
|
|
|
|
+ let mut out = String::with_capacity(s.len() + 2);
|
|
|
|
|
+ out.push('\'');
|
|
|
|
|
+ for c in s.chars() {
|
|
|
|
|
+ if c == '\'' {
|
|
|
|
|
+ out.push_str(r"'\''");
|
|
|
|
|
+ } else {
|
|
|
|
|
+ out.push(c);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ out.push('\'');
|
|
|
|
|
+ out
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/// Upload a local directory to a remote node by tarring, base64-encoding,
|
|
/// Upload a local directory to a remote node by tarring, base64-encoding,
|
|
|
/// and piping the result through SSH for decoding and extraction.
|
|
/// and piping the result through SSH for decoding and extraction.
|
|
|
///
|
|
///
|
|
@@ -297,9 +330,11 @@ pub async fn upload_dir(
|
|
|
|
|
|
|
|
// Base64 is safe inside single quotes (charset: A-Za-z0-9+/=).
|
|
// Base64 is safe inside single quotes (charset: A-Za-z0-9+/=).
|
|
|
// `printf '%s'` is portable and doesn't interpret escape sequences.
|
|
// `printf '%s'` is portable and doesn't interpret escape sequences.
|
|
|
|
|
+ // `remote_dir` is shell-quoted — a path with spaces or metachars would
|
|
|
|
|
+ // otherwise break `mkdir -p` / `cd`.
|
|
|
let cmd = format!(
|
|
let cmd = format!(
|
|
|
"mkdir -p {dir} && cd {dir} && printf '%s' '{b64}' | base64 -d | tar xzf -",
|
|
"mkdir -p {dir} && cd {dir} && printf '%s' '{b64}' | base64 -d | tar xzf -",
|
|
|
- dir = remote_dir,
|
|
|
|
|
|
|
+ dir = shq(remote_dir),
|
|
|
b64 = b64
|
|
b64 = b64
|
|
|
);
|
|
);
|
|
|
|
|
|