diff options
Diffstat (limited to 'code/environments/production/modules/certregen/lib/puppet_x')
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 |