aboutsummaryrefslogtreecommitdiff
path: root/lib/puppet
diff options
context:
space:
mode:
authorJacob Helwig <jacob@technosorcery.net>2018-06-04 11:30:09 -0700
committerJacob Helwig <jacob@technosorcery.net>2018-06-21 14:27:04 -0700
commitd1719de1d77b9c139b1b5f5832330807c0fe11fe (patch)
tree69c541233bc64c5d47746e062e0efcba0c5436b5 /lib/puppet
downloadpuppet-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.rb105
-rw-r--r--lib/puppet/provider/sshkey/parsed.rb50
-rw-r--r--lib/puppet/type/ssh_authorized_key.rb143
-rw-r--r--lib/puppet/type/sshkey.rb83
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