Migrating from 0.x to 1.0¶
This guide covers every change you need to make when upgrading from a SpindleX 0.x beta release to the stable 1.0.0 release. Read it top-to-bottom before upgrading.
Which 0.x release are you on?
If you are on 0.7.3 already, only sections marked [0.7.x → 1.0] apply. If you are on an earlier release, all sections apply.
Removed APIs¶
KeyExchange.generate_keys() - removed¶
Deprecated in 0.7.2, removed in 1.0.
This internal method was part of the KeyExchange class and was never part of the documented public API. Access session keys by working with the Transport object after key exchange completes. If you were calling this method directly, you were relying on an undocumented internal.
# 0.x (broken in 1.0)
kex = transport._kex
keys = kex.generate_keys()
# 1.0 - session keys are not part of the public API surface.
# Use the high-level Transport, SSHClient, or AsyncSSHClient instead.
Changed Defaults and Behavior¶
compress=True and gss_kex=True now raise immediately¶
In 0.6.x and early 0.7.x, passing compress=True or gss_kex=True to SSHClient.connect() was silently ignored. Both parameters are now validated on entry and raise ConfigurationException immediately.
# 0.x - silently ignored
client.connect('host', username='user', compress=True)
# 1.0 - raises ConfigurationException
# Remove these arguments; compression is not supported.
client.connect('host', username='user')
If you need GSSAPI key exchange, it is not currently implemented. Remove gss_kex=True from your call sites. GSSAPI authentication (not KEX) is available via the gssapi extra if you are on a Unix host.
AutoAddPolicy now requires accept_risk=True¶
AutoAddPolicy always disabled MITM protection silently. It now emits a UserWarning if constructed without accept_risk=True.
# 0.x - no warning
from spindlex.hostkeys.policy import AutoAddPolicy
client.set_missing_host_key_policy(AutoAddPolicy())
# 1.0 - pass accept_risk=True to silence the warning
client.set_missing_host_key_policy(AutoAddPolicy(accept_risk=True))
In production, do not use AutoAddPolicy at all. Use RejectPolicy (the default) and load your known hosts file explicitly.
Async host key policy errors are no longer swallowed¶
In earlier 0.7.x releases, AsyncSSHClient._verify_host_key() caught and logged policy exceptions but continued connecting. It now re-raises the policy exception as SSHException, matching the behavior of the sync SSHClient.
If you have async code that relied on connection continuing despite a rejection policy raising, that connection will now fail with SSHException instead of proceeding silently. This is the correct behavior.
AsyncSSHClient.connect(sock=Channel) raises instead of silently misconfiguring¶
In earlier 0.7.x releases, passing a Channel object as the sock= argument to AsyncSSHClient.connect() set reader, writer = None, None and continued with a broken transport. It now raises SSHException immediately with a message directing you to use the sync SSHClient for ProxyJump connections.
# 0.x - silently broken
await async_client.connect(hostname='target', sock=jump_channel)
# 1.0 - use the sync client for ProxyJump
from spindlex import SSHClient
jump = SSHClient()
jump.connect('bastion')
channel = jump.get_transport().open_channel(
'direct-tcpip', ('target', 22), ('', 0)
)
target = SSHClient()
target.connect('target', sock=channel)
ChannelFile.write() always writes the full buffer¶
In 0.x, the file-like write() returned by exec_command() (stdin) forwarded Channel.send()'s partial byte count, which could silently drop data when the remote window was small. It now always sends the entire buffer and returns len(data). No action required unless you looped over partial writes yourself - that loop is now redundant.
Cryptography Changes¶
SHA-1 removed from the crypto backend¶
CryptographyBackend.HASH_ALGORITHMS no longer includes SHA-1. Any direct call to crypto_backend.hash_data('sha1', ...) now raises CryptoException.
If you were using SHA-1 directly through the crypto backend, switch to SHA-256. The only remaining SHA-1 path in SpindleX is the explicit legacy opt-in for ssh-rsa host key verification (pkey.allow_sha1 = True), which is gated and emits deprecation warnings.
PKey.__eq__ propagates unexpected exceptions¶
Previously PKey.__eq__ caught all exceptions and returned False. It now only suppresses CryptoException (the expected "no key loaded" uninitialized state). All other exceptions propagate to the caller.
If you have code that compared PKey objects and depended on == returning False when a key was in an unexpected state (corrupted, wrong type), those situations will now raise instead of silently returning False. Fix the underlying issue rather than relying on comparison to suppress it.
PKey.verify() propagates unexpected exceptions¶
For the same reason, Ed25519Key.verify(), ECDSAKey.verify(), and RSAKey.verify() no longer catch every exception. An invalid signature or a malformed signature blob still returns False; anything else (for example passing a non-bytes value) now propagates instead of masquerading as a failed verification.
Host Key Behavior¶
Hostnames normalized to lowercase in HostKeyStorage¶
HostKeyStorage previously stored and matched hostnames case-sensitively. Server.example.com and server.example.com were treated as different hosts. Hostnames are now normalized to lowercase on add(), get(), get_all(), and remove().
If your known-hosts file contains mixed-case hostnames, they will be case-folded on the next write. This is the correct behavior per DNS semantics. No action required in most cases.
RSA host key algorithm aliases unified¶
ssh-rsa, rsa-sha2-256, and rsa-sha2-512 are now treated as the same underlying RSA key type for known-host lookup. Previously, a host key stored under ssh-rsa was not recognized when the server later negotiated rsa-sha2-256, causing RejectPolicy connections to fail for known hosts after a server upgraded its key algorithm preference.
If you have RSA host keys in your known-hosts store and were working around this by storing duplicate entries, the duplicates are now redundant and can be cleaned up. No action required for correctness.
API Additions (no action required)¶
These features were added during the 0.7.x line and are fully available in 1.0. No migration is needed - you can start using them as-is.
| Feature | Available since |
|---|---|
keyboard_interactive_handler= on connect() | 0.7.3 |
AsyncSFTPClient.getcwd() | 0.7.3 |
SFTPClient.truncate(path, size) | 0.7.3 |
AsyncSFTPClient.truncate(path, size) | 0.7.3 |
Channel.fileno(), .getsockname(), .getpeername() | 0.7.3 |
PortForwardingManager importable from spindlex | 0.7.3 |
AsyncPortForwardingManager importable from spindlex | 0.7.3 |
ChaCha20-Poly1305 preferred cipher | 0.7.0 |
Adaptive SFTP write chunks via limits@openssh.com | 0.7.0 |
Sync vs Async Differences¶
The sync SSHClient and async AsyncSSHClient are now at parity for all documented features. The key behavioral differences between the two that persist into 1.0:
- ProxyJump: Use
SSHClient(sync) withsock=Channel. The async client does not supportsock=Channeland raises immediately. - Keyboard-interactive: Both clients now accept
keyboard_interactive_handler=onconnect(). compress=True: RaisesConfigurationExceptionon both clients.- Host key policy errors: Both clients now fail-closed (raise) when a policy raises.
Exception Hierarchy¶
The exception classes themselves have not changed. The following classes remain stable and are part of the v1 public API surface:
from spindlex import (
SSHException, # base
AuthenticationException,
BadHostKeyException,
ChannelException,
SFTPError,
TransportException,
ProtocolException,
CryptoException,
TimeoutException,
ConfigurationException,
IncompatiblePeer,
)
Error messages in some cases have been made more specific (particularly around host key failures and transport initialization). If you parse error message strings, update those to catch by exception class instead.
Checking Your Upgrade¶
After updating to 1.0.0, run through this checklist:
- [ ] Remove any
compress=Trueorgss_kex=Truearguments fromconnect()calls. - [ ] Add
accept_risk=Trueto anyAutoAddPolicy()construction, or switch toRejectPolicy(preferred). - [ ] Remove any calls to
KeyExchange.generate_keys(). - [ ] Verify your async code handles
SSHExceptionfromAsyncSSHClient.connect()for host key policy rejections. - [ ] If you compare
PKeyobjects, ensure the underlying keys are in a valid state before comparing. - [ ] Run your integration tests against your actual SSH servers.
Getting Help¶
- Changelog - full history of every change.
- Compatibility - tested servers and known incompatibilities.
- Security Guide - security expectations and safe defaults.
- GitHub Issues - report problems.