transports: assert an SSH algorithm floor on the SFTP hop (#178) - #317
Open
wshallwshall wants to merge 1 commit into
Open
transports: assert an SSH algorithm floor on the SFTP hop (#178)#317wshallwshall wants to merge 1 commit into
wshallwshall wants to merge 1 commit into
Conversation
… TLS one (#178) The SSH hop was the one transport hop with no cipher assertion. Measured 2026-08-10: harden_cipher_suites is called at 7 sites across 5 files (the API/UI listener, MLLP in both directions, both DICOM contexts, FTPS, and the SMTP alert sink), while disabled_algorithms appeared 0 times anywhere in the package. Driven against a live paramiko server, the SFTP client accepted hmac-md5, hmac-sha1 and 3des-cbc without complaint -- a partner, or an on-path attacker steering the negotiation, could pick any of them and nothing would say so. config/ssh_policy.py is the missing assertion, and it is the TLS floor re-expressed rather than a second opinion. The shipped TLS contexts resolve to 17 suites whose measured properties are Kx in {ECDH, DH, any}, Enc in {AES, AESGCM, CHACHA20} and Mac in {AEAD, SHA256, SHA384}; each cell maps to one SSH rule (forward-secret key exchange, no 64-bit-block or broken cipher, no MD5/SHA-1 MAC). Two of those three are properties OpenSSL's default cipher string gives the TLS side for free and paramiko's defaults do not, which is why the SSH side has to name them. Two operations, deliberately separated. BELOW-floor names are disabled -- SSH negotiates from the offer, so pruning the offer IS the enforcement, unlike TLS where the context already resolves forward-secret and harden_cipher_suites only has to check it. Everything surviving is asserted recognisable, so an unclassified algorithm raises at connect naming itself rather than being quietly dropped and narrowing reachability with no signal. Against paramiko 5.0.0 the floor prunes 5 of 24 offered names: 3des-cbc, hmac-md5, hmac-md5-96, hmac-sha1, hmac-sha1-96. Every algorithm a current OpenSSH negotiates by default survives, AES-CBC included -- dropping it would re-introduce on this hop exactly the interop regression harden_cipher_suites measured and declined to take on TLS. Fails closed without failing silently: a partner offering nothing above the floor is refused with a permanent error naming the category, the algorithms refused, why they are refused, and what to enable instead -- not a timeout and not a bare "Incompatible ssh server (no acceptable macs)", which reads like a partner defect. paramiko stays lazily imported behind the [sftp] extra: ssh_policy imports it never, the caller passes the offer in, and an install without paramiko behaves exactly as it did. No operator override, deliberately, so nothing is added to security_loosenings. Nothing plausible needs loosening from this floor, and a switch nobody asked for is a second posture by the back door. If a partner ever forces the question it belongs there as a REQUIRED parameter, the way the TLS deviations do. The operator-configurable cipher/KEX/MAC allow-list that is #178's other half stays DEMAND-GATED and unbuilt -- this module has no configuration surface at all. Verified against a real key exchange rather than by attribute, because a floor asserted only on a constructed object passes identically when disabled_algorithms never reaches the connect call. tests/test_ssh_algorithm_floor.py stands up a live paramiko SSH server on loopback: the three algorithms that connected before are each refused after, with the message asserted, and a modern partner plus a paramiko-defaults partner both still negotiate. The pure-policy half imports no SSH library and runs without the extra.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
BACKLOG #178
The owner ruled #178 BUILD, scoped narrowly: the SSH/SFTP hop was the single transport hop without a
forward-secrecy floor assertion. The configurable allow-list stays deferred.
Handed over by the authoring session at a usage cutoff. Reported COMPLETE and verified there against a
live paramiko server.
NOT INDEPENDENTLY VERIFIED BY THE COORDINATOR -- pushed and routed to preserve the work off a local
ref, not merged. Run the standard battery before landing.
Leak-scanned before pushing: zero hits, negative control firing.
Deliberately NOT armed for auto-merge.