require 'puppet/face' require 'puppet_x/certregen/ca' require 'puppet_x/certregen/certificate' require 'puppet_x/certregen/crl' require 'puppet/feature/chloride' Puppet::Face.define(:certregen, '0.1.0') do copyright "Puppet", 2016 summary "Regenerate the Puppet CA and client certificates" description <<-EOT This subcommand provides tools for monitoring the health of the Puppet CA, regenerating expiring CA certificates, and remediation for expired CA certificates. EOT action(:ca) do summary "Refresh the Puppet CA certificate and CRL" option('--ca_serial SERIAL') do summary 'The serial number (in hexadecimal) of the CA to rotate.' end when_invoked do |opts| cert = Puppet::Face[:certregen, :current].cacert(:ca_serial => opts[:ca_serial]) crl = Puppet::Face[:certregen, :current].crl() [cert, crl] end when_rendering :console do |(cert, crl)| "CA expiration is now #{cert.content.not_after}\n" + \ "CRL next update is now #{crl.content.next_update}" end end action(:cacert) do summary "Regenerate the Puppet CA certificate" description <<-EOT This subcommand generates a new CA certificate that can replace the existing CA certificate. The new CA certificate uses the same subject as the current CA certificate and reuses the key pair associated with the current CA certificate, so all certificates signed by the old CA certificate will remain valid. EOT option('--ca_serial SERIAL') do summary 'The serial number (in hexadecimal) of the CA to rotate.' end when_invoked do |opts| ca = PuppetX::Certregen::CA.setup current_ca_serial = ca.host.certificate.content.serial.to_s(16) if opts[:ca_serial].nil? raise "The serial number of the CA certificate to rotate must be provided. If you " \ "are sure that you want to rotate the CA certificate, rerun this command with " \ "--ca_serial #{current_ca_serial}" elsif opts[:ca_serial] != current_ca_serial raise "The serial number of the current CA certificate (#{current_ca_serial}) "\ "does not match the serial number given on the command line (#{opts[:ca_serial]}). "\ "If you are sure that you want to rotate the CA certificate, rerun this command with "\ "--ca_serial #{current_ca_serial}" end PuppetX::Certregen::CA.backup PuppetX::Certregen::CA.regenerate(ca) Puppet::SSL::Certificate.indirection.find(Puppet::SSL::CA_NAME) end when_rendering(:console) do |cert| "CA expiration is now #{cert.content.not_after}" end end action(:crl) do summary 'Update the lastUpdate and nextUpdate field for the CA CRL' when_invoked do |opts| ca = PuppetX::Certregen::CA.setup PuppetX::Certregen::CRL.refresh(ca) end when_rendering(:console) do |crl| "CRL next update is now #{crl.content.next_update}" end end action(:healthcheck) do summary "Check for expiring certificates" description <<-EOT This subcommand checks for certificates that are nearing or past expiration. EOT option('--all') do summary "Report certificate expiry for all nodes, including nodes that aren't near expiration." end when_invoked do |opts| ca = PuppetX::Certregen::CA.setup certs = Puppet::SSL::Certificate.indirection.search('*').select do |cert| opts[:all] || PuppetX::Certregen::Certificate.expiring?(cert) end cacert = ca.host.certificate certs << cacert if (opts[:all] || PuppetX::Certregen::Certificate.expiring?(cacert)) certs.sort { |a, b| a.content.not_after <=> b.content.not_after } end when_rendering :console do |certs| if certs.empty? "No certificates are approaching expiration." else certs.map do |cert| str = "#{cert.name.inspect} #{cert.digest.to_s}\n" expiry = PuppetX::Certregen::Certificate.expiry(cert) str << "Status: #{expiry[:status]}\n" str << "Expiration date: #{expiry[:expiration_date]}\n" if expiry[:expires_in] str << "Expires in: #{expiry[:expires_in]}\n" end str end end end when_rendering :pson do |certs| certs.map do |cert| { :name => cert.name, :digest => cert.digest.to_s, :expiry => PuppetX::Certregen::Certificate.expiry(cert) } end end when_rendering :yaml do |certs| certs.map do |cert| { :name => cert.name, :digest => cert.digest.to_s, :expiry => PuppetX::Certregen::Certificate.expiry(cert) } end end end action(:redistribute) do summary "Redistribute the regenerated CA certificate and CRL to nodes in PuppetDB" description <<-EOT Redistribute the regenerated CA certificate and CRL to active nodes in PuppetDB. This command is only necessary if the CA certificate is expired and a new CA certificate needs to be manually distributed via SSH. This subcommand depends on the `chloride` gem, which is not included with this Puppet face. Distributing the CA certificate via SSH requires either a private ssh key (given by the `--ssh_key_file` flag) or entering the password when prompted. If password auth is used, the `highline` gem should be installed so that the entered password is not echoed to the terminal. EOT option('--username USER') do summary "The username to use when logging into the remote machine" end option('--ssh_key_file FILE') do summary "The SSH key file to use for authentication" default_to { "~/.ssh/id_rsa" } end when_invoked do |opts| unless Puppet.features.chloride? raise "Unable to distribute CA certificate: the chloride gem is not available." end config = {} config.merge!(username: opts[:username]) if opts[:username] config.merge!(ssh_key_file: File.expand_path(opts[:ssh_key_file])) if opts[:ssh_key_file] ca = PuppetX::Certregen::CA.setup cacert = ca.host.certificate if PuppetX::Certregen::Certificate.expiring?(cacert) Puppet.err "Refusing to distribute CA certificate: certificate is pending expiration." exit 1 end rv = {succeeded: [], failed: []} PuppetX::Certregen::CA.certnames.each do |certname| begin PuppetX::Certregen::CA.distribute(certname, config) rv[:succeeded] << certname rescue => e Puppet.log_exception(e) rv[:failed] << certname end end rv end end end