Skip to content

Releases: lablup/bssh

v2.4.1

Choose a tag to compare

@inureyes inureyes released this 03 Aug 13:21

A patch release that closes the four items listed as known issues in the v2.4.0 release notes.

New Features

None. BSSH_CERT_AUTHORITY_POLICY=reject is new but is an opt-in hardening switch for existing behavior; see Improvements.

Improvements

  • accept-new keeps a process-lifetime host key pin when no known_hosts path can be determined, instead of disabling verification entirely (#242). Previously, in containers and service environments where HOME is unset and no passwd entry supplies a home directory, the CLI default mode accepted any server key unconditionally. The first key seen for a host:port is now pinned for the life of the process and a different key later in the same run is rejected. Nothing persists past the process, so this is still weaker than file-backed TOFU.
  • The known_hosts path is validated before it is treated as trust state (#242). A path that is a non-regular file, cannot be opened, or is a symlink that cannot be resolved now fails the connection and names which of the three it was, in both accept-new and strict mode. Previously all three were indistinguishable from an absent file, so accept-new treated every host as a first use and strict mode treated every host as unknown. A genuinely absent file still behaves as an empty one.
  • First-use recording is serialized across bssh processes, not just within one (#242). A sibling advisory lock file (known_hosts.lock, created 0600 under a 0700 parent) now wraps the check-then-record window, and the path probe, marker scan, and lookup all run again under the lock. Two bssh processes connecting to the same new host at once previously could both read known_hosts before either wrote and both append an entry.
  • BSSH_CERT_AUTHORITY_POLICY=reject makes a matching known_hosts @cert-authority line a hard rejection (#242). The default is unchanged: a match warns and falls through to ordinary TOFU, because bssh has no CA signature validation and failing closed would break every working CA setup with no workaround.

Bug Fixes

  • Forwarding target hostnames are sent to the server instead of being resolved on the client (#257). -L targets, SOCKS5 -D domain requests, later jump-chain hops, and the destination behind a jump chain now go out in the direct-tcpip request as written, and the remote sshd resolves them, matching OpenSSH. Any target whose name resolves only from the server's network position previously failed before a single byte of SSH traffic was sent, which is one of the primary reasons to use a bastion. Forcing -4 or -6 still resolves locally and sends a numeric address, since that is the only way the family request can affect a connection the server makes.
  • SOCKS4 dynamic forwarding honors the address family preference (#255). The SOCKS4 handler called a family-agnostic opener that hardcoded AddressFamily::Any, so it ignored -4, -6, and the ssh_config AddressFamily keyword while every other path in v2.4.0 honored them: bssh -6 -D 1080 user@host hard-failed every other path when IPv6 was unavailable and still handed back a proxy that tunneled IPv4. A SOCKS4 request under forced IPv6 is now refused with the protocol's 0x5B reply before an SSH channel is opened. Any and forced IPv4 are unchanged.
  • SOCKS5 parses IPv6 destination literals (#256). The ATYP 0x04 arm read none of the request body and replied 0x08 "address type not supported", so every SOCKS5 client sending an IPv6 destination was refused and bssh -6 -D 1080 was self-contradictory. The arm now consumes its 16 address bytes and 2 port bytes and forwards a bracketed [ipv6]:port target.
  • A socket address slice reports its first address as the hostname instead of a comma-joined list of every address (#243). A comma is the known_hosts host-list separator, so the joined form is rejected by the hostname validation added in v2.4.0.
  • A known_hosts file created by the first recording no longer starts with a blank line (#243). Cosmetic; it was never a parse problem. Files that already existed are untouched.

CI/CD Improvements

None.

Technical Details

  • Performance: the known_hosts write locks are skipped when the offered key already matches a recorded entry (#243). The marker scan and lookup run first without either lock and return on a definite match, so parallel connections to an already-recorded cluster no longer serialize on a lock they do not need. Both locks are still taken for a host that looks unknown, with the full check repeated under them so the fast path cannot race a concurrent first-time recording.
  • Tests: the nondeterministic test_expand_path_with_tilde failure under the full library suite is fixed (#243). Seven tests repoint the process-global HOME, and cargo runs tests in parallel threads, so HOME could change between expand_path's internal dirs::home_dir() call and the assertion's own call. EnvGuard now takes a dedicated HOME mutex when it sets or removes that variable, and EnvGuard::lock_home() gives read-only tests the same lock. No library behavior changed.
  • Validation: cargo fmt --all --check, cargo clippy --lib --tests --all-targets -- -D warnings, cargo test --lib (1386 passed, 0 failed, 9 ignored), and the focused address_family_test and socks_ipv6_literal_test suites all pass on the tagged commit.

Dependencies

No Cargo.toml requirement changed, and no crate entered or left the graph. Seven resolved versions moved in Cargo.lock: aho-corasick 1.1.4 to 1.1.5, darling, darling_core, and darling_macro 0.23.0 to 0.24.0, data-encoding 2.11.0 to 2.11.1, instability 0.3.12 to 0.3.13, and line-clipping 0.3.7 to 0.3.8.

Breaking Changes

None. The library API changed additively only. ServerCheckMethod gained AcceptNewKnownHostsFile(String) and AcceptNewInMemory in both the bssh::ssh::tokio_client enum and its bssh::shared::auth_types mirror, with From conversions in both directions; both enums are #[non_exhaustive], so an exhaustive downstream match already required a wildcard arm. ToSocketAddrsWithHostname gained a host_port method with a default body, so external implementors compile unchanged.

Note one user-visible behavior change that is a fix rather than a break: with no forced address family, forwarding targets are now resolved by the remote server rather than the client (#257). A deployment that relied on client-side resolution of a forwarding target name can force -4 or -6 to restore local resolution.

Known Issues

  • @cert-authority lines still fall through to ordinary TOFU by default. bssh has no CA signature validation, so the strict behavior is opt-in through BSSH_CERT_AUTHORITY_POLICY=reject rather than the default.
  • With no determinable known_hosts path, accept-new pins keys in memory only. The pin does not survive the process, so a first connection in a new process is still an unverified first use.
  • The cross-process known_hosts lock is advisory and cooperative. It serializes bssh against other bssh processes, not against an unrelated writer editing known_hosts directly.
  • SOCKS4 destinations remain IPv4-only by protocol definition. Under forced IPv6 they are now refused rather than silently tunneled, which is the intended behavior but means SOCKS4 and -6 cannot be combined.
  • tests/integration_test.rs has a localhost-auth fixture mismatch: its precheck lets system ssh use an agent while ParallelExecutor::new disables agent auth and supplies only ~/.ssh/id_rsa, so three localhost cases fail locally in agent-only environments. CI's --skip integration_test does not exclude the test binary.

What's Changed

  • fix: close host key fail-open paths by @inureyes in #259
  • chore: resolve host key verification follow-ups by @inureyes in #260
  • fix: preserve forwarded target hostnames by default by @inureyes in #258
  • fix: repair merged host-port integration by @inureyes in #262
  • fix: honor address family for SOCKS4 forwarding by @inureyes in #263
  • fix: support SOCKS5 IPv6 literal destinations by @inureyes in #261

Full Changelog: v2.4.0...v2.4.1

v2.4.0

Choose a tag to compare

@inureyes inureyes released this 03 Aug 09:48

Host key verification in the default accept-new mode now actually verifies. Address family selection (-4/-6, ssh_config AddressFamily) works across every connection path. bssh ping reports a real exit code.

New Features

  • Address family selection works end to end. -4/-6 and the ssh_config AddressFamily keyword now govern direct connections for exec, interactive sessions, ping, and SFTP upload/download, plus port forwarding listeners and targets and every hop of a -J chain (#246, #248). Precedence follows OpenSSH: command line flag, then config keyword, then the any default.
  • Bracketed IPv6 address literals are accepted in host specifications: [::1], [::1]:22, user@[::1], and user@[::1]:22, in -H, SSH-style destinations, and both simple and detailed cluster node entries (#251).
  • bssh ping follows a 0/1/255 exit code contract, so health checks can branch on it (#245). 0 means every targeted host connected and authenticated, 1 means at least one succeeded and at least one failed, and 255 means no host succeeded or bssh failed before attempting a connection.

Improvements

  • ssh_config connection settings resolve against the actual target host instead of once per dispatch, so a per-host Host block applies to exec, ping, upload, and download rather than only to SSH mode (#249). This covers AddressFamily, Compression, ServerAliveInterval, and ServerAliveCountMax. A jump hop now resolves against its own Host bastion block instead of inheriting the destination's settings.
  • Interactive connection failures print the full anyhow context chain, so a jump-host failure names the hop that failed instead of collapsing to a single outer message (#238).
  • Forcing an address family that has no resolved address fails with no IPv6 address found for <host> (or the IPv4 equivalent) instead of the generic could not resolve to any addresses (#246).
  • The man page describes only current behavior. Two NOTES subsections that were release history moved to or were already covered by the changelog.

Bug Fixes

  • bssh ping always exited 0 while its help text promised otherwise, so every script branching on it passed unconditionally (#245).
  • -4/-6 and ssh_config AddressFamily were advertised in --help and the man page, parsed, and then discarded, so bssh -6 against a dual-stack host could still connect over IPv4 (#246).
  • Jump hops past the first tunneled over the wrong family. Those hops resolve their target locally and send a literal IP in the direct-tcpip request, so a -6 invocation could reach IPv4 for every hop after the first (#248).
  • ssh_config lookups outside SSH mode queried the literal "*" and missed per-host blocks, so bssh -H v6node uptime ignored a Host v6node stanza (#249).
  • Bracketed IPv6 literals died in the hostlist expander before any connection was attempted, because [ and ] are its range expression delimiters (#251). A bare ::1 reached the connect path and failed resolution instead.
  • The intermediate jump-hop error interpolated the jump host twice, printing Failed to connect to jump host bastion (hop 2): bastion.
  • The direct-connect and file-transfer paths built their anyhow chain backwards, so the raw SSH error rendered above the friendly message as a near-duplicate.
  • A context message that restated its own cause printed the same text twice on one line, such as Password authentication failed.: Password authentication failed.
  • Fixed the occured misspelling and the Ssh capitalization in the SshError and SftpError messages.

CI/CD Improvements

None

Technical Details

Four defects in accept-new and strict host key verification are fixed (#239):

  • accept-new, the documented default, mapped to NoCheck. Any server key was accepted unconditionally, nothing was written to known_hosts, and changed keys were never rejected. It now checks the offered key, records unknown hosts with an OpenSSH-style "Permanently added" notice, and rejects changed keys with the standard warning banner including the SHA256 fingerprint, offending file and line, and a remediation hint. Strict (yes) mode no longer downgrades to NoCheck when the known_hosts file is missing.
  • A host with two or more keys of one algorithm was rejected as changed as soon as one entry differed, a false man-in-the-middle alarm whose own remediation would have deleted the legitimate pin. Lookup now applies OpenSSH's rule: any recorded key equal to the offered key verifies.
  • Hostnames were recorded verbatim with no escaping, so a name containing a newline, space, #, or comma could pin a key against an unintended host, make a host permanently unconnectable, or hide the entry from later lookups. Names that cannot round-trip are now rejected before anything is looked up or written.
  • @revoked and @cert-authority marker lines were silently ignored, so an explicitly revoked key was recorded and accepted like any first use. A dedicated scan now runs first: a matching @revoked line is a hard rejection, and a @cert-authority match warns. Hostname matching is case-insensitive, and ~/.ssh and known_hosts are created with mode 0700/0600 directly rather than under the process umask.

SshConnectionConfigResolver is the new seam for per-target settings, combining command line overrides, YAML defaults, and ssh_config Host blocks. It runs at the ParallelExecutor per-node boundary and is threaded through SshClient and JumpHostChain.

The address family filter for tunneled targets reuses open_direct_tcpip_channel_with_family, the same mechanism already used for -L forwarding targets.

Dependencies

No Cargo.toml requirement changed. Locked versions moved: russh 0.62.1 to 0.62.5, tokio 1.52.3 to 1.53.1, clap 4.6.1 to 4.6.5, serde 1.0.228 to 1.0.229, and ratatui 0.30.0 to 0.30.2. The transitive set shifted with them, adding ratatui-termina, termina, palette, sponge-cursor, and approx and dropping scc, sdd, prettyplease, and the wit-bindgen / wasm-encoder / wasmparser toolchain crates.

Breaking Changes

Behavior:

  • bssh ping against hosts that are all unreachable now exits 255. It previously exited 0, which is also not the 1 the old help text implied.
  • accept-new now performs real verification. A changed host key is rejected where it was previously accepted, and connections that silently succeeded against a rotated or substituted key will now fail until the entry is reconciled.
  • With -6, a -L or -D specification that does not name a bind address listens on ::1 instead of 127.0.0.1, and the *:port wildcard form listens on :: instead of 0.0.0.0. Scripts that pass -6 and assume an IPv4 loopback listener should name the bind address explicitly, for example -L 127.0.0.1:8080:example.com:80.
  • An unbracketed IPv6 literal is rejected with IPv6 address literals must be enclosed in brackets, for example '[::1]' instead of failing during resolution.

Library API, for consumers of the bssh crate:

  • bssh::commands::ping::ping_nodes returns Result<PingOutcome> instead of Result<()> (#245). Callers that ignored the old Ok(()) keep compiling; callers that matched on it exhaustively update the pattern.
  • The four SshClient::*_with_jump_hosts helpers take a &SshConnectionConfig and an Option<&SshConnectionConfigResolver> in place of their former trailing argument (#246, #249).
  • ForwardingSpec::parse_local, parse_dynamic, and parse take an AddressFamily argument (#246).
  • ForwardingConfig gained a public address_family field, bssh::ssh::tokio_client::Error gained a NoAddressForFamily variant, and bssh::ssh::tokio_client exports the new SshConnectionConfigResolver (#246, #249).

Known Issues

  • @cert-authority lines warn but do not gate the connection. bssh has no CA signature validation, so a match falls through to ordinary TOFU rather than failing closed, which would break every working CA setup with no workaround.
  • When no home directory can be determined, accept-new falls back to no verification after printing a warning to stderr, since there is no persistent trust state to record against.
  • The known_hosts first-write lock is process-wide. Two concurrent bssh processes recording the same new host are not serialized against each other.
  • SOCKS4 requests carry a literal IPv4 destination by protocol definition and are passed through unfiltered by the address family preference. Forwarding target filtering for -L and SOCKS5 is a best-effort hint, since the remote server performs the actual connect.

What's Changed

  • fix: show full anyhow error chain on interactive connect failure by @inureyes in #240
  • fix(security): implement real TOFU for accept-new host key mode by @inureyes in #241
  • fix: wire up the -4/-6 address family flags and AddressFamily by @inureyes in #247
  • fix: report a real exit code from bssh ping by @inureyes in #250
  • feat: filter tunneled jump targets by address family by @inureyes in #252
  • fix: resolve ssh config per connection target by @inureyes in #254
  • fix: accept bracketed IPv6 host literals (#251) by @inureyes in #253

Full Changelog: v2.3.1...v2.4.0

v2.3.1

Choose a tag to compare

@inureyes inureyes released this 29 Jul 08:50

New Features

  • Preserved Kitty keyboard flags, xterm modifyOtherKeys, and the selected terminal screen when bssh runs inside an outer TUI. (#237)

Improvements

  • Raised the workspace MSRV and Debian/Launchpad build toolchain from Rust 1.93 to 1.96. (#231, #232)
  • Removed stale vendored-russh attribution and kept the README release summary bounded. (#230)
  • Fixed the file-transfer filter example so it compiled as a doctest.

Bug Fixes

  • Restored ordinary keyboard input after a remote PTY disconnect left Kitty keyboard or xterm modifyOtherKeys modes enabled. (#235)

CI/CD Improvements

  • Matched Homebrew formula URLs and checksums by artifact, rejected failed or malformed downloads, and validated formula content before committing tap updates. (#233)

Technical Details

  • Queried both terminal screens before starting the input reader, forwarded non-reply input, tracked delivered DEC 1049 transitions, and shared one idempotent cleanup path across normal and forced teardown. (#235, #237)

Dependencies

None

Breaking Changes

None

Known Issues

None

What's Changed

  • docs: tidy README Recent Updates and drop stale bssh-russh from NOTICE by @inureyes in #230
  • build: bump Launchpad PPA toolchain and MSRV to Rust 1.96 by @inureyes in #231
  • build: source resolute toolchain from rustc-release PPA and pin vendor_rust to 1.96.0 by @inureyes in #232
  • fix: harden tap bump against layout changes and bad downloads by @inureyes in #233
  • fix: restore enhanced keyboard modes on PTY teardown by @inureyes in #235
  • feat: preserve outer TUI keyboard state by @inureyes in #237

Full Changelog: v2.3.0...v2.3.1

v2.3.0

Choose a tag to compare

@inureyes inureyes released this 18 Jul 17:26

New Features

  • Server-side SSH compression is now configurable via server.compression (YAML), BSSH_COMPRESSION (env), and ServerConfigBuilder::compression, replacing the hard-disabled behavior from v2.2.x. The default stays off; enabling it logs a warning about the russh delayed-zlib desync (#220).

Improvements

  • Roughly double single-connection SFTP write throughput on bssh-server. build_russh_config now advertises a 65535-byte maximum packet size and an 8 MiB window (both configurable via server.maximum_packet_size/server.window_size and BSSH_MAX_PACKET_SIZE/BSSH_WINDOW_SIZE), the server SFTP loop pipelines writes with sequential coalescing, and sequential handlers elide the per-chunk seek. Local loopback 1 GiB benchmark: upload 71 to 437 MiB/s (6.2x), download 73 to 282 MiB/s (3.9x) (#187, #224).
  • Set TCP_NODELAY on accepted server sockets, removing a ~37 ms delayed-ACK stall per strictly sequential SFTP round trip (#227).
  • Wire the ssh_config Compression yes|no directive into the russh client; it was parsed but silently ignored before. Compression yes advertises eager zlib, unset/no advertises only none, and zlib@openssh.com is never advertised client-side (#219).

Bug Fixes

  • Fix an SFTP session deadlock under paramiko's unbounded READ prefetch that froze every bssh-server download to a paramiko client at its initial 2 MiB channel window. The SFTP intake queue is now byte-bounded (max_buffered_request_bytes, default 8 MiB) instead of count-bounded, so the reader never stops draining the channel. Client-initiated disconnects are no longer logged at ERROR (#227).
  • Make sftp.root/scp.root chroot usable by re-anchoring client paths under the root; cd, get, open, and stat previously failed with "path outside root" and only bare / and readdir worked. SCP is unified with the sftp.root behavior (#214).
  • Confine absolute SFTP symlink targets to the chroot so a chrooted client can no longer create a link that resolves to the host filesystem (#214).
  • Advertise only none SSH compression by default so clients that negotiate zlib@openssh.com (Cyberduck, sftp -C) no longer complete the handshake and then drop mid-session with SshEncoding: length invalid (#215).

CI/CD Improvements

  • Add a CI MSRV job that reads rust-version from Cargo.toml and runs cargo check --workspace --locked on that toolchain, so the declared MSRV can no longer silently drift from what actually builds.

Technical Details

  • Server SFTP write path: the strict read-process-write-flush loop is restructured into a reader task with a byte-bounded intake queue plus an in-order processor. Responses flush once per burst instead of once per request, and consecutive sequential SSH_FXP_WRITE requests to the same handle are coalesced (max_write_coalesce_len, default 256 KiB) while every merged request id still receives its own status reply.
  • Tuned [profile.release] (lto = "fat", codegen-units = 1) with documented RUSTFLAGS="-C target-cpu=..." guidance for self-builds.
  • New tools/bench/ harness: bench.sh (bssh-server vs OpenSSH SFTP throughput, with optional before/after and single-core pinning), profile.sh (perf plus inferno flamegraph of the upload path), and interop/ (sshj and paramiko round-trip checks). Loopback-only and byte-for-byte verified (#228).
  • Unify the auxiliary binaries on the "Broadcast SSH" name in --help/version output, and add an Apache-2.0 NOTICE file (#210).

Dependencies

  • Drop the vendored bssh-russh fork and build against crates.io russh 0.62.1 now that both fork patches are upstream (PTY Handle::data fix in russh 0.62.0, SHA-1 MAC exclusion in 0.60.2). ssh-key is bumped to =0.7.0-rc.11 to match russh 0.62.x, and channel_open_session is adapted to the new 0.62 handler signature (#212).

Breaking Changes

  • The workspace minimum supported Rust version is raised from 1.88 to 1.93. The old 1.88 floor was already inaccurate (rustyline 18 needs File::lock, stabilized in Rust 1.89); 1.93 aligns with the Launchpad PPA and Ubuntu 26.04 toolchains. Building from source now requires rustc 1.93 or newer.

Known Issues

  • Server-side SSH compression stays off by default because russh's delayed-zlib (zlib@openssh.com) transport desyncs a few packets after compression activates post-auth (reproduced on russh 0.61.1 and 0.62.1). Enabling server.compression advertises zlib/zlib@openssh.com and logs a warning; the underlying desync is left to be fixed upstream.
  • Native Windows client execution is not supported. Linux and macOS are fully supported; WSL2 is the recommended path on Windows (see #213).

What's Changed

  • build: standardize Rust toolchain on 1.93 (PPA build, MSRV, CI verification) by @inureyes in #209
  • docs: add Apache-2.0 NOTICE and align tool naming to Broadcast SSH by @inureyes in #210
  • chore(deps): drop bssh-russh fork, use upstream russh 0.62.1 by @Yaminyam in #212
  • fix(sftp): resolve chroot client absolute paths relative to the root (fixes #214) by @Yaminyam in #218
  • fix(server): offer only none SSH compression to avoid zlib@openssh.com desync (fixes #215) by @Yaminyam in #217
  • docs: state native Windows client support is not supported by @inureyes in #221
  • feat(server): make SSH compression configurable instead of hard-disabled by @inureyes in #222
  • fix: wire ssh_config Compression directive into the russh client by @inureyes in #223
  • perf(sftp): raise channel sizing, pipeline server writes, tune release build by @inureyes in #224
  • feat(tools): add SFTP benchmark, flamegraph, and interop harness by @inureyes in #228
  • fix(sftp): fix paramiko prefetch deadlock and enable TCP_NODELAY by @inureyes in #229

Full Changelog: v2.2.3...v2.3.0

v2.2.3

Choose a tag to compare

@inureyes inureyes released this 25 May 11:45

bssh v2.2.3

A dependency-modernization and security release: both internal russh forks are synced to their latest upstream releases (bringing the current RustCrypto generation), ssh-key is unified onto a single version, and a transitive time advisory is patched.

Minimum supported Rust is now 1.88 (raised from 1.85), required by the time 0.3.47 security fix below. This affects building from source only; prebuilt binaries, Homebrew, and Debian packages are unaffected.

Security

  • RUSTSEC-2026-0009 (medium, 6.8): a stack-exhaustion denial of service in time 0.3.45, pulled transitively via ratatui 0.30. Fixed by bumping time to 0.3.47 (plus num-conv and time-core); cargo audit now reports 0 vulnerabilities. Because time 0.3.47 requires Rust 1.88, the workspace MSRV is raised to 1.88, which also keeps the MSRV-aware resolver from reverting time to the vulnerable 0.3.45 (#208).

New Features

None

Improvements

  • SFTP server error replies now carry a human-readable message (via russh-sftp 2.3.0's StatusReply), not just a numeric status code (#207).

Bug Fixes

None

CI/CD Improvements

  • Bump action-gh-release to v3 for the Node 24 runtime.
  • Adjust the download shield badge.

Technical Details

  • bssh-russh synced to upstream russh 0.61.1 (from a 0.60.3 base), adopting the new RustCrypto generation: sha2/sha1 0.11, hmac 0.13, aes 0.9, cbc 0.2, ctr 0.10, digest 0.11, pbkdf2 0.13, ssh-key 0.7.0-rc.10, ssh-encoding 0.3.0-rc.9. The high-frequency PTY Handle::data() drain fix is re-ported onto the new server session loop (still absent upstream in 0.61.1, so the fork stays necessary), three now-upstreamed patches are removed, and a PTY regression test is added (#207).
  • bssh-russh-sftp synced to upstream russh-sftp 2.3.0 (from 2.1.2), with the pipelined File I/O helpers re-applied and the SFTP server adapted to the new Into<StatusReply> handler API (#207).
  • Fork maintenance tooling is now self-contained (create-patch.sh clones upstream; sync-upstream.sh applies all patches/*.patch), and both fork READMEs were rewritten (#207).
  • Minimum supported Rust raised from 1.85 to 1.88 (declared via rust-version on the root crate and bssh-russh), required by time 0.3.47.

Dependencies

  • Internal forks: bssh-russh 0.60.3 to 0.61.1 and bssh-russh-sftp 2.1.2 to 2.3.0, both published to crates.io.
  • ssh-key unified to a single =0.7.0-rc.10; argon2 gains its std feature.
  • time 0.3.45 to 0.3.47 (security, see above).

Breaking Changes

  • Minimum supported Rust is now 1.88 (was 1.85). This affects building bssh from source only; prebuilt release binaries, Homebrew, and Debian packages are unaffected.

Known Issues

None

What's Changed

  • update: sync russh/russh-sftp forks to upstream and unify ssh-key by @inureyes in #207
  • fix: bump time to 0.3.47 for RUSTSEC-2026-0009 by @inureyes in #208

Full Changelog: v2.2.2...v2.2.3

v2.2.2

Choose a tag to compare

@inureyes inureyes released this 25 May 04:27

This is a focused bug-fix release that stops bssh from dropping idle but healthy SSH sessions (#206). It is a drop-in upgrade from v2.2.1 with no configuration changes required.

New Features

None

Improvements

None

Bug Fixes

  • Keep idle SSH sessions alive (#206). Three related changes now work together so an open SSH session is no longer closed while it is still healthy:
    • Lower the default keepalive interval from 60s to 30s. The default --server-alive-interval now sits below common one-minute idle reapers (load balancers, NAT gateways, and sshd's own ClientAliveInterval), so bssh sends keepalive traffic before any of them decide the connection is idle.
    • Normalize --server-alive-interval 0 to fully disabled keepalive. Passing 0 previously constructed a zero-duration russh timer. Some(0) is now treated exactly like None in both the russh client config and the TCP SO_KEEPALIVE path.
    • Never close idle interactive sessions locally. The client-side inactivity_timeout is now disabled unconditionally, so a healthy session that legitimately produces no inbound data for a long time (tmux, an idle shell, a long-running REPL) is never torn down by bssh itself. When keepalive is enabled, russh's keepalive counter is the sole dead-peer detector.

CI/CD Improvements

None

Technical Details

  • Dead-peer detection now resolves in about 120s (three unanswered 30s probes plus the next timer tick that observes them) rather than the previous 180s.
  • Changes are confined to src/cli/bssh.rs, src/config/types.rs, and src/ssh/tokio_client/connection.rs, with expanded coverage in tests/ssh_keepalive_test.rs.

Dependencies

None

Breaking Changes

None. The default keepalive interval changed from 60s to 30s, but this only increases keepalive frequency and stays fully overridable via the --server-alive-interval flag and the server_alive_interval config field.

Known Issues

None

Full Changelog: v2.2.1...v2.2.2

v2.2.1

Choose a tag to compare

@inureyes inureyes released this 18 May 16:54

New Features

None.

Improvements

  • Bump workspace dependencies and sync both internal russh forks to upstream stable (#203). Main bssh: lru 0.17 to 0.18 (lifetime fix in get_or_insert_mut_ref), signal-hook 0.3 to 0.4 (only the unused low_level::pipe API changed), opentelemetry / opentelemetry_sdk / opentelemetry-otlp 0.31 to 0.32, plus nix 0.31.2 to 0.31.3 and transitive pin-project / tower-http / zerofrom patches via cargo update.
  • Sync bssh-russh 0.60.1 to 0.60.3 (upstream stable). Picks up aws-lc-rs 1.16.3 to 1.17.0 and the upstream v0.60.2 unreleased fixes our previous PR #193 forward-port had already brought in (#690 SHA-1 MAC exclusion, #693 channel write ordering).
  • Sync bssh-russh-sftp 2.1.1 to 2.1.2 (upstream stable). The fork's original serde_bytes perf fix was absorbed by upstream 2.1.2 and is moved to patches/historical/ for provenance; the remaining custom value-add is the two pipelined File I/O helpers (write_all_pipelined / read_to_writer_pipelined), re-ported on top of upstream 2.1.2's new Features API with chunk sizing derived from features.limits.{write,read}_len or features.max_packet_len.saturating_sub({WRITE,READ}_OVERHEAD_LENGTH) instead of the removed MAX_*_LENGTH constants. crates/bssh-russh-sftp/Cargo.toml swaps flurry for dashmap 6.1.0 and adds serde_bytes as a direct dep to match upstream's set.

Bug Fixes

  • Add the missing [dev-dependencies] block to the bssh-russh fork so its inline test target actually compiles for the first time since the fork's inception (#204). The fork's src/client/test.rs, src/keys/mod.rs, and src/tests.rs were imported verbatim from upstream russh during the initial sync (commit 508aa3f0), but the matching [dev-dependencies] were never copied across, so cargo test -p bssh-russh had failed with E0433 on env_logger / tempfile and cascading E0282 type-inference errors. Adds a minimal block with env_logger, tempfile, and tokio with process / macros features (additive merge). 75 tests now run, covering agent client/server round-trip, PKCS#8 / OpenSSH key decoding, channel lifecycle, GEX, compression, future certificate auth, and server kex junk handling. The agent tests directly exercise the new frame-length cap from PR #203 (the CVE-2026-46673 mitigation) by spawning a real ssh-agent over a Unix-domain socket. Workspace test aggregate climbs from 1796 to 1871 passed.
  • Drop a redundant .into_iter() on a Iterator::chain argument in the synced SFTP session loop (crates/bssh-russh-sftp/src/client/session.rs:194) to satisfy rustc 1.95's stricter clippy::useless_conversion lint (#205). The line was imported verbatim from upstream russh-sftp 2.1.2 in PR #203 and broke CI after the toolchain bump. PR #203 was developed on rustc 1.93.1 where the case did not lint-fire.

CI/CD Improvements

None.

Technical Details

The agent frame-length cap forward-port (CVE-2026-46673 SSH-agent half) lives in crates/bssh-russh/src/keys/agent/{client,server}.rs. Both files now declare const MAX_AGENT_FRAME_LEN: usize = 256 * 1024; and route the receive path through a new read_frame() helper that reads the 4-byte big-endian length prefix, rejects values above the cap with Error::AgentProtocolError before resizing the receive buffer, then reads exactly len bytes. Mirror of upstream russh commit a2d48a7 by Mika Cohen. Recorded as crates/bssh-russh/patches/agent-frame-length-cap.patch so sync-upstream.sh's reverse-apply dry-run will auto-skip on the next sync.

The pipelined SFTP File I/O port lives in crates/bssh-russh-sftp/src/client/fs/file.rs. The original constants (MAX_READ_LENGTH = 261120, MAX_WRITE_LENGTH = 261120) and the bounded_chunk_size(limit, default) helper are gone; chunk size is now derived per-call from self.features.limits.{write,read}_len (when negotiated) or self.features.max_packet_len.saturating_sub(WRITE_OVERHEAD_LENGTH + handle.len() as u32) as u64 (write side) / self.features.max_packet_len.saturating_sub(READ_OVERHEAD_LENGTH) as u64 (read side). The high-level pipelining loops (FuturesUnordered for in-flight requests, BTreeMap reorder buffer for reads, file_end size-cap for EOF detection) are unchanged. The fork's [dev-dependencies] adds futures directly because upstream 2.1.2 no longer depends on it transitively for this code path.

Dependencies

  • lru 0.17.0 to 0.18.0 (lifetime fix in get_or_insert_mut_ref; no API surface change for our LruCache<PathBuf, CacheEntry> usage in src/ssh/config_cache/manager.rs).
  • signal-hook 0.3 to 0.4 (only low_level::pipe API changed: IntoRawFd to OwnedFd. Our consts::SIGWINCH and iterator::Signals usages in src/pty/ are stable across the bump).
  • opentelemetry / opentelemetry_sdk / opentelemetry-otlp 0.31 to 0.32 (the removed ExportConfig / HasExportConfig / with_export_config() trio isn't called from our src/server/audit/otel.rs; WithExportConfig::with_endpoint, LogExporter::builder().with_tonic(), SdkLoggerProvider::builder(), Resource::builder(), and the LogRecord API all remain source-compatible). Note: 0.32 now errors when an https:// endpoint is configured without a TLS feature; our current features stay at [grpc-tonic, logs] because all uses today are http://, but a follow-up will be needed if anyone runs the audit exporter against TLS in production.
  • nix 0.31.2 to 0.31.3 plus transitive patches in pin-project 1.1.12 to 1.1.13, tower-http 0.6.10 to 0.6.11, zerofrom 0.1.7 to 0.1.8 via cargo update.
  • aws-lc-rs 1.16.3 to 1.17.0 (also pulls aws-lc-sys 0.40.0 to 0.41.0).
  • russh-cryptovec 0.59.0 to 0.60.3 inside the bssh-russh fork. Brings in the cryptovec hardening half of CVE-2026-46673.
  • bssh-russh-sftp fork: replace flurry 0.5 with dashmap 6.1.0 to match upstream russh-sftp 2.1.2; add serde_bytes 0.11 as a direct dep (no longer transitive via removed code paths).

Breaking Changes

None for users of the bssh CLI or bssh-server daemon. The package alias for the SFTP crate stays russh-sftp so all use russh_sftp::* import paths continue to work unchanged. Within the vendored bssh-russh-sftp crate, the upstream 2.1.2 Extensions to Features rename and the flurry to dashmap migration would matter to anyone depending on the fork directly, but no external consumer does today (the fork is workspace-internal).

Known Issues

  • The remaining "outdated" direct deps in bssh-russh (aes 0.9, hmac 0.13, sha1 / sha2 0.11, digest 0.11, cbc / ctr / des new majors, pkcs5 / pkcs8 stable, and the RC family ecdsa rc.18 / rsa rc.18 / elliptic-curve rc.32 / ed25519-dalek pre.7) are blocked by RustCrypto's coordinated release model: shared trait crates, and the pre-release crypto crates we depend on still require the older trait versions. Upstream russh stable v0.60.3 carries the same constraints, so we deliberately stay aligned with it.
  • Transitive lru@0.16.4 and signal-hook@0.3.18 are pulled in by ratatui-core and crossterm respectively and will resolve themselves whenever those upstream crates bump.
  • opentelemetry-otlp 0.32 errors when an https:// endpoint is configured without an explicit tls-ring or tls-aws-lc feature. We did not enable a TLS feature in this release because all current uses are http://; users running the audit exporter against a TLS-protected OTLP collector will need to either disable the warning by switching to http:// or to enable a TLS feature in a follow-up build.

What's Changed

  • update: bump workspace deps and sync russh forks to upstream stable by @inureyes in #203
  • fix: add missing dev-dependencies to bssh-russh fork by @inureyes in #204
  • fix: drop useless .into_iter() on chain arg in synced sftp session by @inureyes in #205

Full Changelog: v2.2.0...v2.2.1

v2.2.0

Choose a tag to compare

@inureyes inureyes released this 18 May 14:14

v2.2.0 — Single-prompt password collection, dependency cleanup

This release reworks --password collection to mirror the existing Arc<SudoPassword> pattern (prompt once in the dispatcher, share an Arc<Password> across all parallel tasks), making -S consistent across subcommands, and continues the dependency-hygiene work started in v2.1.4 by dropping five stale or redundant direct dependencies and clearing all cargo-audit findings.

New Features

  • BSSH_PASSWORD environment variable for non-interactive password authentication (#201). Documented in the man page and the README environment-variables section. Discouraged for production deployments; recommended for automated test pipelines and CI scenarios where SSH agent or key-based auth is not feasible.

Improvements

  • Collect --password once up-front in the dispatcher and share the secret across all parallel SSH tasks via Arc<Password> (backed by secrecy::SecretString, auto-zeroized on drop) (#201, closes #200). The prompt now runs before any executor or indicatif MultiProgress is initialized, so the terminal is in a clean state when reading the password. The pre-collected secret is threaded through ParallelExecutor, ExecutionConfig, ConnectionConfig, FileTransferParams, the jump-host determine_auth_method path, the download glob-resolution path (SshClient::connect_and_execute_with_host_check), the SFTP *_with_jump_hosts helpers, and the legacy execute_command_with_forwarding path. The in-task rpassword::prompt_password() call remains only as the fallback for the OpenSSH-style "all key methods failed" opportunistic prompt path, which has no dispatcher-side collection.
  • Warn on stderr when -S / --sudo-password is passed to subcommands where it has no effect (ping, upload, download, list, cache-stats, and interactive shells) (#201, follow-up #200). The warning routes through eprintln! so bssh ... ping | grep ... pipelines stay clean. exec and the SSH-mode interactive path continue to honor -S as before.
  • Tighten sudo-password collection so it only applies to exec paths that can inject sudo responses, and avoid unused SSH password collection for local-only dispatcher paths.

Bug Fixes

  • Fix --password race across parallel SSH connections (#200, #201). Multiple per-node tasks previously raced for stdin: the prompt could be missed, repeated per node, or interleaved with the indicatif progress UI rendering.
  • Fix -S being silently dropped by ping, upload, and download (#201, closes #200). The dispatcher only read cli.sudo_password in the exec and interactive branches, so users could pass -S to other subcommands without any error or feedback that the flag had no effect.
  • Fix the -S ignored-warning landing on stdout (#201, follow-up). The previous tracing::warn! was being mixed into stdout under the default tracing subscriber, breaking shell pipelines that consumed bssh output.

CI/CD Improvements

None.

Technical Details

  • New src/security/password.rs module mirroring the existing src/security/sudo.rs design: a Password wrapper backed by secrecy::SecretString with auto-zeroize on drop, prompt_password() with a non-host-specific prompt, get_password_from_env() reading BSSH_PASSWORD, and get_password(warn_env) combining both. Wrapped in Arc<Password> for sharing across per-node tasks without duplicating secret material.
  • AuthContext (src/ssh/auth.rs) gains a password: Option<Arc<Password>> field and a with_pre_collected_password() builder. password_auth() consumes the pre-collected value when available.
  • Migrated four call sites from lazy_static!/once_cell::sync::Lazy/OnceCell to std::sync::LazyLock/OnceLock: ui/tui/progress.rs, executor/output_sync.rs, pty/terminal.rs, utils/logging.rs, ssh/ssh_config/env_cache/global.rs, ssh/config_cache/global.rs (edition 2024 mandates Rust 1.85+).
  • Migrated three forwarding reconnect-jitter sites from fastrand::u64(range) to rand::random_range(range); rand 0.10 was already a direct dependency.
  • Migrated five callers of std::io::stdin().is_terminal()/stdout().is_terminal() from the unmaintained atty crate.
  • 9 new unit tests for Password (creation, empty rejection, debug redaction, clone independence, Arc sharing, env var handling, dispatcher-collection-pattern verification) and 2 new tests in ssh/auth.rs confirming the builder semantics and that password_auth() never blocks on stdin when a pre-collected value is present.

Dependencies

  • Drop five stale or redundant direct dependencies (#199): arrayvec (unused), ctrlc (replaced with tokio::signal::ctrl_c), directories (replaced with dirs, already used in 16 other sites), signal-hook 0.4.4 (downgraded to 0.3 to share the crate crossterm already pulls in via signal-hook-mio), plus the macOS objc2/block2/dispatch2 chain pulled in by ctrlc. Three more crates (lazy_static, once_cell, fastrand) remain transitively but are no longer directly referenced. Cargo.lock loses 76 lines including the entire signal-hook 0.4.4 subtree.
  • Replace atty with std::io::IsTerminal (#198): drops RUSTSEC-2024-0375 (unmaintained) and RUSTSEC-2021-0145 (unsound unaligned read).
  • 33 transitive patch bumps from cargo update within current semver constraints (#198): tokio 1.52.1 to 1.52.3, rustls 0.23.39 to 0.23.40, h2 0.4.13 to 0.4.14, digest 0.11.2 to 0.11.3, rpassword 7.4.0 to 7.5.2, and others.

Breaking Changes

None. BSSH_PASSWORD and the -S warning are both additive: existing scripts that happen to pass -S to non-applicable subcommands keep working but now receive visible stderr feedback.

Known Issues

  • RUSTSEC-2023-0071 (RSA Marvin Attack) acknowledged via .cargo/audit.toml ignore with an explanatory comment (#198). Both rsa 0.9.10 (via ssh-key 0.6.x) and rsa 0.10.0-rc.17 (via the vendored bssh-russh fork) are affected, and no fixed upstream rsa release exists. Bumping to 0.10.0-rc.18 conflicts with the bssh-russh pkcs5 = "=0.8.0-rc.13" pin. Users handling untrusted hosts should prefer Ed25519 or ECDSA keys instead of RSA.

What's Changed

  • update: clean cargo-audit by removing atty and pinning rsa advisory by @inureyes in #198
  • refactor: drop stale and redundant dependencies by @inureyes in #199
  • fix: collect --password once up-front and honor -S consistently across subcommands by @inureyes in #201
  • fix: warn when sudo password is ignored outside exec by @inureyes in #202

Full Changelog: v2.1.4...v2.2.0

v2.1.4

Choose a tag to compare

@inureyes inureyes released this 10 May 12:09

SFTP transfer performance release. Streams uploads/downloads in 255 KiB chunks instead of buffering whole files, pipelines up to 64 concurrent SFTP requests, and raises the server-side MAX_READ_SIZE to the 255 KiB SFTP standard. On a 1 GiB transfer over loopback this drops upload peak RSS from ~3.23 GB to ~20 MB and wall time from 38.6 s to 3.5 s; multi-GB transfers no longer OOM the client.

New Features

None.

Improvements

  • Stream SFTP uploads/downloads instead of buffering whole files in memory (#195). upload_file/upload_dir_recursive previously loaded the entire local file into a Vec<u8> via tokio::fs::read before calling write_all, and download_file/download_dir_recursive called read_to_end into a pooled buffer plus a clone() to a separate Vec before writing locally — so multi-GB transfers had peak RSS that scaled with file size and large files OOM'd the client. Each path now uses a stream_copy() helper looping on 255 KiB reads/writes through the existing AsyncRead/AsyncWrite impls on tokio::fs::File and russh_sftp::client::fs::File. Buffer size matches MAX_WRITE_LENGTH so each chunk maps to one SFTP packet without further fragmentation.

    Verified locally on macOS arm64 against bssh-server v2.1.3 over loopback with a 1 GiB file:

    Op Build real RSS
    upload unpatched 38.65s 3.23 GB
    upload streaming 3.47s 20 MB
    download unpatched 3.93s 2.17 GB
    download streaming 3.41s 16 MB
  • Pipeline up to 64 concurrent SFTP requests for upload and download (#196). Bounded pipelined SFTP upload/download helpers replace the previous strictly-sequential request/response loop. Review follow-ups also: cap server-advertised read/write lengths against local maxima to avoid oversized allocations from untrusted SFTP metadata, bound the download reorder queue across both in-flight and pending out-of-order responses, use fstat size info where available to avoid reads past EOF and validate unexpected short reads, and preserve remote download handle shutdown after syncing with main.

  • Raise bssh-server SFTP MAX_READ_SIZE from 64 KiB to the 255 KiB SFTP standard (#197). The server previously hard-capped every SFTP READ reply at 64 KiB regardless of what the client requested, while bssh-russh-sftp and OpenSSH's sftp-server both use MAX_READ_LENGTH = 261120 (255 KiB). A client asking for a 256 KiB chunk only ever got 64 KiB back, forcing four extra requests per byte stream. Bumped to 261120 so server replies match the chunk size used by the rest of the stack — combined with client-side pipelining (#196), this cuts the per-MiB request count on downloads from 16 to 4. Memory exposure stays bounded: handles are still capped at MAX_HANDLES = 1000 per session and each in-flight read still uses a single per-request buffer of this size.

Bug Fixes

None.

CI/CD Improvements

None.

Technical Details

  • Streaming buffer size (255 KiB) is aligned with the russh-sftp packet limit so each chunk stays within one SFTP read/write request without further fragmentation.
  • Downloaded remote handles are now explicitly closed after successful copies; chunk-size capping and in-memory pipelined upload/download behavior are covered by new SFTP crate tests.

Dependencies

None — Cargo.lock only changed because the workspace package bssh itself bumped from 2.1.3 to 2.1.4.

Breaking Changes

None.

Known Issues

None at release time. The 64-way SFTP pipelining is a fixed concurrency cap (not user-configurable in this release); workloads that need a different parallelism point should track follow-up issues on the repository.

What's Changed

  • perf: stream SFTP uploads/downloads instead of buffering whole file by @Yaminyam in #195
  • perf: pipeline SFTP requests for upload/download (~2-3x speedup) by @Yaminyam in #196
  • perf: raise server MAX_READ_SIZE to SFTP standard 255 KiB by @Yaminyam in #197

Full Changelog: v2.1.3...v2.1.4

v2.1.3

Choose a tag to compare

@inureyes inureyes released this 30 Apr 18:02

bssh v2.1.3

This release fixes several SCP/SFTP path-resolution bugs in bssh-server (issue #186), vendors russh-sftp with a serde_bytes performance optimization (+29% upload throughput), forward-ports unreleased upstream russh fixes, and standardizes man page trailers.

New Features

  • scp.root configuration field (#186): SCP transfers now honor a chroot setting separate from SFTP. When unset, SCP falls back to sftp.root, so a single top-level chroot setting governs both subsystems unless an admin explicitly wants them split.
  • Internal fork of russh-sftp as bssh-russh-sftp (#188) with a serde_bytes performance fix for SSH_FXP_WRITE/SSH_FXP_DATA packets. The upstream serde derive routed Vec<u8> through deserialize_seq (byte-by-byte), accounting for ~42% of server CPU during 1 GiB SFTP uploads in perf profiling. Annotating the data fields with #[serde(with = "serde_bytes")] and implementing wire-compatible serialize_bytes on the SFTP Serializer routes through the existing bulk deserialize_byte_buf/try_get_bytes path. Measured impact on a CPU-bound host (Xeon Silver 4214): 1 GiB SFTP upload throughput improves from 74.8 MiB/s to 96.4 MiB/s (+29%), closing the gap to OpenSSH sftp-server from ~26% to ~5%.

Improvements

  • Default file-transfer behavior is no longer chrooted to the user's home directory (#186). With sftp.root/scp.root unset (the default), absolute client paths are honored verbatim and relative paths resolve from the user's home directory, matching OpenSSH sftp-server/scp defaults. Deployments that intentionally want chroot-at-home-dir must now set sftp.root: <home dir> (or equivalent) explicitly.
  • Forward-port unreleased upstream russh fixes (#193): exclude SHA-1 MACs from Preferred::DEFAULT/COMPRESSED (upstream russh #690) and fix channel write ordering when pending_data is non-empty (upstream russh #693). Refactored sync-upstream.sh to iterate patches/ and reverse-apply --dry-run first so already-merged patches are auto-skipped.
  • Switched the top-level russh-sftp dependency from crates.io russh-sftp = "2.1.1" to the vendored bssh-russh-sftp package; existing use russh_sftp::... imports continue to work unchanged.

Bug Fixes

  • bssh-server SCP/SFTP path doubling on absolute client paths (#186): ScpHandler::resolve_path and SftpHandler::resolve_path_static previously re-rooted every absolute client path under the user's home directory, so scp local user@host:/home/work/file.bin wrote to /home/work/home/work/file.bin and bssh upload local /abs/remote.bin failed with No such file. The resolver now treats absolute client paths verbatim when no chroot is configured and rejects out-of-chroot absolute paths with permission_denied when one is. Path-traversal and symlink-escape protections continue to apply.
  • SCP single-file destinations no longer have the source filename appended (#186): ScpHandler::receive_file now consults target_is_directory (parsed from -d/-r) and the filesystem state of the resolved target. scp local.bin user@host:/tmp/dest.bin now writes to /tmp/dest.bin instead of /tmp/dest.bin/local.bin. Directory destinations (/tmp/dir/, existing directory, or -d/-r flag) keep the previous filename-appending behavior.
  • Configured sftp.root is no longer dead code (#186): the handler-construction sites in SshHandler previously hard-coded user_info.home_dir as the chroot root and ignored config.sftp.root entirely. Setting sftp.root in the YAML configuration now actually changes the SFTP chroot. The same plumbing now exists for scp.root.
  • Chroot bypass via intermediate-directory symlink: the chroot resolver previously checked only lexical containment for paths whose final component did not exist (typical for new-file creates and mkdir). A symlink inside the chroot pointing to a directory outside the chroot would let a client target chroot/escape/newfile and have open(...)/create_dir(...) follow the symlink, writing outside the chroot. Both ScpHandler::resolve_path and SftpHandler::resolve_path_static now canonicalize the closest existing ancestor of the target path and verify it stays inside the canonicalized chroot, blocking the parent-symlink escape. Found during PR #194 review.

CI/CD Improvements

  • Bump GitHub Actions to Node.js 24-compatible versions to address Node.js 20 deprecation warnings that become errors on 2026-06-02 (#191):
    • actions/checkout v4 → v6
    • actions/cache v4 → v5
    • actions/upload-artifact v4 → v7
    • apple-actions/import-codesign-certs v3 → v7

Technical Details

  • Path resolver now walks up to the closest existing ancestor of the target path, canonicalizes both that ancestor and the chroot root, and verifies the canonical ancestor stays inside the canonical root. Operator-misconfigured chroots that don't exist on disk fall back to the lexical check.
  • Removed an unreachable starts_with check in the relative-path chroot resolver after ParentDir clamping (already guarded by if resolved != root).
  • Added focused unit tests for scp.root/sftp.root precedence rules and the no-chroot default.
  • Added tests/scp_sftp_path_resolution_test.rs covering Backend.AI reproduction scenarios (absolute SCP/SFTP paths, no doubling, no chroot honors absolute paths, chroot rejects out-of-root paths, .. clamped, chroot / round-trips through realpath) plus symlink-escape blocking under chroot. Six regression tests added for the parent-symlink chroot bypass.
  • Hardened packet length handling in vendored SFTP with checked u32 conversions; removed an extra byte-buffer copy; added wire-format tests; narrowed lockfile delta; fixed clippy warnings; made Keychain-backed tests skip cleanly when local authorization is unavailable.

Documentation

  • Standardize man page trailers across bssh.1, bssh-keygen.1, and bssh-server.8 into a consistent BUGS / AUTHORS / COPYRIGHT / SEE ALSO order (#192). Author attribution, contact email, Apache-2.0 license notice, and project homepage link are now uniform across all three pages.
  • Document sftp.root and scp.root in bssh-server.8 configuration sections, and add intermediate-directory-symlink chroot protection to SECURITY CONSIDERATIONS.

Dependencies

  • tokio 1.52.1, clap 4.6.1, tracing 0.1.44, lru 0.17, uuid 1.23.1, tokio-util 0.7.18
  • bssh-russh: aws-lc-rs 1.16.3, ecdsa rc.17, elliptic-curve rc.31, p256/p384/p521 rc.9, tokio 1.52.1
  • Pinned pkcs5="=0.8.0-rc.13" because pkcs8 0.11.0-rc.11 still calls the rc.13-era Parameters::recommended API; stable 0.8.0 renamed it to generate_recommended and breaks the build

Breaking Changes

  • Default chroot behavior change (#186): the implicit chroot at the user's home directory is removed. With no sftp.root/scp.root set, absolute client paths are honored verbatim (matching OpenSSH defaults). Deployments that relied on the implicit confinement must explicitly set sftp.root: <home dir> (or equivalent) in the server YAML.

Known Issues

None.

Full Changelog: v2.1.2...v2.1.3

What's Changed

  • chore: bump GitHub Actions to Node.js 24-compatible versions by @inureyes in #191
  • docs: standardize man page trailers with AUTHORS, COPYRIGHT, and SEE ALSO by @inureyes in #192
  • update: upgrade deps and forward-port unreleased upstream russh fixes by @inureyes in #193
  • feat: vendor russh-sftp with serde_bytes perf fix by @Yaminyam in #188
  • fix: SCP/SFTP path resolution and dead chroot config (#186) by @inureyes in #194

New Contributors

Full Changelog: v2.1.2...v2.1.3