diff --git a/lib/vagrant/action/general/package.rb b/lib/vagrant/action/general/package.rb index ee7e30ec3..99c0002ec 100644 --- a/lib/vagrant/action/general/package.rb +++ b/lib/vagrant/action/general/package.rb @@ -1,4 +1,5 @@ require 'fileutils' +require "pathname" require 'vagrant/util/safe_chdir' require 'vagrant/util/subprocess' @@ -36,6 +37,9 @@ module Vagrant @app.call(env) + @env[:ui].info I18n.t("vagrant.actions.general.package.compressing", tar_path: tar_path) + copy_include_files + setup_private_key compress end @@ -81,11 +85,6 @@ module Vagrant # Compress the exported file into a package def compress - @env[:ui].info I18n.t("vagrant.actions.general.package.compressing", tar_path: tar_path) - - # Copy over the included files - copy_include_files - # Get the output path. We have to do this up here so that the # pwd returns the proper thing. output_path = tar_path.to_s @@ -100,6 +99,39 @@ module Vagrant end end + # This will copy the generated private key into the box and use + # it for SSH by default. We have to do this because we now generate + # random keypairs on boot, so packaged boxes would stop working + # without this. + def setup_private_key + # If we don't have machine, we do nothing (weird) + return if !@env[:machine] + + # If we don't have a data dir, we also do nothing (base package) + return if !@env[:machine].data_dir + + # If we don't have a generated private key, we do nothing + path = @env[:machine].data_dir.join("private_key") + return if !path.file? + + # Copy it into our box directory + dir = Pathname.new(@env["package.directory"]) + new_path = dir.join("vagrant_private_key") + FileUtils.cp(path, new_path) + + # Append it to the Vagrantfile (or create a Vagrantfile) + vf_path = dir.join("Vagrantfile") + mode = "w+" + mode = "a" if vf_path.file? + vf_path.open(mode) do |f| + f.binmode + f.puts + f.puts %Q[Vagrant.configure("2") do |config|] + f.puts %Q[ config.ssh.private_key_path = File.expand_path("../vagrant_private_key", __FILE__)] + f.puts %Q[end] + end + end + # Path to the final box output file def tar_path File.expand_path(@env["package.output"], FileUtils.pwd) diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index b1ec6f35c..623073a97 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -620,6 +620,10 @@ module Vagrant error_key(:ssh_invalid_shell) end + class SSHInsertKeyUnsupported < VagrantError + error_key(:ssh_insert_key_unsupported) + end + class SSHIsPuttyLink < VagrantError error_key(:ssh_is_putty_link) end diff --git a/lib/vagrant/util/keypair.rb b/lib/vagrant/util/keypair.rb new file mode 100644 index 000000000..7fec84b2a --- /dev/null +++ b/lib/vagrant/util/keypair.rb @@ -0,0 +1,46 @@ +require "base64" +require "openssl" + +module Vagrant + module Util + class Keypair + # Creates an SSH keypair and returns it. + # + # @param [String] password Password for the key, or nil for no password. + # @return [Array] PEM-encoded public and private key, + # respectively. The final element is the OpenSSH encoded public + # key. + def self.create(password=nil) + rsa_key = OpenSSL::PKey::RSA.new(2048) + public_key = rsa_key.public_key + private_key = rsa_key.to_pem + + if password + cipher = OpenSSL::Cipher::Cipher.new('des3') + private_key = rsa_key.to_pem(cipher, password) + end + + # Generate the binary necessary for the OpenSSH public key. + binary = [7].pack("N") + binary += "ssh-rsa" + ["e", "n"].each do |m| + val = public_key.send(m) + data = val.to_s(2) + + first_byte = data[0,1].unpack("c").first + if val < 0 + data[0] = [0x80 & first_byte].pack("c") + elsif first_byte < 0 + data = 0.chr + data + end + + binary += [data.length].pack("N") + data + end + + openssh_key = "ssh-rsa #{Base64.encode64(binary).gsub("\n", "")} vagrant" + public_key = public_key.to_pem + return [public_key, private_key, openssh_key] + end + end + end +end diff --git a/plugins/communicators/ssh/communicator.rb b/plugins/communicators/ssh/communicator.rb index cb9452d1c..35506e31e 100644 --- a/plugins/communicators/ssh/communicator.rb +++ b/plugins/communicators/ssh/communicator.rb @@ -11,6 +11,7 @@ require 'net/scp' require 'vagrant/util/ansi_escape_code_remover' require 'vagrant/util/file_mode' +require 'vagrant/util/keypair' require 'vagrant/util/platform' require 'vagrant/util/retryable' @@ -88,6 +89,8 @@ module VagrantPlugins raise rescue Vagrant::Errors::SSHKeyBadPermissions raise + rescue Vagrant::Errors::SSHInsertKeyUnsupported + raise rescue Vagrant::Errors::VagrantError => e # Ignore it, SSH is not ready, some other error. end @@ -142,20 +145,41 @@ module VagrantPlugins # If we used a password, then insert the insecure key ssh_info = @machine.ssh_info - if ssh_info[:password] && ssh_info[:private_key_path].empty? - @logger.info("Inserting insecure key to avoid password") - @machine.ui.info(I18n.t("vagrant.inserting_insecure_key")) - @machine.guest.capability( - :insert_public_key, - Vagrant.source_root.join("keys", "vagrant.pub").read.chomp) + insert = ssh_info[:password] && ssh_info[:private_key_path].empty? + ssh_info[:private_key_path].each do |pk| + if insecure_key?(pk) + insert = true + @machine.ui.detail("\n"+I18n.t("vagrant.inserting_insecure_detected")) + break + end + end + + if insert + # If we don't have the power to insert/remove keys, then its an error + cap = @machine.guest.capability?(:insert_public_key) && + @machine.guest.capability?(:remove_public_key) + raise Vagrant::Errors::SSHInsertKeyUnsupported if !cap + + _pub, priv, openssh = Vagrant::Util::Keypair.create + + @logger.info("Inserting key to avoid password: #{openssh}") + @machine.ui.detail("\n"+I18n.t("vagrant.inserting_random_key")) + @machine.guest.capability(:insert_public_key, openssh) # Write out the private key in the data dir so that the # machine automatically picks it up. @machine.data_dir.join("private_key").open("w+") do |f| - f.write(Vagrant.source_root.join("keys", "vagrant").read) + f.write(priv) end - @machine.ui.info(I18n.t("vagrant.inserted_key")) + # Remove the old key if it exists + @machine.ui.detail(I18n.t("vagrant.inserting_remove_key")) + @machine.guest.capability( + :remove_public_key, + Vagrant.source_root.join("keys", "vagrant.pub").read.chomp) + + # Done, restart. + @machine.ui.detail(I18n.t("vagrant.inserted_key")) @connection.close @connection = nil @@ -597,6 +621,16 @@ module VagrantPlugins # Otherwise, just raise the error up raise end + + # This will test whether path is the Vagrant insecure private key. + # + # @param [String] path + def insecure_key?(path) + return false if !path + return false if !File.file?(path) + source_path = Vagrant.source_root.join("keys", "vagrant") + return File.read(path).chomp == source_path.read.chomp + end end end end diff --git a/plugins/guests/freebsd/cap/remove_public_key.rb b/plugins/guests/freebsd/cap/remove_public_key.rb new file mode 100644 index 000000000..13abc864b --- /dev/null +++ b/plugins/guests/freebsd/cap/remove_public_key.rb @@ -0,0 +1,21 @@ +require "vagrant/util/shell_quote" + +module VagrantPlugins + module GuestFreeBSD + module Cap + class RemovePublicKey + def self.remove_public_key(machine, contents) + contents = contents.chomp + contents = Vagrant::Util::ShellQuote.escape(contents, "'") + + machine.communicate.tap do |comm| + if comm.test("test -f ~/.ssh/authorized_keys") + comm.execute( + "sed -i '/^.*#{contents}.*$/d' ~/.ssh/authorized_keys") + end + end + end + end + end + end +end diff --git a/plugins/guests/freebsd/plugin.rb b/plugins/guests/freebsd/plugin.rb index 49aa3298b..a4ece8de2 100644 --- a/plugins/guests/freebsd/plugin.rb +++ b/plugins/guests/freebsd/plugin.rb @@ -36,6 +36,11 @@ module VagrantPlugins Cap::MountNFSFolder end + guest_capability("freebsd", "remove_public_key") do + require_relative "cap/remove_public_key" + Cap::RemovePublicKey + end + guest_capability("freebsd", "rsync_install") do require_relative "cap/rsync" Cap::RSync diff --git a/plugins/guests/linux/cap/remove_public_key.rb b/plugins/guests/linux/cap/remove_public_key.rb new file mode 100644 index 000000000..a8d773a40 --- /dev/null +++ b/plugins/guests/linux/cap/remove_public_key.rb @@ -0,0 +1,21 @@ +require "vagrant/util/shell_quote" + +module VagrantPlugins + module GuestLinux + module Cap + class RemovePublicKey + def self.remove_public_key(machine, contents) + contents = contents.chomp + contents = Vagrant::Util::ShellQuote.escape(contents, "'") + + machine.communicate.tap do |comm| + if comm.test("test -f ~/.ssh/authorized_keys") + comm.execute( + "sed -i '/^.*#{contents}.*$/d' ~/.ssh/authorized_keys") + end + end + end + end + end + end +end diff --git a/plugins/guests/linux/plugin.rb b/plugins/guests/linux/plugin.rb index a2bb1167c..cadd4aa95 100644 --- a/plugins/guests/linux/plugin.rb +++ b/plugins/guests/linux/plugin.rb @@ -62,6 +62,11 @@ module VagrantPlugins Cap::ReadIPAddress end + guest_capability("linux", "remove_public_key") do + require_relative "cap/remove_public_key" + Cap::RemovePublicKey + end + guest_capability("linux", "rsync_installed") do require_relative "cap/rsync" Cap::RSync diff --git a/plugins/guests/netbsd/cap/remove_public_key.rb b/plugins/guests/netbsd/cap/remove_public_key.rb new file mode 100644 index 000000000..d25e97679 --- /dev/null +++ b/plugins/guests/netbsd/cap/remove_public_key.rb @@ -0,0 +1,21 @@ +require "vagrant/util/shell_quote" + +module VagrantPlugins + module GuestNetBSD + module Cap + class RemovePublicKey + def self.remove_public_key(machine, contents) + contents = contents.chomp + contents = Vagrant::Util::ShellQuote.escape(contents, "'") + + machine.communicate.tap do |comm| + if comm.test("test -f ~/.ssh/authorized_keys") + comm.execute( + "sed -i '/^.*#{contents}.*$/d' ~/.ssh/authorized_keys") + end + end + end + end + end + end +end diff --git a/plugins/guests/netbsd/plugin.rb b/plugins/guests/netbsd/plugin.rb index 490c73644..9636c6630 100644 --- a/plugins/guests/netbsd/plugin.rb +++ b/plugins/guests/netbsd/plugin.rb @@ -36,6 +36,11 @@ module VagrantPlugins Cap::MountNFSFolder end + guest_capability("netbsd", "remove_public_key") do + require_relative "cap/remove_public_key" + Cap::RemovePublicKey + end + guest_capability("netbsd", "rsync_install") do require_relative "cap/rsync" Cap::RSync diff --git a/plugins/guests/openbsd/cap/remove_public_key.rb b/plugins/guests/openbsd/cap/remove_public_key.rb new file mode 100644 index 000000000..94cfce03b --- /dev/null +++ b/plugins/guests/openbsd/cap/remove_public_key.rb @@ -0,0 +1,21 @@ +require "vagrant/util/shell_quote" + +module VagrantPlugins + module GuestOpenBSD + module Cap + class RemovePublicKey + def self.remove_public_key(machine, contents) + contents = contents.chomp + contents = Vagrant::Util::ShellQuote.escape(contents, "'") + + machine.communicate.tap do |comm| + if comm.test("test -f ~/.ssh/authorized_keys") + comm.execute( + "sed -i '/^.*#{contents}.*$/d' ~/.ssh/authorized_keys") + end + end + end + end + end + end +end diff --git a/plugins/guests/openbsd/plugin.rb b/plugins/guests/openbsd/plugin.rb index 7a64b16d6..a1ddfe9c1 100644 --- a/plugins/guests/openbsd/plugin.rb +++ b/plugins/guests/openbsd/plugin.rb @@ -36,6 +36,11 @@ module VagrantPlugins Cap::MountNFSFolder end + guest_capability("openbsd", "remove_public_key") do + require_relative "cap/remove_public_key" + Cap::RemovePublicKey + end + guest_capability("openbsd", "rsync_install") do require_relative "cap/rsync" Cap::RSync diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 9fc465aac..333091fbb 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -130,8 +130,13 @@ en: Starting Docker containers... inserted_key: |- Key inserted! Disconnecting and reconnecting using new SSH key... - inserting_insecure_key: |- - Inserting Vagrant public key within guest... + inserting_insecure_detected: |- + Vagrant insecure key detected. Vagrant will automatically replace + this with a newly generated keypair for better security. + inserting_random_key: |- + Inserting generated public key within guest... + inserting_remove_key: |- + Removing insecure key from the guest if its present... list_commands: |- Below is a listing of all available Vagrant commands and a brief description of what they do. @@ -1071,6 +1076,16 @@ en: using a shell that is unavailable on the system. Please verify you're using the full path to the shell and that the shell is executable by the SSH user. + ssh_insert_key_unsupported: |- + Vagrant is configured to generate a random keypair and insert it + onto the guest machine, but it appears Vagrant doesn't know how to do + this with your guest OS. Please disable key insertion by setting + `config.ssh.insert_key = false` in the Vagrantfile. + + After doing so, run `vagrant reload` for the setting to take effect. + + If you'd like Vagrant to learn how to insert keys on this OS, please + open an issue with details about your environment. ssh_is_putty_link: |- The `ssh` executable found in the PATH is a PuTTY Link SSH client. Vagrant is only compatible with OpenSSH SSH clients. Please install diff --git a/test/unit/vagrant/util/keypair_test.rb b/test/unit/vagrant/util/keypair_test.rb new file mode 100644 index 000000000..30ab76eba --- /dev/null +++ b/test/unit/vagrant/util/keypair_test.rb @@ -0,0 +1,34 @@ +require "openssl" + +require File.expand_path("../../../base", __FILE__) + +require "vagrant/util/keypair" + +describe Vagrant::Util::Keypair do + describe ".create" do + it "generates a usable keypair with no password" do + # I don't know how to validate the final return value yet... + pubkey, privkey, _ = described_class.create + + pubkey = OpenSSL::PKey::RSA.new(pubkey) + privkey = OpenSSL::PKey::RSA.new(privkey) + + encrypted = pubkey.public_encrypt("foo") + decrypted = privkey.private_decrypt(encrypted) + + expect(decrypted).to eq("foo") + end + + it "generates a keypair that requires a password" do + pubkey, privkey, _ = described_class.create("password") + + pubkey = OpenSSL::PKey::RSA.new(pubkey) + privkey = OpenSSL::PKey::RSA.new(privkey, "password") + + encrypted = pubkey.public_encrypt("foo") + decrypted = privkey.private_decrypt(encrypted) + + expect(decrypted).to eq("foo") + end + end +end diff --git a/website/docs/source/v2/boxes/base.html.md b/website/docs/source/v2/boxes/base.html.md index 7e34bf1b5..e771187a0 100644 --- a/website/docs/source/v2/boxes/base.html.md +++ b/website/docs/source/v2/boxes/base.html.md @@ -116,6 +116,10 @@ that OpenSSH is very picky about file permissions. Therefore, make sure that `~/.ssh` has `0700` permissions and the authorized keys file has `0600` permissions. +When Vagrant boots a box and detects the insecure keypair, it will +automatically replace it with a randomly generated keypair for additional +security while the box is running. + ### Root Password: "vagrant" Vagrant doesn't actually use or expect any root password. However, having