hiera-eyaml 1.1.4 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/.gitignore +2 -0
  2. data/Gemfile +7 -0
  3. data/Gemfile.lock +28 -0
  4. data/PLUGINS.md +4 -0
  5. data/README.md +51 -41
  6. data/bin/eyaml +9 -357
  7. data/features/decrypts.feature +44 -0
  8. data/features/edit.feature +54 -0
  9. data/features/encrypts.feature +26 -0
  10. data/features/keys.feature +13 -0
  11. data/features/outputs.feature +30 -0
  12. data/features/plugin.feature +35 -0
  13. data/features/plugin_api.feature +16 -0
  14. data/features/puppet.feature +15 -0
  15. data/features/sandbox/convert_decrypted_values_to_uppercase.sh +2 -0
  16. data/features/sandbox/keys/private_key.pkcs7.pem +27 -0
  17. data/features/sandbox/keys/public_key.pkcs7.pem +18 -0
  18. data/features/sandbox/pipe_string.sh +5 -0
  19. data/features/sandbox/puppet/environments/local/test.eyaml +3 -0
  20. data/features/sandbox/puppet/hiera.yaml +17 -0
  21. data/features/sandbox/puppet/manifests/init.pp +3 -0
  22. data/features/sandbox/puppet/modules/test/manifests/init.pp +18 -0
  23. data/features/sandbox/puppet/puppet.conf +6 -0
  24. data/features/sandbox/supply_password.sh +7 -0
  25. data/features/sandbox/test_input.bin +0 -0
  26. data/features/sandbox/test_input.encrypted.txt +1 -0
  27. data/features/sandbox/test_input.txt +3 -0
  28. data/features/sandbox/test_input.yaml +114 -0
  29. data/features/step_definitions/environment_overrides.rb +3 -0
  30. data/features/support/env.rb +26 -0
  31. data/features/support/setup_sandbox.rb +21 -0
  32. data/features/valid_encryption.feature +12 -0
  33. data/hiera-eyaml.gemspec +1 -1
  34. data/lib/hiera/backend/eyaml.rb +19 -0
  35. data/lib/hiera/backend/eyaml/CLI.rb +110 -0
  36. data/lib/hiera/backend/eyaml/actions/createkeys_action.rb +24 -0
  37. data/lib/hiera/backend/eyaml/actions/decrypt_action.rb +67 -0
  38. data/lib/hiera/backend/eyaml/actions/edit_action.rb +47 -0
  39. data/lib/hiera/backend/eyaml/actions/encrypt_action.rb +80 -0
  40. data/lib/hiera/backend/eyaml/encryptor.rb +71 -0
  41. data/lib/hiera/backend/eyaml/encryptors/pkcs7.rb +99 -0
  42. data/lib/hiera/backend/eyaml/options.rb +32 -0
  43. data/lib/hiera/backend/eyaml/plugins.rb +65 -0
  44. data/lib/hiera/backend/eyaml/utils.rb +83 -0
  45. data/lib/hiera/backend/eyaml_backend.rb +108 -113
  46. data/sublime_text/README.md +16 -0
  47. data/sublime_text/eyaml.sublime-package +0 -0
  48. data/sublime_text/eyaml.syntax_definition.json +288 -0
  49. data/{bin → tools}/regem.sh +1 -1
  50. metadata +71 -7
  51. data/keys/.keepme +0 -0
  52. data/lib/hiera/backend/version.rb +0 -7
@@ -0,0 +1,99 @@
1
+ require 'openssl'
2
+ require 'hiera/backend/eyaml/encryptor'
3
+ require 'hiera/backend/eyaml/utils'
4
+ require 'hiera/backend/eyaml/options'
5
+
6
+ class Hiera
7
+ module Backend
8
+ module Eyaml
9
+ module Encryptors
10
+
11
+ class Pkcs7 < Encryptor
12
+
13
+ self.options = {
14
+ :private_key => { :desc => "Private key directory",
15
+ :type => :string,
16
+ :default => "./keys/private_key.pkcs7.pem" },
17
+ :public_key => { :desc => "Public key directory",
18
+ :type => :string,
19
+ :default => "./keys/public_key.pkcs7.pem" }
20
+ }
21
+
22
+ self.tag = "PKCS7"
23
+
24
+ def self.encrypt plaintext
25
+
26
+ public_key = self.option :public_key
27
+ raise StandardError, "pkcs7_public_key is not defined" unless public_key
28
+
29
+ public_key_pem = File.read public_key
30
+ public_key_x509 = OpenSSL::X509::Certificate.new( public_key_pem )
31
+
32
+ cipher = OpenSSL::Cipher::AES.new(256, :CBC)
33
+ OpenSSL::PKCS7::encrypt([public_key_x509], plaintext, cipher, OpenSSL::PKCS7::BINARY).to_der
34
+
35
+ end
36
+
37
+ def self.decrypt ciphertext
38
+
39
+ public_key = self.option :public_key
40
+ private_key = self.option :private_key
41
+ raise StandardError, "pkcs7_public_key is not defined" unless public_key
42
+ raise StandardError, "pkcs7_private_key is not defined" unless private_key
43
+
44
+ private_key_pem = File.read private_key
45
+ private_key_rsa = OpenSSL::PKey::RSA.new( private_key_pem )
46
+
47
+ public_key_pem = File.read public_key
48
+ public_key_x509 = OpenSSL::X509::Certificate.new( public_key_pem )
49
+
50
+ pkcs7 = OpenSSL::PKCS7.new( ciphertext )
51
+ pkcs7.decrypt(private_key_rsa, public_key_x509)
52
+
53
+ end
54
+
55
+ def self.create_keys
56
+
57
+ # Try to do equivalent of:
58
+ # openssl req -x509 -nodes -days 100000 -newkey rsa:2048 -keyout privatekey.pem -out publickey.pem -subj '/'
59
+
60
+ public_key = self.option :public_key
61
+ private_key = self.option :private_key
62
+
63
+ key = OpenSSL::PKey::RSA.new(2048)
64
+ Utils.write_important_file :filename => private_key, :content => key.to_pem
65
+
66
+ name = OpenSSL::X509::Name.parse("/")
67
+ cert = OpenSSL::X509::Certificate.new()
68
+ cert.serial = 0
69
+ cert.version = 2
70
+ cert.not_before = Time.now
71
+ cert.not_after = Time.now + 50 * 365 * 24 * 60 * 60
72
+ cert.public_key = key.public_key
73
+
74
+ ef = OpenSSL::X509::ExtensionFactory.new
75
+ ef.subject_certificate = cert
76
+ ef.issuer_certificate = cert
77
+ cert.extensions = [
78
+ ef.create_extension("basicConstraints","CA:TRUE", true),
79
+ ef.create_extension("subjectKeyIdentifier", "hash"),
80
+ ]
81
+ cert.add_extension ef.create_extension("authorityKeyIdentifier",
82
+ "keyid:always,issuer:always")
83
+
84
+ cert.sign key, OpenSSL::Digest::SHA1.new
85
+
86
+ Utils.write_important_file :filename => public_key, :content => cert.to_pem
87
+ puts "Keys created OK"
88
+
89
+ end
90
+
91
+ end
92
+
93
+ end
94
+
95
+ end
96
+
97
+ end
98
+
99
+ end
@@ -0,0 +1,32 @@
1
+ class Hiera
2
+ module Backend
3
+ module Eyaml
4
+ class Options
5
+
6
+ def self.[]= key, value
7
+ @@options ||= {}
8
+ @@options[ key.to_sym ] = value
9
+ end
10
+
11
+ def self.[] key
12
+ @@options ||= {}
13
+ @@options[ key.to_sym ]
14
+ end
15
+
16
+ def self.set hash
17
+ @@options = {}
18
+ hash.each do |k, v|
19
+ @@options[ k.to_sym ] = v
20
+ end
21
+ end
22
+
23
+ def self.debug
24
+ @@options.each do |k, v|
25
+ puts "#{k.class.name}:#{k} = #{v.class.name}:#{v}"
26
+ end
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,65 @@
1
+ require 'rubygems'
2
+
3
+ class Hiera
4
+ module Backend
5
+ module Eyaml
6
+ class Plugins
7
+
8
+ @@plugins = []
9
+ @@options = {}
10
+
11
+ def self.register_options args
12
+ options_hash = args[ :options ]
13
+ plugin = args[ :plugin ]
14
+ options_hash.each do |key, value|
15
+ @@options.merge!({ "#{plugin}_#{key}" => value })
16
+ end
17
+ end
18
+
19
+ def self.options
20
+ @@options
21
+ end
22
+
23
+ def self.find
24
+
25
+ this_version = Gem::Version.create(Hiera::Backend::Eyaml::VERSION)
26
+ index = Gem::VERSION >= "1.8.0" ? Gem::Specification : Gem.source_index
27
+
28
+ [index].flatten.each do |source|
29
+ specs = Gem::VERSION >= "1.6.0" ? source.latest_specs(true) : source.latest_specs
30
+
31
+ specs.each do |spec|
32
+ next if @@plugins.include? spec
33
+
34
+ # If this gem depends on Vagrant, verify this is a valid release of
35
+ # Vagrant for this gem to load into.
36
+ dependency = spec.dependencies.find { |d| d.name == "hiera-eyaml" }
37
+ next if dependency && !dependency.requirement.satisfied_by?( this_version )
38
+
39
+ file = nil
40
+ if Gem::VERSION >= "1.8.0"
41
+ file = spec.matches_for_glob("**/eyaml_init.rb").first
42
+ else
43
+ file = Gem.searcher.matching_files(spec, "eyaml_init.rb").first
44
+ end
45
+
46
+ next unless file
47
+
48
+ @@plugins << spec
49
+ load file
50
+ end
51
+
52
+ end
53
+
54
+ @@plugins
55
+
56
+ end
57
+
58
+ def self.plugins
59
+ @@plugins
60
+ end
61
+
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,83 @@
1
+ require 'highline/import'
2
+ require 'tempfile'
3
+
4
+ class Hiera
5
+ module Backend
6
+ module Eyaml
7
+ class Utils
8
+
9
+ def self.read_password
10
+ ask("Enter password: ") {|q| q.echo = "*" }
11
+ end
12
+
13
+ def self.confirm? message
14
+ result = ask("#{message} (y/N): ")
15
+ if result.downcase == "y" or result.downcase == "yes"
16
+ true
17
+ else
18
+ false
19
+ end
20
+ end
21
+
22
+ def self.camelcase string
23
+ return string if string !~ /_/ && string =~ /[A-Z]+.*/
24
+ string.split('_').map{|e| e.capitalize}.join
25
+ end
26
+
27
+ def self.find_editor
28
+ editor = ENV['EDITOR']
29
+ editor ||= %w{ /usr/bin/sensible-editor /usr/bin/editor /usr/bin/vim /usr/bin/vi }.collect {|e| e if FileTest.executable? e}.compact.first
30
+ raise StandardError, "Editor not found. Please set your EDITOR env variable" if editor.nil?
31
+ editor
32
+ end
33
+
34
+ def self.secure_file_delete args
35
+ file = File.open(args[:file], 'r+')
36
+ num_bytes = args[:num_bytes]
37
+ [0xff, 0x55, 0xaa, 0x00].each do |byte|
38
+ file.seek(0, IO::SEEK_SET)
39
+ num_bytes.times { file.print(byte.chr) }
40
+ file.fsync
41
+ end
42
+ File.delete file
43
+ end
44
+
45
+ def self.write_tempfile data_to_write
46
+ file = Tempfile.open('eyaml_edit')
47
+ path = file.path
48
+
49
+ file.puts data_to_write
50
+ file.close
51
+
52
+ path
53
+ end
54
+
55
+ def self.write_important_file args
56
+ filename = args[ :filename ]
57
+ content = args[ :content ]
58
+ if File.file? "#{filename}"
59
+ raise StandardError, "User aborted" unless Utils::confirm? "Are you sure you want to overwrite \"#{filename}\"?"
60
+ end
61
+ open( "#{filename}", "w" ) do |io|
62
+ io.write(content)
63
+ end
64
+ end
65
+
66
+ def self.ensure_key_dir_exists key_file
67
+ key_dir = File.dirname key_file
68
+
69
+ unless File.directory? key_dir
70
+ begin
71
+ Dir.mkdir key_dir
72
+ puts "Created key directory: #{key_dir}"
73
+ rescue
74
+ raise StandardError, "Cannot create key directory: #{key_dir}"
75
+ end
76
+ end
77
+
78
+ end
79
+
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,129 +1,124 @@
1
- class Hiera
2
- module Backend
3
- class Eyaml_backend
4
-
5
- def initialize
6
- require 'openssl'
7
- require 'base64'
8
- end
1
+ require 'hiera/backend/eyaml/encryptor'
2
+ require 'hiera/backend/eyaml/actions/decrypt_action'
3
+ require 'hiera/backend/eyaml/utils'
4
+ require 'yaml'
9
5
 
10
- def lookup(key, scope, order_override, resolution_type)
11
-
12
- debug("Lookup called for key #{key}")
13
- answer = nil
14
-
15
- Backend.datasources(scope, order_override) do |source|
16
- eyaml_file = Backend.datafile(:eyaml, scope, source, "eyaml") || next
17
-
18
- debug("Processing datasource: #{eyaml_file}")
19
-
20
- data = YAML.load(File.read( eyaml_file ))
21
-
22
- next if !data
23
- next if data.empty?
24
- debug ("Data contains valid YAML")
25
-
26
- next unless data.include?(key)
27
- debug ("Key #{key} found in YAML document")
28
-
29
- parsed_answer = parse_answer(data[key], scope)
30
-
31
- begin
32
- case resolution_type
33
- when :array
34
- debug("Appending answer array")
35
- raise Exception, "Hiera type mismatch: expected Array and got #{parsed_answer.class}" unless parsed_answer.kind_of? Array or parsed_answer.kind_of? String
36
- answer ||= []
37
- answer << parsed_answer
38
- when :hash
39
- debug("Merging answer hash")
40
- raise Exception, "Hiera type mismatch: expected Hash and got #{parsed_answer.class}" unless parsed_answer.kind_of? Hash
41
- answer ||= {}
42
- answer = parsed_answer.merge answer
43
- else
44
- debug("Assigning answer variable")
45
- answer = parsed_answer
46
- break
47
- end
48
- rescue NoMethodError
49
- raise Exception, "Resolution type is #{resolution_type} but parsed_answer is a #{parsed_answer.class}"
50
- end
51
- end
52
-
53
- return answer
54
- end
55
-
56
- def parse_answer(data, scope, extra_data={})
57
- if data.is_a?(Numeric) or data.is_a?(TrueClass) or data.is_a?(FalseClass)
58
- # Can't be encrypted
59
- return data
60
- elsif data.is_a?(String)
61
- parsed_string = Backend.parse_string(data, scope)
62
- return decrypt(parsed_string, scope)
63
- elsif data.is_a?(Hash)
64
- answer = {}
65
- data.each_pair do |key, val|
66
- answer[key] = parse_answer(val, scope, extra_data)
67
- end
68
- return answer
69
- elsif data.is_a?(Array)
70
- answer = []
71
- data.each do |item|
72
- answer << parse_answer(item, scope, extra_data)
73
- end
74
- return answer
75
- end
6
+ class Hiera
7
+ module Backend
8
+ class Eyaml_backend
9
+
10
+ def initialize
11
+ end
12
+
13
+ def lookup(key, scope, order_override, resolution_type)
14
+
15
+ debug("Lookup called for key #{key}")
16
+ answer = nil
17
+
18
+ Backend.datasources(scope, order_override) do |source|
19
+ eyaml_file = Backend.datafile(:eyaml, scope, source, "eyaml") || next
20
+
21
+ debug("Processing datasource: #{eyaml_file}")
22
+
23
+ data = YAML.load(File.read( eyaml_file ))
24
+
25
+ next if data.nil? or data.empty?
26
+ debug ("Data contains valid YAML")
27
+
28
+ next unless data.include?(key)
29
+ debug ("Key #{key} found in YAML document")
30
+
31
+ parsed_answer = parse_answer(key, data[key], scope)
32
+
33
+ begin
34
+ case resolution_type
35
+ when :array
36
+ debug("Appending answer array")
37
+ raise Exception, "Hiera type mismatch: expected Array and got #{parsed_answer.class}" unless parsed_answer.kind_of? Array or parsed_answer.kind_of? String
38
+ answer ||= []
39
+ answer << parsed_answer
40
+ when :hash
41
+ debug("Merging answer hash")
42
+ raise Exception, "Hiera type mismatch: expected Hash and got #{parsed_answer.class}" unless parsed_answer.kind_of? Hash
43
+ answer ||= {}
44
+ answer = parsed_answer.merge answer
45
+ else
46
+ debug("Assigning answer variable")
47
+ answer = parsed_answer
48
+ break
76
49
  end
50
+ rescue NoMethodError
51
+ raise Exception, "Resolution type is #{resolution_type} but parsed_answer is a #{parsed_answer.class}"
52
+ end
53
+ end
77
54
 
78
- def decrypt(value, scope)
55
+ answer
56
+ end
57
+
58
+ def parse_answer(key, data, scope, extra_data={})
59
+ if data.is_a?(Numeric) or data.is_a?(TrueClass) or data.is_a?(FalseClass)
60
+ # Can't be encrypted
61
+ data
62
+ elsif data.is_a?(String)
63
+ parsed_string = Backend.parse_string(data, scope)
64
+ decrypt(key, parsed_string, scope)
65
+ elsif data.is_a?(Hash)
66
+ answer = {}
67
+ data.each_pair do |key, val|
68
+ answer[key] = parse_answer(val, scope, extra_data)
69
+ end
70
+ answer
71
+ elsif data.is_a?(Array)
72
+ answer = []
73
+ data.each do |item|
74
+ answer << parse_answer(item, scope, extra_data)
75
+ end
76
+ answer
77
+ end
78
+ end
79
79
 
80
- if is_encrypted(value)
80
+ def deblock block_string
81
+ block_string.gsub(/[ \n]/, '')
82
+ end
81
83
 
82
- # remove enclosing 'ENC[]'
83
- ciphertext = value[4..-2]
84
- ciphertext_decoded = Base64.decode64(ciphertext)
84
+ def decrypt(key, value, scope)
85
85
 
86
- debug("Decrypting value")
86
+ if encrypted? value
87
87
 
88
- private_key_path = Backend.parse_string(Config[:eyaml][:private_key], scope) || '/etc/hiera/keys/private_key.pem'
89
- public_key_path = Backend.parse_string(Config[:eyaml][:public_key], scope) || '/etc/hiera/keys/public_key.pem'
88
+ debug "Attempting to decrypt: #{key}"
89
+
90
+ Config[:eyaml].each do |config_key, config_value|
91
+ config_value = Backend.parse_string(Config[:eyaml][config_key], scope)
92
+ debug "Setting: #{config_key} = #{config_value}"
93
+ Eyaml::Options[config_key] = config_value
94
+ end
90
95
 
91
- private_key_pem = File.read( private_key_path )
92
- private_key = OpenSSL::PKey::RSA.new( private_key_pem )
96
+ Eyaml::Options[:source] = "hiera"
93
97
 
94
- public_key_pem = File.read( public_key_path )
95
- public_key = OpenSSL::X509::Certificate.new( public_key_pem )
98
+ plaintext = value.gsub( /ENC\[([^\]]*)\]/ ) { |match|
99
+ Eyaml::Options[:input_data] = deblock match.to_s
100
+ Eyaml::Options[:output] = "raw"
101
+ Eyaml::Actions::DecryptAction.execute
102
+ }
96
103
 
97
- pkcs7 = OpenSSL::PKCS7.new( ciphertext_decoded )
104
+ plaintext
98
105
 
99
- begin
100
- plaintext = pkcs7.decrypt(private_key, public_key)
101
- rescue
102
- raise Exception, "Hiera eyaml backend: Unable to decrypt hiera data. Do the keys match and are they the same as those used to encrypt?"
103
- end
104
-
105
- return plaintext
106
-
107
- else
108
- return value
109
- end
110
- end
106
+ else
107
+ value
108
+ end
109
+ end
111
110
 
112
- def is_encrypted(value)
113
- if value.start_with?('ENC[')
114
- return true
115
- else
116
- return false
117
- end
118
- end
111
+ def encrypted?(value)
112
+ if value.match(/.*ENC\[.*?\]/) then true else false end
113
+ end
119
114
 
120
- def debug(msg)
121
- Hiera.debug("[eyaml_backend]: #{msg}")
122
- end
115
+ def debug(msg)
116
+ Hiera.debug("[eyaml_backend]: #{msg}")
117
+ end
123
118
 
124
- def warn(msg)
125
- Hiera.warn("[eyaml_backend]: #{msg}")
126
- end
127
- end
119
+ def warn(msg)
120
+ Hiera.warn("[eyaml_backend]: #{msg}")
121
+ end
128
122
  end
123
+ end
129
124
  end