chef-vault 1.2.5 → 2.0.1.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -17,19 +17,23 @@
17
17
  #
18
18
 
19
19
  require 'chef'
20
+ require 'chef/user'
21
+ require 'chef-vault/version'
22
+ require 'chef-vault/exceptions'
23
+ require 'chef-vault/item'
24
+ require 'chef-vault/item_keys'
20
25
  require 'chef-vault/user'
21
26
  require 'chef-vault/certificate'
22
- require 'chef-vault/version'
23
- require 'chef-vault/chef/offline'
27
+ require 'chef-vault/chef_patch/api_client'
28
+ require 'chef-vault/chef_patch/user'
24
29
 
25
30
  class ChefVault
26
31
 
27
- attr_accessor :data_bag
28
- attr_accessor :chef_config_file
32
+ attr_accessor :vault
29
33
 
30
- def initialize(data_bag, chef_config_file=nil)
31
- @data_bag = data_bag
32
- @chef_config_file = chef_config_file
34
+ def initialize(vault, chef_config_file=nil)
35
+ @vault = vault
36
+ ChefVault.load_config(chef_config_file) if chef_config_file
33
37
  end
34
38
 
35
39
  def version
@@ -37,10 +41,14 @@ class ChefVault
37
41
  end
38
42
 
39
43
  def user(username)
40
- ChefVault::User.new(data_bag, username, chef_config_file)
44
+ ChefVault::User.new(vault, username)
41
45
  end
42
46
 
43
47
  def certificate(name)
44
- ChefVault::Certificate.new(data_bag, name, chef_config_file)
48
+ ChefVault::Certificate.new(vault, name)
49
+ end
50
+
51
+ def self.load_config(chef_config_file)
52
+ Chef::Config.from_file(chef_config_file)
45
53
  end
46
54
  end
@@ -15,38 +15,17 @@
15
15
 
16
16
  class ChefVault
17
17
  class Certificate
18
- attr_accessor :name
19
-
20
- def initialize(data_bag, name, chef_config_file)
21
- @name = name
22
- @data_bag = data_bag
18
+ def initialize(vault, name)
19
+ @item = ChefVault::Item.load(vault, name)
20
+ end
23
21
 
24
- if chef_config_file
25
- chef = ChefVault::ChefOffline.new(chef_config_file)
26
- chef.connect
27
- end
22
+ def [](key)
23
+ @item[key]
28
24
  end
29
25
 
30
26
  def decrypt_contents
31
- # use the private client_key file to create a decryptor
32
- private_key = open(Chef::Config[:client_key]).read
33
- private_key = OpenSSL::PKey::RSA.new(private_key)
34
-
35
- begin
36
- keys = Chef::DataBagItem.load(@data_bag, "#{name}_keys")
37
- rescue
38
- throw "Could not find data bag item #{name}_keys in data bag #{@data_bag}"
39
- end
40
-
41
- unless keys[Chef::Config[:node_name]]
42
- throw "#{name} is not encrypted for you! Rebuild the certificate data bag"
43
- end
44
-
45
- node_key = Base64.decode64(keys[Chef::Config[:node_name]])
46
- shared_secret = private_key.private_decrypt(node_key)
47
- certificate = Chef::EncryptedDataBagItem.load(@data_bag, @name, shared_secret)
48
-
49
- certificate["contents"]
27
+ puts "WARNING: This method is deprecated, please switch to item['value'] calls"
28
+ @item["contents"]
50
29
  end
51
30
  end
52
31
  end
@@ -0,0 +1,40 @@
1
+ # Author:: Kevin Moser <kevin.moser@nordstrom.com>
2
+ # Copyright:: Copyright 2013, Nordstrom, Inc.
3
+ # License:: Apache License, Version 2.0
4
+
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ class ChefVault
18
+ module ChefPatch
19
+ class ApiClient < Chef::ApiClient
20
+ # Fix an issue where core Chef::ApiClient does not load
21
+ # the private key for Chef 10
22
+ def self.load(name)
23
+ response = http_api.get("clients/#{name}")
24
+ if response.kind_of?(Chef::ApiClient)
25
+ response
26
+ else
27
+ client = Chef::ApiClient.new
28
+ client.name(response['clientname'])
29
+
30
+ if response['certificate']
31
+ der = OpenSSL::X509::Certificate.new response['certificate']
32
+ client.public_key der.public_key.to_s
33
+ end
34
+
35
+ client
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,33 @@
1
+ # Author:: Kevin Moser <kevin.moser@nordstrom.com>
2
+ # Copyright:: Copyright 2013, Nordstrom, Inc.
3
+ # License:: Apache License, Version 2.0
4
+
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ class ChefVault
18
+ module ChefPatch
19
+ class User < Chef::User
20
+ # def from_hash for our implementation because name is not being
21
+ # set correctly for Chef 10 server
22
+ def superclass.from_hash(user_hash)
23
+ user = Chef::User.new
24
+ user.name user_hash['username'] ? user_hash['username'] : user_hash['name']
25
+ user.private_key user_hash['private_key'] if user_hash.key?('private_key')
26
+ user.password user_hash['password'] if user_hash.key?('password')
27
+ user.public_key user_hash['public_key']
28
+ user.admin user_hash['admin']
29
+ user
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,27 @@
1
+ # Author:: Kevin Moser <kevin.moser@nordstrom.com>
2
+ # Copyright:: Copyright 2013, Nordstrom, Inc.
3
+ # License:: Apache License, Version 2.0
4
+
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ class ChefVault::Exceptions
18
+ class SecretDecryption < RuntimeError; end
19
+ class NoKeysDefined < RuntimeError; end
20
+ class ItemNotEncrypted < RuntimeError; end
21
+ class KeysActionNotValue < RuntimeError; end
22
+ class AdminNotFound < RuntimeError; end
23
+ class ClientNotFound < RuntimeError; end
24
+ class KeysNotFound < RuntimeError; end
25
+ class ItemNotFound < RuntimeError; end
26
+ class ItemAlreadyExists < RuntimeError; end
27
+ end
@@ -0,0 +1,243 @@
1
+ # Author:: Kevin Moser <kevin.moser@nordstrom.com>
2
+ # Copyright:: Copyright 2013, Nordstrom, Inc.
3
+ # License:: Apache License, Version 2.0
4
+
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ class ChefVault::Item < Chef::DataBagItem
18
+ attr_accessor :keys
19
+ attr_accessor :encrypted_data_bag_item
20
+
21
+ def initialize(vault, name)
22
+ super() # Don't pass parameters
23
+ @data_bag = vault
24
+ @raw_data["id"] = name
25
+ @keys = ChefVault::ItemKeys.new(vault, "#{name}_keys")
26
+ @secret = generate_secret
27
+ @encrypted = false
28
+ end
29
+
30
+ def load_keys(vault, keys)
31
+ @keys = ChefVault::ItemKeys.load(vault, keys)
32
+ @secret = secret
33
+ end
34
+
35
+ def clients(search=nil, action=:add)
36
+ if search
37
+ results_returned = false
38
+
39
+ query = Chef::Search::Query.new
40
+ query.search(:node, search)[0].each do |node|
41
+ results_returned = true
42
+
43
+ case action
44
+ when :add
45
+ begin
46
+ keys.add(ChefVault::ChefPatch::ApiClient.load(node.name), @secret, "clients")
47
+ rescue Net::HTTPServerException => http_error
48
+ if http_error.response.code == "404"
49
+ raise ChefVault::Exceptions::ClientNotFound,
50
+ "#{node.name} is not a valid chef client and/or node"
51
+ else
52
+ raise http_error
53
+ end
54
+ end
55
+ when :delete
56
+ keys.delete(node.name, "clients")
57
+ else
58
+ raise ChefVault::Exceptions::KeysActionNotValid,
59
+ "#{action} is not a valid action"
60
+ end
61
+ end
62
+
63
+ unless results_returned
64
+ puts "WARNING: No clients were returned from search, you may not have "\
65
+ "got what you expected!!"
66
+ end
67
+ else
68
+ keys.clients
69
+ end
70
+ end
71
+
72
+ def admins(admins=nil, action=:add)
73
+ if admins
74
+ admins.split(",").each do |admin|
75
+ admin.strip!
76
+ case action
77
+ when :add
78
+ begin
79
+ keys.add(ChefVault::ChefPatch::User.load(admin), @secret, "admins")
80
+ rescue Net::HTTPServerException => http_error
81
+ if http_error.response.code == "404"
82
+ raise ChefVault::Exceptions::AdminNotFound,
83
+ "#{admin} is not a valid chef admin"
84
+ else
85
+ raise http_error
86
+ end
87
+ end
88
+ when :delete
89
+ keys.delete(admin, "admins")
90
+ else
91
+ raise ChefVault::Exceptions::KeysActionNotValid,
92
+ "#{action} is not a valid action"
93
+ end
94
+ end
95
+ else
96
+ keys.admins
97
+ end
98
+ end
99
+
100
+ def remove(key)
101
+ @raw_data.delete(key)
102
+ end
103
+
104
+ def secret
105
+ if @keys.include?(Chef::Config[:node_name])
106
+ private_key = OpenSSL::PKey::RSA.new(open(Chef::Config[:client_key]).read())
107
+ private_key.private_decrypt(Base64.decode64(@keys[Chef::Config[:node_name]]))
108
+ else
109
+ raise ChefVault::Exceptions::SecretDecryption,
110
+ "#{data_bag}/#{id} is not encrypted with your public key. "\
111
+ "Contact an administrator of the vault item to encrypt for you!"
112
+ end
113
+ end
114
+
115
+ def rotate_keys!
116
+ @secret = generate_secret
117
+
118
+ unless clients.empty?
119
+ clients.each do |client|
120
+ clients("name:#{client}")
121
+ end
122
+ end
123
+
124
+ unless admins.empty?
125
+ admins.each do |admin|
126
+ admins(admin)
127
+ end
128
+ end
129
+
130
+ save
131
+ reload_raw_data
132
+ end
133
+
134
+ def generate_secret
135
+ OpenSSL::PKey::RSA.new(245).to_pem.lines.to_a[1..-2].join
136
+ end
137
+
138
+ def []=(key, value)
139
+ reload_raw_data if @encrypted
140
+ super
141
+ end
142
+
143
+ def [](key)
144
+ reload_raw_data if @encrypted
145
+ super
146
+ end
147
+
148
+ def save(item_id=@raw_data['id'])
149
+ # save the keys first, raising an error if no keys were defined
150
+ if keys.admins.empty? && keys.clients.empty?
151
+ raise ChefVault::Exceptions::NoKeysDefined,
152
+ "No keys defined for #{item_id}"
153
+ end
154
+
155
+ keys.save
156
+
157
+ # Make sure the item is encrypted before saving
158
+ encrypt! unless @encrypted
159
+
160
+ # Now save the encrypted data
161
+ if Chef::Config[:solo]
162
+ data_bag_path = File.join(Chef::Config[:data_bag_path],
163
+ data_bag)
164
+ data_bag_item_path = File.join(data_bag_path, item_id)
165
+
166
+ FileUtils.mkdir(data_bag_path) unless File.exists?(data_bag_path)
167
+ File.open("#{data_bag_item_path}.json",'w') do |file|
168
+ file.write(JSON.pretty_generate(self))
169
+ end
170
+
171
+ self
172
+ else
173
+ begin
174
+ chef_data_bag = Chef::DataBag.load(data_bag)
175
+ rescue Net::HTTPServerException => http_error
176
+ if http_error.response.code == "404"
177
+ chef_data_bag = Chef::DataBag.new
178
+ chef_data_bag.name data_bag
179
+ chef_data_bag.create
180
+ end
181
+ end
182
+
183
+ super
184
+ end
185
+ end
186
+
187
+ def to_json(*a)
188
+ json = super
189
+ json.gsub(self.class.name, self.class.superclass.name)
190
+ end
191
+
192
+ def destroy
193
+ keys.destroy
194
+
195
+ if Chef::Config[:solo]
196
+ data_bag_path = File.join(Chef::Config[:data_bag_path],
197
+ data_bag)
198
+ data_bag_item_path = File.join(data_bag_path, @raw_data["id"])
199
+
200
+ FileUtils.rm("#{data_bag_item_path}.json")
201
+
202
+ nil
203
+ else
204
+ super(data_bag, id)
205
+ end
206
+ end
207
+
208
+ def self.load(vault, name)
209
+ item = new(vault, name)
210
+ item.load_keys(vault, "#{name}_keys")
211
+
212
+ begin
213
+ item.raw_data =
214
+ Chef::EncryptedDataBagItem.load(vault, name, item.secret).to_hash
215
+ rescue Net::HTTPServerException => http_error
216
+ if http_error.response.code == "404"
217
+ raise ChefVault::Exceptions::ItemNotFound,
218
+ "#{vault}/#{name} could not be found"
219
+ else
220
+ raise http_error
221
+ end
222
+ rescue Chef::Exceptions::ValidationFailed
223
+ raise ChefVault::Exceptions::ItemNotFound,
224
+ "#{vault}/#{name} could not be found"
225
+ end
226
+
227
+ item
228
+ end
229
+
230
+ private
231
+ def encrypt!
232
+ @raw_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(self, @secret)
233
+ @encrypted = true
234
+ end
235
+
236
+ def reload_raw_data
237
+ @raw_data =
238
+ Chef::EncryptedDataBagItem.load(@data_bag, @raw_data["id"], secret).to_hash
239
+ @encrypted = false
240
+
241
+ @raw_data
242
+ end
243
+ end
@@ -0,0 +1,121 @@
1
+ # Author:: Kevin Moser <kevin.moser@nordstrom.com>
2
+ # Copyright:: Copyright 2013, Nordstrom, Inc.
3
+ # License:: Apache License, Version 2.0
4
+
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ class ChefVault::ItemKeys < Chef::DataBagItem
18
+ def initialize(vault, name)
19
+ super() # parenthesis required to strip off parameters
20
+ @data_bag = vault
21
+ @raw_data["id"] = name
22
+ @raw_data["admins"] = []
23
+ @raw_data["clients"] = []
24
+ end
25
+
26
+ def include?(key)
27
+ @raw_data.keys.include?(key)
28
+ end
29
+
30
+ def add(chef_client, data_bag_shared_secret, type)
31
+ public_key = OpenSSL::PKey::RSA.new chef_client.public_key
32
+ self[chef_client.name] =
33
+ Base64.encode64(public_key.public_encrypt(data_bag_shared_secret))
34
+
35
+ @raw_data[type] << chef_client.name unless @raw_data[type].include?(chef_client.name)
36
+ @raw_data[type]
37
+ end
38
+
39
+ def delete(chef_client, type)
40
+ raw_data.delete(chef_client)
41
+ raw_data[type].delete(chef_client)
42
+ end
43
+
44
+ def clients
45
+ @raw_data["clients"]
46
+ end
47
+
48
+ def admins
49
+ @raw_data["admins"]
50
+ end
51
+
52
+ def save(item_id=@raw_data['id'])
53
+ if Chef::Config[:solo]
54
+ data_bag_path = File.join(Chef::Config[:data_bag_path],
55
+ data_bag)
56
+ data_bag_item_path = File.join(data_bag_path, item_id)
57
+
58
+ FileUtils.mkdir(data_bag_path) unless File.exists?(data_bag_path)
59
+ File.open("#{data_bag_item_path}.json",'w') do |file|
60
+ file.write(JSON.pretty_generate(self))
61
+ end
62
+
63
+ self
64
+ else
65
+ begin
66
+ chef_data_bag = Chef::DataBag.load(data_bag)
67
+ rescue Net::HTTPServerException => http_error
68
+ if http_error.response.code == "404"
69
+ chef_data_bag = Chef::DataBag.new
70
+ chef_data_bag.name data_bag
71
+ chef_data_bag.create
72
+ end
73
+ end
74
+
75
+ super
76
+ end
77
+ end
78
+
79
+ def destroy
80
+ if Chef::Config[:solo]
81
+ data_bag_path = File.join(Chef::Config[:data_bag_path],
82
+ data_bag)
83
+ data_bag_item_path = File.join(data_bag_path, @raw_data["id"])
84
+
85
+ FileUtils.rm("#{data_bag_item_path}.json")
86
+
87
+ nil
88
+ else
89
+ super(data_bag, id)
90
+ end
91
+ end
92
+
93
+ def to_json(*a)
94
+ json = super
95
+ json.gsub(self.class.name, self.class.superclass.name)
96
+ end
97
+
98
+ def self.from_data_bag_item(data_bag_item)
99
+ item = new(data_bag_item.data_bag, data_bag_item.name)
100
+ item.raw_data = data_bag_item.raw_data
101
+ item
102
+ end
103
+
104
+ def self.load(vault, name)
105
+ begin
106
+ data_bag_item = Chef::DataBagItem.load(vault, name)
107
+ rescue Net::HTTPServerException => http_error
108
+ if http_error.response.code == "404"
109
+ raise ChefVault::Exceptions::KeysNotFound,
110
+ "#{vault}/#{name} could not be found"
111
+ else
112
+ raise http_error
113
+ end
114
+ rescue Chef::Exceptions::ValidationFailed
115
+ raise ChefVault::Exceptions::KeysNotFound,
116
+ "#{vault}/#{name} could not be found"
117
+ end
118
+
119
+ from_data_bag_item(data_bag_item)
120
+ end
121
+ end