-
Notifications
You must be signed in to change notification settings - Fork 8
/
vault_cert.rb
330 lines (273 loc) · 10.5 KB
/
vault_cert.rb
1
2
3
4
5
6
7
8
9
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
require 'etc'
require 'json'
require "#{File.dirname(__FILE__)}/../../../puppet_x/vault_secrets/vaultsession.rb"
Puppet::Type.type(:vault_cert).provide(:vault_cert) do
desc 'Issue certificates from Hashicorp Vault'
mk_resource_methods
def initialize(value = {})
super(value)
@cert_dir = Facter.value(:vault_cert_dir)
@property_flush = {}
end
def self.instances
# @summary Enables resource discovery for the vault_cert custom type.
instances = []
cert_dir = Facter.value(:vault_cert_dir)
cert_filenames = Dir.glob("#{cert_dir}/*.json")
cert_filenames.each do |info_file|
name = File.basename(info_file, '.json')
info_content, info_owner, info_group, info_mode = load_file(info_file)
cert_info = JSON.parse(info_content)
next unless ['data', 'cert_data', 'cert_chain_file', 'cert_file', 'key_file'].all? { |k| cert_info.key? k }
cert_chain, cert_chain_owner, cert_chain_group, cert_chain_mode = load_file(cert_info['cert_chain_file'])
cert, cert_owner, cert_group, cert_mode = load_file(cert_info['cert_file'])
key, key_owner, key_group, key_mode = load_file(cert_info['key_file'])
begin
expiration = cert_info['data']['expiration']
rescue
expiration = nil
end
instances << new(
ensure: :present,
name: name,
cert_data: cert_info['cert_data'],
expiration: expiration,
# Info file
info_owner: info_owner,
info_group: info_group,
info_mode: info_mode,
# CA Chain
cert_chain_file: cert_info['cert_chain_file'],
cert_chain_owner: cert_chain_owner,
cert_chain_group: cert_chain_group,
cert_chain_mode: cert_chain_mode,
cert_chain: cert_chain,
info_cert_chain: [cert_info['data']['certificate'], cert_info['data']['ca_chain'].join('')].join(''),
# Certificate
cert_file: cert_info['cert_file'],
cert_owner: cert_owner,
cert_group: cert_group,
cert_mode: cert_mode,
cert: cert,
info_cert: cert_info['data']['certificate'],
# Private Key
key_file: cert_info['key_file'],
key_owner: key_owner,
key_group: key_group,
key_mode: key_mode,
key: key,
info_key: cert_info['data']['private_key'],
)
end
instances
end
def self.prefetch(resources)
instances.each do |prov|
if (resource = resources[prov.name])
resource.provider = prov
end
end
end
def exists?
@property_hash[:ensure] == :present
end
def create
@property_flush[:ensure] = :present
end
def destroy
@property_flush[:ensure] = :absent
end
def issue_cert
# @summary Request a certificate from the Vault API.
Puppet.info("Requesting certificate #{@resource[:name]}")
connection = {
'uri' => @resource[:vault_uri],
'auth_path' => @resource[:auth_path],
'auth_name' => @resource[:auth_name],
'ca_trust' => @resource[:ca_trust],
'timeout' => @resource[:timeout],
}
# Use the Vault class for the lookup
vault = VaultSession.new(connection)
response = vault.post(URI(@resource[:vault_uri]).path, @resource[:cert_data])
vault.parse_response(response)
end
def self.load_file(file)
if file && File.exist?(file)
content = File.read(file)
stat = File::Stat.new(file)
owner = Etc.getpwuid(stat.uid).name
group = Etc.getgrgid(stat.gid).name
mode = '%04o' % (stat.mode & 0o7777)
[content, owner, group, mode]
else
[nil, nil, nil, nil]
end
end
def self.chown_file(file, owner, group)
uid = owner ? Etc.getpwnam(owner).uid : nil
gid = group ? Etc.getgrnam(group).gid : nil
File.chown(uid, gid, file) unless uid.nil? && gid.nil?
end
def self.chmod_file(file, mode)
File.chmod(mode.to_i(8), file) if mode
end
def self.delete_if_exists(file)
File.delete(file) if !file.to_s.empty? && File.exist?(file)
end
def expires_soon_or_expired
# @summary Determine the certificate expiration date.
time_now = Time.now.to_i
expiry_time = @property_hash[:expiration]
renewal_threshold_seconds = @resource[:renewal_threshold] * 3600 * 24
renewal_time = expiry_time - renewal_threshold_seconds
time_now >= renewal_time
end
def needs_issue?
return true if @property_hash[:ensure] != :present
return true if @property_flush.include?(:cert_data) && @property_hash[:cert_data] != @property_flush[:cert_data]
return true if expires_soon_or_expired
false
end
def info_owner=(value)
@property_flush[:info_owner] = value
end
def info_group=(value)
@property_flush[:info_group] = value
end
def info_mode=(value)
@property_flush[:info_mode] = value
end
def cert_data=(value)
@property_flush[:cert_data] = value
end
def cert_chain_file=(value)
@property_flush[:cert_chain_file] = value
end
def cert_chain_owner=(value)
@property_flush[:cert_chain_owner] = value
end
def cert_chain_group=(value)
@property_flush[:cert_chain_group] = value
end
def cert_chain_mode=(value)
@property_flush[:cert_chain_mode] = value
end
def cert_file=(value)
@property_flush[:cert_file] = value
end
def cert_owner=(value)
@property_flush[:cert_owner] = value
end
def cert_group=(value)
@property_flush[:cert_group] = value
end
def cert_mode=(value)
@property_flush[:cert_mode] = value
end
def key_file=(value)
@property_flush[:key_file] = value
end
def key_owner=(value)
@property_flush[:key_owner] = value
end
def key_group=(value)
@property_flush[:key_group] = value
end
def key_mode=(value)
@property_flush[:key_mode] = value
end
def expiration=(value)
# Property should be read only, do not change
end
def cert_chain=(value); end
def cert=(value); end
def key=(value); end
def flush_file_attributes(file, owner, group, mode, force = false)
# Update the file ownership if not in sync
if force || (@property_flush.include?(owner) && @property_hash[owner] != @property_flush[owner]) || (@property_flush.include?(group) && @property_hash[group] != @property_flush[group])
self.class.chown_file(file, @property_flush[owner], @property_flush[group])
@property_hash[owner] = @property_flush[owner]
@property_hash[group] = @property_flush[group]
end
# Update the file permissions if not in sync
# rubocop:disable Style/GuardClause
if force || (@property_flush.include?(mode) && @property_hash[mode] != @property_flush[mode])
self.class.chmod_file(file, @property_flush[mode])
@property_hash[mode] = @property_flush[mode]
end
# rubocop:enable Style/GuardClause
end
def flush_file(file, content, owner, group, mode)
# If the file will be created (new, or moved), we must reset the attributes,
# since Puppet may not have signalled a change by calling the setter methods
force_reset_attributes = !@property_hash.include?(content) || (@property_hash[content].nil? || @property_hash[content].empty?)
# If the file path is being changed, delete the old file first and force all attributes
# to be reset, since they won't be correct on the new file, even if they were
# correct on the old file before
if @property_flush.include?(file) && @property_hash[file] != @property_flush[file]
self.class.delete_if_exists(@property_hash[file])
force_reset_attributes = true
# Indicate that we did change the file path
@property_hash[file] = @property_flush[file]
end
if force_reset_attributes
@property_flush[owner] = @resource[owner]
@property_flush[group] = @resource[group]
@property_flush[mode] = @resource[mode]
end
# Update the file content if not in sync
if force_reset_attributes || (@property_flush.include?(content) && @property_hash[content] != @property_flush[content])
File.write(@resource[file], @property_flush[content])
@property_hash[content] = @property_flush[content]
end
flush_file_attributes(@resource[file], owner, group, mode, force_reset_attributes)
end
def flush
info_file = "#{@cert_dir}/#{@resource[:name]}.json"
if @property_flush[:ensure] == :absent
self.class.delete_if_exists(@resource[:cert_chain_file])
self.class.delete_if_exists(@resource[:cert_file])
self.class.delete_if_exists(@resource[:key_file])
self.class.delete_if_exists(info_file)
# Remove all other attributes so that `puppet resource`
# shows the correct state after removal
@property_hash = @property_hash.slice(:ensure)
return
end
if needs_issue?
response = issue_cert
# Add trailing newlines to file content values
response['ca_chain'] = response['ca_chain'].map { |data| "#{data}\n" }
response['certificate'] = "#{response['certificate']}\n"
response['private_key'] = "#{response['private_key']}\n"
info = JSON.generate({
data: response,
cert_data: @resource[:cert_data],
cert_chain_file: @resource[:cert_chain_file],
cert_file: @resource[:cert_file],
key_file: @resource[:key_file],
})
File.write(info_file, info)
@property_flush[:info_mode] = @resource[:info_mode]
flush_file_attributes(info_file, :info_owner, :info_group, :info_mode, true)
# These are read-only properties which will never
# be set in the puppet resource, but we will set once
# a cert has been issued.
# These are flushed to disk later if @property_hash != @resource
@property_flush[:cert_chain] = [response['certificate'], response['ca_chain'].join('')].join('')
@property_flush[:cert] = response['certificate']
@property_flush[:key] = response['private_key']
else
flush_file_attributes(info_file, :info_owner, :info_group, :info_mode)
# Re-read the info file to make sure the intended contents of the chain/cert/key files are correct
cert_info = JSON.parse(File.read(info_file))
@property_flush[:cert_chain] = [cert_info['data']['certificate'], cert_info['data']['ca_chain'].join('')].join('')
@property_flush[:cert] = cert_info['data']['certificate']
@property_flush[:key] = cert_info['data']['private_key']
end
flush_file(:cert_chain_file, :cert_chain, :cert_chain_owner, :cert_chain_group, :cert_chain_mode)
flush_file(:cert_file, :cert, :cert_owner, :cert_group, :cert_mode)
flush_file(:key_file, :key, :key_owner, :key_group, :key_mode)
end
end