#!/usr/bin/ruby # # NAME # symbiosis-ssl - Manage and generate SSL certificates # # SYNOPSIS # symbiosis-ssl [ --threshold days ] [ --no-generate ] [ --no-rollover ] [ --select set ] # [ --list ] [ --prefix prefix ] [ --verbose ] [ --debug ] [ --manual ] [ --help ] # [ domain domain ... ] # # OPTIONS # --force Re-generate certificates, and roll over to the new set even # if they're not due to be renewed. Implies --verbose. # # --threshold days Number of days before expiry that certificates should be renewed. Defaults to 21. # # --select set Select a specific set for a single domain. A domain must be specified. # # --list List available SSL certificate sets for a domain. # # --no-generate Do not try and generate keys or certificates. # # --no-rollover Do not try and generate keys or certificates. # # --prefix prefix Set the directory prefix for Symbiosis. Defaults to /srv. # # --help Show the help information for this script. # # --manual Show the manual for this script # # --verbose Show verbose information. # # --debug Show debugging information. # # USAGE # # This command is used to manage certificate sets automatically for domains on # a Symbiosis system. It can request certificates from LetsEncrypt or generate # self-signed ones (see PROVIDERS). # # PROVIDERS # # Currently two providers are supported, namely LetsEncrypt and SelfSigned. A # domain can be set up to use either provider by setting a file # /srv/example.com/config/ssl-provider with the name of the desired provider in # it. # # If the provider is set to something else (e.g. CertificateProviderDuJour) # then no certificates will be generated, but it is possible to manage updating # certificates with this program. # # AUTHOR # Patrick J. Cherry # # # Modules we require # require 'English' require 'getoptlong' opts = GetoptLong.new( ['--help', '-h', GetoptLong::NO_ARGUMENT], ['--manual', '-m', GetoptLong::NO_ARGUMENT], ['--verbose', '-v', GetoptLong::NO_ARGUMENT], ['--debug', '-d', GetoptLong::NO_ARGUMENT], ['--force', '-f', GetoptLong::NO_ARGUMENT], ['--list', '-l', GetoptLong::NO_ARGUMENT], ['--threshold', '-t', GetoptLong::REQUIRED_ARGUMENT], ['--no-generate', '-G', GetoptLong::NO_ARGUMENT], ['--no-rollover', '-R', GetoptLong::NO_ARGUMENT], ['--select', '-s', GetoptLong::REQUIRED_ARGUMENT], ['--prefix', '-p', GetoptLong::REQUIRED_ARGUMENT], ['--root-dir', '-r', GetoptLong::REQUIRED_ARGUMENT] ) manual = help = false $VERBOSE = false $DEBUG = false prefix = '/srv' do_list = do_generate = do_rollover = nil rollover_to = nil threshold = 21 root = '/' opts.each do |opt,arg| case opt when '--no-generate' do_generate = false when '--no-rollover' do_rollover = false when '--select' rollover_to = arg.to_s when '--force' do_generate = do_rollover = true $VERBOSE = true when '--threshold' begin threshold = Integer(arg) rescue ArgumentError warn "** Could not parse #{arg.inspect} as an integer for --threshold" end when '--help' help = true when '--manual' manual = true when '--prefix' prefix = arg when '--root-dir' root = arg when '--list' do_list = true when '--verbose' $VERBOSE = true when '--debug' $DEBUG = true end end prefix == '/srv' && prefix = File.join(root, '/srv') # # Output help as required. # if help || manual require 'symbiosis/utils' Symbiosis::Utils.show_help(__FILE__) if help Symbiosis::Utils.show_manual(__FILE__) if manual exit 0 end # # The requires spawn a massive stack of warnings in verbose mode. So let's # hide them. # v = $VERBOSE $VERBOSE = false require 'symbiosis/domains' require 'symbiosis/domain/ssl' require 'symbiosis/ssl' require 'symbiosis/ssl/letsencrypt' require 'symbiosis/ssl/selfsigned' # # And unhide. Ugh. # $VERBOSE = v domains = [] ARGV.each do |arg| domain = Symbiosis::Domains.find(arg.to_s, prefix) if domain.nil? warn "** Unable to find/parse domain #{arg.inspect}" next end domains << domain end if rollover_to && ARGV.length != 1 warn '** Exactly one domain must be specfied when rolling over to a specific set.' exit 1 end domains = Symbiosis::Domains.all(prefix) if ARGV.empty? exit_code = 0 %w[INT TERM].each do |sig| trap(sig) do if Process.uid.zero? Process.euid = 0 Process.egid = 0 end exit 1 end end now = Time.now domains_altered = [] domains.sort_by(&:name).each do |domain| if do_list || rollover_to puts "Certificate sets for #{domain}:" if domain.ssl_available_sets.empty? puts "\t** No sets found\n\n" next end domain.ssl_available_sets.each do |this_set| if this_set.certificate.issuer == this_set.certificate.subject puts "\tSSL set #{this_set.name}: self-signed for #{this_set.certificate.issuer}, expires #{this_set.certificate.not_after}" else puts "\tSSL set #{this_set.name}: signed by #{this_set.certificate.issuer}, expires #{this_set.certificate.not_after}" end end current = domain.ssl_current_set puts "\tCurrent SSL set: #{current.name}\n" unless $VERBOSE next if rollover_to.nil? to_set = domain.ssl_available_sets.find { |s| s.name.to_s == rollover_to } if to_set.nil? puts "\tThere is no set '#{rollover_to}' available for this domain." next end if to_set == current puts "\tNo need to change to set #{to_set.name} as this is already current." next end puts "\tRolling over from set #{current.name} to #{to_set.name}" domain.ssl_rollover(to_set) puts "\tCurrent SSL set now: #{domain.ssl_current_set.name}\n" next end begin rollover_performed = domain.ssl_magic(threshold, do_generate, do_rollover, now) domains_altered.push domain.name if rollover_performed rescue StandardError => err puts "\t!! Failed: #{err.to_s.gsub($RS, '')}" if $VERBOSE puts err.backtrace.join("\n") if $DEBUG exit_code = 1 end end Symbiosis::SSL.call_hooks domains_altered exit exit_code