Inspect guest for supported key types

Updates the SSH communicator to inspect the guest sshd configuration for
supported key types when creating a new key to replace the default
insecure public key. If the guest cannot be inspected, the connection
will be inspected. If the connection cannot be inspected, it will simply
fall back to the original behavior of using an rsa type key.
This commit is contained in:
Chris Roberts 2024-01-18 11:53:00 -08:00
parent 55ec0d4391
commit 5a7fd6b302
2 changed files with 188 additions and 30 deletions

View File

@ -838,10 +838,20 @@ module VagrantPlugins
end
def supported_key_types
return @supported_key_types if @supported_key_types
if @connection.nil?
raise Vagrant::Errors::SSHNotReady
end
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&.
@ -855,9 +865,32 @@ module VagrantPlugins
raise ServerDataError, "unexpected type encountered (expecting Hash, got #{server_data.class})"
end
@logger.debug("server supported key type list: #{server_data[:host_key]}")
@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
server_data[:host_key]
# 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
# 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}")
raise ServerDataError, "no data available"
end
end
end

View File

@ -69,7 +69,8 @@ describe VagrantPlugins::CommunicatorSSH::Communicator do
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`
@ -93,13 +94,16 @@ describe VagrantPlugins::CommunicatorSSH::Communicator do
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
@ -162,7 +166,7 @@ describe VagrantPlugins::CommunicatorSSH::Communicator do
end
end
describe "reset!" do
describe "#reset!" do
let(:connection) { double("connection") }
before do
@ -182,7 +186,7 @@ describe VagrantPlugins::CommunicatorSSH::Communicator do
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)
@ -248,8 +252,6 @@ describe VagrantPlugins::CommunicatorSSH::Communicator do
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).
@ -264,7 +266,6 @@ describe VagrantPlugins::CommunicatorSSH::Communicator do
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
@ -297,7 +298,7 @@ describe VagrantPlugins::CommunicatorSSH::Communicator do
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
@ -371,8 +372,7 @@ describe VagrantPlugins::CommunicatorSSH::Communicator do
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
@ -385,7 +385,7 @@ describe VagrantPlugins::CommunicatorSSH::Communicator do
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/)
@ -579,7 +579,7 @@ describe VagrantPlugins::CommunicatorSSH::Communicator do
end
end
describe ".test" do
describe "#test" do
before(&connection_setup)
context "with exit code as zero" do
it "returns true" do
@ -598,7 +598,7 @@ describe VagrantPlugins::CommunicatorSSH::Communicator do
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)
@ -704,7 +704,7 @@ describe VagrantPlugins::CommunicatorSSH::Communicator do
end
end
describe ".download" do
describe "#download" do
before do
expect(communicator).to receive(:scp_connect).and_yield(scp)
end
@ -715,7 +715,7 @@ describe VagrantPlugins::CommunicatorSSH::Communicator do
end
end
describe ".connect" do
describe "#connect" do
it "cannot be called directly" do
expect{ communicator.connect }.to raise_error(NoMethodError)
@ -1030,7 +1030,7 @@ describe VagrantPlugins::CommunicatorSSH::Communicator do
end
end
describe ".insecure_key?" do
describe "#insecure_key?" do
let(:key_data) { "" }
let(:key_file) {
if !@key_file
@ -1069,7 +1069,7 @@ describe VagrantPlugins::CommunicatorSSH::Communicator do
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
@ -1082,4 +1082,129 @@ describe VagrantPlugins::CommunicatorSSH::Communicator do
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