From 2b1edbc6c085838855182bd28fabf2266701bae2 Mon Sep 17 00:00:00 2001 From: Roland Moriz Date: Tue, 4 Apr 2017 07:11:50 +0200 Subject: [PATCH 1/3] initial implementation of persistence --- resources/persistence.rb | 143 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 resources/persistence.rb diff --git a/resources/persistence.rb b/resources/persistence.rb new file mode 100644 index 0000000..a113b3b --- /dev/null +++ b/resources/persistence.rb @@ -0,0 +1,143 @@ +resource_name :acme_persistence + +property :cn, String, name_property: true +property :alt_names, Array +property :key, String, required: true +property :crt, String +property :chain, String +property :fullchain, String + +property :master, [TrueClass, FalseClass], default: false + +property :data_bag_name, String, required: true +property :encrypt, [TrueClass, FalseClass], default: true +property :secret, String, default: Chef::EncryptedDataBagItem.load_secret(Chef::Config[:encrypted_data_bag_secret]) + +property :owner, String, default: 'root' +property :group, String, default: 'root' + +action :save do + unless master + Chef::Log.warn "master property not set, will not save #{cn} certificates" + return + end + + data = { + 'id' => cn, + 'alt_names' => alt_names, + } + + data['key'] = ::File.read(new_resource.key) if new_resource.key + data['crt'] = ::File.read(new_resource.crt) if new_resource.crt + data['chain'] = ::File.read(new_resource.chain) if new_resource.chain + data['fullchain'] = ::File.read(new_resource.fullchain) if new_resource.fullchain + + data['created_by'] = node['fqdn'] + data['created_at'] = Time.now + + chef_data_bag_item "#{data_bag_name}/#{cn}" do + raw_data data + if new_resource.encrypt && new_resource.secret + encrypt true + encryption_version 2 + secret new_resource.secret + end + end +end + +# Matrix: +# +# +------------------------+-----------------+--------------------------------------+ +# | file | data bag item | action | +# | ---------------------- | --------------- | ------------------------------------ | +# | does not exist | exists | create from data bag item | +# | does not exist | does not exist | nothing (-> acme_selfsigned) | +# | exists (self-signed) | exists | create from data bag item | +# | exists (self-signed) | does not exist | nothing (~> renew acme_certificate) | +# | exists (valid) | is newer | create from data bag item | +# | exists (valid) | is older | nothing (~> renew acme_certificate) | +# | exists (expired) | is newer | create from data bag item | +# | exists (expired) | does not exist | nothing (~> renew acme_certificate | +# +------------------------+-----------------+--------------------------------------+ +# +action :load do + begin + existing_cert = ::OpenSSL::X509::Certificate.new(::File.read(crt || fullchain)) + rescue Errno::ENOENT => e + Chef::Log.warn("certificate file #{crt || fullchain} does not exist yet: #{e}") + rescue OpenSSL::X509::CertificateError => e + Chef::Log.error("certificate file #{crt || fullchain} exists but is broken: #{e}") + end + + item = load_data_bag_item(data_bag_name, 'id:' + cn, secret) + return unless item + + render_to_files(item) if !existing_cert || + self_signed?(existing_cert) || + item_newer?(item, existing_cert) +end + +action_class do + def load_data_bag_item(data_bag_name, _data_bag_item, secret = nil) + item = search(data_bag_name, 'id:' + cn).first + item = ::Chef::EncryptedDataBagItem.new(item, secret) if item && secret + item + end + + def self_signed?(cert) + cert.issuer == cert.subject + end + + def item_newer?(item, existing_cert) + item_cert = ::OpenSSL::X509::Certificate.new item['crt'] if item['crt'] + item_cert ||= ::OpenSSL::X509::Certificate.new item['fullchain'] if item['fullchain'] + item_cert.not_before > existing_cert.not_before + rescue OpenSSL::X509::CertificateError => e + Chef::Log.error("data bag item #{new_resource.data_bag_name}/#{item['id']} is broken: #{e}") + end + + def render_to_files(item) + file "acme_store: #{new_resource.cn} SSL key" do + path new_resource.key + owner new_resource.owner + group new_resource.group + mode 00400 + content item['key'] + sensitive true + action :create + end + + file "acme_store: #{new_resource.cn} SSL crt" do + path new_resource.crt + owner new_resource.owner + group new_resource.group + mode 00644 + content item['crt'] + action :create + + only_if { !!item['crt'] } + end + + file "acme_store: #{new_resource.cn} SSL fullchain" do + path new_resource.fullchain + owner new_resource.owner + group new_resource.group + mode 00644 + content item['fullchain'] + action :create + + only_if { !!item['fullchain'] } + end + + file "acme_store: #{new_resource.cn} SSL chain" do + path new_resource.chain + owner new_resource.owner + group new_resource.group + mode 00644 + content item['chain'] + action :create + + only_if { !!item['chain'] } + end + end +end From fbb6023a064d649ed3087dadabbb868b673c4173 Mon Sep 17 00:00:00 2001 From: Roland Moriz Date: Tue, 4 Apr 2017 10:51:38 +0200 Subject: [PATCH 2/3] compatibility with certificate cookbook --- resources/persistence.rb | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/resources/persistence.rb b/resources/persistence.rb index a113b3b..ae26bc0 100644 --- a/resources/persistence.rb +++ b/resources/persistence.rb @@ -11,7 +11,7 @@ property :data_bag_name, String, required: true property :encrypt, [TrueClass, FalseClass], default: true -property :secret, String, default: Chef::EncryptedDataBagItem.load_secret(Chef::Config[:encrypted_data_bag_secret]) +property :secret, String property :owner, String, default: 'root' property :group, String, default: 'root' @@ -23,24 +23,26 @@ end data = { - 'id' => cn, - 'alt_names' => alt_names, + 'id' => cn, + 'alt_names' => alt_names, + 'created_by' => node['fqdn'], + 'created_at' => Time.now } + # 'key', 'cert', 'chain' are also used in the data bag format used by + # https://github.com/atomic-penguin/cookbook-certificate/blob/master/providers/manage.rb data['key'] = ::File.read(new_resource.key) if new_resource.key - data['crt'] = ::File.read(new_resource.crt) if new_resource.crt + data['cert'] = ::File.read(new_resource.crt) if new_resource.crt data['chain'] = ::File.read(new_resource.chain) if new_resource.chain - data['fullchain'] = ::File.read(new_resource.fullchain) if new_resource.fullchain - data['created_by'] = node['fqdn'] - data['created_at'] = Time.now + data['fullchain'] = ::File.read(new_resource.fullchain) if new_resource.fullchain chef_data_bag_item "#{data_bag_name}/#{cn}" do raw_data data - if new_resource.encrypt && new_resource.secret + if new_resource.encrypt && (new_resource.secret || default_data_bag_secret) encrypt true encryption_version 2 - secret new_resource.secret + secret new_resource.secret || default_data_bag_secret end end end @@ -89,7 +91,7 @@ def self_signed?(cert) end def item_newer?(item, existing_cert) - item_cert = ::OpenSSL::X509::Certificate.new item['crt'] if item['crt'] + item_cert = ::OpenSSL::X509::Certificate.new item['cert'] if item['cert'] item_cert ||= ::OpenSSL::X509::Certificate.new item['fullchain'] if item['fullchain'] item_cert.not_before > existing_cert.not_before rescue OpenSSL::X509::CertificateError => e @@ -112,10 +114,10 @@ def render_to_files(item) owner new_resource.owner group new_resource.group mode 00644 - content item['crt'] + content item['cert'] action :create - only_if { !!item['crt'] } + only_if { !!item['cert'] } end file "acme_store: #{new_resource.cn} SSL fullchain" do @@ -140,4 +142,10 @@ def render_to_files(item) only_if { !!item['chain'] } end end + + def default_data_bag_secret + Chef::EncryptedDataBagItem.load_secret(Chef::Config[:encrypted_data_bag_secret]) + rescue => e + Chef::Log.error "property 'secret' is not provided and the default encrypted_data_bag_secret file does not exist: #{e}" + end end From c8f4bbddf47c98f540d5e721dc12fa6e90c82f51 Mon Sep 17 00:00:00 2001 From: Roland Moriz Date: Tue, 18 Apr 2017 18:16:46 +0200 Subject: [PATCH 3/3] add matchers for persistence --- libraries/matchers.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/libraries/matchers.rb b/libraries/matchers.rb index b5909bc..e64e090 100644 --- a/libraries/matchers.rb +++ b/libraries/matchers.rb @@ -1,9 +1,12 @@ # # Author:: Thijs Houtenbos +# Author:: Roland Moriz + # Cookbook:: acme # Library:: matchers # # Copyright 2015-2017 Schuberg Philis +# Copyright 2017 Moriz GmbH # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,6 +24,7 @@ if defined?(ChefSpec) ChefSpec.define_matcher(:acme_certificate) ChefSpec.define_matcher(:acme_selfsigned) + ChefSpec.define_matcher(:acme_persistence) def create_acme_selfsigned(resource_name) ChefSpec::Matchers::ResourceMatcher.new(:acme_selfsigned, :create, resource_name) @@ -29,4 +33,12 @@ def create_acme_selfsigned(resource_name) def create_acme_certificate(resource_name) ChefSpec::Matchers::ResourceMatcher.new(:acme_certificate, :create, resource_name) end + + def save_acme_persistence(resource_name) + ChefSpec::Matchers::ResourceMatcher.new(:acme_persistence, :save, resource_name) + end + + def load_acme_persistence(resource_name) + ChefSpec::Matchers::ResourceMatcher.new(:acme_persistence, :load, resource_name) + end end