#!/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 ] [ --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 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 'getoptlong' opts = GetoptLong.new( [ '--help', '-h', GetoptLong::NO_ARGUMENT ], [ '--manual', '-m', GetoptLong::NO_ARGUMENT ], [ '--verbose', '-v', 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 ] ) manual = help = false $VERBOSE = false prefix = "/srv" do_list = do_generate = do_rollover = nil rollover_to = nil threshold = 21 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 '--list' do_list = true when '--verbose' $VERBOSE = true end end # # Output help as required. # if help or manual require 'symbiosis/utils' Symbiosis::Utils.show_help(__FILE__) if help Symbiosis::Utils.show_manual(__FILE__) if manual exit 0 end # # The required 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 and ARGV.length != 1 warn "** Exactly one domain must be specfied when rolling over to a specific set." exit 1 end if ARGV.empty? domains = Symbiosis::Domains.all(prefix) end exit_code = 0 %w(INT TERM).each do |sig| trap(sig) do if 0 == Process.uid Process.euid = 0 Process.egid = 0 end exit 1 end end now = Time.now domains.sort{|a,b| a.name <=> b.name}.each do |domain| if do_list or 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 if rollover_to.nil? next end 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 domain.ssl_magic(threshold, do_generate, do_rollover, now) rescue StandardError => err puts "\t!! Failed: #{err.to_s.gsub($/,'')}" if $VERBOSE puts err.backtrace.join("\n") if $DEBUG exit_code = 1 end end exit exit_code