sudoers command now creates a safe wrapper script.

Sudoers now creates a safe wrapper script that performs sanity checks on sudo :
* wrapper generated in /usr/local/bin (name includes version to allow multiple wrappers on the same system)
* sudoers command now generates a one-line file in /etc/sudoers.d
* SudoWrapper use the new wrapper
* Removed unused Config#validate method
This commit is contained in:
Jef Mathiot 2014-04-08 19:58:07 +02:00 committed by Eric Hartmann
parent 47cf361b98
commit 94e175dc07
5 changed files with 169 additions and 78 deletions

View file

@ -6,5 +6,9 @@ module Vagrant
def self.source_root def self.source_root
@source_root ||= Pathname.new(File.dirname(__FILE__)).join('..').expand_path @source_root ||= Pathname.new(File.dirname(__FILE__)).join('..').expand_path
end end
def self.sudo_wrapper_path
"/usr/local/bin/vagrant-lxc-wrapper-#{VERSION}"
end
end end
end end

View file

@ -4,6 +4,12 @@ module Vagrant
module LXC module LXC
module Command module Command
class Sudoers < Vagrant.plugin("2", :command) class Sudoers < Vagrant.plugin("2", :command)
def initialize(argv, env)
super
@env = env
end
def execute def execute
options = { user: ENV['USER'] } options = { user: ENV['USER'] }
@ -18,68 +24,168 @@ module Vagrant
argv = parse_options(opts) argv = parse_options(opts)
return unless argv return unless argv
filename = "vagrant-lxc-#{options[:user]}" wrapper_path = Vagrant::LXC.sudo_wrapper_path
to_sudoers!(create_tempfile!(options[:user], filename), filename) wrapper = create_wrapper!
sudoers = create_sudoers!(options[:user], wrapper_path)
su_copy([
{source: wrapper, target: wrapper_path, mode: "0555"},
{source: sudoers, target: sudoers_path, mode: "0440"}
])
end
def sudoers_path
"/etc/sudoers.d/vagrant-lxc-#{Vagrant::LXC::VERSION.gsub( /\./, '-')}"
end end
private private
# REFACTOR: Make use ERB rendering after https://github.com/mitchellh/vagrant/issues/3231
# lands into core
def create_wrapper!
wrapper = Tempfile.new('lxc-wrapper').tap do |file|
file.puts "#!/usr/bin/env ruby"
file.puts "# Automatically created by vagrant-lxc"
file.puts <<-EOF
class Whitelist
class << self
def add(command, *args)
list[command] << args
end
def list
@list ||= Hash.new do |key, hsh|
key[hsh] = []
end
end
def allowed(command)
list[command] || []
end
def run!(argv)
begin
command, args = `which \#{argv.shift}`.chomp, argv || []
check!(command, args)
puts `\#{command} \#{args.join(" ")}`
exit $?.to_i
rescue => e
STDERR.puts e.message
exit 1
end
end
private
def check!(command, args)
allowed(command).each do |checks|
return if valid_args?(args, checks)
end
raise_invalid(command, args)
end
def valid_args?(args, checks)
return false unless valid_length?(args, checks)
check = nil
args.each_with_index do |provided, i|
check = checks[i] unless check == '**'
return false unless match?(provided, check)
end
true
end
def valid_length?(args, checks)
args.length == checks.length || checks.last == '**'
end
def match?(arg, check)
check == '**' || check.is_a?(Regexp) && !!check.match(arg) || arg == check
end
def raise_invalid(command, args)
raise "Invalid arguments for command \#{command}, " <<
"provided args: \#{args.inspect}"
end
end
end
base = "/var/lib/lxc"
base_path = %r{\\A\#{base}/.*\\z}
templates_path = %r{\\A/usr/(share|lib|lib64|local/lib)/lxc/templates/.*\\z}
boxes_path = %r{\\A#{Regexp.escape(@env.boxes_path.to_s)}/.*\\z}
gems_path = %r{\\A#{Regexp.escape(@env.gems_path.to_s)}/.*\\z}
template_src = %r{\\A#{Vagrant::LXC.source_root.join('scripts/lxc-template').to_s}\\z}
##
# Commands from provider.rb
# - Check lxc is installed
Whitelist.add '/usr/bin/which', /\\Alxc-\\w+\\z/
##
# Commands from driver.rb
# - Container config file
Whitelist.add '/bin/cat', base_path
# - Shared folders
Whitelist.add '/bin/mkdir', '-p', base_path
# - Container config customizations and pruning
Whitelist.add '/bin/su', 'root', '-c', %r{\\A"sed -e '.*' -ibak \#{base}/.*/config"\\z}
Whitelist.add '/bin/su', 'root', '-c', %r{\\A"echo '.*' >> \#{base}/.*/config"\\z}
# - Template import
Whitelist.add '/bin/cp', boxes_path, templates_path
Whitelist.add '/bin/cp', gems_path, templates_path
Whitelist.add '/bin/cp', template_src, templates_path
Whitelist.add '/bin/chmod', '+x', templates_path
# - Template removal
Whitelist.add '/bin/rm', templates_path
# - Packaging
Whitelist.add '/bin/su', 'root', '-c', %r{\\A"cd \#{base}/.* && rm -f rootfs\.tar\.gz && tar --numeric-owner -czf /tmp/.*/rootfs\.tar\.gz -C \#{base}/.*/rootfs '\./\.'"\\z}
Whitelist.add '/bin/chown', /\\A\\d+:\\d+\\z/, %r{\\A/tmp/.*/rootfs\.tar\.gz\\z}
##
# Commands from driver/cli.rb
Whitelist.add '/usr/bin/lxc-version'
Whitelist.add '/usr/bin/lxc-ls'
Whitelist.add '/usr/bin/lxc-info', '--name', /.*/
Whitelist.add '/usr/bin/lxc-create', '--template', /.*/, '--name', /.*/, '**'
Whitelist.add '/usr/bin/lxc-destroy', '--name', /.*/
Whitelist.add '/usr/bin/lxc-start', '-d', '--name', /.*/, '**'
Whitelist.add '/usr/bin/lxc-stop', '--name', /.*/
Whitelist.add '/usr/bin/lxc-shutdown', '--name', /.*/
Whitelist.add '/usr/bin/lxc-attach', '--name', /.*/, '**'
Whitelist.add '/usr/bin/lxc-attach', '-h'
##
# Commands from driver/action/remove_temporary_files.rb
Whitelist.add '/bin/rm', '-rf', %r{\\A\#{base}/.*/rootfs/tmp/.*}
# Watch out for stones
Whitelist.run!(ARGV)
EOF
end
wrapper.close
wrapper.path
end
# REFACTOR: Make use ERB rendering after https://github.com/mitchellh/vagrant/issues/3231 # REFACTOR: Make use ERB rendering after https://github.com/mitchellh/vagrant/issues/3231
# lands into core # lands into core
def create_tempfile!(user, filename) def create_sudoers!(user, command)
sudoers = Tempfile.new(filename).tap do |file| sudoers = Tempfile.new('vagrant-lxc-sudoers').tap do |file|
file.write "# Automatically created by vagrant-lxc\n" file.puts "# Automatically created by vagrant-lxc"
commands.each do |command| file.puts "Cmnd_Alias LXC = #{command}"
file.write sudoers_policy(user, command[:cmd], command[:args]) file.puts "#{user} ALL=(root) NOPASSWD: LXC"
end
end end
sudoers.close sudoers.close
File.chmod(0644, sudoers.path)
sudoers.path sudoers.path
end end
def to_sudoers!(source, destination) def su_copy(files)
destination = "/etc/sudoers.d/#{destination}" commands = files.map { |file|
commands = [ [
"rm -f #{destination}", "rm -f #{file[:target]}",
"cp #{source} #{destination}", "cp #{file[:source]} #{file[:target]}",
"chmod 440 #{destination}" "chown root:root #{file[:target]}",
] "chmod #{file[:mode]} #{file[:target]}"
`echo "#{commands.join('; ')}" | sudo sh` ]
end }.flatten
system "echo \"#{commands.join("; ")}\" | sudo sh"
def sudoers_policy(user, command, args)
vagrant_home = "#{`echo ~#{user}`.chomp}/.vagrant.d"
args = args.gsub /%\{VAGRANT_D\}/, vagrant_home
args = args.gsub /%\{BOXES\}/, "#{vagrant_home}/boxes"
"#{user} ALL=(root) NOPASSWD: #{command} #{args}\n"
end
def commands
[
{ cmd: '/usr/bin/lxc-ls', args: '' },
{ cmd: '/usr/bin/lxc-info', args: '' },
{ cmd: '/usr/bin/lxc-attach', args: '' },
{ cmd: '/usr/bin/which', args: 'lxc-*' },
{ cmd: '/bin/cat', args: '/var/lib/lxc/*' },
{ cmd: '/bin/mkdir', args: '-p /var/lib/lxc/*' },
{ cmd: '/bin/su', args: "root -c sed -e '*' -ibak /var/lib/lxc/*" },
{ cmd: '/bin/su', args: "root -c echo '*' >> /var/lib/lxc/*" },
{ cmd: '/usr/bin/lxc-start', args: '-d --name *' },
{ cmd: '/bin/cp', args: '%{BOXES}/*/lxc/lxc-template /usr/lib/lxc/templates/*' },
{ cmd: '/bin/cp', args: '%{VAGRANT_D}/gems/gems/vagrant-lxc-*/scripts/lxc-template /usr/lib/lxc/templates/*' },
{ cmd: '/bin/cp', args: '%{BOXES}/*/lxc/lxc-template /usr/share/lxc/templates/*' },
{ cmd: '/bin/cp', args: '%{VAGRANT_D}/gems/gems/vagrant-lxc-*/scripts/lxc-template /usr/share/lxc/templates/*' },
{ cmd: '/bin/rm', args: '/usr/lib/lxc/templates/*' },
{ cmd: '/bin/rm', args: '/usr/share/lxc/templates/*' },
{ cmd: '/bin/chmod', args: '+x /usr/lib/lxc/*' },
{ cmd: '/bin/chmod', args: '+x /usr/share/lxc/*' },
{ cmd: '/usr/bin/lxc-create', args: '--template * --name * -- --tarball %{BOXES}/*' },
{ cmd: '/bin/rm', args: '-rf /var/lib/lxc/*/rootfs/tmp/*' },
{ cmd: '/usr/bin/lxc-shutdown', args: '--name *' },
{ cmd: '/usr/bin/lxc-stop', args: '--name *' },
{ cmd: '/usr/bin/lxc-destroy', args: '--name *' }
]
end end
end end
end end

View file

@ -6,12 +6,6 @@ module Vagrant
# @return [Array] # @return [Array]
attr_reader :customizations attr_reader :customizations
# A String that points to a file that acts as a wrapper for sudo commands.
#
# This allows us to have a single entry when whitelisting NOPASSWD commands
# on /etc/sudoers
attr_accessor :sudo_wrapper
# A string to explicitly set the container name. To use the vagrant # A string to explicitly set the container name. To use the vagrant
# machine name, set this to :machine # machine name, set this to :machine
attr_accessor :container_name attr_accessor :container_name
@ -41,21 +35,6 @@ module Vagrant
@sudo_wrapper = nil if @sudo_wrapper == UNSET_VALUE @sudo_wrapper = nil if @sudo_wrapper == UNSET_VALUE
@container_name = nil if @container_name == UNSET_VALUE @container_name = nil if @container_name == UNSET_VALUE
end end
def validate(machine)
errors = []
if @sudo_wrapper
hostpath = Pathname.new(@sudo_wrapper).expand_path(machine.env.root_path)
if ! hostpath.file?
errors << I18n.t('vagrant_lxc.sudo_wrapper_not_found', path: hostpath.to_s)
elsif ! hostpath.executable?
errors << I18n.t('vagrant_lxc.sudo_wrapper_not_executable', path: hostpath.to_s)
end
end
{ "lxc provider" => errors }
end
end end
end end
end end

View file

@ -19,8 +19,9 @@ module Vagrant
def sudo_wrapper def sudo_wrapper
@shell ||= begin @shell ||= begin
wrapper = @machine.provider_config.sudo_wrapper wrapper = Pathname.new(LXC.sudo_wrapper_path).exist? &&
wrapper = Pathname(wrapper).expand_path(@machine.env.root_path).to_s if wrapper LXC.sudo_wrapper_path || nil
@logger.debug("Found sudo wrapper : #{wrapper}") if wrapper
SudoWrapper.new(wrapper) SudoWrapper.new(wrapper)
end end
end end

View file

@ -10,13 +10,14 @@ module Vagrant
end end
def run(*command) def run(*command)
command.unshift @wrapper_path if @wrapper_path options = command.last.is_a?(Hash) ? command.last : {}
command.unshift @wrapper_path if @wrapper_path && !options[:no_wrapper]
execute *(['sudo'] + command) execute *(['sudo'] + command)
end end
def su_c(command) def su_c(command, options={})
su_command = if @wrapper_path su_command = if @wrapper_path && !options[:no_wrapper]
"#{@wrapper_path} \"#{command}\"" "#{@wrapper_path} su root -c \"\\\"#{command}\\\"\""
else else
"su root -c \"#{command}\"" "su root -c \"#{command}\""
end end