Skip to content

Commit

Permalink
Merge pull request #13334 from chrisroberts/ssh-supported-key-type-in…
Browse files Browse the repository at this point in the history
…spect

Inspect guest for supported key types
  • Loading branch information
chrisroberts authored Jan 18, 2024
2 parents 4ca49e5 + 5a7fd6b commit 66c39b7
Show file tree
Hide file tree
Showing 2 changed files with 187 additions and 29 deletions.
57 changes: 45 additions & 12 deletions plugins/communicators/ssh/communicator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -838,26 +838,59 @@ def supports_key_type?(type)
end

def supported_key_types
return @supported_key_types if @supported_key_types

if @connection.nil?
raise Vagrant::Errors::SSHNotReady
end

server_data = @connection.
transport&.
algorithms&.
instance_variable_get(:@server_data)
if server_data.nil?
@logger.warn("No server data available for key type support check")
raise ServerDataError, "no data available"
list = ""
result = sudo("sshd -T | grep key", {error_check: false}) do |type, data|
list << data
end

# If the command failed, attempt to extract some supported
# key information from within net-ssh
if result != 0
server_data = @connection.
transport&.
algorithms&.
instance_variable_get(:@server_data)
if server_data.nil?
@logger.warn("No server data available for key type support check")
raise ServerDataError, "no data available"
end
if !server_data.is_a?(Hash)
@logger.warn("Server data is not expected type (expecting Hash, got #{server_data.class})")
raise ServerDataError, "unexpected type encountered (expecting Hash, got #{server_data.class})"
end

@logger.debug("server supported key type list (extracted from connection server info using host key): #{server_data[:host_key]}")
return @supported_key_types = server_data[:host_key]
end
if !server_data.is_a?(Hash)
@logger.warn("Server data is not expected type (expecting Hash, got #{server_data.class})")
raise ServerDataError, "unexpected type encountered (expecting Hash, got #{server_data.class})"

# Convert the options into a Hash for easy access
opts = Hash[*list.split("\n").map{|line| line.split(" ", 2)}.flatten]

# Define the option names to check for in preferred order
# NOTE: pubkeyacceptedkeytypes has been renamed to pubkeyacceptedalgorithms
# ref: https://github.com/openssh/openssh-portable/commit/ee9c0da8035b3168e8e57c1dedc2d1b0daf00eec
["pubkeyacceptedalgorithms", "pubkeyacceptedkeytypes", "hostkeyalgorithms"].each do |opt_name|
next if !opts.key?(opt_name)

@supported_key_types = opts[opt_name].split(",")
@logger.debug("server supported key type list (using #{opt_name}): #{@supported_key_types}")

return @supported_key_types
end

@logger.debug("server supported key type list: #{server_data[:host_key]}")
# Still here means unable to determine key types
# so log what information was returned and toss
# and error
@logger.warn("failed to determine supported key types from remote inspection")
@logger.debug("data returned for supported key types remote inspection: #{list.inspect}")

server_data[:host_key]
raise ServerDataError, "no data available"
end
end
end
Expand Down
159 changes: 142 additions & 17 deletions test/unit/plugins/communicators/ssh/communicator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@
let(:command_stderr_data) { '' }
# Mock for net-ssh scp
let(:scp) { double("scp") }

# Value returned from remote ssh supported key check
let(:sudo_supported_key_list) { "pubkeyacceptedalgorithms ssh-rsa" }

# Setup for commands using the net-ssh connection. This can be reused where needed
# by providing to `before`
Expand All @@ -93,13 +94,16 @@
and_yield(nil, exit_data)
# Return mocked net-ssh connection during setup
allow(communicator).to receive(:retryable).and_return(connection)
# Stub in a response for supported key types check
allow(communicator).to receive(:sudo).with("sshd -T | grep key", any_args).
and_yield(:stdout, sudo_supported_key_list).and_return(0)
end

before do
allow(host).to receive(:capability?).and_return(false)
end

describe ".wait_for_ready" do
describe "#wait_for_ready" do
before(&connection_setup)
context "with no static config (default scenario)" do
context "when ssh_info requires a multiple tries before it is ready" do
Expand Down Expand Up @@ -162,7 +166,7 @@
end
end

describe "reset!" do
describe "#reset!" do
let(:connection) { double("connection") }

before do
Expand All @@ -182,7 +186,7 @@
end
end

describe ".ready?" do
describe "#ready?" do
before(&connection_setup)
it "returns true if shell test is successful" do
expect(communicator.ready?).to be(true)
Expand Down Expand Up @@ -248,8 +252,6 @@
let(:path_joiner){ double("path_joiner") }
let(:algorithms) { double(:algorithms) }
let(:transport) { double(:transport, algorithms: algorithms) }
let(:valid_key_types) { [] }
let(:server_data) { { host_key: valid_key_types} }

before do
allow(Vagrant::Util::Keypair).to receive(:create).
Expand All @@ -264,7 +266,6 @@
allow(guest).to receive(:capability).with(:insert_public_key)
allow(guest).to receive(:capability).with(:remove_public_key)
allow(connection).to receive(:transport).and_return(transport)
allow(algorithms).to receive(:instance_variable_get).with(:@server_data).and_return(server_data)
allow(communicator).to receive(:supported_key_types).and_raise(described_class.const_get(:ServerDataError))
end

Expand Down Expand Up @@ -297,7 +298,7 @@

context "with server algorithm support data" do
before do
allow(communicator).to receive(:supported_key_types).and_call_original
allow(communicator).to receive(:supported_key_types).and_return(valid_key_types)
end

context "when rsa is the only match" do
Expand Down Expand Up @@ -371,8 +372,7 @@

context "when an error is encountered getting server data" do
before do
expect(communicator).to receive(:supported_key_types).and_call_original
expect(connection).to receive(:transport).and_raise(StandardError)
expect(communicator).to receive(:supported_key_types).and_raise(StandardError)
end

it "should default to rsa key" do
Expand All @@ -385,7 +385,7 @@
end
end

describe ".execute" do
describe "#execute" do
before(&connection_setup)
it "runs valid command and returns successful status code" do
expect(command_channel).to receive(:send_data).with(/ls \/\n/)
Expand Down Expand Up @@ -579,7 +579,7 @@
end
end

describe ".test" do
describe "#test" do
before(&connection_setup)
context "with exit code as zero" do
it "returns true" do
Expand All @@ -598,7 +598,7 @@
end
end

describe ".upload" do
describe "#upload" do
before do
expect(communicator).to receive(:scp_connect).and_yield(scp)
allow(communicator).to receive(:create_remote_directory)
Expand Down Expand Up @@ -704,7 +704,7 @@
end
end

describe ".download" do
describe "#download" do
before do
expect(communicator).to receive(:scp_connect).and_yield(scp)
end
Expand All @@ -715,7 +715,7 @@
end
end

describe ".connect" do
describe "#connect" do

it "cannot be called directly" do
expect{ communicator.connect }.to raise_error(NoMethodError)
Expand Down Expand Up @@ -1030,7 +1030,7 @@
end
end

describe ".insecure_key?" do
describe "#insecure_key?" do
let(:key_data) { "" }
let(:key_file) {
if !@key_file
Expand Down Expand Up @@ -1069,7 +1069,7 @@
end
end

describe ".generate_environment_export" do
describe "#generate_environment_export" do
it "should generate bourne shell compatible export" do
expect(communicator.send(:generate_environment_export, "TEST", "value")).to eq("export TEST=\"value\"\n")
end
Expand All @@ -1082,4 +1082,129 @@
end
end
end

describe "#supported_key_types" do
let(:sudo_result) { 0 }
let(:sudo_data) { "" }
let(:server_data_error) { VagrantPlugins::CommunicatorSSH::Communicator::ServerDataError }
let(:transport) { double("transport", algorithms: algorithms) }
let(:algorithms) { double("algorithms") }

before do
allow(communicator).to receive(:ready?).and_return(true)
expect(communicator).to receive(:sudo).
with("sshd -T | grep key", any_args).
and_yield(:stdout, sudo_data).
and_return(sudo_result)
# The @connection value is checked to determine if supported key types
# can be checked. To facilitate this, set it to a non-nil value
communicator.instance_variable_set(:@connection, connection)
allow(connection).to receive(:transport).and_return(transport)
end

it "should raise an error when no data is returned" do
expect { communicator.send(:supported_key_types) }.to raise_error(server_data_error)
end

context "when sudo command is unsuccessful" do
let(:sudo_result) { 1 }

it "should inspect the net-ssh connection" do
expect(algorithms).to receive(:instance_variable_get).
with(:@server_data).and_return({})
communicator.send(:supported_key_types)
end
end

context "when data includes pubkeyacceptedalgorithms" do
let(:sudo_data) do
"pubkeyauthentication yes
gssapikeyexchange no
gssapistorecredentialsonrekey no
trustedusercakeys none
revokedkeys none
authorizedkeyscommand none
authorizedkeyscommanduser none
hostkeyagent none
hostbasedacceptedkeytypes ecdsa-sha2-nistp521,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa
hostkeyalgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa
pubkeyacceptedalgorithms rsa-sha2-512,rsa-sha2-256,ssh-rsa
authorizedkeysfile .ssh/authorized_keys
hostkey /etc/ssh/ssh_host_rsa_key
rekeylimit 0 0"
end

it "should return expected values" do
expect(communicator.send(:supported_key_types)).to eq(["rsa-sha2-512", "rsa-sha2-256", "ssh-rsa"])
end
end

context "when data includes pubkeyacceptedkeytypes" do
let(:sudo_data) do
"pubkeyauthentication yes
gssapikeyexchange no
gssapistorecredentialsonrekey no
trustedusercakeys none
revokedkeys none
authorizedkeyscommand none
authorizedkeyscommanduser none
hostkeyagent none
hostbasedacceptedkeytypes ecdsa-sha2-nistp521,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa
hostkeyalgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa
pubkeyacceptedkeytypes rsa-sha2-512,rsa-sha2-256,ssh-rsa
authorizedkeysfile .ssh/authorized_keys
hostkey /etc/ssh/ssh_host_rsa_key
rekeylimit 0 0"
end

it "should return expected values" do
expect(communicator.send(:supported_key_types)).
to eq(["rsa-sha2-512", "rsa-sha2-256", "ssh-rsa"])
end
end

context "when data does not include pubkeyacceptedalgorithms or pubkeyacceptedkeytypes" do
let(:sudo_data) do
"pubkeyauthentication yes
gssapikeyexchange no
gssapistorecredentialsonrekey no
trustedusercakeys none
revokedkeys none
authorizedkeyscommand none
authorizedkeyscommanduser none
hostkeyagent none
hostbasedacceptedkeytypes ecdsa-sha2-nistp521,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa
hostkeyalgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa
authorizedkeysfile .ssh/authorized_keys
hostkey /etc/ssh/ssh_host_rsa_key
rekeylimit 0 0"
end

it "should use hostkeyalgorithms" do
expect(communicator.send(:supported_key_types)).
to eq(["ssh-ed25519", "rsa-sha2-512", "rsa-sha2-256", "ssh-rsa"])
end
end

context "when data does not include defined config options" do
let(:sudo_data) do
"pubkeyauthentication yes
gssapikeyexchange no
gssapistorecredentialsonrekey no
trustedusercakeys none
revokedkeys none
authorizedkeyscommand none
authorizedkeyscommanduser none
hostkeyagent none
authorizedkeysfile .ssh/authorized_keys
hostkey /etc/ssh/ssh_host_rsa_key
rekeylimit 0 0"
end

it "should raise error" do
expect { communicator.send(:supported_key_types) }.
to raise_error(server_data_error)
end
end
end
end

0 comments on commit 66c39b7

Please sign in to comment.