Skip to content

Commit 1e94e77

Browse files
committed
feat: add SSH agent authentication support with auto-detection
1 parent e3bd6a2 commit 1e94e77

5 files changed

Lines changed: 173 additions & 56 deletions

File tree

README.md

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ A high-performance parallel SSH command execution tool for cluster management, b
88
- **Cluster Management**: Define and manage node clusters via configuration files
99
- **Progress Tracking**: Real-time progress indicators for each node
1010
- **Flexible Authentication**: Support for SSH keys and SSH agent
11+
- **Host Key Verification**: Secure host key checking with known_hosts support
1112
- **Cross-Platform**: Works on Linux and macOS
1213

1314
## Installation
@@ -30,6 +31,9 @@ bssh -c production "df -h"
3031
# With custom SSH key
3132
bssh -c staging -i ~/.ssh/custom_key "systemctl status nginx"
3233

34+
# Use SSH agent for authentication
35+
bssh --use-agent -c production "systemctl status nginx"
36+
3337
# Limit parallel connections
3438
bssh -c production --parallel 5 "apt update"
3539
```
@@ -110,16 +114,18 @@ clusters:
110114
111115
```
112116
Options:
113-
-H, --hosts <HOSTS> Comma-separated list of hosts
114-
-c, --cluster <CLUSTER> Cluster name from configuration
115-
--config <CONFIG> Config file path [default: ~/.bssh/config.yaml]
116-
-u, --user <USER> Default username for SSH
117-
-i, --identity <IDENTITY> SSH private key file
118-
-p, --parallel <PARALLEL> Max parallel connections [default: 10]
119-
--output-dir <OUTPUT_DIR> Output directory for results
120-
-v, --verbose Increase verbosity (-v, -vv, -vvv)
121-
-h, --help Print help
122-
-V, --version Print version
117+
-H, --hosts <HOSTS> Comma-separated list of hosts
118+
-c, --cluster <CLUSTER> Cluster name from configuration
119+
--config <CONFIG> Config file path [default: ~/.bssh/config.yaml]
120+
-u, --user <USER> Default username for SSH
121+
-i, --identity <IDENTITY> SSH private key file
122+
-A, --use-agent Use SSH agent for authentication
123+
--strict-host-key-checking <MODE> Host key checking mode [yes|no|accept-new] [default: accept-new]
124+
-p, --parallel <PARALLEL> Max parallel connections [default: 10]
125+
--output-dir <OUTPUT_DIR> Output directory for results
126+
-v, --verbose Increase verbosity (-v, -vv, -vvv)
127+
-h, --help Print help
128+
-V, --version Print version
123129
```
124130

125131
## Examples
@@ -178,5 +184,12 @@ Licensed under the Apache License, Version 2.0
178184
## Changelog
179185

180186
### Recent Updates
181-
- **v0.2.0 (2025/08/21):** Backend.AI multi-node session support with automatic cluster detection and SSH port 2200
187+
- **v0.2.0 (2025/08/21):**
188+
- Backend.AI multi-node session support with automatic cluster detection
189+
- SSH agent authentication support with auto-detection
190+
- Host key verification with StrictHostKeyChecking modes
191+
- Environment variable expansion in configuration (${VAR})
192+
- Connection and execution timeouts
193+
- Fixed list command to work without host specification
194+
- Basic SCP file copy support
182195
- **v0.1.0 (2025/08/21):** Initial release with parallel SSH execution using async-ssh2-tokio

src/cli.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ pub struct Cli {
5050
#[arg(short = 'i', long, help = "SSH private key file path")]
5151
pub identity: Option<PathBuf>,
5252

53+
#[arg(
54+
short = 'A',
55+
long,
56+
help = "Use SSH agent for authentication (Unix/Linux/macOS only)"
57+
)]
58+
pub use_agent: bool,
59+
5360
#[arg(
5461
short = 'p',
5562
long,

src/executor.rs

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub struct ParallelExecutor {
2727
max_parallel: usize,
2828
key_path: Option<String>,
2929
strict_mode: StrictHostKeyChecking,
30+
use_agent: bool,
3031
}
3132

3233
impl ParallelExecutor {
@@ -50,6 +51,23 @@ impl ParallelExecutor {
5051
max_parallel,
5152
key_path,
5253
strict_mode,
54+
use_agent: false,
55+
}
56+
}
57+
58+
pub fn new_with_strict_mode_and_agent(
59+
nodes: Vec<Node>,
60+
max_parallel: usize,
61+
key_path: Option<String>,
62+
strict_mode: StrictHostKeyChecking,
63+
use_agent: bool,
64+
) -> Self {
65+
Self {
66+
nodes,
67+
max_parallel,
68+
key_path,
69+
strict_mode,
70+
use_agent,
5371
}
5472
}
5573

@@ -70,6 +88,7 @@ impl ParallelExecutor {
7088
let command = command.to_string();
7189
let key_path = self.key_path.clone();
7290
let strict_mode = self.strict_mode;
91+
let use_agent = self.use_agent;
7392
let semaphore = Arc::clone(&semaphore);
7493
let pb = multi_progress.add(ProgressBar::new_spinner());
7594
pb.set_style(style.clone());
@@ -82,9 +101,14 @@ impl ParallelExecutor {
82101

83102
pb.set_message("Executing command...");
84103

85-
let result =
86-
execute_on_node(node.clone(), &command, key_path.as_deref(), strict_mode)
87-
.await;
104+
let result = execute_on_node(
105+
node.clone(),
106+
&command,
107+
key_path.as_deref(),
108+
strict_mode,
109+
use_agent,
110+
)
111+
.await;
88112

89113
match &result {
90114
Ok(cmd_result) => {
@@ -141,6 +165,7 @@ impl ParallelExecutor {
141165
let remote_path = remote_path.to_string();
142166
let key_path = self.key_path.clone();
143167
let strict_mode = self.strict_mode;
168+
let use_agent = self.use_agent;
144169
let semaphore = Arc::clone(&semaphore);
145170
let pb = multi_progress.add(ProgressBar::new_spinner());
146171
pb.set_style(style.clone());
@@ -159,6 +184,7 @@ impl ParallelExecutor {
159184
&remote_path,
160185
key_path.as_deref(),
161186
strict_mode,
187+
use_agent,
162188
)
163189
.await;
164190

@@ -198,13 +224,14 @@ async fn execute_on_node(
198224
command: &str,
199225
key_path: Option<&str>,
200226
strict_mode: StrictHostKeyChecking,
227+
use_agent: bool,
201228
) -> Result<CommandResult> {
202229
let mut client = SshClient::new(node.host.clone(), node.port, node.username.clone());
203230

204231
let key_path = key_path.map(Path::new);
205232

206233
client
207-
.connect_and_execute_with_host_check(command, key_path, Some(strict_mode))
234+
.connect_and_execute_with_host_check(command, key_path, Some(strict_mode), use_agent)
208235
.await
209236
}
210237

@@ -214,13 +241,20 @@ async fn copy_to_node(
214241
remote_path: &str,
215242
key_path: Option<&str>,
216243
strict_mode: StrictHostKeyChecking,
244+
use_agent: bool,
217245
) -> Result<()> {
218246
let mut client = SshClient::new(node.host.clone(), node.port, node.username.clone());
219247

220248
let key_path = key_path.map(Path::new);
221249

222250
client
223-
.copy_file(local_path, remote_path, key_path, Some(strict_mode))
251+
.copy_file(
252+
local_path,
253+
remote_path,
254+
key_path,
255+
Some(strict_mode),
256+
use_agent,
257+
)
224258
.await
225259
}
226260

src/main.rs

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,14 @@ async fn main() -> Result<()> {
6363
// Handle remaining commands
6464
match cli.command {
6565
Some(Commands::Ping) => {
66-
ping_nodes(nodes, cli.parallel, cli.identity.as_deref(), strict_mode).await?;
66+
ping_nodes(
67+
nodes,
68+
cli.parallel,
69+
cli.identity.as_deref(),
70+
strict_mode,
71+
cli.use_agent,
72+
)
73+
.await?;
6774
}
6875
Some(Commands::Copy {
6976
source,
@@ -76,6 +83,7 @@ async fn main() -> Result<()> {
7683
cli.parallel,
7784
cli.identity.as_deref(),
7885
strict_mode,
86+
cli.use_agent,
7987
)
8088
.await?;
8189
}
@@ -88,6 +96,7 @@ async fn main() -> Result<()> {
8896
cli.identity.as_deref(),
8997
cli.verbose > 0,
9098
strict_mode,
99+
cli.use_agent,
91100
)
92101
.await?;
93102
}
@@ -154,12 +163,18 @@ async fn ping_nodes(
154163
max_parallel: usize,
155164
key_path: Option<&Path>,
156165
strict_mode: StrictHostKeyChecking,
166+
use_agent: bool,
157167
) -> Result<()> {
158168
println!("Pinging {} nodes...\n", nodes.len());
159169

160170
let key_path = key_path.map(|p| p.to_string_lossy().to_string());
161-
let executor =
162-
ParallelExecutor::new_with_strict_mode(nodes.clone(), max_parallel, key_path, strict_mode);
171+
let executor = ParallelExecutor::new_with_strict_mode_and_agent(
172+
nodes.clone(),
173+
max_parallel,
174+
key_path,
175+
strict_mode,
176+
use_agent,
177+
);
163178

164179
let results = executor.execute("echo 'pong'").await?;
165180

@@ -191,12 +206,18 @@ async fn execute_command(
191206
key_path: Option<&Path>,
192207
verbose: bool,
193208
strict_mode: StrictHostKeyChecking,
209+
use_agent: bool,
194210
) -> Result<()> {
195211
println!("Executing command on {} nodes: {}\n", nodes.len(), command);
196212

197213
let key_path = key_path.map(|p| p.to_string_lossy().to_string());
198-
let executor =
199-
ParallelExecutor::new_with_strict_mode(nodes, max_parallel, key_path, strict_mode);
214+
let executor = ParallelExecutor::new_with_strict_mode_and_agent(
215+
nodes,
216+
max_parallel,
217+
key_path,
218+
strict_mode,
219+
use_agent,
220+
);
200221

201222
let results = executor.execute(command).await?;
202223

@@ -225,6 +246,7 @@ async fn copy_file(
225246
max_parallel: usize,
226247
key_path: Option<&Path>,
227248
strict_mode: StrictHostKeyChecking,
249+
use_agent: bool,
228250
) -> Result<()> {
229251
// Check if source file exists
230252
if !source.exists() {
@@ -244,8 +266,13 @@ async fn copy_file(
244266
);
245267

246268
let key_path = key_path.map(|p| p.to_string_lossy().to_string());
247-
let executor =
248-
ParallelExecutor::new_with_strict_mode(nodes, max_parallel, key_path, strict_mode);
269+
let executor = ParallelExecutor::new_with_strict_mode_and_agent(
270+
nodes,
271+
max_parallel,
272+
key_path,
273+
strict_mode,
274+
use_agent,
275+
);
249276

250277
let results = executor.copy_file(source, destination).await?;
251278

0 commit comments

Comments
 (0)