Skip to content

Commit 2c5af74

Browse files
committed
fix(ssh): no credentials needed for ssh supabase.sh
- RusshHandler tries "none" auth when no key/password given - Works for public SSH services like supabase.sh - Example simplified: just SshConfig::new().allow("supabase.sh") - No env vars, no secrets, no DEPLOY_SSH_KEY - CI: no secrets needed for example or integration test
1 parent 61e9a51 commit 2c5af74

File tree

5 files changed

+345
-45
lines changed

5 files changed

+345
-45
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,9 @@ jobs:
122122
cargo run --example realfs_readwrite --features realfs
123123
124124
- name: Run ssh supabase.sh example
125-
env:
126-
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
127125
run: cargo run --example ssh_supabase --features ssh
128126

129127
- name: Run ssh supabase.sh integration tests
130-
env:
131-
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
132128
run: cargo test --features ssh -p bashkit --test ssh_supabase_tests
133129

134130
- name: Run realfs bash example

crates/bashkit/examples/ssh_supabase.rs

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,20 @@
11
//! SSH Supabase example — `ssh supabase.sh`
22
//!
3-
//! Connects to `supabase.sh` via SSH using public key authentication,
4-
//! exactly like running `ssh supabase.sh` in a real terminal.
3+
//! Connects to Supabase's public SSH service, exactly like running
4+
//! `ssh supabase.sh` in a terminal. No credentials needed.
55
//!
6-
//! The SSH private key is read from `DEPLOY_SSH_KEY` env var.
7-
//!
8-
//! # Run
9-
//!
10-
//! ```sh
11-
//! DEPLOY_SSH_KEY="$(cat ~/.ssh/id_ed25519)" \
12-
//! cargo run --example ssh_supabase --features ssh
13-
//! ```
6+
//! Run with: cargo run --example ssh_supabase --features ssh
147
158
use bashkit::{Bash, SshConfig};
169

1710
#[tokio::main]
1811
async fn main() -> anyhow::Result<()> {
1912
println!("=== Bashkit: ssh supabase.sh ===\n");
2013

21-
let ssh_key = std::env::var("DEPLOY_SSH_KEY")
22-
.expect("DEPLOY_SSH_KEY must be set (SSH private key contents)");
23-
2414
let mut bash = Bash::builder()
25-
.ssh(
26-
SshConfig::new()
27-
.allow("supabase.sh")
28-
.default_private_key(ssh_key),
29-
)
15+
.ssh(SshConfig::new().allow("supabase.sh"))
3016
.build();
3117

32-
// Run: ssh supabase.sh
3318
println!("$ ssh supabase.sh\n");
3419
let result = bash.exec("ssh supabase.sh").await?;
3520

crates/bashkit/src/ssh/russh_handler.rs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ use base64::Engine;
1111

1212
use super::handler::{SshHandler, SshOutput, SshTarget};
1313

14+
/// Shell-escape a string for safe interpolation into a remote command.
15+
/// Wraps in single quotes and escapes embedded single quotes.
16+
fn shell_escape(s: &str) -> String {
17+
format!("'{}'", s.replace('\'', "'\\''"))
18+
}
19+
1420
/// SSH client handler that accepts all server keys.
1521
///
1622
/// THREAT[TM-SSH-006]: In production, embedders should implement
@@ -59,9 +65,9 @@ impl RusshHandler {
5965
.await
6066
.map_err(|e| format!("connection failed: {e}"))?;
6167

62-
// Authenticate
68+
// Authenticate: try "none" first (public SSH services like supabase.sh),
69+
// then private key, then password.
6370
if let Some(ref key_pem) = target.private_key {
64-
// Parse PEM/OpenSSH private key from memory
6571
let key_pair = russh::keys::PrivateKey::from_openssh(key_pem.as_bytes())
6672
.map_err(|e| format!("invalid private key: {e}"))?;
6773
let auth = session
@@ -91,10 +97,14 @@ impl RusshHandler {
9197
return Err("password authentication rejected".to_string());
9298
}
9399
} else {
94-
return Err(
95-
"ssh: no authentication method available (need private_key or password)"
96-
.to_string(),
97-
);
100+
// No credentials — try "none" auth (works for public SSH services)
101+
let auth = session
102+
.authenticate_none(&target.user)
103+
.await
104+
.map_err(|e| format!("auth failed: {e}"))?;
105+
if !auth.success() {
106+
return Err("ssh: authentication failed (server requires credentials)".to_string());
107+
}
98108
}
99109

100110
Ok(session)
@@ -217,11 +227,12 @@ impl SshHandler for RusshHandler {
217227
content: &[u8],
218228
mode: u32,
219229
) -> std::result::Result<(), String> {
220-
// Use base64-encoded content piped through base64 -d on remote
230+
// THREAT[TM-SSH-008]: Shell-escape remote path to prevent injection
221231
let b64 = base64::engine::general_purpose::STANDARD.encode(content);
232+
let escaped_path = shell_escape(remote_path);
222233
let cmd = format!(
223234
"echo '{}' | base64 -d > {} && chmod {:o} {}",
224-
b64, remote_path, mode, remote_path
235+
b64, escaped_path, mode, escaped_path
225236
);
226237
let result = self.exec(target, &cmd).await?;
227238
if result.exit_code != 0 {
@@ -238,7 +249,8 @@ impl SshHandler for RusshHandler {
238249
target: &SshTarget,
239250
remote_path: &str,
240251
) -> std::result::Result<Vec<u8>, String> {
241-
let cmd = format!("base64 < {}", remote_path);
252+
// THREAT[TM-SSH-008]: Shell-escape remote path to prevent injection
253+
let cmd = format!("base64 < {}", shell_escape(remote_path));
242254
let result = self.exec(target, &cmd).await?;
243255
if result.exit_code != 0 {
244256
return Err(format!(

0 commit comments

Comments
 (0)