Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initial implementation of persistence [RFC/WIP] #63

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions libraries/matchers.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#
# Author:: Thijs Houtenbos <[email protected]>
# Author:: Roland Moriz <[email protected]>

# 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.
Expand All @@ -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)
Expand All @@ -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
151 changes: 151 additions & 0 deletions resources/persistence.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
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

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,
'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['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

chef_data_bag_item "#{data_bag_name}/#{cn}" do
raw_data data
if new_resource.encrypt && (new_resource.secret || default_data_bag_secret)
encrypt true
encryption_version 2
secret new_resource.secret || default_data_bag_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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you don’t need this custom method, and can instead use the built-in data_bag_item() method, since you’re in a scope that has it.

Copy link
Contributor Author

@rmoriz rmoriz Mar 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My solution was supposed to support three cases:

  • no encrypted data bag -> empty secret
  • custom data bag secret -> manual set secret
  • default data bag secret -> manual set secret

using chef's data_bag_item without a secret automatically falls back to the secret defined in Chef::Config[:encrypted_data_bag_secret] in case of an encrypted data bag (no way to customize and iirc providing nil as secret throws an exception, too)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rmoriz ah! I forgot about the nil throwing an exception. In that case, this is great.

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['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
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['cert']
action :create

only_if { !!item['cert'] }
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

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