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.
- data/.gitignore +2 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +28 -0
- data/PLUGINS.md +4 -0
- data/README.md +51 -41
- data/bin/eyaml +9 -357
- data/features/decrypts.feature +44 -0
- data/features/edit.feature +54 -0
- data/features/encrypts.feature +26 -0
- data/features/keys.feature +13 -0
- data/features/outputs.feature +30 -0
- data/features/plugin.feature +35 -0
- data/features/plugin_api.feature +16 -0
- data/features/puppet.feature +15 -0
- data/features/sandbox/convert_decrypted_values_to_uppercase.sh +2 -0
- data/features/sandbox/keys/private_key.pkcs7.pem +27 -0
- data/features/sandbox/keys/public_key.pkcs7.pem +18 -0
- data/features/sandbox/pipe_string.sh +5 -0
- data/features/sandbox/puppet/environments/local/test.eyaml +3 -0
- data/features/sandbox/puppet/hiera.yaml +17 -0
- data/features/sandbox/puppet/manifests/init.pp +3 -0
- data/features/sandbox/puppet/modules/test/manifests/init.pp +18 -0
- data/features/sandbox/puppet/puppet.conf +6 -0
- data/features/sandbox/supply_password.sh +7 -0
- data/features/sandbox/test_input.bin +0 -0
- data/features/sandbox/test_input.encrypted.txt +1 -0
- data/features/sandbox/test_input.txt +3 -0
- data/features/sandbox/test_input.yaml +114 -0
- data/features/step_definitions/environment_overrides.rb +3 -0
- data/features/support/env.rb +26 -0
- data/features/support/setup_sandbox.rb +21 -0
- data/features/valid_encryption.feature +12 -0
- data/hiera-eyaml.gemspec +1 -1
- data/lib/hiera/backend/eyaml.rb +19 -0
- data/lib/hiera/backend/eyaml/CLI.rb +110 -0
- data/lib/hiera/backend/eyaml/actions/createkeys_action.rb +24 -0
- data/lib/hiera/backend/eyaml/actions/decrypt_action.rb +67 -0
- data/lib/hiera/backend/eyaml/actions/edit_action.rb +47 -0
- data/lib/hiera/backend/eyaml/actions/encrypt_action.rb +80 -0
- data/lib/hiera/backend/eyaml/encryptor.rb +71 -0
- data/lib/hiera/backend/eyaml/encryptors/pkcs7.rb +99 -0
- data/lib/hiera/backend/eyaml/options.rb +32 -0
- data/lib/hiera/backend/eyaml/plugins.rb +65 -0
- data/lib/hiera/backend/eyaml/utils.rb +83 -0
- data/lib/hiera/backend/eyaml_backend.rb +108 -113
- data/sublime_text/README.md +16 -0
- data/sublime_text/eyaml.sublime-package +0 -0
- data/sublime_text/eyaml.syntax_definition.json +288 -0
- data/{bin → tools}/regem.sh +1 -1
- metadata +71 -7
- data/keys/.keepme +0 -0
- 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
|
-
|
2
|
-
|
3
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
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
|
-
|
80
|
+
def deblock block_string
|
81
|
+
block_string.gsub(/[ \n]/, '')
|
82
|
+
end
|
81
83
|
|
82
|
-
|
83
|
-
ciphertext = value[4..-2]
|
84
|
-
ciphertext_decoded = Base64.decode64(ciphertext)
|
84
|
+
def decrypt(key, value, scope)
|
85
85
|
|
86
|
-
|
86
|
+
if encrypted? value
|
87
87
|
|
88
|
-
|
89
|
-
|
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
|
-
|
92
|
-
private_key = OpenSSL::PKey::RSA.new( private_key_pem )
|
96
|
+
Eyaml::Options[:source] = "hiera"
|
93
97
|
|
94
|
-
|
95
|
-
|
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
|
-
|
104
|
+
plaintext
|
98
105
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
-
|
113
|
-
|
114
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
115
|
+
def debug(msg)
|
116
|
+
Hiera.debug("[eyaml_backend]: #{msg}")
|
117
|
+
end
|
123
118
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
end
|
119
|
+
def warn(msg)
|
120
|
+
Hiera.warn("[eyaml_backend]: #{msg}")
|
121
|
+
end
|
128
122
|
end
|
123
|
+
end
|
129
124
|
end
|