Skip to content

transports: assert an SSH algorithm floor on the SFTP hop (#178) - #317

Open
wshallwshall wants to merge 1 commit into
mainfrom
w3-sftp-cipher-floor
Open

transports: assert an SSH algorithm floor on the SFTP hop (#178)#317
wshallwshall wants to merge 1 commit into
mainfrom
w3-sftp-cipher-floor

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

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.

… 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant