summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorelijah <elijah@riseup.net>2016-08-31 14:54:46 -0700
committerelijah <elijah@riseup.net>2016-09-01 10:49:22 -0700
commit8116e007cfd4dbee8282247348cf45473dcde45e (patch)
treeecf8cfbc790ef57c3519c947a1fa76d0c1a4e5a2
parentd679399af0898b959b8b84a8e8d1e2e03c4e21b5 (diff)
added support for Let's Encrypt
-rw-r--r--lib/leap_cli/acme.rb101
-rw-r--r--lib/leap_cli/commands/ca.rb178
-rw-r--r--lib/leap_cli/config/manager.rb18
-rw-r--r--lib/leap_cli/config/node_cert.rb4
-rw-r--r--lib/leap_cli/config/object.rb26
-rw-r--r--lib/leap_cli/config/object_list.rb6
-rw-r--r--lib/leap_cli/log_filter.rb4
-rw-r--r--lib/leap_cli/ssh/backend.rb7
-rw-r--r--lib/leap_cli/ssh/formatter.rb2
-rw-r--r--lib/leap_cli/ssh/scripts.rb10
-rw-r--r--platform.rb2
-rw-r--r--provider_base/common.json7
-rw-r--r--provider_base/common.rb72
-rw-r--r--puppet/modules/site_apache/files/conf.d/acme.conf10
-rw-r--r--puppet/modules/site_apache/manifests/common.pp2
-rw-r--r--puppet/modules/site_apache/manifests/common/acme.pp38
-rw-r--r--puppet/modules/site_config/manifests/x509/commercial/ca.pp10
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
+ }
}
}