Skip to content

Instantly share code, notes, and snippets.

@lzap
Created August 17, 2026 14:56
Show Gist options
  • Select an option

  • Save lzap/8023a876d991961149b80a238ff31944 to your computer and use it in GitHub Desktop.

Select an option

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
#!/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