knife-openvpn 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8dd1f7ab9cad0728ed9c378d00f4572308435bad
4
+ data.tar.gz: 7426c914244fad2bfd8a9ae0d11b858369d20586
5
+ SHA512:
6
+ metadata.gz: 95308e27b0a782af5a32f28687b6d3c53bd1a05778b44bec4177ae14e780b898b70caaae04cea30af14f075cb214b136cf904be272c7249bdf11cac8e221f36f
7
+ data.tar.gz: 74f43c6fead9c46d76edb409ba435e4aece085be7841f1ec6fd6f5892cc43f05a4326b0323c1f5ebdd57dd3cc8cbca58bb7a3acf0ce3194584a5fcbe8f8d0cee
data/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # knife-openvpn
2
+ A knife plugin for Express 42 openvpn cookbook
@@ -0,0 +1,16 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = 'knife-openvpn'
6
+ gem.version = '0.0.1'
7
+ gem.summary = 'A knife plugin for Express 42 openvpn cookbook'
8
+ gem.description = gem.summary
9
+ gem.authors = ['LLC Express 42']
10
+ gem.email = 'cookbooks@express42.com'
11
+ gem.homepage = 'https://github.com/express42/knife-openvpn'
12
+ gem.license = 'MIT'
13
+
14
+ gem.files = `git ls-files`.split("\n")
15
+ gem.require_paths = ['lib']
16
+ end
@@ -0,0 +1,441 @@
1
+ #
2
+ # Cookbook Name:: openvpn
3
+ # OpenVPN knife plugin
4
+ #
5
+ # Copyright 2013, LLC Express 42
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining
8
+ # a copy of this software and associated documentation files (the
9
+ # "Software"), to deal in the Software without restriction, including
10
+ # without limitation the rights to use, copy, modify, merge, publish,
11
+ # distribute, sublicense, and/or sell copies of the Software, and to
12
+ # permit persons to whom the Software is furnished to do so, subject to
13
+ # the following conditions:
14
+ #
15
+ # The above copyright notice and this permission notice shall be
16
+ # included in all copies or substantial portions of the Software.
17
+ #
18
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+ #
26
+
27
+ module OpenvpnPlugin
28
+ class Openvpn < Chef::Knife
29
+ def run
30
+ ui.info 'knife openvpn (user|server) action ARGS OPTS'
31
+ end
32
+
33
+ deps do
34
+ require 'chef/encrypted_data_bag_item'
35
+ require 'json'
36
+ require 'openssl'
37
+ end
38
+
39
+ def check_databag_secret
40
+ databag_secret_file = File.join(Dir.pwd, '.chef/encrypted_data_bag_secret')
41
+ unless File.exist? databag_secret_file
42
+ fail_with "Can't find encrypted databag secret file at #{databag_secret_file}."
43
+ end
44
+ end
45
+
46
+ def check_existing_databag(server_name, fail_if_exists = false)
47
+ databag_directory = File.join(Dir.pwd, "data_bags/openvpn-#{server_name}")
48
+ if File.directory? databag_directory
49
+ if fail_if_exists # databag exists and we want to create new
50
+ fail_with "Data bag directory #{databag_directory} already exists."
51
+ end
52
+ else
53
+ unless fail_if_exists # no such databag, but we want to use it
54
+ fail_with "Data bag #{databag_directory} not exists."
55
+ end
56
+ end
57
+ end
58
+
59
+ def fail_with(error_message)
60
+ ui.error "Error: #{error_message}"
61
+ exit 1
62
+ end
63
+
64
+ def make_name(cn, cert_config)
65
+ name = OpenSSL::X509::Name.new
66
+ name.add_entry 'CN', cn
67
+ %w(C L O OU ST mail).each { |entry| name.add_entry(entry, cert_config[entry]) }
68
+ name
69
+ end
70
+
71
+ def get_databag_secret
72
+ databag_secret_file = File.join(Dir.pwd, '.chef/encrypted_data_bag_secret')
73
+ secret = Chef::EncryptedDataBagItem.load_secret(databag_secret_file)
74
+ secret
75
+ end
76
+
77
+ def get_extensions_factory(subject_cert, issuer_cert)
78
+ factory = OpenSSL::X509::ExtensionFactory.new
79
+ factory.subject_certificate = subject_cert
80
+ factory.issuer_certificate = issuer_cert
81
+ factory
82
+ end
83
+
84
+ def add_ca_extensions(ca_cert)
85
+ ef = get_extensions_factory ca_cert, ca_cert
86
+ ca_cert.add_extension(ef.create_extension('basicConstraints', 'CA:TRUE', true))
87
+ ca_cert.add_extension(ef.create_extension('keyUsage', 'keyCertSign, cRLSign', true))
88
+ ca_cert.add_extension(ef.create_extension('subjectKeyIdentifier', 'hash', false))
89
+ ca_cert.add_extension(ef.create_extension('authorityKeyIdentifier', 'keyid:always', false))
90
+ end
91
+
92
+ def add_endentity_extensions(entity_cert, ca_cert)
93
+ ef = get_extensions_factory entity_cert, ca_cert
94
+ entity_cert.add_extension(ef.create_extension('keyUsage', 'digitalSignature', true))
95
+ entity_cert.add_extension(ef.create_extension('subjectKeyIdentifier', 'hash', false))
96
+ end
97
+
98
+ def generate_cert_and_key(subject, cert_config, selfsigned = false, ca_cert = nil, ca_key = nil)
99
+ key = OpenSSL::PKey::RSA.generate(cert_config['rsa_keysize'])
100
+ cert = OpenSSL::X509::Certificate.new
101
+ cert.version = 2
102
+ cert.serial = Time.now.to_i
103
+ cert.public_key = key.public_key
104
+
105
+ cert.not_after = Time.now + (cert_config['years_to_expire'] * 365 * 24 * 60 * 60)
106
+ cert.not_before = Time.now - (24 * 60 * 60)
107
+
108
+ if selfsigned
109
+ cert.subject = subject
110
+ cert.issuer = subject
111
+ add_ca_extensions(cert)
112
+ cert.sign(key, OpenSSL::Digest::SHA1.new)
113
+ else
114
+ if ca_cert.nil? || ca_key.nil?
115
+ fail_with "CA key or cert isn't specified"
116
+ end
117
+ cert.subject = subject
118
+ cert.issuer = ca_cert.subject
119
+ add_endentity_extensions(cert, ca_cert)
120
+ cert.sign(ca_key, OpenSSL::Digest::SHA1.new)
121
+ end
122
+
123
+ [cert, key]
124
+ end
125
+
126
+ def issue_crl(revoke_info, serial, lastup, nextup, extensions,
127
+ issuer, issuer_key, digest)
128
+ crl = OpenSSL::X509::CRL.new
129
+ crl.issuer = issuer.subject
130
+ crl.version = 1
131
+ crl.last_update = lastup
132
+ crl.next_update = nextup
133
+ revoke_info.each do|rserial, time, reason_code|
134
+ revoked = OpenSSL::X509::Revoked.new
135
+ revoked.serial = rserial
136
+ revoked.time = time
137
+ enum = OpenSSL::ASN1::Enumerated(reason_code)
138
+ ext = OpenSSL::X509::Extension.new('CRLReason', enum)
139
+ revoked.add_extension(ext)
140
+ crl.add_revoked(revoked)
141
+ end
142
+ ef = OpenSSL::X509::ExtensionFactory.new
143
+ ef.issuer_certificate = issuer
144
+ ef.crl = crl
145
+ crlnum = OpenSSL::ASN1::Integer(serial)
146
+ crl.add_extension(OpenSSL::X509::Extension.new('crlNumber', crlnum))
147
+ extensions.each do|oid, value, critical|
148
+ crl.add_extension(ef.create_extension(oid, value, critical))
149
+ end
150
+ crl.sign(issuer_key, digest)
151
+ crl
152
+ end
153
+
154
+ def load_cert_and_key(cert_str, key_str)
155
+ cert = OpenSSL::X509::Certificate.new cert_str
156
+ key = OpenSSL::PKey::RSA.new key_str
157
+ [cert, key]
158
+ end
159
+
160
+ def get_databag_path(server_name)
161
+ directory_path = File.join(Dir.pwd, "data_bags/openvpn-#{server_name}")
162
+ directory_path
163
+ end
164
+
165
+ def get_databag_name(server_name)
166
+ databag_name = "openvpn-#{server_name}"
167
+ databag_name
168
+ end
169
+
170
+ def save_databag_item(id, server_name, item_hash)
171
+ databag_path = get_databag_path server_name
172
+ item_hash['id'] = id
173
+ item_path = File.join(databag_path, "#{id}.json")
174
+ secret = get_databag_secret
175
+ encrypted_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(item_hash, secret)
176
+ unless File.exist? item_path
177
+ File.write item_path, JSON.pretty_generate(encrypted_data)
178
+ else
179
+ fail_with "#{item_path} already exists"
180
+ end
181
+ end
182
+
183
+ def load_databag_item(databag_name, item_id)
184
+ secret = get_databag_secret
185
+ # puts "Loading [#{databag_name}:#{item_id}]"
186
+ item = Chef::EncryptedDataBagItem.load(databag_name, item_id, secret)
187
+ item
188
+ end
189
+ end
190
+
191
+ class OpenvpnServerCreate < Openvpn
192
+ banner 'knife openvpn server create NAME (options)'
193
+ deps do
194
+ require 'readline'
195
+ end
196
+
197
+ def run
198
+ check_arguments
199
+ vpn_server_name = name_args.first
200
+ check_existing_databag vpn_server_name, true
201
+ check_databag_secret
202
+ create_new_server vpn_server_name
203
+ end
204
+
205
+ def create_new_server(vpn_server_name)
206
+ now = Time.at(Time.now.to_i)
207
+ cert_config = ask_for_cert_config
208
+ ca_subject = make_name 'CA', cert_config
209
+ ca_cert, ca_key = generate_cert_and_key ca_subject, cert_config, true
210
+ server_subject = make_name vpn_server_name, cert_config
211
+ server_cert, server_key = generate_cert_and_key server_subject, cert_config, false, ca_cert, ca_key
212
+ dh_params = make_dh_params cert_config
213
+ crl = issue_crl([], 1, now, now + 3600, [], ca_cert, ca_key, OpenSSL::Digest::SHA1.new)
214
+ databag_path = get_databag_path vpn_server_name
215
+ ui.info "Creating data bag directory at #{databag_path}"
216
+ create_databag_dir vpn_server_name
217
+ save_databag_item('openvpn-config', vpn_server_name, cert_config)
218
+ save_databag_item('openvpn-ca', vpn_server_name, 'cert' => ca_cert.to_pem, 'key' => ca_key.to_pem)
219
+ save_databag_item('openvpn-crl', vpn_server_name, 'crl' => crl.to_pem, 'revoke_info' => [])
220
+
221
+ save_databag_item('openvpn-server', vpn_server_name, 'cert' => server_cert.to_pem, 'key' => server_key.to_pem)
222
+ save_databag_item('openvpn-dh', vpn_server_name, 'dh' => dh_params.to_pem)
223
+ end
224
+
225
+ def check_arguments
226
+ unless name_args.size == 1
227
+ fail_with 'Specify NAME of new openvpn server!'
228
+ end
229
+ end
230
+
231
+ def create_databag_dir(server_name)
232
+ databag_path = get_databag_path server_name
233
+ Dir.mkdir(databag_path, 0755)
234
+ databag_path
235
+ end
236
+
237
+ def read_with_prompt_and_default(prompt, default)
238
+ answer = Readline.readline("#{prompt} [#{default}]: ").strip
239
+ if answer.empty?
240
+ default
241
+ else
242
+ answer
243
+ end
244
+ end
245
+
246
+ def make_dh_params(cert_config)
247
+ keysize = cert_config['dh_keysize']
248
+ dh_params = OpenSSL::PKey::DH.new keysize
249
+ dh_params
250
+ end
251
+
252
+ def ask_for_cert_config
253
+ cert_config = {}
254
+ strings_prompt_default = [
255
+ ['C', 'Country Name', 'RU'],
256
+ ['ST', 'State or Province Name', 'MSK'],
257
+ ['L', 'Locality Name', 'Moscow'],
258
+ ['O', 'Organization Name', 'Express 42'],
259
+ ['OU', 'Organizational Unit Name', 'OPS'],
260
+ ['mail', 'Email', 'ops@example.com']
261
+ ]
262
+ numeric_prompt_default = [
263
+ ['rsa_keysize', 'RSA key size (1024/2048/4096)', '2048'],
264
+ ['dh_keysize', 'DH key size (1024/2048/4096)', '1024'],
265
+ ['years_to_expire', 'Expiration (in years from now)', '5']
266
+ ]
267
+ strings_prompt_default.each { |entry| cert_config[entry[0]] = read_with_prompt_and_default(entry[1], entry[2]) }
268
+ numeric_prompt_default.each { |entry| cert_config[entry[0]] = read_with_prompt_and_default(entry[1], entry[2]).to_i }
269
+ %w(rsa_keysize dh_keysize).each do |keysize|
270
+ unless [1024, 2048, 4096].include? cert_config[keysize]
271
+ fail_with "Wrong value for #{keysize}, must be one of 1024/2048/4096"
272
+ end
273
+ end
274
+ cert_config
275
+ end
276
+ end
277
+
278
+ class OpenvpnUserCreate < Openvpn
279
+ banner 'knife openvpn user create SERVERNAME USERNAME (options)'
280
+
281
+ def run
282
+ check_arguments
283
+ server_name = name_args[0]
284
+ user_name = name_args[1]
285
+ check_existing_databag server_name, false
286
+ check_databag_secret
287
+ create_new_user server_name, user_name
288
+ end
289
+
290
+ def create_new_user(server_name, user_name)
291
+ databag_name = get_databag_name server_name
292
+ ca_item = load_databag_item(databag_name, 'openvpn-ca')
293
+ ca_cert, ca_key = load_cert_and_key ca_item['cert'], ca_item['key']
294
+ config_item = load_databag_item(databag_name, 'openvpn-config')
295
+ cert_config = config_item.to_hash
296
+ user_subject = make_name user_name, cert_config
297
+ user_cert, user_key = generate_cert_and_key user_subject, cert_config, false, ca_cert, ca_key
298
+ save_databag_item(user_name, server_name, 'cert' => user_cert.to_pem, 'key' => user_key.to_pem)
299
+ ui.info "Done, now you can upload #{databag_name}/#{user_name}.json"
300
+ end
301
+
302
+ def check_arguments
303
+ unless name_args.size == 2
304
+ fail_with 'Specify SERVERNAME and USERNAME for new openvpn user!'
305
+ end
306
+ end
307
+ end
308
+
309
+ class OpenvpnUserExport < Openvpn
310
+ banner 'knife openvpn user export SERVERNAME USERNAME (options)'
311
+
312
+ deps do
313
+ require 'chef/search/query'
314
+ end
315
+
316
+ def run
317
+ check_arguments
318
+ server_name = name_args[0]
319
+ user_name = name_args[1]
320
+ check_existing_databag server_name, false
321
+ check_databag_secret
322
+ export_user server_name, user_name
323
+ end
324
+
325
+ def export_user(server_name, user_name)
326
+ databag_name = get_databag_name server_name
327
+ ca_item = load_databag_item(databag_name, 'openvpn-ca')
328
+ ca_cert, ca_key = load_cert_and_key ca_item['cert'], ca_item['key']
329
+
330
+ user_item = load_databag_item(databag_name, user_name)
331
+ user_cert, user_key = load_cert_and_key user_item['cert'], user_item['key']
332
+ tmpdir = Dir.mktmpdir
333
+ ui.info "tmpdir: #{tmpdir}"
334
+ begin
335
+ user_dir = "#{tmpdir}/#{user_name}-vpn"
336
+ Dir.mkdir user_dir
337
+ ui.info "userdir: #{user_dir}"
338
+ export_file "#{user_dir}/ca.crt", ca_cert.to_pem
339
+ export_file "#{user_dir}/#{user_name}.crt", user_cert.to_pem
340
+ export_file "#{user_dir}/#{user_name}.key", user_key.to_pem
341
+ config_content = generate_client_config server_name, user_name
342
+ export_file "#{user_dir}/#{user_name}.ovpn", config_content
343
+ exitcode = system("cd #{tmpdir} && tar cfz /tmp/#{user_name}-vpn.tar.gz *")
344
+ if exitcode
345
+ ui.info "Done, archive at /tmp/#{user_name}-vpn.tar.gz"
346
+ else
347
+ ui.error "Something went wrong, cant create archive at /tmp/#{user_name}-vpn.tar.gz"
348
+ end
349
+ ensure
350
+ FileUtils.rm_rf(tmpdir)
351
+ end
352
+ end
353
+
354
+ def export_file(file_path, content)
355
+ File.write file_path, content
356
+ FileUtils.chmod 'u=wr,go-wr', file_path
357
+ end
358
+
359
+ def generate_client_config(server_name, user_name)
360
+ query = "openvpn_server_name:#{server_name}"
361
+ query_nodes = Chef::Search::Query.new
362
+ search_result = query_nodes.search('node', query)[0]
363
+ unless search_result.length == 1
364
+ fail_with "Found #{search_result.length} vpn servers for #{server_name}"
365
+ end
366
+ config_content = ''
367
+ newline = "\n"
368
+ node = search_result[0]
369
+ config_content << 'client' << newline
370
+ config_content << "dev #{node['openvpn'][server_name]['dev']}" << newline
371
+ config_content << "proto #{node['openvpn'][server_name]['proto']}" << newline
372
+ config_content << "remote #{node['openvpn'][server_name]['remote_host']} "
373
+ config_content << "#{node['openvpn'][server_name]['port']}" << newline
374
+ config_content << "verb #{node['openvpn'][server_name]['verb']}" << newline
375
+ config_content << 'comp-lzo' << newline
376
+ config_content << 'ca ca.crt' << newline
377
+ config_content << "cert #{user_name}.crt" << newline
378
+ config_content << "key #{user_name}.key" << newline
379
+ config_content << 'nobind' << newline
380
+ config_content << 'persist-key' << newline
381
+ config_content << 'persist-tun' << newline
382
+ config_content
383
+ end
384
+
385
+ def check_arguments
386
+ unless name_args.size == 2
387
+ fail_with 'Specify SERVERNAME and USERNAME for new openvpn user!'
388
+ end
389
+ end
390
+ end
391
+
392
+ class OpenvpnUserRevoke < Openvpn
393
+ banner 'knife openvpn user revoke SERVERNAME USERNAME'
394
+
395
+ deps do
396
+ require 'chef/search/query'
397
+ end
398
+
399
+ def run
400
+ check_arguments
401
+ server_name = name_args[0]
402
+ user_name = name_args[1]
403
+ check_existing_databag server_name, false
404
+ check_databag_secret
405
+ revoke_user server_name, user_name
406
+ end
407
+
408
+ def revoke_user(server_name, user_name)
409
+ now = Time.at(Time.now.to_i)
410
+ databag_name = get_databag_name server_name
411
+ ca_item = load_databag_item(databag_name, 'openvpn-ca')
412
+ ca_cert, ca_key = load_cert_and_key ca_item['cert'], ca_item['key']
413
+ begin
414
+ crl_item = load_databag_item(databag_name, 'openvpn-crl')
415
+ old_crl = OpenSSL::X509::CRL.new crl_item['crl']
416
+ revoke_info = crl_item['revoke_info']
417
+ rescue
418
+ old_crl = issue_crl([], 1, now, now + 3600, [], ca_cert, ca_key, OpenSSL::Digest::SHA1.new)
419
+ revoke_info = []
420
+ end
421
+ user_item = load_databag_item(databag_name, user_name)
422
+ user_cert, user_key = load_cert_and_key user_item['cert'], user_item['key']
423
+ user_revoke_info = [[user_cert.serial, now, 0]]
424
+ new_revoke_info = revoke_info + user_revoke_info
425
+ new_crl = add_user_to_crl ca_cert, ca_key, old_crl, new_revoke_info
426
+ save_databag_item('openvpn-crl', server_name, 'crl' => new_crl.to_pem, 'revoke_info' => new_revoke_info)
427
+ ui.info "revoked #{user_name}, do not forget to upload CRL databag item"
428
+ end
429
+
430
+ def add_user_to_crl(ca_cert, ca_key, old_crl, revoke_info)
431
+ new_crl = issue_crl(revoke_info, old_crl.version + 1, Time.at(Time.now.to_i), Time.at(Time.now.to_i) + 3600, [], ca_cert, ca_key, OpenSSL::Digest::SHA1.new)
432
+ new_crl
433
+ end
434
+
435
+ def check_arguments
436
+ unless name_args.size == 2
437
+ fail_with 'Specify SERVERNAME and USERNAME for existing openvpn user!'
438
+ end
439
+ end
440
+ end
441
+ end
metadata ADDED
@@ -0,0 +1,47 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: knife-openvpn
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - LLC Express 42
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-13 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A knife plugin for Express 42 openvpn cookbook
14
+ email: cookbooks@express42.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - README.md
20
+ - knife-openvpn.gemspec
21
+ - lib/chef/knife/openvpn.rb
22
+ homepage: https://github.com/express42/knife-openvpn
23
+ licenses:
24
+ - MIT
25
+ metadata: {}
26
+ post_install_message:
27
+ rdoc_options: []
28
+ require_paths:
29
+ - lib
30
+ required_ruby_version: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ required_rubygems_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ requirements: []
41
+ rubyforge_project:
42
+ rubygems_version: 2.4.1
43
+ signing_key:
44
+ specification_version: 4
45
+ summary: A knife plugin for Express 42 openvpn cookbook
46
+ test_files: []
47
+ has_rdoc: