Created
August 17, 2026 14:56
-
-
Save lzap/8023a876d991961149b80a238ff31944 to your computer and use it in GitHub Desktop.
Reproducer: net-ssh 7.3.3 vs RHEL10 (OpenSSH 9.9) compatibility issues
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
| #!/usr/bin/env ruby | |
| # Reproducer: net-ssh 7.3.3 vs RHEL10 (OpenSSH 9.9) | |
| # | |
| # Run with: bundle exec ruby rhel10_ssh_reproducer.rb [host] [user] | |
| require "bundler/setup" | |
| require "net/ssh" | |
| HOST = ARGV[0] || "192.168.122.73" | |
| USER = ARGV[1] || "lzap" | |
| RSA_KEY = File.expand_path("~/.ssh/id_rsa") | |
| ED25519_KEY = File.expand_path("~/.ssh/id_ed25519") | |
| passed = 0 | |
| failed = 0 | |
| def test(name) | |
| print " #{name}... " | |
| begin | |
| result = yield | |
| puts "PASS — #{result}" | |
| return true | |
| rescue Exception => e | |
| puts "FAIL — #{e.class}: #{e.message}" | |
| return false | |
| end | |
| end | |
| puts "=" * 70 | |
| puts "net-ssh #{Net::SSH::Version::STRING} vs RHEL10 compatibility report" | |
| puts "Target: #{USER}@#{HOST}" | |
| puts "=" * 70 | |
| # ----------------------------------------------------------------------- | |
| # 1. Algorithm support audit | |
| # ----------------------------------------------------------------------- | |
| puts "\n## 1. Algorithm support in net-ssh #{Net::SSH::Version::STRING}\n\n" | |
| kex_algos = Net::SSH::Transport::Algorithms::ALGORITHMS[:kex] | |
| hk_algos = Net::SSH::Transport::Algorithms::ALGORITHMS[:host_key] | |
| enc_algos = Net::SSH::Transport::Algorithms::ALGORITHMS[:encryption] | |
| rhel10_kex = %w[ | |
| mlkem768x25519-sha256 curve25519-sha256 curve25519-sha256@libssh.org | |
| ecdh-sha2-nistp256 ecdh-sha2-nistp384 ecdh-sha2-nistp521 | |
| diffie-hellman-group-exchange-sha256 diffie-hellman-group14-sha256 | |
| diffie-hellman-group16-sha512 diffie-hellman-group18-sha512 | |
| ] | |
| rhel10_host_key = %w[ | |
| ecdsa-sha2-nistp256 sk-ecdsa-sha2-nistp256@openssh.com | |
| ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 | |
| ssh-ed25519 sk-ssh-ed25519@openssh.com | |
| rsa-sha2-256 rsa-sha2-512 | |
| ] | |
| rhel10_ciphers = %w[ | |
| aes256-gcm@openssh.com chacha20-poly1305@openssh.com | |
| aes256-ctr aes128-gcm@openssh.com aes128-ctr | |
| ] | |
| puts "KEX algorithms (RHEL10 DEFAULT vs net-ssh):" | |
| rhel10_kex.each do |a| | |
| supported = kex_algos.include?(a) | |
| puts " #{supported ? '[OK]' : '[!!]'} #{a}" | |
| end | |
| kex_overlap = rhel10_kex & kex_algos | |
| puts " Overlap: #{kex_overlap.length}/#{rhel10_kex.length} — #{kex_overlap.join(', ')}" | |
| puts "\nHost key algorithms (RHEL10 DEFAULT vs net-ssh):" | |
| rhel10_host_key.each do |a| | |
| supported = hk_algos.include?(a) | |
| puts " #{supported ? '[OK]' : '[!!]'} #{a}" | |
| end | |
| hk_overlap = rhel10_host_key & hk_algos | |
| puts " Overlap: #{hk_overlap.length}/#{rhel10_host_key.length} — #{hk_overlap.join(', ')}" | |
| puts "\nCiphers (RHEL10 DEFAULT vs net-ssh):" | |
| rhel10_ciphers.each do |a| | |
| supported = enc_algos.include?(a) | |
| puts " #{supported ? '[OK]' : '[!!]'} #{a}" | |
| end | |
| enc_overlap = rhel10_ciphers & enc_algos | |
| puts " Overlap: #{enc_overlap.length}/#{rhel10_ciphers.length} — #{enc_overlap.join(', ')}" | |
| ed25519_gem = begin; require "ed25519"; true; rescue LoadError; false; end | |
| bcrypt_gem = begin; require "bcrypt_pbkdf"; true; rescue LoadError; false; end | |
| puts "\nOptional gems: ed25519=#{ed25519_gem} bcrypt_pbkdf=#{bcrypt_gem}" | |
| # ----------------------------------------------------------------------- | |
| # 2. Connection tests | |
| # ----------------------------------------------------------------------- | |
| puts "\n## 2. Connection tests against #{HOST}\n" | |
| puts "\n### 2a. RSA key authentication" | |
| r = test("RSA key (keys_only, config disabled)") do | |
| Net::SSH.start(HOST, USER, timeout: 10, non_interactive: true, config: false, | |
| keys: [RSA_KEY], keys_only: true, verify_host_key: :never) do |ssh| | |
| ssh.exec!("cat /etc/redhat-release").strip | |
| end | |
| end | |
| passed += 1 if r; failed += 1 unless r | |
| puts "\n### 2b. ED25519 key authentication" | |
| r = test("ED25519 key (keys_only, config disabled)") do | |
| Net::SSH.start(HOST, USER, timeout: 10, non_interactive: true, config: false, | |
| keys: [ED25519_KEY], keys_only: true, verify_host_key: :never) do |ssh| | |
| ssh.exec!("cat /etc/redhat-release").strip | |
| end | |
| end | |
| passed += 1 if r; failed += 1 unless r | |
| puts "\n### 2c. Default key discovery (auto — loads all keys from ~/.ssh/)" | |
| r = test("Default key discovery") do | |
| Net::SSH.start(HOST, USER, timeout: 10, non_interactive: true, config: false, | |
| verify_host_key: :never) do |ssh| | |
| ssh.exec!("cat /etc/redhat-release").strip | |
| end | |
| end | |
| passed += 1 if r; failed += 1 unless r | |
| puts "\n### 2d. Foreman-style connection (simulated key_data with RSA)" | |
| r = test("Foreman-style with RSA key_data") do | |
| key_data = File.read(RSA_KEY) | |
| Net::SSH.start(HOST, USER, timeout: 10, non_interactive: true, config: false, | |
| key_data: [key_data], keys_only: true, keys: [], | |
| auth_methods: ["publickey"], verify_host_key: :never) do |ssh| | |
| ssh.exec!("cat /etc/redhat-release").strip | |
| end | |
| end | |
| passed += 1 if r; failed += 1 unless r | |
| puts "\n### 2e. Foreman-style connection (simulated key_data with ED25519)" | |
| r = test("Foreman-style with ED25519 key_data") do | |
| key_data = File.read(ED25519_KEY) | |
| Net::SSH.start(HOST, USER, timeout: 10, non_interactive: true, config: false, | |
| key_data: [key_data], keys_only: true, keys: [], | |
| auth_methods: ["publickey"], verify_host_key: :never) do |ssh| | |
| ssh.exec!("cat /etc/redhat-release").strip | |
| end | |
| end | |
| passed += 1 if r; failed += 1 unless r | |
| puts "\n### 2f. Host key verification (ed25519 host key only)" | |
| r = test("Force ssh-ed25519 host key negotiation") do | |
| Net::SSH.start(HOST, USER, timeout: 10, non_interactive: true, config: false, | |
| keys: [RSA_KEY], keys_only: true, | |
| host_key: "ssh-ed25519", verify_host_key: :never) do |ssh| | |
| ssh.exec!("cat /etc/redhat-release").strip | |
| end | |
| end | |
| passed += 1 if r; failed += 1 unless r | |
| puts "\n### 2g. Force curve25519 KEX" | |
| r = test("Force curve25519-sha256 KEX") do | |
| Net::SSH.start(HOST, USER, timeout: 10, non_interactive: true, config: false, | |
| keys: [RSA_KEY], keys_only: true, | |
| kex: "curve25519-sha256", verify_host_key: :never) do |ssh| | |
| ssh.exec!("cat /etc/redhat-release").strip | |
| end | |
| end | |
| passed += 1 if r; failed += 1 unless r | |
| puts "\n### 2h. Force chacha20-poly1305 cipher" | |
| r = test("Force chacha20-poly1305@openssh.com cipher") do | |
| Net::SSH.start(HOST, USER, timeout: 10, non_interactive: true, config: false, | |
| keys: [RSA_KEY], keys_only: true, | |
| encryption: "chacha20-poly1305@openssh.com", verify_host_key: :never) do |ssh| | |
| ssh.exec!("cat /etc/redhat-release").strip | |
| end | |
| end | |
| passed += 1 if r; failed += 1 unless r | |
| puts "\n### 2i. Fog::SSH (as used by Foreman)" | |
| begin | |
| require "fog/core" | |
| r = test("Fog::SSH with RSA key") do | |
| conn = Fog::SSH.new(HOST, USER, | |
| keys: [RSA_KEY], keys_only: true, | |
| config: false, timeout: 10, verify_host_key: :never) | |
| results = conn.run("cat /etc/redhat-release") | |
| results.first.stdout.strip | |
| end | |
| passed += 1 if r; failed += 1 unless r | |
| rescue LoadError | |
| puts " Fog::SSH — SKIP (fog-core not available)" | |
| end | |
| # ----------------------------------------------------------------------- | |
| # 3. Summary | |
| # ----------------------------------------------------------------------- | |
| puts "\n" + "=" * 70 | |
| puts "RESULTS: #{passed} passed, #{failed} failed" | |
| puts "=" * 70 | |
| puts "\n## Issues found:\n" | |
| issues = [] | |
| unless kex_algos.any? { |a| a.include?("curve25519") } | |
| issues << "MISSING KEX: curve25519-sha256 — RHEL10's 2nd-preference KEX algorithm.\n" \ | |
| " net-ssh falls back to ecdh-sha2-nistp521, but this means no overlap\n" \ | |
| " if RHEL10 restricts to curve25519/mlkem-only KEX." | |
| end | |
| unless kex_algos.any? { |a| a.include?("mlkem") } | |
| issues << "MISSING KEX: mlkem768x25519-sha256 — RHEL10's top-preference post-quantum\n" \ | |
| " KEX algorithm. Not critical yet but will become mandatory." | |
| end | |
| unless hk_algos.include?("ssh-ed25519") | |
| issues << "MISSING HOST KEY: ssh-ed25519 — If a RHEL10 host has ONLY an ed25519\n" \ | |
| " host key (no ecdsa/rsa), net-ssh cannot negotiate a host key algorithm\n" \ | |
| " and the connection fails entirely." | |
| end | |
| unless ed25519_gem | |
| issues << "MISSING GEM: ed25519 — Required for ed25519 client key authentication\n" \ | |
| " and ssh-ed25519 host key verification. Not in Foreman's Gemfile.\n" \ | |
| " Fix: Add 'ed25519' and 'bcrypt_pbkdf' to Gemfile." | |
| end | |
| unless enc_algos.include?("chacha20-poly1305@openssh.com") | |
| issues << "MISSING CIPHER: chacha20-poly1305@openssh.com — RHEL10's preferred cipher.\n" \ | |
| " net-ssh falls back to aes256-gcm/aes256-ctr." | |
| end | |
| issues << "MISSING STRICT KEX: net-ssh #{Net::SSH::Version::STRING} does not implement\n" \ | |
| " the strict KEX extension (kex-strict-*-v00@openssh.com) for CVE-2023-48795\n" \ | |
| " (Terrapin). Connection works but is vulnerable to prefix truncation attacks." | |
| if issues.empty? | |
| puts " None — all algorithms supported." | |
| else | |
| issues.each_with_index { |issue, i| puts "#{i + 1}. #{issue}\n" } | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment