Commit 764ebd54 authored by Patrick J Cherry's avatar Patrick J Cherry
Browse files

common: Massive commits suck

* Refactored symbiosis-ssl code into the library
* Added tests to test this new code.
* symbiosis-ssl tries to regain privs after creating the certs if it
  thinks it has them.
* Changed what gets logged when a bit.  Stuff in the SSL validation
  checks is now only shown if $DEBUG is set.
* The cache of available SSL sets is always emptied before rollover
  starts.
* The way available sets are sorted has changed to be done by expiry.
* The symlink to current now uses the full path.
* SSL sets are now kept in config/ssl/sets for neatness/namespace
  goodness.
* CertificateSet#write drops privs if possible when creating a new set.
parent 10ea3b17
......@@ -121,63 +121,32 @@ if ARGV.empty?
domains = Symbiosis::Domains.all(prefix)
end
now = Time.now
threshold = 14
exit_code = 0
domains.each do |domain|
puts "* Examining certificates for #{domain.name}" if $VERBOSE
#
# Stage 0: verify and check expiriy
#
set = domain.ssl_current_set
set = domain.ssl_available_sets.last unless set.is_a?(Symbiosis::SSL::CertificateSet)
expires_in = nil
#
#
#
%w(QUIT INT TERM).each do |sig|
trap(sig) do
if set.is_a?(Symbiosis::SSL::CertificateSet)
expires_in = ((set.certificate.not_after - now)/86400.0).round
if expires_in < 14
puts "\tThe certificate is due to expire in #{expires_in} days" if $VERBOSE
if 0 == Process.uid
Process.euid = 0
Process.egid = 0
end
else
puts "\tNo valid certificates found." if $VERBOSE
exit exit_code
end
end
#
# Stage 1: Generate
#
if false == domain.ssl_provider
puts "\tSkipping because the ssl-provider has been set to false." if $VERBOSE
elsif (expires_in.is_a?(Integer) and expires_in < threshold) or set.nil?
#
# Default to letsencrypt
#
domain.ssl_provider = "letsencrypt" if domain.ssl_provider.nil?
puts "\tFetching a new certificate from #{domain.ssl_provider}." if $VERBOSE
begin
cert_set = domain.ssl_fetch_new_certificate
cert_set.name = domain.ssl_next_set
cert_set.write
rescue StandardError => err
puts "\t!! Failed: #{err.to_s.gsub($/,'')}" if $VERBOSE
puts err.backtrace.join("\n") if $DEBUG
exit_code = 1
end
now = Time.now
domains.each do |domain|
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
#
# Stage 2: Roll over
#
domain.ssl_rollover
end
exit exit_code
......@@ -2,7 +2,6 @@ require 'symbiosis/domain'
require 'symbiosis/ssl'
require 'symbiosis/ssl/certificate_set'
require 'openssl'
require 'tmpdir'
require 'erb'
module Symbiosis
......@@ -183,16 +182,14 @@ module Symbiosis
return ssl_provider
end
def ssl_next_set
def ssl_next_set_name
next_set = self.ssl_available_sets.last
if next_set.nil?
next_set = "0"
"0"
else
next_set.succ!
next_set.name.succ
end
next_set
end
#
......@@ -258,7 +255,11 @@ module Symbiosis
this_set = self.ssl_legacy_set if this_set.nil?
if this_set.is_a?(Symbiosis::SSL::CertificateSet)
puts "\tCurrent set is #{this_set.name}" if $VERBOSE
if this_set.certificate.issuer == this_set.certificate.subject
puts "\tCurrent SSL set #{this_set.name}: self-signed for #{this_set.certificate.issuer}, expires #{this_set.certificate.not_after}" if $VERBOSE
else
puts "\tCurrent SSL set #{this_set.name}: signed by #{this_set.certificate.issuer}, expires #{this_set.certificate.not_after}" if $VERBOSE
end
end
@ssl_current_set = this_set
......@@ -280,7 +281,11 @@ module Symbiosis
return @ssl_available_sets unless @ssl_available_sets.empty?
Dir.glob(File.join(self.config_dir, 'ssl' ,'*')).sort.each do |cert_dir|
#
#
#
Dir.glob(File.join(self.config_dir, 'ssl', 'sets', '*')).
sort_by{|i| i.to_s.split(/(\d+)/).map{|e| [e.to_i, e]}}.each do |cert_dir|
begin
this_set = Symbiosis::SSL::CertificateSet.new(self, cert_dir)
......@@ -288,11 +293,6 @@ module Symbiosis
next
end
#
# Always miss out the "current" set
#
next if this_set.name == "current"
#
# If this certificate verifies, add it to our list. We allow 18 as an
# error code, as this covers self-signed certificates.
......@@ -302,7 +302,7 @@ module Symbiosis
@ssl_available_sets << this_set
end
return @ssl_available_sets.sort!
return @ssl_available_sets.sort!{|a,b| a.certificate.not_after <=> b.certificate.not_after}
end
#
......@@ -311,8 +311,9 @@ module Symbiosis
# if a rollover was performed, or false otherwise.
#
def ssl_rollover
@ssl_available_sets = []
current = self.ssl_current_set
latest = self.ssl_available_sets.sort.last
latest = self.ssl_available_sets.last
if latest.nil? or !File.directory?(latest.directory)
warn "\tNo valid sets of certificates found." if $VERBOSE
......@@ -344,17 +345,106 @@ module Symbiosis
Process.euid = self.uid if Process.uid == 0
File.unlink(current_dir) unless stat.nil?
File.symlink(latest.name, current_dir)
File.symlink(latest.directory, current_dir)
Process.euid = 0 if Process.uid == 0
Process.egid = 0 if Process.gid == 0
#
# Restore privileges
#
Process.euid = 0 if Process.uid == 0 and Process.euid != Process.uid
Process.egid = 0 if Process.gid == 0 and Process.egid != Process.gid
#
# Update our latest
#
@ssl_current_set = latest
puts "\tRolled over to SSL set #{latest.name}" if $VERBOSE
return true
ensure
#
# Make sure we restore privs.
#
Process.euid = 0 if Process.uid == 0 and Process.euid != Process.uid
Process.egid = 0 if Process.gid == 0 and Process.egid != Process.gid
end
#
# This method does
# * validation
# * generation
# * rollover
#
def ssl_magic(threshold = 14, do_generate = true, do_rollover = true, now = Time.now)
puts "* Examining certificates for #{self.name}" if $VERBOSE
#
# Stage 0: verify and check expiriy
#
set = self.ssl_current_set
set = self.ssl_available_sets.last unless set.is_a?(Symbiosis::SSL::CertificateSet)
expires_in = nil
if set.is_a?(Symbiosis::SSL::CertificateSet)
expires_in = ((set.certificate.not_after - now)/86400.0).round
if expires_in < 14
puts "\tThe certificate is due to expire in #{expires_in} days" if $VERBOSE
end
else
puts "\tNo valid certificates found." if $VERBOSE
end
#
# Stage 1: Generate
#
if do_generate and (set.nil? or (expires_in.is_a?(Integer) and expires_in < threshold))
#
# If ssl-provision has been disabled, move on.
#
if false == self.ssl_provider
puts "\tNot fetching new certificate as the ssl-provider has been set to 'false'" if $VERBOSE
elsif !self.ssl_provider_class.is_a?(Class) or !(self.ssl_provider_class <= Symbiosis::SSL::CertificateSet)
puts "\tNot fetching new certificate as the ssl-provider #{ssl_provider.inspect} cannot be found." if $VERBOSE
else
#
# Default to letsencrypt
#
puts "\tFetching a new certificate from #{self.ssl_provider_class.to_s.split("::").last}." if $VERBOSE
cert_set = self.ssl_fetch_new_certificate
raise RuntimeError, "Failed to fetch certificate" if cert_set.nil?
cert_set.name = self.ssl_next_set_name
begin
cert_set.write
rescue StandardError
cert_set.name = cert_set.name.succ!
cert_set.directory = cert_set.name
retry
end
@ssl_available_sets << cert_set
end
end
#
# Stage 2: Roll over
#
if do_rollover and self.ssl_rollover
return true
else
return !do_rollover
end
# rescue StandardError => err
# puts "\t!! Failed: #{err.to_s.gsub($/,'')}" if $VERBOSE
# puts err.backtrace.join("\n") if $DEBUG
# return false
end
end
......
module Symbiosis
class SSL
PROVIDERS = []
PROVIDERS ||= []
end
end
......@@ -45,7 +45,7 @@ module Symbiosis
if "legacy" == @name
self.directory = self.domain.config_dir
elsif self.name.is_a?(String)
self.directory = File.join(self.domain.config_dir, "ssl", @name)
self.directory = File.join(self.domain.config_dir, "ssl", "sets", @name)
end
end
......@@ -61,9 +61,9 @@ module Symbiosis
# config directory.
#
def directory=(d)
raise Errno::ENOTDIR.new d if File.exist?(d) and !File.directory?(d)
raise ArgumentError unless d.is_a?(String)
@directory = File.expand_path(d, File.join(self.domain.config_dir, "ssl"))
@directory = File.expand_path(d, File.join(self.domain.config_dir, "ssl", "sets"))
if self.name.nil?
if self.domain.config_dir == @directory
......@@ -404,7 +404,6 @@ module Symbiosis
# OpenSSL::X509::CertificateError is raised.
#
def verify(certificate = self.certificate, key = self.key, store = self.certificate_store, strict_checking=false)
unless certificate.is_a?(OpenSSL::X509::Certificate) and key.is_a?(OpenSSL::PKey::PKey)
return false
end
......@@ -417,7 +416,7 @@ module Symbiosis
if strict_checking
raise OpenSSL::X509::CertificateError, msg
else
warn "\tSSL set #{name}: #{msg}" if $VERBOSE
puts "\tSSL set #{name}: #{msg}" if $VERBOSE
end
end
......@@ -438,15 +437,15 @@ module Symbiosis
# including any bundle that has been uploaded.
#
if store.verify(certificate)
puts "\tSSL set #{name}: certificate signed by \"#{certificate.issuer.to_s}\" for #{@domain.name}" if $VERBOSE
puts "\tSSL set #{name}: Signed by \"#{certificate.issuer.to_s}\" for #{@domain.name}" if $DEBUG
elsif store.error == 18
unless certificate.verify(key)
raise OpenSSL::X509::CertificateError, "\tSSL set #{name}: Certificate is self signed, but the signature doesn't validate."
raise OpenSSL::X509::CertificateError, "\tSSL set #{name}: Self signed, but the signature doesn't validate."
end
puts "\tSSL set #{name}: self-signed certificate for #{@domain.name}." if $VERBOSE
puts "\tSSL set #{name}: Self-signed certificate for #{@domain.name}." if $DEBUG
else
msg = "Certificate is not valid for #{@domain.name} -- "
msg = "Not valid for #{@domain.name} -- "
case store.error
when 2, 20
msg += "the intermediate bundle is missing"
......@@ -465,22 +464,34 @@ module Symbiosis
if strict_checking
raise OpenSSL::X509::CertificateError, msg
else
warn "\tSSL set #{name}: #{msg}" if $VERBOSE
puts "\tSSL set #{name}: #{msg}" if $VERBOSE
end
end
store.error
end
def write
raise ArgumentError, "The directory for this SSL certificate set has been given" if self.directory.nil?
raise Errno::EEXIST.new self.directory if File.exist?(self.directory)
mkdir_p(File.dirname(self.directory))
#
# Drop privs before creating the temporary directory, if required.
#
Process.egid = self.gid if Process.gid == 0
Process.euid = self.uid if Process.uid == 0
tmpdir = Dir.mktmpdir(self.name+"-ssl-")
#
# Restore privs -- set_param will use the owner/group of the directory
# when writing files.
#
Process.euid = 0 if Process.uid == 0
Process.egid = 0 if Process.gid == 0
combined = [:certificate, :bundle, :key].map{|k| self.__send__(k) }.flatten.compact
set_param("ssl.key",self.key.to_pem, tmpdir)
......@@ -493,6 +504,13 @@ module Symbiosis
FileUtils.mv(tmpdir, self.directory)
self.directory
ensure
#
# Make sure we restore privs.
#
Process.euid = 0 if Process.uid == 0 and Process.euid != Process.uid
Process.egid = 0 if Process.gid == 0 and Process.egid != Process.gid
end
end
......
......@@ -130,6 +130,12 @@ class TestDomain < Test::Unit::TestCase
end
def test_ips
domain = Domain.new()
domain.ips
end
def test_crypt_password
domain = Domain.new()
password = "correct horse battery staple"
......
......@@ -3,6 +3,7 @@ $:.unshift "../lib/" if File.directory?("../lib")
require 'test/unit'
require 'tmpdir'
require 'symbiosis/domain/ssl'
require 'symbiosis/ssl/selfsigned'
require 'mocha/test_unit'
class Symbiosis::SSL::Dummy < Symbiosis::SSL::CertificateSet
......@@ -39,6 +40,12 @@ class SSLTest < Test::Unit::TestCase
#
while Symbiosis::SSL::PROVIDERS.pop ; end
#
# And repopulate it with any existing providers
#
ObjectSpace.each_object(Class).select { |k| k < Symbiosis::SSL::CertificateSet }.
collect{|k| Symbiosis::SSL::PROVIDERS << k}
unless $DEBUG
@domain.destroy if @domain.is_a?( Symbiosis::Domain)
FileUtils.rm_rf(@prefix) if File.directory?(@prefix)
......@@ -643,6 +650,7 @@ class SSLTest < Test::Unit::TestCase
end
def test_ssl_provider
while Symbiosis::SSL::PROVIDERS.pop ; end
assert_equal(false, @domain.ssl_provider, "#ssl_provider should return false if no providers available")
......@@ -663,6 +671,8 @@ class SSLTest < Test::Unit::TestCase
end
def test_ssl_provider_class
while Symbiosis::SSL::PROVIDERS.pop ; end
assert_equal(nil, @domain.ssl_provider_class, "#ssl_provider_class should return nil if no providers available")
#
......@@ -682,6 +692,8 @@ class SSLTest < Test::Unit::TestCase
end
def test_ssl_fetch_new_certificate
while Symbiosis::SSL::PROVIDERS.pop ; end
assert_equal(nil, @domain.ssl_fetch_new_certificate, "#ssl_fetch_new_certificate should return nil if no providers available")
Symbiosis::SSL::PROVIDERS << Symbiosis::SSL::Dummy
......@@ -715,15 +727,15 @@ class SSLTest < Test::Unit::TestCase
assert_equal(set.certificate, cert)
assert_equal(set.request, request)
assert_equal("0", @domain.ssl_next_set)
assert_equal("0", @domain.ssl_next_set_name)
set.name = "0"
#
# Now write our set out.
#
dir = set.write
expected_dir = File.join(@prefix, @domain.name, "config", "ssl", "0")
assert_equal(File.join(@prefix, @domain.name, "config", "ssl", "0"), dir)
expected_dir = File.join(@prefix, @domain.name, "config", "ssl", "sets", "0")
assert_equal(expected_dir, dir)
#
# make sure everything verifies OK, both as key, crt, bundle, and combined.
......@@ -763,6 +775,8 @@ class SSLTest < Test::Unit::TestCase
# Set up our stuff
#
now = Time.now
ssl_dir = File.join(@domain.config_dir, "ssl")
sets_dir = File.join(ssl_dir, "sets")
not_before = now - 86400*2
not_after = now - 1
......@@ -779,7 +793,7 @@ class SSLTest < Test::Unit::TestCase
4.times do |i|
key, crt = do_generate_key_and_crt(@domain.name, {:ca_key => ca_key, :ca_cert => ca_cert, :not_before => not_before, :not_after => not_after})
set_dir = File.join(@domain.config_dir, "ssl", i.to_s)
set_dir = File.join(sets_dir, i.to_s)
Symbiosis::Utils.mkdir_p(set_dir)
Symbiosis::Utils.set_param("ssl.key", key, set_dir)
Symbiosis::Utils.set_param("ssl.crt", crt, set_dir)
......@@ -789,9 +803,9 @@ class SSLTest < Test::Unit::TestCase
not_after += 86400
end
current_path = File.join(@domain.config_dir, "ssl", "current")
current_path = File.join(ssl_dir, "current")
FileUtils.ln_sf("2", current_path)
FileUtils.ln_sf(File.expand_path("sets/2", ssl_dir), current_path)
available_sets = @domain.ssl_available_sets
......@@ -807,29 +821,90 @@ class SSLTest < Test::Unit::TestCase
# most recent set, so we should get false back, as nothing has changed.
#
assert_equal(false, @domain.ssl_rollover)
assert_equal("2", File.readlink(current_path))
assert_equal(File.expand_path("2", sets_dir), File.expand_path(File.readlink(current_path), ssl_dir))
#
# Now change the link, and it should get set back to "2"
#
File.unlink(current_path)
assert_equal(true, @domain.ssl_rollover)
assert_equal("2", File.readlink(current_path))
assert_equal(File.expand_path("2", sets_dir), File.expand_path(File.readlink(current_path), ssl_dir))
File.unlink(current_path)
File.symlink("1", current_path)
assert_equal("1", File.readlink(current_path))
File.symlink("sets/1", current_path)
assert_equal(File.expand_path("1", sets_dir), File.expand_path(File.readlink(current_path), ssl_dir))
assert_equal(true, @domain.ssl_rollover)
assert_equal("2", File.readlink(current_path))
assert_equal(File.expand_path("2", sets_dir), File.expand_path(File.readlink(current_path), ssl_dir))
#
# OK now remove all the valid sets of certs
# OK now remove the current set, and see if we cope with broken symlinks
#
FileUtils.remove_entry_secure(File.join(@domain.config_dir, "ssl", "1"))
FileUtils.remove_entry_secure(File.join(@domain.config_dir, "ssl", "2"))
File.unlink(current_path)
FileUtils.remove_entry_secure(File.join(sets_dir, "2"))
assert_equal(true, @domain.ssl_rollover)
assert_equal(File.expand_path("1", sets_dir), File.expand_path(File.readlink(current_path), ssl_dir))
end
assert_equal(false, @domain.ssl_rollover)
def test_ssl_magic
#
# This requires the Self-signed provider to be in place
#
@domain.ssl_provider = "selfsigned"
ssl_dir = File.join(@domain.config_dir, "ssl")
sets_dir = File.join(ssl_dir, "sets")
Symbiosis::Utils.mkdir_p(sets_dir)
#
# Test with the no_generate flag
#
result = nil
assert_nothing_raised{ result = @domain.ssl_magic(14, false) }
assert(!result)
assert(Dir.glob( File.join(sets_dir, '*') ).empty?, "There should be no sets generated if do_generate is false" )
#
# Test with the no_rollover flag. This should generate a set, but not link it to current.
#
expected_dir = File.join(sets_dir, @domain.ssl_next_set_name)
current_dir = File.join(ssl_dir, "current")
assert_nothing_raised{ result = @domain.ssl_magic(14, true, false) }
assert(File.directory?(expected_dir), "A set should have been generated in #{expected_dir}")
assert(!File.exist?(current_dir), "The link to current should not have been generated yet")
#
# Now run it normally. This should create the symlink
#
assert_nothing_raised{ result = @domain.ssl_magic(14) }
assert(File.exist?(current_dir), "The link to current should have been generated.")
assert_equal(expected_dir, File.readlink(current_dir))
#
# Now test rolling over lots of times
#
10.times do |s|
expected_dir = File.join(sets_dir, @domain.ssl_next_set_name)
assert_nothing_raised{ result = @domain.ssl_magic(500) }
assert(File.directory?(expected_dir), "Failed to generate set #{expected_dir} correctly")
assert_equal(expected_dir, File.readlink(current_dir))
end
#
# Now create a directory in the way
#
expected_dir = File.join(sets_dir, @domain.ssl_next_set_name)
Symbiosis::Utils.mkdir_p(expected_dir)
assert_nothing_raised{ result = @domain.ssl_magic(500) }
#
# Now create a file in the way
#
expected_dir = File.join(sets_dir, @domain.ssl_next_set_name)
FileUtils.touch(expected_dir)
assert_nothing_raised{ result = @domain.ssl_magic(500) }
end
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment