bssh (Broadcast SSH / Backend.AI SSH) is a high-performance parallel SSH command execution tool with SSH-compatible interface. This document provides a high-level architecture overview. For detailed component documentation, see docs/architecture/.
- Parallel command execution across multiple nodes
- SSH-compatible command-line interface (drop-in replacement)
- SSH port forwarding (-L, -R, -D/SOCKS proxy)
- SSH jump host support (-J)
- SSH configuration file parsing (-F)
- Interactive PTY sessions with single/multiplex modes
- SFTP file transfers (upload/download) with bounded pipelined streaming
- Backend.AI cluster auto-detection
- pdsh compatibility mode
┌─────────────────────────────────────────────────────────┐
│ CLI Interface │
│ (main.rs) │
│ (-L, -R, -D, -J, -F, -t/T, SSH-compatible) │
└────────────────────────────┬────────────────────────────┘
│
┌─────────────┬──────────────┼──────────────┬─────────────┐
▼ ▼ ▼ ▼ ▼
┌──────────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌──────────┐
│ Commands │ │ Config │ │ Utils │ │Forwarding │ │ Jump │
│ Module │ │ Manager │ │ Module │ │ Manager │ │ Host │
│ (commands/*) │ │(config.rs)│ │ (utils/*) │ │(forward/*)│ │ (jump/*) │
└──────┬───────┘ └─────┬─────┘ └───────────┘ └───┬───────┘ └───┬──────┘
│ │ │ │
│ ▼ │ │
│ ┌──────────────┐ │ │
│ │ SSH Config │ │ │
│ │ Parser │ │ │
│ │(ssh_config/*)│ │ │
│ └──────────────┘ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Executor │◄────────────────────────┤ Node │ │ Port Forwarders │
│ (Parallel) │ │ Parser │ │ (L/R/D modes) │
│(executor.rs) │ │ (node.rs) │ │ + Tunnels │
└──────┬───────┘ └──────────────┘ └────────┬─────────┘
│ │
├──────────┬────────────┬───────────────────────────────────┘
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ SSH │ │ SSH │ │ SSH │
│ Client │ │ Client │ │ Client │
│ (russh) │ │ (russh) │ │ (russh) │
└──────────┘ └──────────┘ └──────────┘
Documentation: docs/architecture/cli-interface.md
The CLI system provides an SSH-compatible command-line interface with multiple operation modes:
- Native bssh mode: Cluster-based parallel execution
- SSH compatibility mode: Drop-in SSH replacement for single-host operations
- pdsh compatibility mode: Compatible with pdsh command-line syntax
Key features:
- clap v4 with derive macros for type-safe argument parsing
- Backend.AI cluster auto-detection
- Hostlist expression support (pdsh-compatible)
- Mode detection based on binary name, environment, or flags
Documentation: docs/architecture/configuration.md
Hierarchical configuration system with multiple sources:
- Backend.AI environment variables (auto-detection)
- Current directory (
./config.yaml) - XDG config directory (
~/.config/bssh/config.yaml) - CLI specified path (via
--configflag)
Features:
- YAML format for human readability
- Environment variable expansion (
${VAR}syntax) - SSH configuration file integration
- Platform-specific paths via XDG Base Directory specification
Documentation: docs/architecture/executor.md
Tokio-based async executor for concurrent command execution:
- Semaphore-based concurrency limiting
- Two-stage signal handling (default) or batch mode
- Fail-fast mode for early termination on errors
- Real-time progress visualization
- Stream mode for live output
Documentation: docs/architecture/ssh-client.md
Built on russh and russh-sftp with custom tokio_client wrapper:
- Connection management with russh
- Multiple authentication methods (agent, key file, password)
- Host key verification (known_hosts support)
- Command execution with streaming output
- SFTP file transfers (upload/download)
- Connection timeout handling
- Configurable SSH keepalive (ServerAliveInterval, ServerAliveCountMax)
- Address family selection (-4/-6, AddressFamily); see Address Family Preference
Documentation: docs/architecture/tui.md
Interactive terminal interface for real-time command monitoring:
- Multiple views (JobList, JobDetail, Logs, System)
- Keyboard navigation and command palette
- Progress parsing from command output
- Real-time log streaming
- Clean shutdown handling
Documentation: docs/architecture/interactive-mode.md
PTY-based interactive SSH sessions:
- Single-host mode: Direct PTY connection to one host
- Multiplex mode: Broadcast input to multiple hosts
- Terminal escape sequence handling
- Raw mode terminal management
- Signal propagation (Ctrl+C, window resize)
Documentation: docs/architecture/ssh-config-parser.md
OpenSSH-compatible configuration file parser:
- Include directive support with recursion limits
- Match directive (Host, LocalUser)
- All standard SSH options
- Configuration caching for performance
- Override chain resolution
Documentation: docs/architecture/ssh-jump-hosts.md
ProxyJump (-J) support for bastion hosts:
- Multiple jump host chains
- IPv6 and custom port support
- Authentication through jump hosts
- Integration with all bssh operations
- Automatic tunnel management
Documentation: docs/architecture/ssh-port-forwarding.md
Full port forwarding support:
- Local forwarding (-L): Forward local port to remote
- Remote forwarding (-R): Forward remote port to local
- Dynamic forwarding (-D): SOCKS proxy mode
- Multiple forwarding rules
- Automatic port allocation
Documentation: docs/architecture/exit-code-strategy.md
MPI-compatible exit code handling:
- MainRank (default): Returns main rank's exit code
- RequireAllSuccess: Returns 0 only if all nodes succeed
- MainRankWithFailureCheck: Hybrid mode for detailed diagnostics
- Automatic main rank detection (Backend.AI integration)
- Preserves actual exit codes (SIGSEGV=139, OOM=137, etc.)
ping is the exception: it runs no user command, so no remote status exists to forward and MainRank does not apply. It reuses RequireAllSuccess for the 0/1 boundary and reports 255 when no host answered or when bssh failed before connecting, following OpenSSH's "ssh itself failed" convention. ping_nodes returns a PingOutcome tally rather than Result<()>, dispatch_command returns Result<i32>, and main::dispatch_and_exit is the single place that converts a nonzero command-level code into the process exit status.
Common utilities for code reuse between bssh client and server implementations:
- Validation: Input validation for usernames, hostnames, paths with security checks
- Rate Limiting: Generic token bucket rate limiter for connection/auth throttling
- Authentication Types: Common auth result types and user info structures
- Error Types: Shared error types for validation, auth, connection, and rate limiting
The security and jump::rate_limiter modules re-export from shared for backward compatibility.
Security features for the SSH server (src/server/security/):
-
AuthRateLimiter: Fail2ban-like authentication rate limiting
- Tracks failed authentication attempts per IP address
- Automatic banning after exceeding configurable threshold
- Time-windowed failure counting (failures outside window not counted)
- Configurable ban duration with automatic expiration
- IP whitelist for exempting trusted addresses from banning
- Memory-safe with configurable maximum tracked IPs
- Automatic cleanup of expired records via background task
- Thread-safe async implementation with
Arc<RwLock<>>
-
IpAccessControl: IP-based connection filtering
- Whitelist mode: Only allow connections from specified CIDR ranges
- Blacklist mode: Block connections from specified CIDR ranges
- Blacklist takes priority over whitelist (blocked IPs are always denied)
- Support for both IPv4 and IPv6 addresses and CIDR notation
- Dynamic updates: Add/remove rules at runtime via
SharedIpAccessControl - Early rejection at connection level before handler creation
- Thread-safe with fail-closed behavior on lock contention
- Configuration via
allowed_ipsandblocked_ipsin server config
Policy-based filtering infrastructure for SFTP and SCP file transfer operations (src/server/filter/):
Structure:
mod.rs-TransferFiltertrait,Operationenum,FilterResultenum,NoOpFilterpolicy.rs-FilterPolicyengine,FilterRule,Matchertrait,SharedFilterPolicypath.rs- Path-based matchers:PrefixMatcher,ExactMatcher,ComponentMatcher,ExtensionMatcherpattern.rs- Pattern-based matchers:GlobMatcher,RegexMatcher,CombinedMatcher,NotMatcher
Key Components:
-
Operation: Enum representing file operations
Upload,Download,Delete,RenameCreateDir,ListDir,Stat,SetStatSymlink,ReadLink
-
FilterResult: Actions to take on matched operations
Allow- Permit the operation (default)Deny- Block the operationLog- Allow but log for auditing
-
TransferFilter Trait: Interface for custom filter implementations
check(path, operation, user)- Check single path operationscheck_with_dest(src, dest, operation, user)- Check two-path operations (rename, symlink)is_enabled()- Check if filtering is active
-
FilterPolicy: First-match-wins rule evaluation engine
- Ordered rule evaluation
- Configurable default action
- Enable/disable filtering
- Create from YAML configuration via
from_config()
-
FilterRule: Combines matcher, action, and optional constraints
- Path pattern matcher
- Per-operation restrictions
- Per-user restrictions
- Named rules for debugging
Built-in Matchers:
| Matcher | Purpose | Example |
|---|---|---|
GlobMatcher |
Wildcard patterns | *.key, *.pem |
RegexMatcher |
Full regex support | (?i)\.exe$ |
PrefixMatcher |
Directory tree matching | /etc/ |
ExactMatcher |
Specific file matching | /etc/shadow |
ComponentMatcher |
Path component matching | .git, .ssh |
ExtensionMatcher |
File extension matching | exe, key |
CombinedMatcher |
OR-combine matchers | Multiple patterns |
NotMatcher |
Invert matcher results | Exclude patterns |
Security Features:
normalize_path()function for path traversal prevention- ReDoS protection via regex size limits
- Case-insensitive extension matching
Usage Example:
use bssh::server::filter::{FilterPolicy, FilterResult, Operation};
use bssh::server::filter::pattern::GlobMatcher;
use bssh::server::filter::policy::FilterRule;
use std::path::Path;
// Create policy that blocks *.key files
let policy = FilterPolicy::new()
.with_default(FilterResult::Allow)
.add_rule(FilterRule::new(
Box::new(GlobMatcher::new("*.key").unwrap()),
FilterResult::Deny,
));
// Check if operation is allowed
let result = policy.check(
Path::new("/etc/secret.key"),
Operation::Download,
"alice"
);
assert_eq!(result, FilterResult::Deny);Configuration (YAML):
filter:
enabled: true
default_action: allow
rules:
- name: block-sensitive-keys
pattern: "*.{key,pem}"
action: deny
operations:
- download
- upload
- name: block-hidden-dirs
path_prefix: "/home"
pattern: ".*"
action: deny
users:
- guestComprehensive audit logging infrastructure for the SSH server (src/server/audit/):
Structure:
mod.rs-AuditManagerfor collecting and distributing audit eventsevent.rs-AuditEventtype definitions and builder patternexporter.rs-AuditExportertrait andNullExporterimplementationfile.rs-FileExporterfor JSON Lines output with rotation support
Key Components:
-
AuditEvent: Represents discrete auditable actions with fields for:
- Unique event ID (UUID v4)
- Timestamp (UTC)
- Event type, session ID, username, client IP
- File paths, bytes transferred, operation result
- Protocol and additional details
-
EventType: Categorizes security and operational events:
- Authentication:
AuthSuccess,AuthFailure,AuthRateLimited - Sessions:
SessionStart,SessionEnd - Commands:
CommandExecuted,CommandBlocked - File operations:
FileOpenRead,FileOpenWrite,FileRead,FileWrite,FileClose,FileUploaded,FileDownloaded,FileDeleted,FileRenamed - Directory operations:
DirectoryCreated,DirectoryDeleted,DirectoryListed - Filters:
TransferDenied,TransferAllowed - Security:
IpBlocked,IpUnblocked,SuspiciousActivity
- Authentication:
-
EventResult: Operation outcomes (
Success,Failure,Denied,Error) -
AuditExporter Trait: Interface for audit event destinations
export()- Export single eventexport_batch()- Export multiple events (optimizable)flush()- Ensure pending events are writtenclose()- Clean up resources
-
NullExporter: No-op exporter for testing and disabled audit logging
-
FileExporter: File-based exporter writing events in JSON Lines format
- Append mode to preserve existing data
- Optional log rotation based on file size (
RotateConfig) - Optional gzip compression for rotated files
- Thread-safe using async Mutex
- Async I/O using tokio
- Automatic parent directory creation
- Restrictive file permissions (0o600 on Unix)
-
AuditManager: Central manager with async processing
- Background worker for non-blocking event processing
- Configurable buffering (buffer size, batch size)
- Periodic flush intervals
- Multiple exporter support
- Graceful shutdown with event flush
Configuration:
let config = AuditConfig::new()
.with_enabled(true)
.with_buffer_size(1000)
.with_batch_size(100)
.with_flush_interval(5);File Exporter Usage:
use bssh::server::audit::file::{FileExporter, RotateConfig};
use std::path::Path;
// Simple file exporter
let exporter = FileExporter::new(Path::new("/var/log/audit.log"))?;
// With rotation (50 MB, 10 backups, gzip compression)
let rotate_config = RotateConfig::new()
.with_max_size(50 * 1024 * 1024)
.with_max_backups(10)
.with_compress(true);
let exporter = FileExporter::new(Path::new("/var/log/audit.log"))?
.with_rotation(rotate_config);Output Format (JSON Lines - one JSON object per line):
{"id":"uuid","timestamp":"2024-01-15T10:30:00Z","event_type":"file_uploaded","session_id":"sess-001","user":"admin","client_ip":"192.168.1.100","path":"/data/report.pdf","bytes":1048576,"result":"success","protocol":"sftp"}-
OtelExporter: OpenTelemetry exporter for distributed tracing and observability
- OTLP/gRPC protocol support using tonic
- Event to LogRecord mapping with proper attribute conversion
- Severity level mapping based on event types and results
- Resource attributes including service.name and service.version
- Graceful shutdown and flush methods
- TLS support for secure audit data transmission
-
LogstashExporter: Logstash exporter for ELK stack integration
- TCP connection with JSON Lines protocol (newline-delimited JSON)
- Optional TLS encryption for secure transmission
- Automatic reconnection on connection failure
- Batch support for efficient event transmission
- Connection timeout handling (default: 10 seconds)
- Configurable host and port
OtelExporter Usage:
use bssh::server::audit::otel::OtelExporter;
use bssh::server::audit::exporter::AuditExporter;
use bssh::server::audit::event::{AuditEvent, EventType};
// Create exporter with OTLP endpoint
let exporter = OtelExporter::new("http://localhost:4317")?;
// Export an audit event
let event = AuditEvent::new(
EventType::AuthSuccess,
"alice".to_string(),
"session-123".to_string(),
);
exporter.export(event).await?;
// Graceful shutdown
exporter.close().await?;LogstashExporter Usage:
use bssh::server::audit::logstash::LogstashExporter;
use bssh::server::audit::exporter::AuditExporter;
use bssh::server::audit::event::{AuditEvent, EventType};
// Create exporter (unencrypted by default)
let exporter = LogstashExporter::new("logstash.example.com", 5044)?
.with_tls(true); // Enable TLS for production
// Export an audit event
let event = AuditEvent::new(
EventType::AuthSuccess,
"alice".to_string(),
"session-123".to_string(),
);
exporter.export(event).await?;
// Graceful shutdown
exporter.close().await?;Binary: bssh-server
The bssh-server binary provides a command-line interface for managing and operating the SSH server:
Subcommands:
- run - Start the SSH server (default when no subcommand specified)
- gen-config - Generate a configuration file template with secure defaults
- hash-password - Hash passwords for configuration using Argon2id (recommended)
- check-config - Validate configuration files and display settings
- gen-host-key - Generate SSH host keys (Ed25519 or RSA)
- version - Show version and build information
Global Options:
-c, --config <FILE>- Configuration file path-b, --bind-address <ADDR>- Override bind address-p, --port <PORT>- Override listen port-k, --host-key <FILE>- Host key file(s) (can be repeated)-v, --verbose- Verbosity level (repeatable: -v, -vv, -vvv)-D, --foreground- Run in foreground (don't daemonize)--pid-file <FILE>- PID file path
Usage Examples:
# Generate configuration template
bssh-server gen-config -o /etc/bssh/server.yaml
# Generate Ed25519 host key (recommended)
bssh-server gen-host-key -t ed25519 -o /etc/bssh/ssh_host_ed25519_key
# Generate RSA host key (for compatibility)
bssh-server gen-host-key -t rsa -o /etc/bssh/ssh_host_rsa_key --bits 4096
# Hash a password for configuration
bssh-server hash-password
# Validate configuration
bssh-server check-config -c /etc/bssh/server.yaml
# Start server with configuration file
bssh-server -c /etc/bssh/server.yaml
# Start server with CLI overrides
bssh-server -c /etc/bssh/server.yaml -p 2222 -b 0.0.0.0 -k /path/to/keyDocumentation: docs/architecture/server-configuration.md
SSH server implementation using the russh library for accepting incoming connections:
Structure (src/server/):
mod.rs-BsshServerstruct andrussh::server::Servertrait implementationconfig/mod.rs- Module exports and backward compatibility layerconfig/types.rs- Comprehensive configuration types with serdeconfig/loader.rs- Config loader with validation and environment overrideshandler.rs-SshHandlerimplementingrussh::server::Handlertraitsession.rs- Session state management (SessionManager,SessionInfo,ChannelState)exec.rs- Command execution for SSH exec requestssftp.rs- SFTP subsystem handler with path traversal preventionscp.rs- SCP protocol handler with sink/source modesauth/- Authentication provider infrastructureaudit/- Audit logging infrastructure (event types, exporters, manager)
Key Components:
-
BsshServer: Main server struct managing the SSH server lifecycle
- Accepts connections on configured address
- Loads host keys from OpenSSH format files
- Configures russh with authentication settings
- Creates shared rate limiter for authentication attempts
-
Server Configuration System: Dual configuration system for flexibility
- Builder API (
ServerConfig): Programmatic configuration for embedded use - File-Based (
ServerFileConfig): YAML configuration with environment overrides - Configuration precedence: CLI > Environment > File > Defaults
- Configuration validation at startup (host keys, CIDR ranges, paths)
- Support for BSSH_* environment variable overrides
- Builder API (
-
ServerConfig: Configuration options with builder pattern
- Host key paths and listen address
- Connection limits and timeouts
- Authentication method toggles (password, publickey, keyboard-interactive)
- Public key authentication configuration (authorized_keys location)
- Command execution configuration (shell, timeout, allowed/blocked commands)
-
ServerFileConfig: Comprehensive YAML file configuration
- Server settings (bind address, port, host keys, keepalive)
- Authentication (public key, password with inline or file-based users)
- Shell configuration (default shell, environment, command timeout)
- SFTP/SCP enablement with optional chroot
- File transfer filtering rules
- Audit logging (file, OpenTelemetry, Logstash exporters)
- Security settings (auth attempts, bans, session limits, IP allowlist/blocklist)
-
SshHandler: Per-connection handler for SSH protocol events
- Public key authentication via AuthProvider trait
- Rate limiting for authentication attempts (token bucket)
- Auth rate limiting with ban support (fail2ban-like)
- Channel operations (open, close, EOF, data)
- PTY, exec, shell, and subsystem request handling
- Command execution with stdout/stderr streaming
-
PTY Module (
src/server/pty.rs): Pseudo-terminal management for interactive sessions- PTY master/slave pair creation using POSIX APIs via nix crate
- Window size management with TIOCSWINSZ ioctl
- Async I/O for PTY master file descriptor using tokio's AsyncFd
- Configuration management (terminal type, dimensions, pixel sizes)
- Implements
AsyncReadandAsyncWritefor PTY I/O
-
Shell Session Module (
src/server/shell.rs): Interactive shell session handler- Shell process spawning with login shell configuration (-l flag)
- Terminal environment setup (TERM, HOME, USER, SHELL, PATH)
- Bidirectional I/O forwarding between SSH channel and PTY master
- Window resize event handling forwarded to PTY
- Proper session cleanup on disconnect (SIGHUP to shell, process termination)
- Controlling terminal setup via TIOCSCTTY ioctl
-
CommandExecutor: Executes commands requested by SSH clients
- Shell-based command execution with
-cflag - Environment variable configuration (HOME, USER, SHELL, PATH)
- Stdout/stderr streaming to SSH channel
- Command timeout with graceful process termination
- Command allow/block list validation for security
- Exit code propagation to client
- Shell-based command execution with
-
SessionManager: Tracks active sessions with configurable capacity
- Session creation and cleanup
- Idle session management
- Authentication state tracking
-
SftpHandler: SFTP subsystem handler (
src/server/sftp.rs)- Implements
russh_sftp::server::Handlertrait for file transfer operations - Advertises and serves
limits@openssh.comso clients negotiate the server's packet, read, write, and handle ceilings before bulk transfers - Path traversal prevention with chroot-like isolation
- File operations: open, read, write, close
- Directory operations: opendir, readdir, mkdir, rmdir
- Attribute operations: stat, lstat, fstat, setstat, fsetstat
- Path operations: realpath, rename, remove, readlink, symlink
- Symlink validation ensures targets remain within root directory
- Handle limit enforcement to prevent resource exhaustion
- Read size capping to prevent memory exhaustion
- Pipelined downloads tolerate legal short
READreplies by re-requesting the missing byte range before ordered reassembly, and clamp advertised transfer ceilings to the negotiated packet payload budget
- Implements
-
ScpHandler: SCP protocol handler (
src/server/scp.rs)- Implements SCP server protocol for file transfers via the
scpcommand - Sink mode (
-tflag): receives files from client (upload) - Source mode (
-fflag): sends files to client (download) - Recursive transfer support (
-rflag) for directories - Time preservation (
-pflag) for file modification times - Security features:
- Path traversal prevention with normalized path resolution
- Symlink escape prevention via canonicalization
- Filename validation (rejects
/,..,.) - File size limit (10 GB maximum)
- Mode permission masking (strips setuid/setgid/sticky bits)
- Line length limits to prevent DoS via buffer exhaustion
- Automatic SCP command detection in exec_request handler
- Configurable via
scp_enabledsetting
- Implements SCP server protocol for file transfers via the
The authentication subsystem (src/server/auth/) provides extensible authentication for the SSH server:
Structure:
mod.rs- Module exports and re-exportsprovider.rs-AuthProvidertrait definitionpublickey.rs-PublicKeyVerifierimplementationpassword.rs-PasswordVerifierimplementation with Argon2id hashingcomposite.rs-CompositeAuthProvidercombining multiple auth methods
AuthProvider Trait:
The AuthProvider trait defines the interface for all authentication backends:
#[async_trait]
pub trait AuthProvider: Send + Sync {
async fn verify_publickey(&self, username: &str, key: &PublicKey) -> Result<AuthResult>;
async fn verify_password(&self, username: &str, password: &str) -> Result<AuthResult>;
async fn get_user_info(&self, username: &str) -> Result<Option<UserInfo>>;
async fn user_exists(&self, username: &str) -> Result<bool>;
}PublicKeyVerifier:
Implements public key authentication by parsing OpenSSH authorized_keys files:
-
Key file location modes:
- Directory mode:
{dir}/{username}/authorized_keys - Pattern mode:
/home/{user}/.ssh/authorized_keys
- Directory mode:
-
Supported key types:
- ssh-ed25519, ssh-ed448
- ssh-rsa, ssh-dss
- ecdsa-sha2-nistp256/384/521
- Security keys (sk-ssh-ed25519, sk-ecdsa-sha2-nistp256)
-
Key options parsing:
command="..."- Force specific commandfrom="..."- Restrict source addressesno-pty,no-port-forwarding,no-agent-forwarding,no-X11-forwardingenvironment="..."- Set environment variables
PasswordVerifier:
Implements password authentication with secure password hashing:
-
Argon2id hashing: Uses the OWASP-recommended password hashing algorithm
- Memory cost: 19 MiB
- Time cost: 2 iterations
- Parallelism: 1
-
User configuration:
- External YAML file with user definitions
- Inline users in server configuration
- User attributes: name, password_hash, shell, home, env
-
Security features:
- Timing attack mitigation with constant-time verification
- Minimum verification time (100ms) regardless of user existence
- Dummy hash verification for non-existent users
- Secure memory cleanup using
zeroizecrate - User enumeration protection
-
Hash compatibility:
- Argon2id (recommended, generated by
hash-passwordcommand) - bcrypt (supported for backward compatibility)
- Argon2id (recommended, generated by
CompositeAuthProvider:
Combines multiple authentication methods into a single provider:
- Delegates to
PublicKeyVerifierfor public key auth - Delegates to
PasswordVerifierfor password auth - Prioritizes password verifier for user info (more detailed)
- Supports hot-reloading of password users via
reload_password_users()
Security Features:
- Username validation: Prevents path traversal attacks (e.g.,
../etc/passwd) - File permission checks (Unix): Rejects world/group-writable files and symlinks
- Symlink protection: Uses
symlink_metadata()to detect and reject symlinks - Parent directory validation: Checks parent directory permissions
- Rate limiting: Token bucket rate limiter for authentication attempts
- Timing attack mitigation: Constant-time behavior in password verification and
user_exists()check - Secure memory handling: Password strings cleared from memory after use via
zeroize - Comprehensive logging: All authentication attempts are logged
User Input → CLI Parser → Mode Detection → Node Resolution
↓
Configuration Loading
↓
SSH Config Parsing
↓
Jump Host Chain Creation
↓
Parallel Executor Setup
↓
┌────────────────────┴─────────────────┐
▼ ▼
Connection Pool Task Spawning
↓ ↓
Per-Node Execution Semaphore Control
↓ ↓
Command/Transfer Result Collection
↓ ↓
Output Streaming Exit Code Strategy
└────────────────────┬─────────────────┘
▼
User Output
The OpenSSH-compatible -4 / -6 flags and the ssh_config AddressFamily
keyword share one representation, AddressFamily in
src/ssh/tokio_client/address_family.rs:
pub enum AddressFamily { Any, V4, V6 }Resolution. AddressFamily::resolve(ipv4_flag, ipv6_flag, config_value)
implements the OpenSSH precedence rule: command line flag, then config keyword,
then the any default. AddressFamily::from_config_value accepts
any | inet | inet6 case-insensitively and warns (via tracing) rather than
failing on an unrecognized value, so a configuration file OpenSSH would tolerate
does not become a hard error. The dispatcher builds an
SshConnectionConfigResolver from CLI overrides, YAML defaults, and the parsed
ssh_config; exec, ping, upload, and download resolve that object for each
target node so per-host Host blocks apply to the actual connection target.
Threading. The preference rides on SshConnectionConfig, the struct every
connection path already carries. Client::connect_with_ssh_config passes it to
connect_with_config_inner, which resolves the target, filters the candidate
list, and connects. Any returns the resolver's list untouched, which is what
keeps the unflagged path byte-for-byte identical to the previous behavior.
JumpHostChain receives the same resolver and resolves each jump hop against
that hop's own host name instead of inheriting the destination's settings.
The SFTP paths (upload / download) now receive the full resolved
SshConnectionConfig, so AddressFamily, Compression,
ServerAliveInterval, and ServerAliveCountMax follow the same per-host path
as exec and ping. ForwardingConfig carries its own copy of the resolved
address family for the forwarding-target filter, since forwarders run detached
from the connect config.
Scope. The constraint is a hard filter where bssh opens the socket. For forwarding targets the unforced path preserves server-side resolution, while a forced family switches to locally resolved numeric addresses so the family request has an observable effect:
| Path | Behavior |
|---|---|
| Direct connect (exec, interactive, ping, SFTP) | Hard filter in connect_with_config_inner |
| First jump hop | Hard filter (shares the direct connect path) |
-L / -D listener |
Selects the implicit bind address (::1 / :: under -6); an explicit bind address wins |
-L / SOCKS5 -D target |
With Any, sends the requested hostname in direct-tcpip and lets the remote sshd resolve it. With V4 / V6, resolves locally, filters the candidate list, and sends the matching numeric address |
SOCKS4 -D target |
Unfiltered; SOCKS4 carries a literal IPv4 destination by protocol definition |
| Jump hops past the first, and the destination behind a chain | Uses the same direct-tcpip model as -L/SOCKS5 -D targets: hostname with Any, locally filtered numeric address with V4 / V6. The family also selects the best-effort address recorded for host key verification diagnostics |
-R listener |
Not constrained; the server binds it |
bssh-server |
Out of scope; separate CLI |
Failure mode. A forced family with no matching resolved address is a hard
failure with no fallback to the other family, matching OpenSSH. It surfaces as
the dedicated Error::NoAddressForFamily { host, family } variant, rendered as
no IPv6 address found for <host>, which replaces the generic
could not resolve to any addresses so a family mismatch is distinguishable
from a genuine resolution failure.
- Connection errors: Retry with exponential backoff
- Authentication failures: Immediate failure with clear diagnostics
- Command execution errors: Captured with exit codes
- Timeout handling: Configurable per-connection and per-command
- Signal handling: Clean shutdown on Ctrl+C with two-stage confirmation
Several test suites must temporarily set or remove process-wide environment
variables (e.g. BACKENDAI_CLUSTER_HOSTS, HOME, SSH_AUTH_SOCK). Under
Rust 2024 edition, std::env::set_var and std::env::remove_var are marked
unsafe because concurrent mutation of the environment is undefined behaviour
at the libc level on glibc, musl, and macOS. EnvGuard centralises all such
mutations in src/test_helpers/env_guard.rs.
Soundness contract: every test that constructs an EnvGuard MUST be
annotated with #[serial_test::serial]. Every other test in the same crate
binary that reads or mutates the same variable MUST also carry #[serial] (or
a matching #[serial(key)] group). Note that #[serial] only serializes
against other #[serial] / #[parallel] tests — unannotated tests may still
run concurrently with serial ones and would race on environment reads. This is
not an EnvGuard limitation; it is an inherent constraint of the libc
environment-variable API.
use serial_test::serial;
use crate::test_helpers::EnvGuard;
#[test]
#[serial]
fn my_test() {
let _host = EnvGuard::set("BACKENDAI_CLUSTER_HOSTS", "node1,node2");
// Variable is automatically restored when `_host` drops at end of scope.
}Integration tests access the same struct via a #[path]-based re-export
in tests/common/mod.rs, which avoids making EnvGuard part of the public
bssh crate API while keeping a single source of truth. When adding a new
integration-test binary that needs EnvGuard, add mod common; at the top of
that file and use common::EnvGuard.
Use #[serial(key)] (a named group) when two sets of tests touch different,
non-overlapping variables and can therefore run concurrently with each other
but not with themselves; omit the key (plain #[serial]) when in doubt.
- SSH agent authentication (auto-detection)
- Private key files with passphrase support
- Password authentication (discouraged in production)
- Public key authentication preferred
- known_hosts file verification
- Three modes: Yes (strict), No (insecure), AcceptNew (recommended)
- Per-host configuration support
- Host key fingerprint display
russh 0.63 widened client::Handler::check_server_key from &PublicKey to
&PublicKeyOrCertificate, so the callback can now also receive an OpenSSH host
certificate. bssh does not participate in that scheme:
- It never advertises certificate host key algorithms. Both
Preferredoverrides (src/ssh/tokio_client/connection.rsfor the client,src/server/mod.rsfor the server) change onlycompressionand inherithost_key_certificatesfromPreferred::DEFAULT, which is empty. A server therefore cannot negotiate a certificate with bssh. - If a peer sends one regardless,
ClientHandler::check_server_keyrefuses it and returnsServerCheckFailed. bssh verifies no CA signatures (the@cert-authorityscan insrc/ssh/tokio_client/host_verification.rsonly warns and falls back to TOFU), so the key inside a certificate has never been vouched for by anything bssh trusts. Matching it against known_hosts would answer a different question than the one the certificate poses. ServerCheckMethod::NoCheckstill accepts, because there the operator has turned host verification off outright.
server::Config likewise gained a certificates field; bssh's construction
ends in ..Default::default(), so it stays empty and the server keeps
presenting a plain host key.
- No credential logging
- Secure memory handling for passphrases
- Encrypted SSH transport (via russh)
- Connection timeout enforcement
- Jump host support for bastion architectures
- Port forwarding for secure tunneling
- SSH config directive support for security policies
Official macOS release binaries (bssh, bssh-server, bssh-keygen) are signed
with a "Developer ID Application" certificate and notarized through Apple's
notarytool. This is handled by two composite actions in .github/actions/,
mirrored from continuum-router (originally from backend.ai-go):
- macos-signing-setup extracts the Developer ID p12 to a PEM (via
openssl pkcs12 -legacy, selected by capability since Apple's LibreSSL lacks the option), rejects any p12 that holds no Developer ID Application certificate, and installs rcodesign (keychain-free signing). - macos-sign-notarize-binary signs a staged copy with the hardened
runtime and a pinned reverse-DNS identifier (
BUNDLE_IDbase, with-server/-keygensuffixes), asserts the resulting authority, runtime flag, and identifier, packages the flat zip withditto, submits it tonotarytool --wait, and gates onstatus: Accepted. Bare Mach-O binaries cannot be stapled, so Gatekeeper resolves the ticket online.
Rationale: releases up to v2.4.1 were signed with an "Apple Distribution" certificate (an App Store submission identity without the Developer ID leaf extension) and never notarized. When that certificate was revoked, macOS killed installed binaries on launch and deleted them as malware. The authority assertion exists so a wrong certificate fails the release instead of shipping quietly.
Required release credentials (GitHub packaging environment):
APPLE_CERTIFICATE (base64 Developer ID Application p12),
APPLE_CERTIFICATE_PASSWORD, APPLE_ID, APPLE_TEAM_ID, APPLE_PASSWORD
(app-specific password), and the BUNDLE_ID variable (com.lablup.bssh).
- tokio - Async runtime
- russh / russh-sftp - SSH protocol implementation
- clap - CLI argument parsing
- serde / serde_yaml - Configuration serialization
- tracing / tracing-subscriber - Structured logging
- anyhow / thiserror - Error handling
See LICENSE file for licensing information.
- Parallelism: Adjust
--parallelflag (default: 10) - Connection timeout: Use
--connect-timeout(default: 30s) - Command timeout: Use
--timeout(default: 5min) - Keepalive: Configurable via
--server-alive-interval(default: 60s) and--server-alive-count-max(default: 3)- Interval of 0 disables keepalive
- Connection is considered dead after
interval * (count_max + 1)seconds without response - Equivalent to OpenSSH
ServerAliveIntervalandServerAliveCountMaxoptions
See docs/architecture/configuration.md for complete YAML schema and examples.
- 0: Success (all nodes, or main rank succeeded)
- 1: General failure
- 130: Terminated by SIGINT (Ctrl+C)
- 255: Generated by
pingas a bssh-level signal when no host was reachable, or bssh failed before attempting any connection. The defaultMainRankstrategy can also produce 255 by forwarding a remote command's exit status verbatim - Other: Preserved from main rank (SIGSEGV=139, OOM=137, etc.)
See docs/architecture/exit-code-strategy.md for detailed strategy documentation.
For detailed component documentation, see: