The absolute filename in which to store the SSH key. This
property is optional and should be used only in cases where keys
are stored in a non-standard location, for instance when not in
-`~user/.ssh/authorized_keys`.
+`~user/.ssh/authorized_keys`. The parent directory must be present
+if the target is in a privileged path.
Default value: absent
if you want to specify one key for multiple users, you must use a different
comment for each instance.
+##### `drop_privileges`
+
+Whether to drop privileges when writing the key file. This is
+useful for creating files in paths not writable by the target user. Note
+the possible security implications of managing file ownership and
+permissions as a privileged user.
+
+Default value: `true`
### sshkey
0o600
end
- def user
- uid = Puppet::FileSystem.stat(target).uid
- Etc.getpwuid(uid).name
+ def group_writable_perm
+ 0o020
+ end
+
+ def group_writable?(path)
+ path.stat.mode & group_writable_perm != 0
+ end
+
+ def trusted_path
+ # return if the parent directory does not exist
+ return false unless Puppet::FileSystem.dir_exist?(target)
+ path = Puppet::FileSystem.pathname(target).dirname
+ until path.dirname.root?
+ path = path.realpath if path.symlink?
+ # do not trust if path is world or group writable
+ if path.stat.uid != Process.euid || path.world_writable? || group_writable?(path)
+ Puppet.debug('Path untrusted, will attempt to write as the target user')
+ return false
+ end
+ path = path.dirname
+ end
+ Puppet.debug('Path trusted, writing the file as the current user')
end
def flush
# so calling it here suppresses the later attempt by our superclass's flush method.
self.class.backup_target(target)
- Puppet::Util::SUIDManager.asuser(@resource.should(:user)) do
- unless Puppet::FileSystem.exist?(dir = File.dirname(target))
- Puppet.debug "Creating #{dir} as #{@resource.should(:user)}"
- Dir.mkdir(dir, dir_perm)
- end
+ # attempt to create the file as the specified user if we're not dropping privileges
+ if @resource[:drop_privileges]
+ Puppet::Util::SUIDManager.asuser(@resource.should(:user)) do
+ unless Puppet::FileSystem.exist?(dir = File.dirname(target))
+ Puppet.debug "Creating #{dir} as #{@resource.should(:user)}"
+ Dir.mkdir(dir, dir_perm)
+ end
+ super
+ File.chmod(file_perm, target)
+ end
+ # to avoid race conditions when handling permissions as a privileged user
+ # (CVE-2011-3870) we use the trusted_path method to ensure the entire
+ # directory structure is "safe" to write in
+ else
+ raise Puppet::Error, 'drop_privileges is false but the target path is not trusted' unless trusted_path
super
- File.chmod(file_perm, target)
+ uid = Puppet::Util.uid(@resource.should(:user))
+ gid = Puppet::Util.gid(@resource.should(:user))
+ File.open(target) do |target|
+ target.chown(uid, gid)
+ target.chmod(file_perm)
+ end
end
end
+require 'puppet/parameter/boolean'
+
module Puppet
Type.newtype(:ssh_authorized_key) do
@doc = "Manages SSH authorized keys. Currently only type 2 keys are supported.
isnamevar
end
+ newparam(:drop_privileges, boolean: true, parent: Puppet::Parameter::Boolean) do
+ desc "Whether to drop privileges when writing the key file. This is
+ useful for creating files in paths not writable by the target user. Note
+ the possible security implications of managing file ownership and
+ permissions as a privileged user."
+
+ defaultto true
+ end
+
newproperty(:type) do
desc 'The encryption type used.'
desc "The absolute filename in which to store the SSH key. This
property is optional and should be used only in cases where keys
are stored in a non-standard location, for instance when not in
- `~user/.ssh/authorized_keys`."
+ `~user/.ssh/authorized_keys`. The parent directory must be present
+ if the target is in a privileged path."
defaultto :absent
let(:name) { "pl#{rand(999_999).to_i}" }
let(:custom_key_directory) { "/etc/ssh_authorized_keys_#{name}" }
let(:custom_key) { "#{custom_key_directory}/authorized_keys_#{name}" }
- let(:custom_name) { "custom_#{name}" }
before(:each) do
posix_agents.each do |agent|
- on(agent, "cp #{auth_keys} /tmp/auth_keys", acceptable_exit_codes: [0, 1])
+ on(agent, "cp -a #{auth_keys} /tmp/auth_keys", acceptable_exit_codes: [0, 1])
on(agent, "rm -f #{auth_keys}")
- on(agent, "mkdir #{custom_key_directory}")
end
end
posix_agents.each do |agent|
# (teardown) restore the #{auth_keys} file
on(agent, "mv /tmp/auth_keys #{auth_keys}", acceptable_exit_codes: [0, 1])
- on(agent, "rm -rf #{custom_key_directory}")
end
end
fail_test "didn't find the ssh_authorized_key for #{name}" unless stdout.include? name.to_s
end
end
- it "#{agent} should create an entry for an SSH authorized key in a custom location" do
- custom_args = ['ensure=present',
- 'user=$LOGNAME',
- "type='rsa'",
- "key='mykey'",
- "target='#{custom_key}'"]
- on(agent, puppet_resource('ssh_authorized_key', custom_name.to_s, custom_args))
+ it "#{agent} should create an entry for an SSH authorized key in a custom location" do
+ on(agent, "mkdir #{custom_key_directory}")
+ args = ['ensure=present',
+ 'user=$LOGNAME',
+ "type='rsa'",
+ "key='mykey'",
+ "target='#{custom_key}'"]
+ on(agent, puppet_resource('ssh_authorized_key', name.to_s, args))
on(agent, "cat #{custom_key}") do |_res|
- fail_test "didn't find the ssh_authorized_key for #{custom_name}" unless stdout.include? name.to_s
+ fail_test "didn't find the ssh_authorized_key for #{name}" unless stdout.include? name.to_s
+ end
+ on(agent, "rm -rf #{custom_key_directory}")
+ end
+
+ it "#{agent} should fail if target user doesn't have permissions for symlinked path" do
+ # create a dummy user
+ on(agent, puppet_resource('user', 'testuser', 'ensure=present', 'managehome=true'))
+
+ on(agent, "mkdir #{custom_key_directory}")
+
+ # as the user, symlink an owned directory to something inside /root
+ on(agent, puppet_resource('file', '/home/testuser/tmp', ['ensure=/etc', 'owner=testuser']))
+ args = ['ensure=present',
+ 'user=testuser',
+ "type='rsa'",
+ "key='mykey'",
+ 'drop_privileges=false',
+ "target=/home/testuser/tmp/ssh_authorized_keys_#{name}/authorized_keys_#{name}"]
+ on(agent, puppet_resource('ssh_authorized_key', name.to_s, args)) do |_res|
+ fail_test unless stderr =~ %r{the target path is not trusted}
+ end
+ on(agent, "rm -rf #{custom_key_directory}")
+
+ # purge the user
+ on(agent, puppet_resource('user', 'testuser', 'ensure=absent'))
+ end
+
+ it "#{agent} should not create directories for SSH authorized key in a custom location" do
+ args = ['ensure=present',
+ 'user=$LOGNAME',
+ "type='rsa'",
+ "key='mykey'",
+ 'drop_privileges=false',
+ "target='#{custom_key}'"]
+ on(agent, puppet_resource('ssh_authorized_key', name.to_s, args), acceptable_exit_codes: [0, 1]) do |_res|
+ fail_test unless stderr =~ %r{the target path is not trusted}
end
end
end
let(:auth_keys) { '~/.ssh/authorized_keys' }
let(:name) { "pl#{rand(999_999).to_i}" }
+ let(:custom_key_directory) { "/etc/ssh_authorized_keys_#{name}" }
+ let(:custom_key) { "#{custom_key_directory}/authorized_keys_#{name}" }
before(:each) do
posix_agents.each do |agent|
- on(agent, "cp #{auth_keys} /tmp/auth_keys", acceptable_exit_codes: [0, 1])
-
+ on(agent, "cp -a #{auth_keys} /tmp/auth_keys", acceptable_exit_codes: [0, 1])
+ on(agent, "rm -f #{auth_keys}")
on(agent, "echo '' >> #{auth_keys} && echo 'ssh-rsa mykey #{name}' >> #{auth_keys}")
- on(agent, "chown $LOGNAME #{auth_keys}")
end
end
expect(stdout).not_to include(name.to_s)
end
end
+
+ it "#{agent} should delete an entry for an SSH authorized key in a custom location" do
+ on(agent, "mkdir #{custom_key_directory}")
+ on(agent, "echo '' >> #{custom_key} && echo 'ssh-rsa mykey #{name}' >> #{custom_key}")
+ args = ['ensure=absent',
+ 'user=$LOGNAME',
+ "type='rsa'",
+ "key='mykey'",
+ "target='#{custom_key}'"]
+ on(agent, puppet_resource('ssh_authorized_key', name.to_s, args))
+
+ on(agent, "cat #{custom_key}") do |_res|
+ expect(stdout).not_to include(name.to_s)
+ end
+ on(agent, "rm -rf #{custom_key_directory}")
+ end
end
end
RSpec.context 'sshkeys: Modify' do
let(:auth_keys) { '~/.ssh/authorized_keys' }
let(:name) { "pl#{rand(999_999).to_i}" }
+ let(:custom_key_directory) { "/etc/ssh_authorized_keys_#{name}" }
+ let(:custom_key) { "#{custom_key_directory}/authorized_keys_#{name}" }
before(:each) do
posix_agents.each do |agent|
- on(agent, "cp #{auth_keys} /tmp/auth_keys", acceptable_exit_codes: [0, 1])
+ on(agent, "cp -a #{auth_keys} /tmp/auth_keys", acceptable_exit_codes: [0, 1])
+ on(agent, "rm -f #{auth_keys}")
on(agent, "echo '' >> #{auth_keys} && echo 'ssh-rsa mykey #{name}' >> #{auth_keys}")
- on(agent, "chown $LOGNAME #{auth_keys}")
end
end
expect(stdout).not_to include("mykey #{name}")
end
end
+
+ it "#{agent} should update an entry for an SSH authorized key in a custom location" do
+ on(agent, "mkdir #{custom_key_directory}")
+ on(agent, "echo '' >> #{custom_key} && echo 'ssh-rsa mykey #{name}' >> #{custom_key}")
+ args = ['ensure=present',
+ 'user=$LOGNAME',
+ "type='rsa'",
+ "key='mynewshinykey'",
+ "target='#{custom_key}'"]
+ on(agent, puppet_resource('ssh_authorized_key', name.to_s, args))
+
+ on(agent, "cat #{custom_key}") do |_res|
+ expect(stdout).to include("mynewshinykey #{name}")
+ expect(stdout).not_to include("mykey #{name}")
+ end
+ on(agent, "rm -rf #{custom_key_directory}")
+ end
end
end
end
describe 'when validating attributes' do
- [:name, :provider].each do |param|
+ [:name, :provider, :drop_privileges].each do |param|
it "has a #{param} parameter" do
expect(described_class.attrtype(param)).to eq :param
end
end
end
+ describe 'for drop_privileges' do
+ it 'uses true as a default value' do
+ expect(described_class.new(name: 'whev', user: 'nobody')[:drop_privileges]).to eq true
+ end
+
+ [true, :true, 'true', :yes, 'yes'].each do |value|
+ it "supports #{value} and returns a boolean true" do
+ expect(described_class.new(name: 'whev', user: 'nobody', drop_privileges: value)[:drop_privileges]).to eq true
+ end
+ end
+
+ [false, :false, 'false', :no, 'no'].each do |value|
+ it "supports #{value} and returns a boolean false" do
+ expect(described_class.new(name: 'whev', user: 'nobody', drop_privileges: value)[:drop_privileges]).to eq false
+ end
+ end
+
+ it 'raises an exception on something else' do
+ expect { described_class.new(name: 'whev', user: 'nobody', drop_privileges: 'nope') }.to raise_error(Puppet::Error, %r{Invalid value})
+ end
+ end
+
describe 'for type' do
[
:'ssh-dss', :dsa,