Use ssh key type defined by configuration

If key type is defined as :auto, detect best key type to use. If no
acceptable key type is detected as supported by the server, raise an
error. If unable to determine supported key types from the server,
fallback to original behavior of rsa type key.

If key type is defined as custom value, use that type if the server
supports it, or if the supported types cannot be read. Otherwise, raise
an error informing the user that the key type is not supported.
This commit is contained in:
Chris Roberts 2024-01-10 11:39:47 -08:00
parent 443ff01ab7
commit 96f2039bcd
4 changed files with 132 additions and 30 deletions

View File

@ -863,6 +863,10 @@ module Vagrant
error_key(:ssh_key_type_not_supported) error_key(:ssh_key_type_not_supported)
end end
class SSHKeyTypeNotSupportedByServer < VagrantError
error_key(:ssh_key_type_not_supported_by_server)
end
class SSHNoExitStatus < VagrantError class SSHNoExitStatus < VagrantError
error_key(:ssh_no_exit_status) error_key(:ssh_no_exit_status)
end end

View File

@ -113,6 +113,8 @@ module VagrantPlugins
raise raise
rescue Vagrant::Errors::SSHKeyTypeNotSupported rescue Vagrant::Errors::SSHKeyTypeNotSupported
raise raise
rescue Vagrant::Errors::SSHKeyTypeNotSupportedByServer
raise
rescue Vagrant::Errors::SSHKeyBadOwner rescue Vagrant::Errors::SSHKeyBadOwner
raise raise
rescue Vagrant::Errors::SSHKeyBadPermissions rescue Vagrant::Errors::SSHKeyBadPermissions
@ -188,7 +190,12 @@ module VagrantPlugins
@machine.guest.capability?(:remove_public_key) @machine.guest.capability?(:remove_public_key)
raise Vagrant::Errors::SSHInsertKeyUnsupported if !cap raise Vagrant::Errors::SSHInsertKeyUnsupported if !cap
# Check for supported key type key_type = machine_config_ssh.key_type
begin
# If the key type is set to `:auto` check for supported type. Otherwise
# ensure that the key type is supported by the guest
if key_type == :auto
key_type = catch(:key_type) do key_type = catch(:key_type) do
begin begin
Vagrant::Util::Keypair::PREFER_KEY_TYPES.each do |type_name, type| Vagrant::Util::Keypair::PREFER_KEY_TYPES.each do |type_name, type|
@ -205,9 +212,35 @@ module VagrantPlugins
# If no key type was discovered, default to rsa # If no key type was discovered, default to rsa
if key_type.nil? if key_type.nil?
@logger.debug("Failed to detect supported key type, defaulting to rsa") @logger.debug("Failed to detect supported key type in: #{supported_key_types.join(", ")}")
available_types = supported_key_types.map { |t|
next if !Vagrant::Util::Keypair::PREFER_KEY_TYPES.key?(t)
"#{t} (#{Vagrant::Util::Keypair::PREFER_KEY_TYPES[t]})"
}.compact.join(", ")
raise Vagrant::Errors::SSHKeyTypeNotSupportedByServer,
requested_key_type: ":auto",
available_key_types: available_types
end
else
type_name = Vagrant::Util::Keypair::PREFER_KEY_TYPES.key(key_type)
if !supports_key_type?(type_name)
available_types = supported_key_types.map { |t|
next if !Vagrant::Util::Keypair::PREFER_KEY_TYPES.key?(t)
"#{t} (#{Vagrant::Util::Keypair::PREFER_KEY_TYPES[t]})"
}.compact.join(", ")
raise Vagrant::Errors::SSHKeyTypeNotSupportedByServer,
requested_key_type: "#{type_name} (#{key_type})",
available_key_types: available_types
end
end
rescue ServerDataError
@logger.warn("failed to load server data for key type check")
if key_type.nil? || key_type == :auto
@logger.warn("defaulting key type to :rsa due to failed server data loading")
key_type = :rsa key_type = :rsa
end end
end
@logger.info("Creating new ssh keypair (type: #{key_type.inspect})") @logger.info("Creating new ssh keypair (type: #{key_type.inspect})")
_pub, priv, openssh = Vagrant::Util::Keypair.create(type: key_type) _pub, priv, openssh = Vagrant::Util::Keypair.create(type: key_type)
@ -788,6 +821,8 @@ module VagrantPlugins
protected protected
class ServerDataError < StandardError; end
# Check if server supports given key type # Check if server supports given key type
# #
# @param [String, Symbol] type Key type # @param [String, Symbol] type Key type
@ -798,21 +833,31 @@ module VagrantPlugins
if @connection.nil? if @connection.nil?
raise Vagrant::Errors::SSHNotReady raise Vagrant::Errors::SSHNotReady
end end
supported_key_types.include?(type.to_s)
end
def supported_key_types
if @connection.nil?
raise Vagrant::Errors::SSHNotReady
end
server_data = @connection. server_data = @connection.
transport&. transport&.
algorithms&. algorithms&.
instance_variable_get(:@server_data) instance_variable_get(:@server_data)
if server_data.nil? if server_data.nil?
@logger.warn("No server data available for key type support check") @logger.warn("No server data available for key type support check")
return false raise ServerDataError, "no data available"
end end
if !server_data.is_a?(Hash) if !server_data.is_a?(Hash)
@logger.warn("Server data is not expected type (expecting Hash, got #{server_data.class})") @logger.warn("Server data is not expected type (expecting Hash, got #{server_data.class})")
return false raise ServerDataError, "unexpected type encountered (expecting Hash, got #{server_data.class})"
end end
@logger.debug("server data used for host key support check: #{server_data.inspect}") @logger.debug("server supported key type list: #{server_data[:host_key]}")
server_data[:host_key].include?(type.to_s)
server_data[:host_key]
end end
end end
end end

View File

@ -1621,6 +1621,14 @@ en:
sometimes keys in your ssh-agent can interfere with this as well, sometimes keys in your ssh-agent can interfere with this as well,
so verify the keys are valid there in addition to standard so verify the keys are valid there in addition to standard
file paths. file paths.
ssh_key_type_not_supported_by_server: |-
The private key you are attempting to generate is not supported by
the guest SSH server. Please use one of the available key types defined
below that is supported by the guest SSH server.
Requested: %{requested_key_type}
Available: %{available_key_types}
ssh_not_ready: |- ssh_not_ready: |-
The provider for this Vagrant-managed machine is reporting that it The provider for this Vagrant-managed machine is reporting that it
is not yet ready for SSH. Depending on your provider this can carry is not yet ready for SSH. Depending on your provider this can carry

View File

@ -13,6 +13,7 @@ describe VagrantPlugins::CommunicatorSSH::Communicator do
# SSH configuration information mock # SSH configuration information mock
let(:ssh) do let(:ssh) do
double("ssh", double("ssh",
key_type: :auto,
timeout: 1, timeout: 1,
host: nil, host: nil,
port: 5986, port: 5986,
@ -264,46 +265,48 @@ describe VagrantPlugins::CommunicatorSSH::Communicator do
allow(guest).to receive(:capability).with(:remove_public_key) allow(guest).to receive(:capability).with(:remove_public_key)
allow(connection).to receive(:transport).and_return(transport) allow(connection).to receive(:transport).and_return(transport)
allow(algorithms).to receive(:instance_variable_get).with(:@server_data).and_return(server_data) 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 end
after{ communicator.ready? }
it "should create a new key pair" do it "should create a new key pair" do
expect(Vagrant::Util::Keypair).to receive(:create). expect(Vagrant::Util::Keypair).to receive(:create).
and_return([new_public_key, new_private_key, openssh]) and_return([new_public_key, new_private_key, openssh])
communicator.ready?
end end
it "should call the insert_public_key guest capability" do it "should call the insert_public_key guest capability" do
expect(guest).to receive(:capability).with(:insert_public_key, openssh) expect(guest).to receive(:capability).with(:insert_public_key, openssh)
communicator.ready?
end end
it "should write the new private key" do it "should write the new private key" do
expect(private_key_file).to receive(:write).with(new_private_key) expect(private_key_file).to receive(:write).with(new_private_key)
communicator.ready?
end end
it "should call the set_ssh_key_permissions host capability" do it "should call the set_ssh_key_permissions host capability" do
expect(host).to receive(:capability?).with(:set_ssh_key_permissions).and_return(true) expect(host).to receive(:capability?).with(:set_ssh_key_permissions).and_return(true)
expect(host).to receive(:capability).with(:set_ssh_key_permissions, private_key_file) expect(host).to receive(:capability).with(:set_ssh_key_permissions, private_key_file)
communicator.ready?
end end
it "should remove the default public key" do it "should remove the default public key" do
expect(guest).to receive(:capability).with(:remove_public_key, any_args) expect(guest).to receive(:capability).with(:remove_public_key, any_args)
communicator.ready?
end end
context "with server algorithm support data" do context "with server algorithm support data" do
context "when no key type matches are found" do before do
it "should default to rsa type" do allow(communicator).to receive(:supported_key_types).and_call_original
expect(Vagrant::Util::Keypair).to receive(:create).
with(type: :rsa).and_call_original
end
end end
context "when rsa is the only match" do context "when rsa is the only match" do
let(:valid_key_types) { ["ssh-edsca", "ssh-rsa"] } let(:valid_key_types) { ["ssh-ecdsa", "ssh-rsa"] }
it "should use rsa type" do it "should use rsa type" do
expect(Vagrant::Util::Keypair).to receive(:create). expect(Vagrant::Util::Keypair).to receive(:create).
with(type: :rsa).and_call_original with(type: :rsa).and_call_original
communicator.ready?
end end
end end
@ -313,27 +316,69 @@ describe VagrantPlugins::CommunicatorSSH::Communicator do
it "should use ed25519 type" do it "should use ed25519 type" do
expect(Vagrant::Util::Keypair).to receive(:create). expect(Vagrant::Util::Keypair).to receive(:create).
with(type: :ed25519).and_call_original with(type: :ed25519).and_call_original
communicator.ready?
end end
end end
context "when ed25519 is the only match" do context "when ed25519 is the only match" do
let(:valid_key_types) { ["ssh-edsca", "ssh-ed25519"] } let(:valid_key_types) { ["ssh-ecdsa", "ssh-ed25519"] }
it "should use ed25519 type" do it "should use ed25519 type" do
expect(Vagrant::Util::Keypair).to receive(:create). expect(Vagrant::Util::Keypair).to receive(:create).
with(type: :ed25519).and_call_original with(type: :ed25519).and_call_original
communicator.ready?
end
end
context "with key_type set as :auto in configuration" do
let(:valid_key_types) { ["ssh-ed25519", "ssh-rsa"] }
before { allow(ssh).to receive(:key_type).and_return(:auto) }
it "should use the preferred ed25519 key type" do
expect(Vagrant::Util::Keypair).to receive(:create).
with(type: :ed25519).and_call_original
communicator.ready?
end
context "when no supported key type is detected" do
let(:valid_key_types) { ["fake-type", "other-fake-type"] }
it "should raise an error" do
expect { communicator.ready? }.to raise_error(Vagrant::Errors::SSHKeyTypeNotSupportedByServer)
end
end
end
context "with key_type set as :ecdsa521 in configuration" do
let(:valid_key_types) { ["ssh-ed25519", "ssh-rsa", "ecdsa-sha2-nistp521", "ecdsa-sha2-nistp256"] }
before { allow(ssh).to receive(:key_type).and_return(:ecdsa521) }
it "should use the requested key type" do
expect(Vagrant::Util::Keypair).to receive(:create).
with(type: :ecdsa521).and_call_original
communicator.ready?
end
context "when requested key type is not supported" do
let(:valid_key_types) { ["ssh-ed25519", "ssh-rsa", "ecdsa-sha2-nistp256"] }
it "should raise an error" do
expect { communicator.ready? }.to raise_error(Vagrant::Errors::SSHKeyTypeNotSupportedByServer)
end
end end
end end
end end
context "when an error is encountered getting server data" do context "when an error is encountered getting server data" do
before do before do
expect(communicator).to receive(:supported_key_types).and_call_original
expect(connection).to receive(:transport).and_raise(StandardError) expect(connection).to receive(:transport).and_raise(StandardError)
end end
it "should default to rsa key" do it "should default to rsa key" do
expect(Vagrant::Util::Keypair).to receive(:create). expect(Vagrant::Util::Keypair).to receive(:create).
with(type: :rsa).and_call_original with(type: :rsa).and_call_original
communicator.ready?
end end
end end
end end