diff options
| -rw-r--r-- | lib/leap_cli/acme.rb | 101 | ||||
| -rw-r--r-- | lib/leap_cli/commands/ca.rb | 178 | ||||
| -rw-r--r-- | lib/leap_cli/config/manager.rb | 18 | ||||
| -rw-r--r-- | lib/leap_cli/config/node_cert.rb | 4 | ||||
| -rw-r--r-- | lib/leap_cli/config/object.rb | 26 | ||||
| -rw-r--r-- | lib/leap_cli/config/object_list.rb | 6 | ||||
| -rw-r--r-- | lib/leap_cli/log_filter.rb | 4 | ||||
| -rw-r--r-- | lib/leap_cli/ssh/backend.rb | 7 | ||||
| -rw-r--r-- | lib/leap_cli/ssh/formatter.rb | 2 | ||||
| -rw-r--r-- | lib/leap_cli/ssh/scripts.rb | 10 | ||||
| -rw-r--r-- | platform.rb | 2 | ||||
| -rw-r--r-- | provider_base/common.json | 7 | ||||
| -rw-r--r-- | provider_base/common.rb | 72 | ||||
| -rw-r--r-- | puppet/modules/site_apache/files/conf.d/acme.conf | 10 | ||||
| -rw-r--r-- | puppet/modules/site_apache/manifests/common.pp | 2 | ||||
| -rw-r--r-- | puppet/modules/site_apache/manifests/common/acme.pp | 38 | ||||
| -rw-r--r-- | puppet/modules/site_config/manifests/x509/commercial/ca.pp | 10 | 
17 files changed, 477 insertions, 20 deletions
| diff --git a/lib/leap_cli/acme.rb b/lib/leap_cli/acme.rb new file mode 100644 index 00000000..6c7dbe98 --- /dev/null +++ b/lib/leap_cli/acme.rb @@ -0,0 +1,101 @@ +require 'openssl' +require 'acme-client' + +# +# A little bit of sugar around gem acme-client +# + +module LeapCli +  class Acme + +    if ENV['ACME_STAGING'] +      ENDPOINT = 'https://acme-staging.api.letsencrypt.org/' +      puts "using endpoint " + ENDPOINT +    else +      ENDPOINT = 'https://acme-v01.api.letsencrypt.org/' +    end + +    def initialize(domain: nil, key:) +      @client = ::Acme::Client.new( +        private_key: key, +        endpoint: ENDPOINT, +        connection_options: {request: {open_timeout: 5, timeout: 5}} +      ) +      @domain = domain +    end + +    # +    # static methods +    # + +    def self.new_private_key +      return OpenSSL::PKey::RSA.new(4096) +    end + +    def self.load_private_key(pem_encoded_key) +      return OpenSSL::PKey::RSA.new(pem_encoded_key) +    end + +    def self.load_csr(pem_encoded_csr) +      return OpenSSL::X509::Request.new(pem_encoded_csr) +    end + +    # +    # instance methods +    # + +    # +    # register a new account key with CA +    # +    def register(contact) +      registration = @client.register(contact: 'mailto:' + contact) +      if registration && registration.agree_terms +        return registration +      else +        return false +      end +    end + +    # +    # authorize account key for domain +    # +    def authorize +      authorization = @client.authorize(domain: @domain) +      challenge = nil +      begin +        while true +          if authorization.status == 'pending' +            challenge = authorization.http01 +            yield challenge +            challenge.request_verification +            sleep 1 +            authorization.verify_status +            if challenge.error +              return 'error', challenge.error +            end +          elsif authorization.status == 'invalid' +            challenge_msg = (challenge.nil? ? '' : challenge.error) +            return 'error', 'Something bad happened. %s' % challenge_msg +          elsif authorization.status == 'valid' +            return 'valid', nil +          else +            challenge_msg = (challenge.nil? ? '' : challenge.error) +            return 'error', 'status: %s, response message: %s' % [authorization.status, challenge_msg] +          end +        end +      rescue Interrupt +        return 'error', 'interrupted' +      end +    rescue ::Acme::Client::Error::Unauthorized => exc +      return 'unauthorized', exc.to_s +    end + +    # +    # get new certificate +    # +    def get_certificate(csr) +      return @client.new_certificate(csr) +    end + +  end +end
\ No newline at end of file diff --git a/lib/leap_cli/commands/ca.rb b/lib/leap_cli/commands/ca.rb index f998d0fe..d9ffa6a4 100644 --- a/lib/leap_cli/commands/ca.rb +++ b/lib/leap_cli/commands/ca.rb @@ -38,6 +38,7 @@ module LeapCli; module Commands      cert.long_desc "Unless specified, the CSR is created for the provider's primary domain. "+        "The properties used for this CSR come from `provider.ca.server_certificates`, "+        "but may be overridden here." +    cert.arg_name "DOMAIN"      cert.command :csr do |csr|        csr.flag 'domain', :arg_name => 'DOMAIN', :desc => 'Specify what domain to create the CSR for.'        csr.flag ['organization', 'O'], :arg_name => 'ORGANIZATION', :desc => "Override default O in distinguished name." @@ -52,6 +53,23 @@ module LeapCli; module Commands          generate_csr(global_options, options, args)        end      end + +    cert.desc "Register an authorization key with the CA letsencrypt.org" +    cert.long_desc "This only needs to be done once." +    cert.command :register do |register| +      register.action do |global, options, args| +        do_register_key(global, options, args) +      end +    end + +    cert.desc "Renews a certificate using the CA letsencrypt.org" +    cert.arg_name "DOMAIN" +    cert.command :renew do |renew| +      renew.action do |global, options, args| +        do_renew_cert(global, options, args) +      end +    end +    end    protected @@ -150,7 +168,7 @@ module LeapCli; module Commands      assert_config! 'provider.ca.server_certificates.digest'      server_certificates      = provider.ca.server_certificates -    options[:domain]       ||= provider.domain +    options[:domain]       ||= args.first || provider.domain      options[:organization] ||= provider.name[provider.default_language]      options[:country]      ||= server_certificates['country']      options[:state]        ||= server_certificates['state'] @@ -166,4 +184,162 @@ module LeapCli; module Commands      X509.create_csr_and_cert(options)    end +  # +  # letsencrypt.org +  # + +  def do_register_key(global, options, args) +    require 'leap_cli/acme' +    assert_config! 'provider.contacts.default' +    contact = manager.provider.contacts.default.first + +    if file_exists?(:acme_key) && !global[:force] +      bail! do +        log "the authorization key for letsencrypt.org already exists" +        log "run with --force if you really want to register a new key." +      end +    else +      private_key = Acme.new_private_key +      registration = nil + +      log(:registering, "letsencrypt.org authorization key using contact `%s`" % contact) do +        acme = Acme.new(key: private_key) +        registration = acme.register(contact) +        if registration +          log 'success!', :color => :green, :style => :bold +        else +          bail! "could not register authorization key." +        end +      end + +      log :saving, "authorization key for letsencrypt.org" do +        write_file!(:acme_key, private_key.to_pem) +        write_file!(:acme_info, JSON.sorted_generate({ +          id: registration.id, +          contact: registration.contact, +          key: registration.key, +          uri: registration.uri +        })) +        log :warning, "keep key file private!" +      end +    end +  end + +  def do_renew_cert(global, options, args) +    require 'leap_cli/acme' +    require 'leap_cli/ssh' +    require 'socket' +    require 'net/http' + +    # +    # sanity check the domain +    # +    domain = args.first +    nodes  = nodes_for_domain(domain) +    domain_ready_for_acme!(domain) + +    # +    # load key material +    # +    assert_files_exist!([:commercial_key, domain], [:commercial_csr, domain], +      :msg => 'Please create the CSR first with `leap cert csr %s`' % domain) +    csr = Acme.load_csr(read_file!([:commercial_csr, domain])) +    assert_files_exist!(:acme_key, +      :msg => "Please run `leap cert register` first. This only needs to be done once.") +    account_key = Acme.load_private_key(read_file!(:acme_key)) + +    # +    # check authorization for this domain +    # +    log :checking, "authorization" +    acme = Acme.new(domain: domain, key: account_key) +    status, message = acme.authorize do |challenge| +      log(:uploading, 'challenge to server %s' % domain) do +        SSH.remote_command(nodes) do |ssh, host| +          ssh.scripts.upload_acme_challenge(challenge.token, challenge.file_content) +        end +      end +      log :waiting, "for letsencrypt.org to verify challenge" +    end +    if status == 'valid' +      log 'authorized!', color: :green, style: :bold +    elsif status == 'error' +      bail! :error, message +    elsif status == 'unauthorized' +      bail!(:unauthorized, message, color: :yellow, style: :bold) do +        log 'You must first run `leap cert register` to register the account key with letsencrypt.org' +      end +    end + +    log :fetching, "new certificate from letsencrypt.org" +    cert = acme.get_certificate(csr) +    write_file!([:commercial_cert, domain], cert.fullchain_to_pem) +  end + +  # +  # Returns a hash of nodes that match this domain. It also checks: +  # +  # * a node configuration has this domain +  # * the dns for the domain exists +  # +  # This method will bail if any checks fail. +  # +  def nodes_for_domain(domain) +    bail! { log 'Argument DOMAIN is required' } if domain.nil? || domain.empty? +    nodes = manager.nodes['dns.aliases' => domain] +    if nodes.empty? +      bail! :error, "There are no nodes configured for domain `%s`" % domain +    end +    begin +      ips = Socket.getaddrinfo(domain, 'http').map {|record| record[2]}.uniq +      nodes = nodes['ip_address' => ips] +      if nodes.empty? +        bail! do +          log :error, "The domain `%s` resolves to [%s]" % [domain, ips.join(', ')] +          log :error, "But there no nodes configured for this domain with these adddresses." +        end +      end +    rescue SocketError +      bail! :error, "Could not resolve the DNS for `#{domain}`. Without a DNS " + +        "entry for this domain, authorization will not work." +    end +    return nodes +  end + +  # +  # runs the following checks on the domain: +  # +  # * we are able to get /.well-known/acme-challenge/ok +  # +  # This method will bail if any checks fail. +  # +  def domain_ready_for_acme!(domain) +    begin +      uri = URI("https://#{domain}/.well-known/acme-challenge/ok") +      options = { +        use_ssl: true, +        open_timeout: 5, +        verify_mode: OpenSSL::SSL::VERIFY_NONE +      } +      Net::HTTP.start(uri.host, uri.port, options) do |http| +        http.request(Net::HTTP::Get.new(uri)) do |response| +          if !response.is_a?(Net::HTTPSuccess) +            bail!(:error, "Could not GET %s" % uri) do +              log "%s %s" % [response.code, response.message] +              log "You may need to run `leap deploy`" +            end +          end +        end +      end +    rescue Errno::ETIMEDOUT, Net::OpenTimeout +      bail! :error, "Connection attempt timed out: %s" % uri +    rescue Interrupt +      bail! +    rescue StandardError => exc +      bail!(:error, "Could not GET %s" % uri) do +        log exc.to_s +      end +    end +  end +  end; end diff --git a/lib/leap_cli/config/manager.rb b/lib/leap_cli/config/manager.rb index e39334c8..bdd5b255 100644 --- a/lib/leap_cli/config/manager.rb +++ b/lib/leap_cli/config/manager.rb @@ -405,15 +405,23 @@ module LeapCli        end        # -      # returns a list of 'control' files for this node. -      # a control file is like a service or a tag JSON file, but it contains -      # raw ruby code that gets evaluated in the context of the node. -      # Yes, this entirely breaks our functional programming model -      # for JSON generation. +      # Returns a list of 'control' files for this node. A control file is like +      # a service or a tag JSON file, but it contains raw ruby code that gets +      # evaluated in the context of the node. +      # +      # Yes, this entirely breaks our functional programming model for JSON +      # generation. +      # +      # Control files are evaluated last, after everything else has run.        #        def control_files(node)          files = []          [Path.provider_base, @provider_dir].each do |provider_dir| +          # add common.rb +          common = File.join(provider_dir, 'common.rb') +          files << common if File.exist?(common) + +          # add services/*.rb and tags/*.rb, as appropriate for this node            [['services', :service_config], ['tags', :tag_config]].each do |attribute, path_sym|              node[attribute].each do |attr_value|                path = Path.named_path([path_sym, "#{attr_value}.rb"], provider_dir).sub(/\.json$/,'') diff --git a/lib/leap_cli/config/node_cert.rb b/lib/leap_cli/config/node_cert.rb index 64842ffa..da63d621 100644 --- a/lib/leap_cli/config/node_cert.rb +++ b/lib/leap_cli/config/node_cert.rb @@ -109,10 +109,10 @@ module LeapCli; module Config            path = Path.relative_path([:commercial_cert, domain])            if cert.not_after < Time.now.utc              Util.log :error, "the commercial certificate '#{path}' has EXPIRED! " + -              "You should renew it with `leap cert csr --domain #{domain}`." +              "You should renew it with `leap cert renew #{domain}`."            elsif cert.not_after < Time.now.advance(:months => 2)              Util.log :warning, "the commercial certificate '#{path}' will expire soon (#{cert.not_after}). "+ -              "You should renew it with `leap cert csr --domain #{domain}`." +              "You should renew it with `leap cert renew #{domain}`."            end          end        end diff --git a/lib/leap_cli/config/object.rb b/lib/leap_cli/config/object.rb index b117c2f0..16c41999 100644 --- a/lib/leap_cli/config/object.rb +++ b/lib/leap_cli/config/object.rb @@ -153,6 +153,28 @@ module LeapCli          end        end +      # +      # works like Hash#store(key, value), but supports our nested dot notation, +      # just like get() does. +      # +      def set(key, value) +        key = key.to_s +        # for keys with with '.' in them, we pop off the first part +        # and recursively call ourselves. +        if key =~ /\./ +          keys = key.split('.') +          parent_value = self.get!(keys.first) +          if parent_value.is_a?(Config::Object) +            parent_value.set(keys[1..-1].join('.'), value) +          else +            parent_value.store(keys[1..-1].join('.'), value) +          end +        else +          self.store(key, value) +        end +        return nil +      end +        ##        ## COPYING        ## @@ -376,12 +398,16 @@ module LeapCli        def fetch_value(key, context=@node)          value = fetch(key, nil)          if value.is_a?(String) && value =~ /^=/ +          # strings prefix with '=' are evaluated as ruby code.            if value =~ /^=> (.*)$/              value = evaluate_later(key, $1)            elsif value =~ /^= (.*)$/              value = context.evaluate_ruby(key, $1)            end            self[key] = value +        elsif value.is_a?(Proc) +          # the value might be a proc, set by a 'control' file +          self[key] = value.call          end          return value        end diff --git a/lib/leap_cli/config/object_list.rb b/lib/leap_cli/config/object_list.rb index f9299a61..80f89d92 100644 --- a/lib/leap_cli/config/object_list.rb +++ b/lib/leap_cli/config/object_list.rb @@ -84,6 +84,12 @@ module LeapCli                elsif operator == :not_equal && !value.include?(match_value)                  results[name] = config                end +            elsif match_value.is_a? Array +              if operator == :equal && match_value.include?(value) +                results[name] = config +              elsif operator == :not_equal && !match_value.include?(value) +                results[name] = config +              end              else                if operator == :equal && value == match_value                  results[name] = config diff --git a/lib/leap_cli/log_filter.rb b/lib/leap_cli/log_filter.rb index 28504e1a..c73f3a91 100644 --- a/lib/leap_cli/log_filter.rb +++ b/lib/leap_cli/log_filter.rb @@ -119,14 +119,14 @@ module LeapCli        { :match => /created/, :color => :green, :style => :bold },        { :match => /completed/, :color => :green, :style => :bold },        { :match => /ran/, :color => :green, :style => :bold }, -      { :match => /registered/, :color => :green, :style => :bold }, +      { :match => /^registered/, :color => :green, :style => :bold },        # cyan        { :match => /note/, :replace => 'NOTE:', :color => :cyan, :style => :bold },        # magenta        { :match => /nochange/, :replace => 'no change', :color => :magenta }, -      { :match => /loading/, :color => :magenta }, +      { :match => /^loading/, :color => :magenta },      ]      def self.apply_message_filters(message) diff --git a/lib/leap_cli/ssh/backend.rb b/lib/leap_cli/ssh/backend.rb index c1afc993..3894d815 100644 --- a/lib/leap_cli/ssh/backend.rb +++ b/lib/leap_cli/ssh/backend.rb @@ -120,7 +120,12 @@ module LeapCli        #        def upload!(src, dest, options={})          if options[:mode] -          super(StringIO.new(File.read(src)), dest, options) +          if src.is_a?(StringIO) +            content = src +          else +            content = StringIO.new(File.read(src)) +          end +          super(content, dest, options)          else            super(src, dest, options)          end diff --git a/lib/leap_cli/ssh/formatter.rb b/lib/leap_cli/ssh/formatter.rb index bab43011..c2e386dc 100644 --- a/lib/leap_cli/ssh/formatter.rb +++ b/lib/leap_cli/ssh/formatter.rb @@ -16,7 +16,7 @@ module LeapCli        }        def initialize(logger, host, options={}) -        @logger = logger +        @logger = logger || LeapCli.new_logger          @host = host          @options = DEFAULT_OPTIONS.merge(options)        end diff --git a/lib/leap_cli/ssh/scripts.rb b/lib/leap_cli/ssh/scripts.rb index 9fef6240..3dd6b604 100644 --- a/lib/leap_cli/ssh/scripts.rb +++ b/lib/leap_cli/ssh/scripts.rb @@ -134,6 +134,16 @@ module LeapCli          ssh.execute 'cp', '/home/admin/.ssh/authorized_keys', '/root/.ssh/authorized_keys'        end +      # +      # uploads an acme challenge for renewing certificates using Let's Encrypt CA. +      # +      # Filename is returned from acme api, so it must not be trusted. +      # +      def upload_acme_challenge(filename, content) +        path = '/srv/acme/' + filename.gsub(/[^a-zA-Z0-9_-]/, '') +        ssh.upload! StringIO.new(content), path, :mode => 0444 +      end +        private        def flagize(hsh) diff --git a/platform.rb b/platform.rb index 5a286589..2ff0a27f 100644 --- a/platform.rb +++ b/platform.rb @@ -78,6 +78,8 @@ Leap::Platform.define do      :client_ca_key    => 'files/ca/client_ca.key',      :client_ca_cert   => 'files/ca/client_ca.crt',      :dh_params        => 'files/ca/dh.pem', +    :acme_key         => 'files/ca/lets-encrypt-account.key', +    :acme_info        => 'files/ca/lets-encrypt-account.json',      :commercial_key   => 'files/cert/#{arg}.key',      :commercial_csr   => 'files/cert/#{arg}.csr',      :commercial_cert  => 'files/cert/#{arg}.crt', diff --git a/provider_base/common.json b/provider_base/common.json index e9531eee..622bca38 100644 --- a/provider_base/common.json +++ b/provider_base/common.json @@ -29,12 +29,7 @@    "x509": {      "use": true,      "use_commercial": false, -    "cert": "= x509.use ? file(:node_x509_cert, :missing => 'x509 certificate for node $node. Run `leap cert update`') : nil", -    "key": "= x509.use ? file(:node_x509_key, :missing => 'x509 key for node $node. Run `leap cert update`') : nil", -    "ca_cert": "= try_file :ca_cert", -    "commercial_cert": "= x509.use_commercial ? file([:commercial_cert, try{webapp.domain}||domain.full_suffix], :missing => 'commercial x509 certificate for node $node. Add file $file, or run `leap cert csr --domain %s` to generate a temporary self-signed cert and CSR you can use to purchase a real cert.' % (try{webapp.domain}||domain.full_suffix)) : nil", -    "commercial_key": "= x509.use_commercial ? file([:commercial_key, try{webapp.domain}||domain.full_suffix], :missing => 'commercial x509 certificate for node $node. Add file $file, or run `leap cert csr --domain %s` to generate a temporary self-signed cert and CSR you can use to purchase a real cert.' % (try{webapp.domain}||domain.full_suffix)) : nil", -    "commercial_ca_cert": "= x509.use_commercial ? try_file(:commercial_ca_cert) : nil" +    "ca_cert": "= try_file :ca_cert"    },    "service_type": "internal_service",    "development": { diff --git a/provider_base/common.rb b/provider_base/common.rb new file mode 100644 index 00000000..a8cc6717 --- /dev/null +++ b/provider_base/common.rb @@ -0,0 +1,72 @@ +## +## common.rb -- evaluated (last) for every node. +## +## Because common.rb is evaluated last, it is good practice to only modify +## values here if they are empty. This gives a chance for tags and services +## to set values. +## + +# +# X509 server certificates that use our own CA +# + +if self['x509.use'] +  if self['x509.cert'].nil? +    self.set('x509.cert', lambda{file( +      :node_x509_cert, +      :missing => "x509 certificate for node $node. Run `leap cert update` to generate it." +    )}) +  end +  if self['x509.key'].nil? +    self.set('x509.key', lambda{file( +     :node_x509_key, +      :missing => "x509 key for node $node. Run `leap cert update` to generate it." +    )}) +  end +else +  self.set('x509.cert', nil) +  self.set('x509.key', nil) +end + +# +# X509 server certificates that use an external CA +# + +if self['x509.use_commercial'] +  domain = self['webapp.domain'] || self['domain.full_suffix'] +  if self['x509.commercial_cert'].nil? +    self.set('x509.commercial_cert', lambda{file( +      [:commercial_cert, domain], +      :missing => "commercial x509 certificate for node `$node`. " + +        "Add file $file, or run `leap cert csr %s`." % domain +    )}) +  end +  if self['x509.commercial_key'].nil? +    self.set('x509.commercial_key', lambda{file( +      [:commercial_key, domain], +      :missing => "commercial x509 key for node `$node`. " + +        "Add file $file, or run `leap cert csr %s`" % domain +    )}) +  end + +  # +  # the content of x509.commercial_cert might include the cert +  # and the full CA chain, or it might just be the cert only. +  # +  # if it is the cert only, then we want to additionally specify +  # 'commercial_ca_cert'. Otherwise, we leave this empty. +  # +  if self['x509.commercial_ca_cert'].nil? +    self.set('x509.commercial_ca_cert', lambda{ +      if self['x509.commercial_cert'].scan(/BEGIN CERTIFICATE/).length == 1 +        try_file(:commercial_ca_cert) +      else +        nil +      end +    }) +  end +else +  self.set('x509.commercial_cert', nil) +  self.set('x509.commercial_key', nil) +  self.set('x509.commercial_ca_cert', nil) +end diff --git a/puppet/modules/site_apache/files/conf.d/acme.conf b/puppet/modules/site_apache/files/conf.d/acme.conf new file mode 100644 index 00000000..cdddf53e --- /dev/null +++ b/puppet/modules/site_apache/files/conf.d/acme.conf @@ -0,0 +1,10 @@ +# +# Allow ACME certificate verification if /srv/acme exists. +# +<IfModule mod_headers.c> +  Alias "/.well-known/acme-challenge/" "/srv/acme/" +  <Directory "/srv/acme/*"> +    Require all granted +    Header set Content-Type "application/jose+json" +  </Directory> +</IfModule> diff --git a/puppet/modules/site_apache/manifests/common.pp b/puppet/modules/site_apache/manifests/common.pp index 8a11759a..208c15d5 100644 --- a/puppet/modules/site_apache/manifests/common.pp +++ b/puppet/modules/site_apache/manifests/common.pp @@ -27,4 +27,6 @@ class site_apache::common {    }    include site_apache::common::tls +  include site_apache::common::acme +  } diff --git a/puppet/modules/site_apache/manifests/common/acme.pp b/puppet/modules/site_apache/manifests/common/acme.pp new file mode 100644 index 00000000..eda4148b --- /dev/null +++ b/puppet/modules/site_apache/manifests/common/acme.pp @@ -0,0 +1,38 @@ +# +# Allows for potential ACME validations (aka Let's Encrypt) +# +class site_apache::common::acme { +  # +  # well, this doesn't work: +  # +  # apache::config::global {'acme.conf':} +  # +  # since /etc/apache2/conf.d is NEVER LOADED BY APACHE +  # https://gitlab.com/shared-puppet-modules-group/apache/issues/11 +  # + +  file { +    '/etc/apache2/conf-available/acme.conf': +      ensure  => present, +      source  => 'puppet:///modules/site_apache/conf.d/acme.conf', +      require => Package[apache], +      notify  => Service[apache]; +    '/etc/apache2/conf-enabled/acme.conf': +      ensure  => link, +      target  => '/etc/apache2/conf-available/acme.conf', +      require => Package[apache], +      notify  => Service[apache]; +  } + +  file { +    '/srv/acme': +      ensure => 'directory', +      owner => 'www-data', +      group => 'www-data', +      mode => '0755'; +    '/srv/acme/ok': +      owner => 'www-data', +      group => 'www-data', +      content => 'ok'; +  } +} diff --git a/puppet/modules/site_config/manifests/x509/commercial/ca.pp b/puppet/modules/site_config/manifests/x509/commercial/ca.pp index c76a9dbb..21d57445 100644 --- a/puppet/modules/site_config/manifests/x509/commercial/ca.pp +++ b/puppet/modules/site_config/manifests/x509/commercial/ca.pp @@ -5,7 +5,13 @@ class site_config::x509::commercial::ca {    $x509      = hiera('x509')    $ca        = $x509['commercial_ca_cert'] -  x509::ca { $site_config::params::commercial_ca_name: -    content => $ca +  # +  # CA cert might be empty, if it was bundled with 'commercial_cert' +  # instead of specified separately. +  # +  if ($ca) { +    x509::ca { $site_config::params::commercial_ca_name: +      content => $ca +    }    }  } | 
