Skip to content

Instantly share code, notes, and snippets.

@justinstoller
Last active August 19, 2020 15:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save justinstoller/9724d9dd24454d13089dd8bbc24163e0 to your computer and use it in GitHub Desktop.
Save justinstoller/9724d9dd24454d13089dd8bbc24163e0 to your computer and use it in GitHub Desktop.
Revoke and Clean Certs in Certdir
require 'optparse'
require 'openssl'
require 'fileutils'
options = {
crl: '/etc/puppetlabs/puppet/ssl/ca/ca_crl.pem',
signeddir: '/etc/puppetlabs/puppet/ssl/ca/signed',
ca_key_path: '/etc/puppetlabs/puppet/ssl/ca/ca_key.pem',
certs_to_skip: ['ca.pem', "`hostname -f`.pem"],
timing: false,
trace: false
}
parser = OptionParser.new do |opts|
opts.on('--crl PATH', 'Full path to CRL') { |path| options[:crl] = path }
opts.on('--signed-dir PATH', 'Full path to CA\'s signeddir') { |path| options[:signeddir] = path }
opts.on('--ca-key PATH', 'Full path to CA\'s private key') { |path| options[:ca_key_path] = path }
opts.on('--block LIST', 'comma separated list of certs filenmaes not to revoke & clean') do |names|
options[:certs_to_skip] = names.split(',')
end
opts.on('--timing', 'Display timing information for major sections of work') { |bool| options[:timing] = bool }
opts.on('--trace', 'Log each action taken, very verbose') { |bool| options[:trace] = bool }
end
parser.parse(ARGV)
start = Time.now
# Load the CRL and create a uniqued list of revocations
crl = OpenSSL::X509::CRL.new(File.read(options[:crl]))
old_version = crl.version
revoked_by_serial = Hash.new
crl.revoked.each do |revoked|
if revoked_by_serial[revoked.serial]
puts("Found duplicated serial: #{revoked.serial}") if options[:trace]
else
revoked_by_serial[revoked.serial] = revoked
end
end
finish = Time.now
puts "Deduplicated existing CRL in memory"
puts("Deduplication took #{finish - start} seconds") if options[:timing]
unique_existing_revocations = revoked_by_serial.length
puts "Searching for certs to revoke"
start = Time.now
full_paths_to_all_certs = Dir[options[:signeddir] + '/*.pem']
full_paths_to_certs_to_delete = full_paths_to_all_certs.reject do |path|
if options[:certs_to_skip].include?(File.basename(path))
puts("Skipping revocation and cleaning of #{path}") if options[:trace]
true
end
end
finish = Time.now
puts("Filtering certs in signeddir took #{finish - start} seconds") if options[:timing]
puts "Found and preparing to revoke up to #{full_paths_to_certs_to_delete.length} certs"
start = Time.now
full_paths_to_certs_to_delete.each do |path|
cert = OpenSSL::X509::Certificate.new(File.read(path))
serial = cert.serial
unless revoked_by_serial[serial]
revoked = OpenSSL::X509::Revoked.new
revoked.serial = serial
revoked.time = Time.now
revoked.add_extension(
OpenSSL::X509::Extension.new(
"CRLReason",
OpenSSL::ASN1::Enumerated(OpenSSL::OCSP::REVOKED_STATUS_KEYCOMPROMISE)))
revoked_by_serial[serial] = revoked
# Just revoke, don't delete untile we know we've successfully written the CRL to disk
puts("Revoked serial #{serial}") if options[:trace]
end
end
finish = Time.now
puts("Revocation took #{finish - start} seconds") if options[:timing]
total_revoked = revoked_by_serial.length
unique_newly_revoked = total_revoked - unique_existing_revocations
start = Time.now
crl.revoked = revoked_by_serial.values
# Increment the version by the number of unique additions
crl.version = old_version + unique_newly_revoked
ca_key = OpenSSL::PKey.read(File.read(options[:ca_key_path]))
crl.sign(ca_key, OpenSSL::Digest::SHA256.new)
File.write(options[:crl], crl.to_pem)
finish = Time.now
puts "Revoked #{unique_newly_revoked} certs, now cleaning certdir"
puts("Writing crl took #{finish - start} seconds") if options[:timing]
start = Time.now
full_paths_to_certs_to_delete.each do |path|
puts("Removing cert at #{path}") if options[:trace]
FileUtils.rm_rf(path)
end
finish = Time.now
puts "Cleaned #{full_paths_to_certs_to_delete.length} stale certs"
puts("Cleaning certs took #{finish - start} seconds") if options[:timing]
exit 0
@justinstoller
Copy link
Author

output

 puppet (main *$%) :: ruby pave.rb --help

Usage: pave [options]
        --crl PATH                   Full path to CRL
        --signed-dir PATH            Full path to CA's signeddir
        --ca-key PATH                Full path to CA's private key
        --block LIST                 comma separated list of certs filenmaes not to revoke & clean
        --timing                     Display timing information for major sections of work
        --trace                      Log each action taken, very verbose

 puppet (main *$%) :: ruby pave.rb --crl ~/.puppetlabs/etc/puppet/ssl/ca/ca_crl.pem --signed-dir ~/.puppetlabs/etc/puppet/ssl/ca/signed --ca-key ~/.puppetlabs/etc/puppet/ssl/ca/ca_key.pem --block localhost.pem --timing --trace

Found duplicated serial: 5
Found duplicated serial: 5
Deduplicated existing CRL in memory
Deduplication took 0.000273 seconds
Searching for certs to revoke
Skipping revocation and cleaning of /Users/justin/.puppetlabs/etc/puppet/ssl/ca/signed/localhost.pem
Filtering certs in signeddir took 0.00014 seconds
Found and preparing to revoke up to 4 certs
Revoked serial 6
Revoked serial 3
Revocation took 0.000592 seconds
Revoked 2 certs, now cleaning certdir
Writing crl took 0.00908 seconds
Removing cert at /Users/justin/.puppetlabs/etc/puppet/ssl/ca/signed/sellout.vpn.puppet.net.pem
Removing cert at /Users/justin/.puppetlabs/etc/puppet/ssl/ca/signed/blazz.pem
Removing cert at /Users/justin/.puppetlabs/etc/puppet/ssl/ca/signed/foo.example.com.pem
Removing cert at /Users/justin/.puppetlabs/etc/puppet/ssl/ca/signed/foo.pem
Cleaned 4 stale certs
Cleaning certs took 0.071894 seconds

@reidmv
Copy link

reidmv commented Aug 19, 2020

The use case has been updated to:

Bulk-clear a known set of certificate names which is not equal to the full set of issued certificates. This could be a short list of tens, or a long list of thousands. Individual teams responsible for applications deployments will initiate this as a self-serve action, so it should be accessible via an API call—a Bolt task, for example.

As a Bolt task, this would be the user interface.

{
  "description": "Bulk delete and/or revoke Puppet certificates through direct file operations",
  "input_method": "stdin",
  "parameters": {
    "certnames": {
      "description": "List of certnames to clear",
      "type": "Array[String[1]]"
    },
    "revoke": {
      "description": "Add cleared certificates to the CRL?",
      "type": "Boolean",
      "default": true
    },
    "shutdown_puppet_server": {
      "description": "Shut down Puppet Server during clearing operation?",
      "type": "Boolean",
      "default": false
    }
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment