summaryrefslogtreecommitdiff
path: root/code/environments/production/modules/certregen/lib/puppet_x
diff options
context:
space:
mode:
Diffstat (limited to 'code/environments/production/modules/certregen/lib/puppet_x')
-rw-r--r--code/environments/production/modules/certregen/lib/puppet_x/certregen/ca.rb137
-rw-r--r--code/environments/production/modules/certregen/lib/puppet_x/certregen/certificate.rb42
-rw-r--r--code/environments/production/modules/certregen/lib/puppet_x/certregen/crl.rb56
-rw-r--r--code/environments/production/modules/certregen/lib/puppet_x/certregen/util.rb27
4 files changed, 262 insertions, 0 deletions
diff --git a/code/environments/production/modules/certregen/lib/puppet_x/certregen/ca.rb b/code/environments/production/modules/certregen/lib/puppet_x/certregen/ca.rb
new file mode 100644
index 0000000..c9e7457
--- /dev/null
+++ b/code/environments/production/modules/certregen/lib/puppet_x/certregen/ca.rb
@@ -0,0 +1,137 @@
+require 'securerandom'
+require 'shellwords'
+
+require 'puppet'
+require 'puppet/util/execution'
+require 'puppet/util/package'
+
+require 'puppet/feature/chloride'
+
+module PuppetX
+ module Certregen
+ module CA
+ module_function
+
+ def setup
+ Puppet::SSL::Host.ca_location = :only
+ Puppet.settings.preferred_run_mode = "master"
+
+ if !Puppet::SSL::CertificateAuthority.ca?
+ raise "Unable to set up CA: this node is not a CA server."
+ end
+
+ if Puppet::SSL::Certificate.indirection.find('ca').nil?
+ raise "Unable to set up CA: the CA certificate is not present."
+ end
+
+ Puppet::SSL::CertificateAuthority.instance
+ end
+
+ def backup
+ cacert_backup_path = File.join(Puppet[:cadir], "ca_crt.#{Time.now.to_i}.pem")
+ Puppet.notice("Backing up current CA certificate to #{cacert_backup_path}")
+ FileUtils.cp(Puppet[:cacert], cacert_backup_path)
+ end
+
+ # Generate an updated CA certificate with the same subject as the existing CA certificate
+ # and synchronize the new CA certificate with the local CA certificate.
+ def regenerate(ca, cert = Puppet::SSL::Certificate.indirection.find("ca"))
+ Puppet[:ca_name] = cert.content.subject.to_a[0][1]
+
+ request = Puppet::SSL::CertificateRequest.new(Puppet::SSL::Host::CA_NAME)
+ request.generate(ca.host.key)
+ PuppetX::Certregen::CA.sign(ca, Puppet::SSL::CA_NAME,
+ {allow_dns_alt_names: false, self_signing_csr: request})
+ FileUtils.cp(Puppet[:cacert], Puppet[:localcacert])
+ end
+
+ # Copy the current CA certificate and CRL to the given host.
+ #
+ # @note Only Linux systems are supported and requires that the localcacert/hostcrl setting on the
+ # given host is the default path.
+ #
+ # @param [String] hostname The host to copy the CA cert to
+ # @param [Hash] config the Chloride host config
+ # @return [void]
+ def distribute(hostname, config)
+ host = Chloride::Host.new(hostname, config)
+ host.ssh_connect
+
+ Puppet.debug("SSH status for #{hostname}: #{host.ssh_status}")
+
+ log_events = lambda do |event|
+ event.data[:messages].each do |data|
+ Puppet.info "[#{data.severity}:#{data.hostname}]: #{data.message.inspect}"
+ end
+ end
+
+ distribute_cacert(host, log_events)
+ distribute_crl(host, log_events)
+ end
+
+ def distribute_cacert(host, blk)
+ src = Puppet[:cacert]
+ dst ='/etc/puppetlabs/puppet/ssl/certs/ca.pem' # @todo: query node for localcacert
+ distribute_file(host, src, dst, blk)
+ end
+
+ def distribute_crl(host, blk)
+ src = Puppet[:cacrl]
+ dst ='/etc/puppetlabs/puppet/ssl/crl.pem' # @todo: query node for hostcrl
+ distribute_file(host, src, dst, blk)
+ end
+
+ def distribute_file(host, src, dst, blk)
+ tmp = "#{File.basename(src)}.tmp.#{SecureRandom.uuid}"
+
+ copy_action = Chloride::Action::FileCopy.new(to_host: host, from: src, to: tmp)
+ copy_action.go(&blk)
+ if copy_action.success?
+ Puppet.info "Copied #{src} to #{host.hostname}:#{tmp}"
+ else
+ raise "Failed to copy #{src} to #{host.hostname}:#{tmp}: #{copy_action.status}"
+ end
+
+ move_action = Chloride::Action::Execute.new(host: host, cmd: "cp #{tmp} #{dst}", sudo: true)
+ move_action.go(&blk)
+
+ if move_action.success?
+ Puppet.info "Updated #{host.hostname}:#{dst}"
+ else
+ raise "Failed to copy #{tmp} to #{host.hostname}:#{dst}"
+ end
+
+ end
+
+
+ # Enumerate Puppet nodes without relying on PuppetDB
+ #
+ # If the Puppet CA certificate has expired we cannot rely on PuppetDB working
+ # or being able to connect to Postgres via the network. In order to access
+ # this information while the CA is in a degraded state we perform the query
+ # directly via a local psql call.
+ def certnames
+ psql = '/opt/puppetlabs/server/bin/psql -d pe-puppetdb --pset format=unaligned --pset t=on -c %s'
+ query = 'SELECT certname FROM certnames WHERE deactivated IS NULL AND expired IS NULL;'
+ cmd = psql % Shellwords.escape(query)
+ Puppet::Util::Execution.execute(cmd,
+ uid: 'pe-postgres',
+ gid: 'pe-postgres').split("\n")
+
+ end
+
+ # Abstract API changes for CA cert signing
+ #
+ # @param ca [Puppet::SSL::CertificateAuthority]
+ # @param hostname [String]
+ # @param options [Hash<Symbol, Object>]
+ def sign(ca, hostname, options)
+ if Puppet::Util::Package.versioncmp(Puppet::PUPPETVERSION, "4.6.0") != -1
+ ca.sign(hostname, options)
+ else
+ ca.sign(hostname, options[:allow_dns_alt_names], options[:self_signing_csr])
+ end
+ end
+ end
+ end
+end
diff --git a/code/environments/production/modules/certregen/lib/puppet_x/certregen/certificate.rb b/code/environments/production/modules/certregen/lib/puppet_x/certregen/certificate.rb
new file mode 100644
index 0000000..56ad970
--- /dev/null
+++ b/code/environments/production/modules/certregen/lib/puppet_x/certregen/certificate.rb
@@ -0,0 +1,42 @@
+require 'puppet_x/certregen/util'
+
+module PuppetX
+ module Certregen
+ module Certificate
+ module_function
+
+ # @param cert [Puppet::SSL::Certificate]
+ # @return [Hash<Symbol, String>]
+ def expiry(cert)
+ if cert.content.not_after < Time.now
+ status = :expired
+ elsif expiring?(cert)
+ status = :expiring
+ else
+ status = :ok
+ end
+
+ data = {
+ :status => status,
+ :expiration_date => cert.content.not_after
+ }
+
+ if status != :expired
+ data[:expires_in] = PuppetX::Certregen::Util.duration(cert.content.not_after - Time.now)
+ end
+
+ data
+ end
+
+ # Is this certificate expiring or expired?
+ #
+ # @param cert [Puppet::SSL::Certificate]
+ # @param percent [Integer]
+ def expiring?(cert, percent = 10)
+ remaining = cert.content.not_after - Time.now
+ lifetime = cert.content.not_after - (cert.content.not_before + 86400)
+ remaining / lifetime < (percent / 100.0)
+ end
+ end
+ end
+end
diff --git a/code/environments/production/modules/certregen/lib/puppet_x/certregen/crl.rb b/code/environments/production/modules/certregen/lib/puppet_x/certregen/crl.rb
new file mode 100644
index 0000000..e82f929
--- /dev/null
+++ b/code/environments/production/modules/certregen/lib/puppet_x/certregen/crl.rb
@@ -0,0 +1,56 @@
+require 'fileutils'
+require 'openssl'
+
+module PuppetX
+ module Certregen
+ # @api private
+ # @see {Puppet::SSL::CertificateRevocationList}
+ module CRL
+ module_function
+
+ FIVE_YEARS = 5 * 365*24*60*60
+
+ def refresh(ca)
+ crl = ca.crl
+ crl_content = crl.content
+ update_to_next_crl_number(crl_content)
+ update_valid_time_range_to_start_at(crl_content, Time.now)
+ sign_with(crl_content, ca.host.key.content)
+ Puppet::SSL::CertificateRevocationList.indirection.save(crl)
+ FileUtils.cp(Puppet[:cacrl], Puppet[:hostcrl])
+ Puppet::SSL::CertificateRevocationList.indirection.find("ca")
+ end
+
+ # @api private
+ def update_valid_time_range_to_start_at(crl_content, time)
+ # The CRL is not valid if the time of checking == the time of last_update.
+ # So to have it valid right now we need to say that it was updated one second ago.
+ crl_content.last_update = time - 1
+ crl_content.next_update = time + FIVE_YEARS
+ end
+
+ # @api private
+ def update_to_next_crl_number(crl_content)
+ crl_content.extensions = with_next_crl_number_from(crl_content, crl_content.extensions)
+ end
+
+ # @api private
+ def with_next_crl_number_from(crl_content, existing_extensions)
+ existing_crl_num = existing_extensions.find { |e| e.oid == 'crlNumber' }
+ new_crl_num = existing_crl_num ? existing_crl_num.value.to_i + 1 : 0
+ extensions_without_crl_num = existing_extensions.reject { |e| e.oid == 'crlNumber' }
+ extensions_without_crl_num + [crl_number_of(new_crl_num)]
+ end
+
+ # @api private
+ def crl_number_of(number)
+ OpenSSL::X509::Extension.new('crlNumber', OpenSSL::ASN1::Integer(number))
+ end
+
+ # @api private
+ def sign_with(crl_content, cakey)
+ crl_content.sign(cakey, OpenSSL::Digest::SHA1.new)
+ end
+ end
+ end
+end
diff --git a/code/environments/production/modules/certregen/lib/puppet_x/certregen/util.rb b/code/environments/production/modules/certregen/lib/puppet_x/certregen/util.rb
new file mode 100644
index 0000000..e21298a
--- /dev/null
+++ b/code/environments/production/modules/certregen/lib/puppet_x/certregen/util.rb
@@ -0,0 +1,27 @@
+module PuppetX
+ module Certregen
+ module Util
+ module_function
+
+ def duration(epoch)
+ seconds = epoch.to_i
+ minutes = (epoch / 60).to_i; seconds %= 60 if minutes > 0
+ hours = (minutes / 60).to_i; minutes %= 60 if hours > 0
+ days = (hours / 24).to_i; hours %= 24 if days > 0
+ years = (days / 365).to_i; days %= 365 if years > 0
+
+ list = []
+ list << "#{years} #{pluralize('year', years)}" if years > 0
+ list << "#{days} #{pluralize('day', days)}" if days > 0
+ list << "#{hours} #{pluralize('hour', hours)}" if hours > 0
+ list << "#{minutes} #{pluralize('minute', minutes)}" if minutes > 0
+ list << "#{seconds} #{pluralize('second', seconds)}" if seconds > 0
+ list.join(", ")
+ end
+
+ def pluralize(str, count)
+ count == 1 ? str : str + 's'
+ end
+ end
+ end
+end