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
42 changes: 41 additions & 1 deletion crates/bashkit/src/ssh/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ pub const DEFAULT_PORT: u16 = 22;
/// - Host allowlist is default-deny (empty blocks everything)
/// - Keys are read from VFS only, never from host filesystem
/// - All connections have timeouts to prevent hangs
#[derive(Debug, Clone)]
#[derive(Clone)]
pub struct SshConfig {
/// Host allowlist
pub(crate) allowlist: SshAllowlist,
Expand All @@ -67,6 +67,29 @@ pub struct SshConfig {
pub(crate) default_port: u16,
}

// THREAT[TM-INF-016]: Redact credentials in Debug output to prevent
// passwords and private keys from leaking into logs, error messages, or LLM context.
impl std::fmt::Debug for SshConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SshConfig")
.field("allowlist", &self.allowlist)
.field("default_user", &self.default_user)
.field(
"default_password",
&self.default_password.as_ref().map(|_| "[REDACTED]"),
)
.field(
"default_private_key",
&self.default_private_key.as_ref().map(|_| "[REDACTED]"),
)
.field("timeout", &self.timeout)
.field("max_response_bytes", &self.max_response_bytes)
.field("max_sessions", &self.max_sessions)
.field("default_port", &self.default_port)
.finish()
}
}

impl Default for SshConfig {
fn default() -> Self {
Self {
Expand Down Expand Up @@ -213,6 +236,23 @@ mod tests {
assert_eq!(config.default_port, 2222);
}

#[test]
fn test_debug_redacts_credentials() {
let config = SshConfig::new()
.default_password("super_secret_password")
.default_private_key("-----BEGIN OPENSSH PRIVATE KEY-----");
let debug = format!("{:?}", config);
assert!(
!debug.contains("super_secret_password"),
"password leaked in Debug: {debug}"
);
assert!(
!debug.contains("BEGIN OPENSSH PRIVATE KEY"),
"private key leaked in Debug: {debug}"
);
assert!(debug.contains("[REDACTED]"), "REDACTED missing: {debug}");
}

#[test]
fn test_allowlist_integration() {
let config = SshConfig::new().allow("*.supabase.co").allow_port(22);
Expand Down
45 changes: 44 additions & 1 deletion crates/bashkit/src/ssh/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use async_trait::async_trait;
///
/// Fully resolved by the builtin before passing to the handler.
/// The handler does NOT need to validate the host — that's already done.
#[derive(Debug, Clone)]
#[derive(Clone)]
pub struct SshTarget {
/// Remote hostname or IP.
pub host: String,
Expand All @@ -28,6 +28,22 @@ pub struct SshTarget {
pub password: Option<String>,
}

// THREAT[TM-INF-016]: Redact credentials in Debug output.
impl std::fmt::Debug for SshTarget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SshTarget")
.field("host", &self.host)
.field("port", &self.port)
.field("user", &self.user)
.field(
"private_key",
&self.private_key.as_ref().map(|_| "[REDACTED]"),
)
.field("password", &self.password.as_ref().map(|_| "[REDACTED]"))
.finish()
}
}

/// Output from a remote command execution.
#[derive(Debug, Clone, Default)]
pub struct SshOutput {
Expand Down Expand Up @@ -126,3 +142,30 @@ pub trait SshHandler: Send + Sync {
remote_path: &str,
) -> std::result::Result<Vec<u8>, String>;
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_debug_redacts_credentials() {
let target = SshTarget {
host: "example.com".to_string(),
port: 22,
user: "admin".to_string(),
private_key: Some("-----BEGIN OPENSSH PRIVATE KEY-----".to_string()),
password: Some("super_secret".to_string()),
};
let debug = format!("{:?}", target);
assert!(!debug.contains("super_secret"), "password leaked: {debug}");
assert!(
!debug.contains("BEGIN OPENSSH PRIVATE KEY"),
"key leaked: {debug}"
);
assert!(debug.contains("[REDACTED]"), "REDACTED missing: {debug}");
assert!(
debug.contains("example.com"),
"host should be visible: {debug}"
);
}
}
Loading