From 59ed0aaa2c2a7e88a44570383d286ec56326af2f Mon Sep 17 00:00:00 2001 From: what do you want? Date: Tue, 21 Oct 2014 10:23:51 -0700 Subject: [PATCH 1/3] Add TLS support - Add the missing bootstrap_expect param - Add the missing encrypt param - Add encrypt_enabled param to make gossip encryption explict --- CHANGELOG.md | 3 + README.md | 73 +++++++++++++++- attributes/default.rb | 19 ++++ libraries/encrypt.rb | 21 +++++ recipes/_service.rb | 49 ++++++++++- spec/unit/recipes/_service_spec.rb | 136 ++++++++++++++++++++++++++++- 6 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 libraries/encrypt.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index fec03ce5..a90eba2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 0.5.0 +* Add support for TLS, and gossip encryption + # 0.4.4 * Adds server list to a consul instance running as a cluster with a `bootstrap_expect` value greater than one. diff --git a/README.md b/README.md index 0bf75686..933d9759 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,78 @@ Installs and configures [Consul][1]. {} - + + ['consul']['encrypt_enabled'] + Boolean + + To enable Consul gossip encryption + + nil + + + ['consul']['verify_incoming'] + Boolean + + If set to True, Consul requires that all incoming connections make use of TLS. + + nil + + + ['consul']['verify_outgoing'] + Boolean + + If set to True, Consul requires that all outgoing connections make use of TLS. + + nil + + + ['consul']['key_file'] + String + + The content of PEM encoded private key + + nil + + + ['consul']['key_file_path'] + String + + Path where the private key is stored on the disk + + nil + + + ['consul']['ca_file'] + String + The content of PEM encoded ca cert + + + nil + + + ['consul']['ca_file_path'] + String + + Path where ca is stored on the disk + + nil + + + ['consul']['cert_file'] + String + + The content of PEM encoded cert. It should only contain the public key. + + nil + + + ['consul']['cert_file_path'] + String + + Path where cert is stored on the disk + + nil + ### Consul UI Attributes diff --git a/attributes/default.rb b/attributes/default.rb index f7fd9896..7acb2543 100644 --- a/attributes/default.rb +++ b/attributes/default.rb @@ -43,8 +43,11 @@ } default['consul']['source_revision'] = 'master' +config_dir = "/etc/consul.d" # Service attributes default['consul']['service_mode'] = 'bootstrap' +# In the cluster mode, set the default cluster size to 3 +default['consul']['bootstrap_expect'] = 3 default['consul']['data_dir'] = '/var/lib/consul' default['consul']['config_dir'] = '/etc/consul.d' case node['platform_family'] @@ -56,6 +59,7 @@ default['consul']['etc_config_dir'] = '/etc/sysconfig/consul' end +default['consul']['config_dir'] = config_dir default['consul']['servers'] = [] default['consul']['init_style'] = 'init' # 'init', 'runit' default['consul']['service_user'] = 'consul' @@ -69,6 +73,21 @@ "server" => 8300, } +# Gossip encryption +default['consul']['encrypt_enabled'] = false +default['consul']['encrypt'] = nil +# TLS support +default['consul']['verify_incoming'] = false +default['consul']['verify_outgoing'] = false +# Cert in pem format +default['consul']['ca_cert'] = nil +default['consul']['ca_path'] = "#{config_dir}/ca.pem" +default['consul']['cert_file'] = nil +default['consul']['cert_path'] = "#{config_dir}/cert.pem" +# Cert in pem format. It can be unique for each host +default['consul']['key_file'] = nil +default['consul']['key_file_path'] = "#{config_dir}/key.pem" + # Optionally bind to a specific interface default['consul']['bind_interface'] = nil default['consul']['advertise_interface'] = nil diff --git a/libraries/encrypt.rb b/libraries/encrypt.rb new file mode 100644 index 00000000..632df513 --- /dev/null +++ b/libraries/encrypt.rb @@ -0,0 +1,21 @@ +class Chef + class Recipe + # Don't throw the error if it doesn't exist + def consul_encrypted_dbi + begin + # loads the secret from /etc/chef/encrypted_data_bag_secret + Chef::EncryptedDataBagItem.load('consul', 'encrypt') + rescue Net::HTTPServerException => e + raise e unless e.response.code == '404' + end + end + + def consul_dbi_key_with_node_default(dbi, key) + value = dbi[key] + Chef::Log.warn "Consul encrypt key=#{key} doesn't exist in the databag. \ +Reading it from node's attributes" if value.nil? + value ||= node['consul'][key] + value + end + end +end diff --git a/recipes/_service.rb b/recipes/_service.rb index bebf9a79..378563c9 100644 --- a/recipes/_service.rb +++ b/recipes/_service.rb @@ -111,7 +111,7 @@ end copy_params = [ - :bind_addr, :datacenter, :domain, :log_level, :node_name, :advertise_addr, :ports, :enable_syslog, :encrypt + :bind_addr, :datacenter, :domain, :log_level, :node_name, :advertise_addr, :ports, :enable_syslog ] copy_params.each do |key| if node['consul'][key] @@ -123,6 +123,53 @@ end end +dbi = nil +# Gossip encryption +if node.consul.encrypt_enabled + # Fetch the databag only once, and use empty hash if it doesn't exists + dbi = consul_encrypted_dbi || {} + secret = consul_dbi_key_with_node_default(dbi, 'encrypt') + raise "Consul encrypt key is empty or nil" if secret.nil? or secret.empty? + service_config['encrypt'] = secret +else + # for backward compatibilty + service_config['encrypt'] = node.consul.encrypt unless node.consul.encrypt.nil? +end + +# TLS encryption +if node.consul.verify_incoming || node.consul.verify_outgoing + dbi = consul_encrypted_dbi || {} if dbi.nil? + service_config['verify_outgoing'] = node.consul.verify_outgoing + service_config['verify_incoming'] = node.consul.verify_incoming + + ca_path = node.consul.ca_path + service_config['ca_file'] = ca_path + + cert_path = node.consul.cert_path + service_config['cert_file'] = cert_path + + key_path = node.consul.key_file_path + service_config['key_file'] = key_path + + # Search for key_file_hostname since key and cert file can be unique/host + key_content = dbi['key_file_' + node.fqdn] || consul_dbi_key_with_node_default(dbi, 'key_file') + cert_content = dbi['cert_file_' + node.fqdn] || consul_dbi_key_with_node_default(dbi, 'cert_file') + ca_content = consul_dbi_key_with_node_default(dbi, 'ca_cert') + + # Save the certs if exists + {ca_path => ca_content, key_path => key_content, cert_path => cert_content}.each do |path, content| + unless content.nil? or content.empty? + file path do + user consul_user + group consul_group + mode 0600 + action :create + content content + end + end + end +end + consul_config_filename = File.join(node['consul']['config_dir'], 'default.json') file consul_config_filename do diff --git a/spec/unit/recipes/_service_spec.rb b/spec/unit/recipes/_service_spec.rb index 85531384..73be083c 100644 --- a/spec/unit/recipes/_service_spec.rb +++ b/spec/unit/recipes/_service_spec.rb @@ -114,7 +114,7 @@ it do expect(chef_run).to create_file('/etc/consul.d/default.json') .with_content(/start_join/) - .with_content(/server1/) + .with_content(/server9/) .with_content(/server2/) .with_content(/server3/) end @@ -136,4 +136,138 @@ .with_content(/server3/) end end + + context 'with gossip_encryption turned ON' do + context 'with key exists in the databag' do + let(:chef_run) do + ChefSpec::Runner.new(node_attributes) do |node| + node.set['consul']['encrypt_enabled'] = true + end.converge(described_recipe) + end + before do + allow(Chef::EncryptedDataBagItem).to receive(:load) + .with('consul', 'encrypt') + .and_return({'encrypt' => 'consul_secret'}) + end + it do + expect(chef_run).to create_file('/etc/consul.d/default.json') + .with_content(/consul_secret/) + end + end + context 'with key doesn\'t exist in the databag' do + context 'databag doesn\'t exists' do + let(:chef_run) do + ChefSpec::Runner.new(node_attributes) do |node| + node.set['consul']['encrypt_enabled'] = true + node.set['consul']['encrypt'] = "consul_secret_node_attr" + end.converge(described_recipe) + end + before do + allow(Chef::EncryptedDataBagItem).to receive(:load) + .with('consul', 'encrypt') + .and_raise(Net::HTTPServerException.new("Consul databag not found", Net::HTTPResponse.new('1.1', '404', ''))) + end + it do + expect(chef_run).to create_file('/etc/consul.d/default.json') + .with_content(/consul_secret_node_attr/) + end + end + context 'encrypt is empty in the node attribute' do + let(:chef_run) do + ChefSpec::Runner.new(node_attributes) do |node| + node.set['consul']['encrypt_enabled'] = true + node.set['consul']['encrypt'] = '' + end.converge(described_recipe) + end + before do + allow(Chef::EncryptedDataBagItem).to receive(:load) + .with('consul', 'encrypt') + .and_return({'encrypt' => nil}) + end + it do + expect{chef_run}.to raise_error(Exception, /Consul encrypt key is empty/) + end + end + end + end + context 'with tls enabled' do + context 'when node key file and ca_cert is unique and exists in databag, verify* is true and ca_file doesn\'t exist in databag' do + let(:chef_run) do + ChefSpec::Runner.new(node_attributes) do |node| + node.set['consul']['verify_incoming'] = true + node.set['consul']['verify_outgoing'] = true + node.set['consul']['ca_cert'] = 'begin_consul_node_ca_file_end' + node.automatic['fqdn'] = 'foo_host' + end.converge(described_recipe) + end + before do + allow(Chef::EncryptedDataBagItem).to receive(:load) + .with('consul', 'encrypt') + .and_return({'key_file_foo_host' => 'begin_consul_db_key_file_end' \ + , 'cert_file_foo_host' => 'begin_consul_db_cert_file_end'}) + end + it do + expect(chef_run).to create_file('/etc/consul.d/ca.pem') + .with_content(/begin_consul_node_ca_file_end/) + expect(chef_run).to create_file('/etc/consul.d/key.pem') + .with_content(/consul_db_key_file_end/) + expect(chef_run).to create_file('/etc/consul.d/cert.pem') + .with_content(/begin_consul_db_cert_file_end/) + expect(chef_run).to create_file('/etc/consul.d/default.json') + .with_content(/verify_incoming": true/) + expect(chef_run).to create_file('/etc/consul.d/default.json') + .with_content(/verify_outgoing": true/) + expect(chef_run).to create_file('/etc/consul.d/default.json') + .with_content(/key.pem/) + expect(chef_run).to create_file('/etc/consul.d/default.json') + .with_content(/ca.pem/) + end + end + context 'when node key, cert, and ca is nil, and verify incoming true' do + let(:chef_run) do + ChefSpec::Runner.new(node_attributes) do |node| + node.set['consul']['verify_incoming'] = true + end.converge(described_recipe) + end + before do + allow(Chef::EncryptedDataBagItem).to receive(:load) + .with('consul', 'encrypt') + .and_return({}) + end + it do + expect(chef_run).not_to create_file('/etc/consul.d/ca.pem') + expect(chef_run).not_to create_file('/etc/consul.d/key.pem') + expect(chef_run).not_to create_file('/etc/consul.d/cert.pem') + expect(chef_run).to create_file('/etc/consul.d/default.json') + .with_content(/verify_incoming": true/) + expect(chef_run).to create_file('/etc/consul.d/default.json') + .with_content(/verify_outgoing": false/) + end + end + context 'when key_file, and cert exists as the node\'s attributes, and verify_outgoing true' do + let(:chef_run) do + ChefSpec::Runner.new(node_attributes) do |node| + node.set['consul']['verify_outgoing'] = true + node.set['consul']['key_file'] = 'begin_consul_node_key_file_end' + node.set['consul']['cert_file'] = 'begin_consul_node_cert_file_end' + end.converge(described_recipe) + end + before do + allow(Chef::EncryptedDataBagItem).to receive(:load) + .with('consul', 'encrypt') + .and_raise(Net::HTTPServerException.new("Consul databag not found", Net::HTTPResponse.new('1.1', '404', ''))) + end + it do + expect(chef_run).not_to create_file('/etc/consul.d/ca.pem') + expect(chef_run).to create_file('/etc/consul.d/key.pem') + .with_content(/begin_consul_node_key_file_end/) + expect(chef_run).to create_file('/etc/consul.d/cert.pem') + .with_content(/begin_consul_node_cert_file_end/) + expect(chef_run).to create_file('/etc/consul.d/default.json') + .with_content(/verify_incoming": false/) + expect(chef_run).to create_file('/etc/consul.d/default.json') + .with_content(/verify_outgoing": true/) + end + end + end end From 3edb2d8505c63bd8398b085784cb34e065c99e30 Mon Sep 17 00:00:00 2001 From: what do you want? Date: Tue, 21 Oct 2014 20:11:12 -0700 Subject: [PATCH 2/3] Add missing databag documentation --- README.md | 62 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 933d9759..084dbb07 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ Installs and configures [Consul][1]. To enable Consul gossip encryption - nil + false ['consul']['verify_incoming'] @@ -184,7 +184,7 @@ Installs and configures [Consul][1]. If set to True, Consul requires that all incoming connections make use of TLS. - nil + false ['consul']['verify_outgoing'] @@ -192,7 +192,7 @@ Installs and configures [Consul][1]. If set to True, Consul requires that all outgoing connections make use of TLS. - nil + false ['consul']['key_file'] @@ -208,7 +208,7 @@ Installs and configures [Consul][1]. Path where the private key is stored on the disk - nil + /etc/consul.d/key.pem ['consul']['ca_file'] @@ -224,7 +224,7 @@ Installs and configures [Consul][1]. Path where ca is stored on the disk - nil + /etc/consul.d/ca.pem ['consul']['cert_file'] @@ -240,7 +240,55 @@ Installs and configures [Consul][1]. Path where cert is stored on the disk - nil + /etc/consul.d/cert.pem + + + +### Databag Attributes (optional) +Following attributes, if exist in the [encrypted databag][7], override the node attributes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyDatabag itemTypeDescription
key_file['consul']['encrypt']StringThe content of PEM encoded private key
key_file_{fqdn}['consul']['encrypt']StringNode's(identified by fqdn) unique PEM encoded private key. If it exists, it will override the databag and node key_file attribute
ca_file['consul']['encrypt']StringThe content of PEM encoded ca cert
encrypt['consul']['encrypt']StringConsul Gossip encryption key
cert_file['consul']['encrypt']StringThe content of PEM encoded cert
cert_file_{fqdn}['consul']['encrypt']StringNode's(identified by fqdn) unique PEM encoded cert. If it exists, it will override the databag and node cert_file attribute
@@ -391,3 +439,5 @@ Created and maintained by [John Bellone][3] [@johnbellone][2] ( Date: Thu, 23 Oct 2014 22:28:56 -0700 Subject: [PATCH 3/3] Derive certs path from config_dir More on that: https://coderanger.net/derived-attributes/ --- attributes/default.rb | 8 +++----- recipes/_service.rb | 6 +++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/attributes/default.rb b/attributes/default.rb index 7acb2543..6a42d6ee 100644 --- a/attributes/default.rb +++ b/attributes/default.rb @@ -43,7 +43,6 @@ } default['consul']['source_revision'] = 'master' -config_dir = "/etc/consul.d" # Service attributes default['consul']['service_mode'] = 'bootstrap' # In the cluster mode, set the default cluster size to 3 @@ -59,7 +58,6 @@ default['consul']['etc_config_dir'] = '/etc/sysconfig/consul' end -default['consul']['config_dir'] = config_dir default['consul']['servers'] = [] default['consul']['init_style'] = 'init' # 'init', 'runit' default['consul']['service_user'] = 'consul' @@ -81,12 +79,12 @@ default['consul']['verify_outgoing'] = false # Cert in pem format default['consul']['ca_cert'] = nil -default['consul']['ca_path'] = "#{config_dir}/ca.pem" +default['consul']['ca_path'] = "%{config_dir}/ca.pem" default['consul']['cert_file'] = nil -default['consul']['cert_path'] = "#{config_dir}/cert.pem" +default['consul']['cert_path'] = "%{config_dir}/cert.pem" # Cert in pem format. It can be unique for each host default['consul']['key_file'] = nil -default['consul']['key_file_path'] = "#{config_dir}/key.pem" +default['consul']['key_file_path'] = "%{config_dir}/key.pem" # Optionally bind to a specific interface default['consul']['bind_interface'] = nil diff --git a/recipes/_service.rb b/recipes/_service.rb index 378563c9..62f9fbb3 100644 --- a/recipes/_service.rb +++ b/recipes/_service.rb @@ -142,13 +142,13 @@ service_config['verify_outgoing'] = node.consul.verify_outgoing service_config['verify_incoming'] = node.consul.verify_incoming - ca_path = node.consul.ca_path + ca_path = node.consul.ca_path % { config_dir: node.consul.config_dir } service_config['ca_file'] = ca_path - cert_path = node.consul.cert_path + cert_path = node.consul.cert_path % { config_dir: node.consul.config_dir } service_config['cert_file'] = cert_path - key_path = node.consul.key_file_path + key_path = node.consul.key_file_path % { config_dir: node.consul.config_dir } service_config['key_file'] = key_path # Search for key_file_hostname since key and cert file can be unique/host