Harden forced ssh flags: accept-new host keys, block multiplexing, tighten identity auth
BatchMode=yes already blocked TTY prompts, but a brand-new host with no known_hosts entry failed outright on first connect since ssh had no way to ask for acceptance. Add StrictHostKeyChecking=accept-new (TOFU, still hard-fails on a changed known host) plus LogLevel=ERROR to keep the resulting "permanently added" notice out of profile logs. Also force ControlMaster=no/ControlPath=none and ClearAllForwardings=yes so a user's own ~/.ssh/config can't make porthole's spawned ssh share a multiplexed connection or apply extra forwards - the supervisor's process-based tracking assumes one spawned ssh exclusively owns one tunnel. ServerAliveCountMax=3 makes dead-connection detection time deterministic against the profile's keepalive, and -T is explicit no-pty insurance alongside the existing -N. IdentitiesOnly=yes is added whenever a profile sets an identity file, avoiding auth-failure lockouts from also offering agent/default keys. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
40
src/ssh.rs
40
src/ssh.rs
@@ -15,12 +15,25 @@ pub fn build(profile: &Profile) -> Command {
|
||||
"-o",
|
||||
"BatchMode=yes",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=accept-new",
|
||||
"-o",
|
||||
"LogLevel=ERROR",
|
||||
"-o",
|
||||
"ExitOnForwardFailure=yes",
|
||||
"-o",
|
||||
"ConnectTimeout=10",
|
||||
"-o",
|
||||
"ServerAliveCountMax=3",
|
||||
"-o",
|
||||
"ControlMaster=no",
|
||||
"-o",
|
||||
"ControlPath=none",
|
||||
"-o",
|
||||
"ClearAllForwardings=yes",
|
||||
"-o",
|
||||
&format!("ServerAliveInterval={}", profile.keepalive),
|
||||
"-N",
|
||||
"-T",
|
||||
]);
|
||||
|
||||
let (jumps, target) = profile.ssh_target();
|
||||
@@ -31,6 +44,7 @@ pub fn build(profile: &Profile) -> Command {
|
||||
cmd.arg("-p").arg(profile.ssh_port.to_string());
|
||||
if let Some(identity) = &profile.identity {
|
||||
cmd.arg("-i").arg(identity);
|
||||
cmd.args(["-o", "IdentitiesOnly=yes"]);
|
||||
}
|
||||
if let Some(user) = &profile.user {
|
||||
cmd.arg("-l").arg(user);
|
||||
@@ -86,4 +100,30 @@ mod tests {
|
||||
let l_idx = args.iter().position(|a| a == "-L").expect("-L present");
|
||||
assert_eq!(args[l_idx + 1], "5432:db.internal:5432");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identities_only_set_when_identity_given() {
|
||||
let p = Profile::new(
|
||||
"t".into(),
|
||||
&ProfileEdits {
|
||||
local: Some("5432:db.internal:5432".into()),
|
||||
via: Some(vec!["jumpbox".into()]),
|
||||
identity: Some("/home/me/.ssh/id_ed25519".into()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let args: Vec<String> = build(&p).get_args().map(|a| a.to_string_lossy().into_owned()).collect();
|
||||
let i_idx = args.iter().position(|a| a == "-i").expect("-i present");
|
||||
assert_eq!(args[i_idx + 1], "/home/me/.ssh/id_ed25519");
|
||||
assert!(args.windows(2).any(|w| w == ["-o", "IdentitiesOnly=yes"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identities_only_absent_without_identity() {
|
||||
let p = profile_with(vec!["jumpbox"]);
|
||||
assert!(p.identity.is_none());
|
||||
let args: Vec<String> = build(&p).get_args().map(|a| a.to_string_lossy().into_owned()).collect();
|
||||
assert!(!args.windows(2).any(|w| w == ["-o", "IdentitiesOnly=yes"]));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user