diff options
author | Jacob Helwig <jacob@technosorcery.net> | 2018-06-04 11:30:09 -0700 |
---|---|---|
committer | Jacob Helwig <jacob@technosorcery.net> | 2018-06-21 14:27:04 -0700 |
commit | d1719de1d77b9c139b1b5f5832330807c0fe11fe (patch) | |
tree | 69c541233bc64c5d47746e062e0efcba0c5436b5 /lib/puppet | |
download | puppet-sshkeys_core-d1719de1d77b9c139b1b5f5832330807c0fe11fe.tar.gz puppet-sshkeys_core-d1719de1d77b9c139b1b5f5832330807c0fe11fe.tar.bz2 |
Initial sshkey type import from Puppet repository
Imported from dbf5a8964af9b87446542d24f46534cf90f11f59 in the Puppet repo.
Diffstat (limited to 'lib/puppet')
-rw-r--r-- | lib/puppet/provider/ssh_authorized_key/parsed.rb | 105 | ||||
-rw-r--r-- | lib/puppet/provider/sshkey/parsed.rb | 50 | ||||
-rw-r--r-- | lib/puppet/type/ssh_authorized_key.rb | 143 | ||||
-rw-r--r-- | lib/puppet/type/sshkey.rb | 83 |
4 files changed, 381 insertions, 0 deletions
diff --git a/lib/puppet/provider/ssh_authorized_key/parsed.rb b/lib/puppet/provider/ssh_authorized_key/parsed.rb new file mode 100644 index 0000000..f7ac9f7 --- /dev/null +++ b/lib/puppet/provider/ssh_authorized_key/parsed.rb @@ -0,0 +1,105 @@ +require 'puppet/provider/parsedfile' + +Puppet::Type.type(:ssh_authorized_key).provide( + :parsed, + :parent => Puppet::Provider::ParsedFile, + :filetype => :flat, + :default_target => '' +) do + desc "Parse and generate authorized_keys files for SSH." + + text_line :comment, :match => /^\s*#/ + text_line :blank, :match => /^\s*$/ + + record_line :parsed, + :fields => %w{options type key name}, + :optional => %w{options}, + :rts => /^\s+/, + :match => Puppet::Type.type(:ssh_authorized_key).keyline_regex, + :post_parse => proc { |h| + h[:name] = "" if h[:name] == :absent + h[:options] ||= [:absent] + h[:options] = Puppet::Type::Ssh_authorized_key::ProviderParsed.parse_options(h[:options]) if h[:options].is_a? String + }, + :pre_gen => proc { |h| + # if this name was generated, don't write it back to disk + h[:name] = "" if h[:unnamed] + h[:options] = [] if h[:options].include?(:absent) + h[:options] = h[:options].join(',') + } + + record_line :key_v1, + :fields => %w{options bits exponent modulus name}, + :optional => %w{options}, + :rts => /^\s+/, + :match => /^(?:(.+) )?(\d+) (\d+) (\d+)(?: (.+))?$/ + + def dir_perm + 0700 + end + + def file_perm + 0600 + end + + def user + uid = Puppet::FileSystem.stat(target).uid + Etc.getpwuid(uid).name + end + + def flush + raise Puppet::Error, "Cannot write SSH authorized keys without user" unless @resource.should(:user) + raise Puppet::Error, "User '#{@resource.should(:user)}' does not exist" unless Puppet::Util.uid(@resource.should(:user)) + # ParsedFile usually calls backup_target much later in the flush process, + # but our SUID makes that fail to open filebucket files for writing. + # Fortunately, there's already logic to make sure it only ever happens once, + # 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 + + super + + File.chmod(file_perm, target) + end + end + + # Parse sshv2 option strings, which is a comma-separated list of + # either key="values" elements or bare-word elements + def self.parse_options(options) + result = [] + scanner = StringScanner.new(options) + while !scanner.eos? + scanner.skip(/[ \t]*/) + # scan a long option + if out = scanner.scan(/[-a-z0-9A-Z_]+=\".*?[^\\]\"/) or out = scanner.scan(/[-a-z0-9A-Z_]+/) + result << out + else + # found an unscannable token, let's abort + break + end + # eat a comma + scanner.skip(/[ \t]*,[ \t]*/) + end + result + end + + def self.prefetch_hook(records) + name_index = 0 + records.each do |record| + if record[:record_type] == :parsed && record[:name].empty? + record[:unnamed] = true + # Generate a unique ID for unnamed keys, in case they need purging. + # If you change this, you have to keep + # Puppet::Type::User#unknown_keys_in_file in sync! (PUP-3357) + record[:name] = "#{record[:target]}:unnamed-#{ name_index += 1 }" + Puppet.debug("generating name for on-disk ssh_authorized_key #{record[:key]}: #{record[:name]}") + end + end + end +end + diff --git a/lib/puppet/provider/sshkey/parsed.rb b/lib/puppet/provider/sshkey/parsed.rb new file mode 100644 index 0000000..1c42aeb --- /dev/null +++ b/lib/puppet/provider/sshkey/parsed.rb @@ -0,0 +1,50 @@ +require 'puppet/provider/parsedfile' + +Puppet::Type.type(:sshkey).provide( + :parsed, + :parent => Puppet::Provider::ParsedFile, + :filetype => :flat +) do + desc "Parse and generate host-wide known hosts files for SSH." + + text_line :comment, :match => /^#/ + text_line :blank, :match => /^\s*$/ + + record_line :parsed, :fields => %w{name type key}, + :post_parse => proc { |hash| + names = hash[:name].split(",", -1) + hash[:name] = names.shift + hash[:host_aliases] = names + }, + :pre_gen => proc { |hash| + if hash[:host_aliases] + hash[:name] = [hash[:name], hash[:host_aliases]].flatten.join(",") + hash.delete(:host_aliases) + end + } + + # Make sure to use mode 644 if ssh_known_hosts is newly created + def self.default_mode + 0644 + end + + def self.default_target + case Facter.value(:operatingsystem) + when "Darwin" + # Versions 10.11 and up use /etc/ssh/ssh_known_hosts + version = Facter.value(:macosx_productversion_major) + if version + if Puppet::Util::Package.versioncmp(version, '10.11') >= 0 + "/etc/ssh/ssh_known_hosts" + else + "/etc/ssh_known_hosts" + end + else + "/etc/ssh_known_hosts" + end + else + "/etc/ssh/ssh_known_hosts" + end + end +end + diff --git a/lib/puppet/type/ssh_authorized_key.rb b/lib/puppet/type/ssh_authorized_key.rb new file mode 100644 index 0000000..c6ff5b6 --- /dev/null +++ b/lib/puppet/type/ssh_authorized_key.rb @@ -0,0 +1,143 @@ +module Puppet + Type.newtype(:ssh_authorized_key) do + @doc = "Manages SSH authorized keys. Currently only type 2 keys are supported. + + In their native habitat, SSH keys usually appear as a single long line, in + the format `<TYPE> <KEY> <NAME/COMMENT>`. This resource type requires you + to split that line into several attributes. Thus, a key that appears in + your `~/.ssh/id_rsa.pub` file like this... + + ssh-rsa AAAAB3Nza[...]qXfdaQ== nick@magpie.example.com + + ...would translate to the following resource: + + ssh_authorized_key { 'nick@magpie.example.com': + ensure => present, + user => 'nick', + type => 'ssh-rsa', + key => 'AAAAB3Nza[...]qXfdaQ==', + } + + To ensure that only the currently approved keys are present, you can purge + unmanaged SSH keys on a per-user basis. Do this with the `user` resource + type's `purge_ssh_keys` attribute: + + user { 'nick': + ensure => present, + purge_ssh_keys => true, + } + + This will remove any keys in `~/.ssh/authorized_keys` that aren't being + managed with `ssh_authorized_key` resources. See the documentation of the + `user` type for more details. + + **Autorequires:** If Puppet is managing the user account in which this + SSH key should be installed, the `ssh_authorized_key` resource will autorequire + that user." + + ensurable + + newparam(:name) do + desc "The SSH key comment. This can be anything, and doesn't need to match + the original comment from the `.pub` file. + + Due to internal limitations, this must be unique across all user accounts; + if you want to specify one key for multiple users, you must use a different + comment for each instance." + + isnamevar + + end + + newproperty(:type) do + desc "The encryption type used." + + newvalues :'ssh-dss', :'ssh-rsa', :'ecdsa-sha2-nistp256', :'ecdsa-sha2-nistp384', :'ecdsa-sha2-nistp521', :'ssh-ed25519' + + aliasvalue(:dsa, :'ssh-dss') + aliasvalue(:ed25519, :'ssh-ed25519') + aliasvalue(:rsa, :'ssh-rsa') + end + + newproperty(:key) do + desc "The public key itself; generally a long string of hex characters. The `key` + attribute may not contain whitespace. + + Make sure to omit the following in this attribute (and specify them in + other attributes): + + * Key headers, such as 'ssh-rsa' --- put these in the `type` attribute. + * Key identifiers / comments, such as 'joe@joescomputer.local' --- put these in + the `name` attribute/resource title." + + validate do |value| + raise Puppet::Error, _("Key must not contain whitespace: %{value}") % { value: value } if value =~ /\s/ + end + end + + newproperty(:user) do + desc "The user account in which the SSH key should be installed. The resource + will autorequire this user if it is being managed as a `user` resource." + end + + newproperty(:target) do + 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`." + + defaultto :absent + + def should + return super if defined?(@should) and @should[0] != :absent + + return nil unless user = resource[:user] + + begin + return File.expand_path("~#{user}/.ssh/authorized_keys") + rescue + Puppet.debug "The required user is not yet present on the system" + return nil + end + end + + def insync?(is) + is == should + end + end + + newproperty(:options, :array_matching => :all) do + desc "Key options; see sshd(8) for possible values. Multiple values + should be specified as an array." + + defaultto do :absent end + + validate do |value| + unless value == :absent or value =~ /^[-a-z0-9A-Z_]+(?:=\".*?\")?$/ + raise Puppet::Error, _("Option %{value} is not valid. A single option must either be of the form 'option' or 'option=\"value\". Multiple options must be provided as an array") % { value: value } + end + end + end + + autorequire(:user) do + should(:user) if should(:user) + end + + validate do + # Go ahead if target attribute is defined + return if @parameters[:target].shouldorig[0] != :absent + + # Go ahead if user attribute is defined + return if @parameters.include?(:user) + + # If neither target nor user is defined, this is an error + raise Puppet::Error, _("Attribute 'user' or 'target' is mandatory") + end + + # regular expression suitable for use by a ParsedFile based provider + REGEX = /^(?:(.+)\s+)?(ssh-dss|ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521)\s+([^ ]+)\s*(.*)$/ + def self.keyline_regex + REGEX + end + end +end diff --git a/lib/puppet/type/sshkey.rb b/lib/puppet/type/sshkey.rb new file mode 100644 index 0000000..31e590b --- /dev/null +++ b/lib/puppet/type/sshkey.rb @@ -0,0 +1,83 @@ +module Puppet + Type.newtype(:sshkey) do + @doc = "Installs and manages ssh host keys. By default, this type will + install keys into `/etc/ssh/ssh_known_hosts`. To manage ssh keys in a + different `known_hosts` file, such as a user's personal `known_hosts`, + pass its path to the `target` parameter. See the `ssh_authorized_key` + type to manage authorized keys." + + ensurable + + newproperty(:type) do + desc "The encryption type used. Probably ssh-dss or ssh-rsa." + + newvalues :'ssh-dss', :'ssh-ed25519', :'ssh-rsa', :'ecdsa-sha2-nistp256', :'ecdsa-sha2-nistp384', :'ecdsa-sha2-nistp521' + + aliasvalue(:dsa, :'ssh-dss') + aliasvalue(:ed25519, :'ssh-ed25519') + aliasvalue(:rsa, :'ssh-rsa') + end + + newproperty(:key) do + desc "The key itself; generally a long string of uuencoded characters. The `key` + attribute may not contain whitespace. + + Make sure to omit the following in this attribute (and specify them in + other attributes): + + * Key headers, such as 'ssh-rsa' --- put these in the `type` attribute. + * Key identifiers / comments, such as 'joescomputer.local' --- put these in + the `name` attribute/resource title." + end + + # FIXME This should automagically check for aliases to the hosts, just + # to see if we can automatically glean any aliases. + newproperty(:host_aliases) do + desc 'Any aliases the host might have. Multiple values must be + specified as an array.' + + attr_accessor :meta + + def insync?(is) + is == @should + end + # We actually want to return the whole array here, not just the first + # value. + def should + defined?(@should) ? @should : nil + end + + validate do |value| + if value =~ /\s/ + raise Puppet::Error, _("Aliases cannot include whitespace") + end + if value =~ /,/ + raise Puppet::Error, _("Aliases must be provided as an array, not a comma-separated list") + end + end + end + + newparam(:name) do + desc "The host name that the key is associated with." + + isnamevar + + validate do |value| + raise Puppet::Error, _("Resourcename cannot include whitespaces") if value =~ /\s/ + raise Puppet::Error, _("No comma in resourcename allowed. If you want to specify aliases use the host_aliases property") if value.include?(',') + end + end + + newproperty(:target) do + desc "The file in which to store the ssh key. Only used by + the `parsed` provider." + + defaultto { if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile) + @resource.class.defaultprovider.default_target + else + nil + end + } + end + end +end |