Merge pull request #4707 from mitchellh/f-dynamic-rekey
Generate Random SSH key on `vagrant up`
This commit is contained in:
commit
6a5e56f82e
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<String, String, String>] 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue