From 24a0da470c74ba15c200431abfd63ab4a9cf478c Mon Sep 17 00:00:00 2001 From: Alexander Fisher Date: Fri, 5 Apr 2019 15:01:14 +0100 Subject: [PATCH] Compare decrypted values to see if they are insync When splunk is started, it automatically encrypts certain values when it finds them in config files. To stop puppet reverting these changes, I've overriden `insync?` so that it performs the decryption before comparing. Currently only implemented for splunk >= 7.2 Based on description of algorithm in this python based project. https://github.com/HurricaneLabs/splunksecrets --- lib/puppet_x/puppetlabs/splunk/type.rb | 11 +++++++ lib/puppet_x/voxpupuli/splunk/util.rb | 32 +++++++++++++++++++ spec/acceptance/splunk_enterprise_spec.rb | 30 +++++++++++++---- spec/acceptance/splunk_forwarder_spec.rb | 13 ++++++-- spec/unit/puppet/type/splunk_types_spec.rb | 29 +++++++++++++++++ .../puppet_x/voxpupuli/splunk/util_spec.rb | 21 ++++++++++++ 6 files changed, 127 insertions(+), 9 deletions(-) create mode 100644 lib/puppet_x/voxpupuli/splunk/util.rb create mode 100644 spec/unit/puppet_x/voxpupuli/splunk/util_spec.rb diff --git a/lib/puppet_x/puppetlabs/splunk/type.rb b/lib/puppet_x/puppetlabs/splunk/type.rb index 54cf7ee2..c576200d 100644 --- a/lib/puppet_x/puppetlabs/splunk/type.rb +++ b/lib/puppet_x/puppetlabs/splunk/type.rb @@ -1,3 +1,5 @@ +require File.join(File.dirname(__FILE__), '..', '..', 'voxpupuli/splunk/util') + module PuppetX module Puppetlabs module Splunk @@ -26,6 +28,15 @@ def self.clone_type(type) munge do |v| v.to_s.strip end + def insync?(is) # rubocop:disable Lint/NestedMethodDefinition + secrets_file_path = File.join(provider.class.file_path, 'auth/splunk.secret') + if File.file?(secrets_file_path) + PuppetX::Voxpupuli::Splunk::Util.decrypt(secrets_file_path, is) == should + else + Puppet.warning('Secrets file NOT found') + is == should + end + end end type.newparam(:setting) do desc 'The setting being defined.' diff --git a/lib/puppet_x/voxpupuli/splunk/util.rb b/lib/puppet_x/voxpupuli/splunk/util.rb new file mode 100644 index 00000000..1148cfd3 --- /dev/null +++ b/lib/puppet_x/voxpupuli/splunk/util.rb @@ -0,0 +1,32 @@ +require 'openssl' +require 'base64' + +module PuppetX + module Voxpupuli + module Splunk + class Util + def self.decrypt(secrets_file, value) + return value unless value.start_with?('$7$') + + Puppet.debug "Decrypting splunk >= 7.2 data using secret from #{secrets_file}" + value.slice!(0, 3) + data = Base64.strict_decode64(value) + splunk_secret = IO.binread(secrets_file).chomp + + iv = data.bytes[0, 16].pack('c*') + tag = data.bytes[-16..-1].pack('c*') + ciphertext = data.bytes[16..-17].pack('c*') + + decipher = OpenSSL::Cipher::AES.new(256, :GCM).decrypt + decipher.key = OpenSSL::PKCS5.pbkdf2_hmac(splunk_secret, 'disk-encryption', 1, 32, OpenSSL::Digest::SHA256.new) + decipher.iv_len = 16 + decipher.iv = iv + decipher.auth_tag = tag + decipher.auth_data = '' + + decipher.update(ciphertext) + decipher.final + end + end + end + end +end diff --git a/spec/acceptance/splunk_enterprise_spec.rb b/spec/acceptance/splunk_enterprise_spec.rb index db83ebb4..db497992 100644 --- a/spec/acceptance/splunk_enterprise_spec.rb +++ b/spec/acceptance/splunk_enterprise_spec.rb @@ -1,6 +1,13 @@ require 'spec_helper_acceptance' describe 'splunk enterprise class' do + init = shell('/bin/readlink /sbin/init', acceptable_exit_codes: [0, 1]).stdout + service_name = if init.include? 'systemd' + 'Splunkd' + else + 'splunk' + end + context 'default parameters' do # Using puppet_apply as a helper it 'works idempotently with no errors' do @@ -17,13 +24,6 @@ class { '::splunk::enterprise': } it { is_expected.to be_installed } end - init = shell('/bin/readlink /sbin/init', acceptable_exit_codes: [0, 1]).stdout - service_name = if init.include? 'systemd' - 'Splunkd' - else - 'splunk' - end - describe service(service_name) do it { is_expected.to be_enabled } it { is_expected.to be_running } @@ -39,5 +39,21 @@ class { '::splunk::enterprise': } it { is_expected.to be_grouped_into 'root' } end end + + # Uninstall so that splunkforwarder tests aren't affected by this set of tests + context 'uninstalling splunk' do + it do + pp = <<-EOS + service { '#{service_name}': ensure => stopped } + package { 'splunk': ensure => purged } + file { '/opt/splunk': ensure => absent, force => true, require => Package['splunk'] } + file { '/etc/init.d/splunk': ensure => absent, require => Package['splunk'] } + EOS + apply_manifest(pp, catch_failures: true) + end + describe package('splunk') do + it { is_expected.not_to be_installed } + end + end end end diff --git a/spec/acceptance/splunk_forwarder_spec.rb b/spec/acceptance/splunk_forwarder_spec.rb index f7a55652..074c8859 100644 --- a/spec/acceptance/splunk_forwarder_spec.rb +++ b/spec/acceptance/splunk_forwarder_spec.rb @@ -5,11 +5,14 @@ # Using puppet_apply as a helper it 'works idempotently with no errors' do pp = <<-EOS - class { '::splunk::params': + class { 'splunk::params': } - class { '::splunk::forwarder': + class { 'splunk::forwarder': splunkd_port => 8090, } + splunkforwarder_output { 'tcpout:splunkcloud/sslPassword': + value => 'super_secure_password', + } EOS # Run it twice and test for idempotency @@ -17,6 +20,12 @@ class { '::splunk::forwarder': apply_manifest(pp, catch_changes: true) end + describe file('/opt/splunkforwarder/etc/system/local/outputs.conf') do + it { is_expected.to be_file } + its(:content) { is_expected.to match %r{^sslPassword} } + its(:content) { is_expected.to match %r{^sslPassword = \$7\$} } + end + describe package('splunkforwarder') do it { is_expected.to be_installed } end diff --git a/spec/unit/puppet/type/splunk_types_spec.rb b/spec/unit/puppet/type/splunk_types_spec.rb index 36bbd3cd..76b15a90 100644 --- a/spec/unit/puppet/type/splunk_types_spec.rb +++ b/spec/unit/puppet/type/splunk_types_spec.rb @@ -71,5 +71,34 @@ expect(described_class.provider(:ini_setting).file_name).to eq(file_name) end end + + describe 'value property' do + it 'has a value property' do + expect(described_class.attrtype(:value)).to eq(:property) + end + context 'when testing value is insync' do + let(:resource) { described_class.new(title: 'foo/bar', value: 'value') } + let(:property) { resource.property(:value) } + + before do + Puppet::Type.type(:splunk_config).new( + name: 'config', + server_confdir: '/opt/splunk/etc', + forwarder_confdir: '/opt/splunkforwarder/etc' + ).generate + end + + it 'is insync if unencrypted `is` value matches `should` value' do + property.should = 'value' + expect(property).to be_safe_insync('value') + end + it 'is insync if encrypted `is` value matches `should` value after being decrypted' do + property.should = 'temp1234' + allow(File).to receive(:file?).with(%r{/opt/splunk(forwarder)?/etc/auth/splunk\.secret$}).and_return(true) + allow(IO).to receive(:binread).with(%r{/opt/splunk(forwarder)?/etc/auth/splunk\.secret$}).and_return('JX7cQAnH6Nznmild8MvfN8/BLQnGr8C3UYg3mqvc3ArFkaxj4gUt1RUCaRBD/r0CNn8xOA2oKX8/0uyyChyGRiFKhp6h2FA+ydNIRnN46N8rZov8QGkchmebZa5GAM5U50GbCCgzJFObPyWi5yT8CrSCYmv9cpRtpKyiX+wkhJwltoJzAxWbBERiLp+oXZnN3lsRn6YkljmYBqN9tZLTVVpsLvqvkezPgpv727Fd//5dRoWsWBv2zRp0mwDv3tj') + expect(property).to be_safe_insync('$7$aTVkS01HYVNJUk5wSnR5NIu4GXLhj2Qd49n2B6Y8qmA/u1CdL9JYxQ==') + end + end + end end end diff --git a/spec/unit/puppet_x/voxpupuli/splunk/util_spec.rb b/spec/unit/puppet_x/voxpupuli/splunk/util_spec.rb new file mode 100644 index 00000000..4f44a845 --- /dev/null +++ b/spec/unit/puppet_x/voxpupuli/splunk/util_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' +require 'puppet_x/voxpupuli/splunk/util' + +describe PuppetX::Voxpupuli::Splunk::Util do + describe '.decrypt' do + context 'when called with an unencrypted value' do + it 'returns the value unmodified' do + expect(described_class.decrypt('secrets_file', 'non_encrypted_value')).to eq 'non_encrypted_value' + end + end + context 'when called with splunk 7.2 encrypted value' do + let(:encrypted_value) { '$7$aTVkS01HYVNJUk5wSnR5NIu4GXLhj2Qd49n2B6Y8qmA/u1CdL9JYxQ==' } + let(:splunk_secret) { 'JX7cQAnH6Nznmild8MvfN8/BLQnGr8C3UYg3mqvc3ArFkaxj4gUt1RUCaRBD/r0CNn8xOA2oKX8/0uyyChyGRiFKhp6h2FA+ydNIRnN46N8rZov8QGkchmebZa5GAM5U50GbCCgzJFObPyWi5yT8CrSCYmv9cpRtpKyiX+wkhJwltoJzAxWbBERiLp+oXZnN3lsRn6YkljmYBqN9tZLTVVpsLvqvkezPgpv727Fd//5dRoWsWBv2zRp0mwDv3tj' } + + it 'returns decrypted value' do + allow(IO).to receive(:binread).with('secrets_file').and_return(splunk_secret) + expect(described_class.decrypt('secrets_file', encrypted_value)).to eq 'temp1234' + end + end + end +end