leap_cli 1.2.5
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/leap +81 -0
- data/lib/core_ext/boolean.rb +14 -0
- data/lib/core_ext/hash.rb +35 -0
- data/lib/core_ext/json.rb +42 -0
- data/lib/core_ext/nil.rb +5 -0
- data/lib/core_ext/string.rb +14 -0
- data/lib/leap/platform.rb +52 -0
- data/lib/leap_cli/commands/ca.rb +430 -0
- data/lib/leap_cli/commands/clean.rb +16 -0
- data/lib/leap_cli/commands/compile.rb +134 -0
- data/lib/leap_cli/commands/deploy.rb +172 -0
- data/lib/leap_cli/commands/facts.rb +93 -0
- data/lib/leap_cli/commands/inspect.rb +140 -0
- data/lib/leap_cli/commands/list.rb +122 -0
- data/lib/leap_cli/commands/new.rb +126 -0
- data/lib/leap_cli/commands/node.rb +272 -0
- data/lib/leap_cli/commands/pre.rb +99 -0
- data/lib/leap_cli/commands/shell.rb +67 -0
- data/lib/leap_cli/commands/test.rb +55 -0
- data/lib/leap_cli/commands/user.rb +140 -0
- data/lib/leap_cli/commands/util.rb +50 -0
- data/lib/leap_cli/commands/vagrant.rb +201 -0
- data/lib/leap_cli/config/macros.rb +369 -0
- data/lib/leap_cli/config/manager.rb +369 -0
- data/lib/leap_cli/config/node.rb +37 -0
- data/lib/leap_cli/config/object.rb +336 -0
- data/lib/leap_cli/config/object_list.rb +174 -0
- data/lib/leap_cli/config/secrets.rb +43 -0
- data/lib/leap_cli/config/tag.rb +18 -0
- data/lib/leap_cli/constants.rb +7 -0
- data/lib/leap_cli/leapfile.rb +97 -0
- data/lib/leap_cli/load_paths.rb +15 -0
- data/lib/leap_cli/log.rb +166 -0
- data/lib/leap_cli/logger.rb +216 -0
- data/lib/leap_cli/markdown_document_listener.rb +134 -0
- data/lib/leap_cli/path.rb +84 -0
- data/lib/leap_cli/remote/leap_plugin.rb +204 -0
- data/lib/leap_cli/remote/puppet_plugin.rb +66 -0
- data/lib/leap_cli/remote/rsync_plugin.rb +35 -0
- data/lib/leap_cli/remote/tasks.rb +36 -0
- data/lib/leap_cli/requirements.rb +19 -0
- data/lib/leap_cli/ssh_key.rb +130 -0
- data/lib/leap_cli/util/remote_command.rb +110 -0
- data/lib/leap_cli/util/secret.rb +54 -0
- data/lib/leap_cli/util/x509.rb +32 -0
- data/lib/leap_cli/util.rb +431 -0
- data/lib/leap_cli/version.rb +9 -0
- data/lib/leap_cli.rb +46 -0
- data/lib/lib_ext/capistrano_connections.rb +16 -0
- data/lib/lib_ext/gli.rb +52 -0
- data/lib/lib_ext/markdown_document_listener.rb +122 -0
- data/vendor/certificate_authority/lib/certificate_authority/certificate.rb +200 -0
- data/vendor/certificate_authority/lib/certificate_authority/certificate_revocation_list.rb +77 -0
- data/vendor/certificate_authority/lib/certificate_authority/distinguished_name.rb +97 -0
- data/vendor/certificate_authority/lib/certificate_authority/extensions.rb +266 -0
- data/vendor/certificate_authority/lib/certificate_authority/key_material.rb +148 -0
- data/vendor/certificate_authority/lib/certificate_authority/ocsp_handler.rb +144 -0
- data/vendor/certificate_authority/lib/certificate_authority/pkcs11_key_material.rb +65 -0
- data/vendor/certificate_authority/lib/certificate_authority/revocable.rb +14 -0
- data/vendor/certificate_authority/lib/certificate_authority/serial_number.rb +10 -0
- data/vendor/certificate_authority/lib/certificate_authority/signing_entity.rb +16 -0
- data/vendor/certificate_authority/lib/certificate_authority/signing_request.rb +56 -0
- data/vendor/certificate_authority/lib/certificate_authority.rb +21 -0
- data/vendor/rsync_command/lib/rsync_command/ssh_options.rb +159 -0
- data/vendor/rsync_command/lib/rsync_command/thread_pool.rb +36 -0
- data/vendor/rsync_command/lib/rsync_command/version.rb +3 -0
- data/vendor/rsync_command/lib/rsync_command.rb +96 -0
- data/vendor/rsync_command/test/rsync_test.rb +74 -0
- data/vendor/rsync_command/test/ssh_options_test.rb +61 -0
- data/vendor/vagrant_ssh_keys/vagrant.key +27 -0
- data/vendor/vagrant_ssh_keys/vagrant.pub +1 -0
- metadata +345 -0
data/bin/leap
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
begin
|
3
|
+
require 'leap_cli'
|
4
|
+
rescue LoadError
|
5
|
+
#
|
6
|
+
# When developing a gem with a command, you normally use `bundle exec bin/command-name`
|
7
|
+
# to run your app. At install-time, RubyGems will make sure lib, etc. are in the load path,
|
8
|
+
# so that you can run the command directly.
|
9
|
+
#
|
10
|
+
# However, I don't like using 'bundle exec'. It is slow, and limits which directory you can
|
11
|
+
# run in. So, instead, we fall back to some path manipulation hackery.
|
12
|
+
#
|
13
|
+
# This allows you to run the command directly while developing the gem, and also lets you
|
14
|
+
# run from anywhere (I like to link 'bin/leap' to /usr/local/bin/leap).
|
15
|
+
#
|
16
|
+
require 'rubygems'
|
17
|
+
base_dir = File.expand_path('..', File.dirname(File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__))
|
18
|
+
require File.join(base_dir, 'lib','leap_cli','load_paths')
|
19
|
+
require 'leap_cli'
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'gli'
|
23
|
+
require 'highline'
|
24
|
+
require 'forwardable'
|
25
|
+
require 'lib_ext/gli' # our custom extensions to gli
|
26
|
+
|
27
|
+
#
|
28
|
+
# Typically, GLI and Highline methods are loaded into the global namespace.
|
29
|
+
# Instead, here we load these into the module LeapCli::Commands in order to
|
30
|
+
# ensure that the cli logic and code is kept isolated to leap_cli/commands/*.rb
|
31
|
+
#
|
32
|
+
# no cheating!
|
33
|
+
#
|
34
|
+
module LeapCli::Commands
|
35
|
+
extend GLI::App
|
36
|
+
extend Forwardable
|
37
|
+
|
38
|
+
# delegate highline methods to make them available to sub-commands
|
39
|
+
@terminal = HighLine.new
|
40
|
+
def_delegator :@terminal, :ask, 'self.ask'
|
41
|
+
def_delegator :@terminal, :agree, 'self.agree'
|
42
|
+
def_delegator :@terminal, :choose, 'self.choose'
|
43
|
+
def_delegator :@terminal, :say, 'self.say'
|
44
|
+
def_delegator :@terminal, :color, 'self.color'
|
45
|
+
def_delegator :@terminal, :list, 'self.list'
|
46
|
+
|
47
|
+
# make config manager available as 'manager'
|
48
|
+
def self.manager
|
49
|
+
@manager ||= begin
|
50
|
+
manager = LeapCli::Config::Manager.new
|
51
|
+
manager.load
|
52
|
+
manager
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# make provider config available as 'provider'
|
57
|
+
def self.provider
|
58
|
+
manager.provider
|
59
|
+
end
|
60
|
+
|
61
|
+
# make leapfile available as 'leapfile'
|
62
|
+
def self.leapfile
|
63
|
+
LeapCli::leapfile
|
64
|
+
end
|
65
|
+
|
66
|
+
# info about leap command line suite
|
67
|
+
program_desc LeapCli::SUMMARY
|
68
|
+
program_long_desc LeapCli::DESCRIPTION
|
69
|
+
|
70
|
+
# handle --version ourselves
|
71
|
+
if ARGV.grep(/--version/).any?
|
72
|
+
puts "leap #{LeapCli::VERSION}, ruby #{RUBY_VERSION}"
|
73
|
+
exit(0)
|
74
|
+
end
|
75
|
+
|
76
|
+
# load commands and run
|
77
|
+
commands_from('leap_cli/commands')
|
78
|
+
ORIGINAL_ARGV = ARGV.dup
|
79
|
+
exit_status = run(ARGV)
|
80
|
+
exit(LeapCli::Util.exit_status || exit_status)
|
81
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class Hash
|
2
|
+
|
3
|
+
##
|
4
|
+
## CONVERTING
|
5
|
+
##
|
6
|
+
|
7
|
+
#
|
8
|
+
# convert self into a hash, but only include the specified keys
|
9
|
+
#
|
10
|
+
def pick(*keys)
|
11
|
+
keys.map(&:to_s).inject({}) do |hsh, key|
|
12
|
+
if has_key?(key)
|
13
|
+
hsh[key] = self[key]
|
14
|
+
end
|
15
|
+
hsh
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
# recursive merging (aka deep merge)
|
21
|
+
# taken from ActiveSupport::CoreExtensions::Hash::DeepMerge
|
22
|
+
#
|
23
|
+
def deep_merge(other_hash)
|
24
|
+
self.merge(other_hash) do |key, oldval, newval|
|
25
|
+
oldval = oldval.to_hash if oldval.respond_to?(:to_hash)
|
26
|
+
newval = newval.to_hash if newval.respond_to?(:to_hash)
|
27
|
+
oldval.class.to_s == 'Hash' && newval.class.to_s == 'Hash' ? oldval.deep_merge(newval) : newval
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def deep_merge!(other_hash)
|
32
|
+
replace(deep_merge(other_hash))
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module JSON
|
2
|
+
#
|
3
|
+
# Output JSON from ruby objects in such a manner that all the hashes and arrays are output in alphanumeric sorted order.
|
4
|
+
# This is required so that our generated configs don't throw puppet or git for a tizzy fit.
|
5
|
+
#
|
6
|
+
# Beware: some hacky stuff ahead.
|
7
|
+
#
|
8
|
+
# This relies on the pure ruby implementation of JSON.generate (i.e. require 'json/pure')
|
9
|
+
# see https://github.com/flori/json/blob/master/lib/json/pure/generator.rb
|
10
|
+
#
|
11
|
+
# The Oj way that we are not using: Oj.dump(obj, :mode => :compat, :indent => 2)
|
12
|
+
#
|
13
|
+
def self.sorted_generate(obj)
|
14
|
+
# modify hash and array
|
15
|
+
Array.class_eval do
|
16
|
+
alias_method :each_without_sort, :each
|
17
|
+
def each(&block)
|
18
|
+
sorted = sort {|a,b| a.to_s <=> b.to_s }
|
19
|
+
for i in 0..(sorted.length-1) do
|
20
|
+
yield sorted[i]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
Hash.class_eval do
|
25
|
+
alias_method :each_without_sort, :each
|
26
|
+
def each(&block)
|
27
|
+
self.keys.each do |key|
|
28
|
+
yield key, self.fetch(key) # fetch is used so we don't trigger Config::Object auto-eval
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# generate json
|
34
|
+
json_str = JSON.pretty_generate(obj)
|
35
|
+
|
36
|
+
# restore hash and array
|
37
|
+
Hash.class_eval {alias_method :each, :each_without_sort}
|
38
|
+
Array.class_eval {alias_method :each, :each_without_sort}
|
39
|
+
|
40
|
+
return json_str
|
41
|
+
end
|
42
|
+
end
|
data/lib/core_ext/nil.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'versionomy'
|
2
|
+
|
3
|
+
module Leap
|
4
|
+
|
5
|
+
class Platform
|
6
|
+
class << self
|
7
|
+
#
|
8
|
+
# configuration
|
9
|
+
#
|
10
|
+
|
11
|
+
attr_reader :version
|
12
|
+
attr_reader :compatible_cli
|
13
|
+
attr_accessor :facts
|
14
|
+
attr_accessor :paths
|
15
|
+
attr_accessor :node_files
|
16
|
+
attr_accessor :puppet_destination
|
17
|
+
|
18
|
+
def define(&block)
|
19
|
+
self.instance_eval(&block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def version=(version)
|
23
|
+
@version = Versionomy.parse(version)
|
24
|
+
end
|
25
|
+
|
26
|
+
def compatible_cli=(range)
|
27
|
+
@compatible_cli = range
|
28
|
+
@minimum_cli_version = Versionomy.parse(range.first)
|
29
|
+
@maximum_cli_version = Versionomy.parse(range.last)
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# return true if the cli_version is compatible with this platform.
|
34
|
+
#
|
35
|
+
def compatible_with_cli?(cli_version)
|
36
|
+
cli_version = Versionomy.parse(cli_version)
|
37
|
+
cli_version >= @minimum_cli_version && cli_version <= @maximum_cli_version
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# return true if the platform version is within the specified range.
|
42
|
+
#
|
43
|
+
def version_in_range?(range)
|
44
|
+
minimum_platform_version = Versionomy.parse(range.first)
|
45
|
+
maximum_platform_version = Versionomy.parse(range.last)
|
46
|
+
@version >= minimum_platform_version && @version <= maximum_platform_version
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,430 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'certificate_authority'
|
3
|
+
require 'date'
|
4
|
+
require 'digest/md5'
|
5
|
+
|
6
|
+
module LeapCli; module Commands
|
7
|
+
|
8
|
+
desc "Manage X.509 certificates"
|
9
|
+
command :cert do |cert|
|
10
|
+
|
11
|
+
cert.desc 'Creates two Certificate Authorities (one for validating servers and one for validating clients).'
|
12
|
+
cert.long_desc 'See see what values are used in the generation of the certificates (like name and key size), run `leap inspect provider` and look for the "ca" property. To see the details of the created certs, run `leap inspect <file>`.'
|
13
|
+
cert.command :ca do |ca|
|
14
|
+
ca.action do |global_options,options,args|
|
15
|
+
assert_config! 'provider.ca.name'
|
16
|
+
generate_new_certificate_authority(:ca_key, :ca_cert, provider.ca.name)
|
17
|
+
generate_new_certificate_authority(:client_ca_key, :client_ca_cert, provider.ca.name + ' (client certificates only!)')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
cert.desc 'Creates or renews a X.509 certificate/key pair for a single node or all nodes, but only if needed.'
|
22
|
+
cert.long_desc 'This command will a generate new certificate for a node if some value in the node has changed ' +
|
23
|
+
'that is included in the certificate (like hostname or IP address), or if the old certificate will be expiring soon. ' +
|
24
|
+
'Sometimes, you might want to force the generation of a new certificate, ' +
|
25
|
+
'such as in the cases where you have changed a CA parameter for server certificates, like bit size or digest hash. ' +
|
26
|
+
'In this case, use --force. If <node-filter> is empty, this command will apply to all nodes.'
|
27
|
+
cert.arg_name 'FILTER'
|
28
|
+
cert.command :update do |update|
|
29
|
+
update.switch 'force', :desc => 'Always generate new certificates', :negatable => false
|
30
|
+
update.action do |global_options,options,args|
|
31
|
+
assert_files_exist! :ca_cert, :ca_key, :msg => 'Run `leap cert ca` to create them'
|
32
|
+
assert_config! 'provider.ca.server_certificates.bit_size'
|
33
|
+
assert_config! 'provider.ca.server_certificates.digest'
|
34
|
+
assert_config! 'provider.ca.server_certificates.life_span'
|
35
|
+
assert_config! 'common.x509.use'
|
36
|
+
|
37
|
+
nodes = manager.filter!(args)
|
38
|
+
nodes.each_node do |node|
|
39
|
+
if !node.x509.use
|
40
|
+
remove_file!([:node_x509_key, node.name])
|
41
|
+
remove_file!([:node_x509_cert, node.name])
|
42
|
+
elsif options[:force] || cert_needs_updating?(node)
|
43
|
+
generate_cert_for_node(node)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
cert.desc 'Creates a Diffie-Hellman parameter file.' # (needed for server-side of some TLS connections)
|
50
|
+
cert.command :dh do |dh|
|
51
|
+
dh.action do |global_options,options,args|
|
52
|
+
long_running do
|
53
|
+
if cmd_exists?('certtool')
|
54
|
+
log 0, 'Generating DH parameters (takes a long time)...'
|
55
|
+
output = assert_run!('certtool --generate-dh-params --sec-param high')
|
56
|
+
output.sub! /.*(-----BEGIN DH PARAMETERS-----.*-----END DH PARAMETERS-----).*/m, '\1'
|
57
|
+
output << "\n"
|
58
|
+
write_file!(:dh_params, output)
|
59
|
+
else
|
60
|
+
log 0, 'Generating DH parameters (takes a REALLY long time)...'
|
61
|
+
output = OpenSSL::PKey::DH.generate(3248).to_pem
|
62
|
+
write_file!(:dh_params, output)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# hints:
|
70
|
+
#
|
71
|
+
# inspect CSR:
|
72
|
+
# openssl req -noout -text -in files/cert/x.csr
|
73
|
+
#
|
74
|
+
# generate CSR with openssl to see how it compares:
|
75
|
+
# openssl req -sha256 -nodes -newkey rsa:2048 -keyout example.key -out example.csr
|
76
|
+
#
|
77
|
+
# validate a CSR:
|
78
|
+
# http://certlogik.com/decoder/
|
79
|
+
#
|
80
|
+
# nice details about CSRs:
|
81
|
+
# http://www.redkestrel.co.uk/Articles/CSR.html
|
82
|
+
#
|
83
|
+
cert.desc "Creates a CSR for use in buying a commercial X.509 certificate."
|
84
|
+
cert.long_desc "Unless specified, the CSR is created for the provider's primary domain. The properties used for this CSR come from `provider.ca.server_certificates`."
|
85
|
+
cert.command :csr do |csr|
|
86
|
+
csr.flag 'domain', :arg_name => 'DOMAIN', :desc => 'Specify what domain to create the CSR for.'
|
87
|
+
csr.action do |global_options,options,args|
|
88
|
+
assert_config! 'provider.domain'
|
89
|
+
assert_config! 'provider.name'
|
90
|
+
assert_config! 'provider.default_language'
|
91
|
+
assert_config! 'provider.ca.server_certificates.bit_size'
|
92
|
+
assert_config! 'provider.ca.server_certificates.digest'
|
93
|
+
domain = options[:domain] || provider.domain
|
94
|
+
assert_files_missing! [:commercial_key, domain], [:commercial_csr, domain], :msg => 'If you really want to create a new key and CSR, remove these files first.'
|
95
|
+
|
96
|
+
server_certificates = provider.ca.server_certificates
|
97
|
+
|
98
|
+
# RSA key
|
99
|
+
keypair = CertificateAuthority::MemoryKeyMaterial.new
|
100
|
+
log :generating, "%s bit RSA key" % server_certificates.bit_size do
|
101
|
+
keypair.generate_key(server_certificates.bit_size)
|
102
|
+
write_file! [:commercial_key, domain], keypair.private_key.to_pem
|
103
|
+
end
|
104
|
+
|
105
|
+
# CSR
|
106
|
+
dn = CertificateAuthority::DistinguishedName.new
|
107
|
+
csr = CertificateAuthority::SigningRequest.new
|
108
|
+
dn.common_name = domain
|
109
|
+
dn.organization = provider.name[provider.default_language]
|
110
|
+
dn.country = server_certificates['country'] # optional
|
111
|
+
dn.state = server_certificates['state'] # optional
|
112
|
+
dn.locality = server_certificates['locality'] # optional
|
113
|
+
|
114
|
+
log :generating, "CSR with commonName => '%s', organization => '%s'" % [dn.common_name, dn.organization] do
|
115
|
+
csr.distinguished_name = dn
|
116
|
+
csr.key_material = keypair
|
117
|
+
csr.digest = server_certificates.digest
|
118
|
+
request = csr.to_x509_csr
|
119
|
+
write_file! [:commercial_csr, domain], csr.to_pem
|
120
|
+
end
|
121
|
+
|
122
|
+
# Sign using our own CA, for use in testing but hopefully not production.
|
123
|
+
# It is not that commerical CAs are so secure, it is just that signing your own certs is
|
124
|
+
# a total drag for the user because they must click through dire warnings.
|
125
|
+
#if options[:sign]
|
126
|
+
log :generating, "self-signed x509 server certificate for testing purposes" do
|
127
|
+
cert = csr.to_cert
|
128
|
+
cert.serial_number.number = cert_serial_number(domain)
|
129
|
+
cert.not_before = yesterday
|
130
|
+
cert.not_after = years_from_yesterday(1)
|
131
|
+
cert.parent = ca_root
|
132
|
+
cert.sign! domain_test_signing_profile
|
133
|
+
write_file! [:commercial_cert, domain], cert.to_pem
|
134
|
+
log "please replace this file with the real certificate you get from a CA using #{Path.relative_path([:commercial_csr, domain])}"
|
135
|
+
end
|
136
|
+
#end
|
137
|
+
|
138
|
+
# FAKE CA
|
139
|
+
unless file_exists? :commercial_ca_cert
|
140
|
+
log :using, "generated CA in place of commercial CA for testing purposes" do
|
141
|
+
write_file! :commercial_ca_cert, read_file!(:ca_cert)
|
142
|
+
log "please also replace this file with the CA cert from the commercial authority you use."
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
def generate_new_certificate_authority(key_file, cert_file, common_name)
|
152
|
+
assert_files_missing! key_file, cert_file
|
153
|
+
assert_config! 'provider.ca.name'
|
154
|
+
assert_config! 'provider.ca.bit_size'
|
155
|
+
assert_config! 'provider.ca.life_span'
|
156
|
+
|
157
|
+
root = CertificateAuthority::Certificate.new
|
158
|
+
|
159
|
+
# set subject
|
160
|
+
root.subject.common_name = common_name
|
161
|
+
possible = ['country', 'state', 'locality', 'organization', 'organizational_unit', 'email_address']
|
162
|
+
provider.ca.keys.each do |key|
|
163
|
+
if possible.include?(key)
|
164
|
+
root.subject.send(key + '=', provider.ca[key])
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# set expiration
|
169
|
+
root.not_before = yesterday
|
170
|
+
root.not_after = years_from_yesterday(provider.ca.life_span.to_i)
|
171
|
+
|
172
|
+
# generate private key
|
173
|
+
root.serial_number.number = 1
|
174
|
+
root.key_material.generate_key(provider.ca.bit_size)
|
175
|
+
|
176
|
+
# sign self
|
177
|
+
root.signing_entity = true
|
178
|
+
root.parent = root
|
179
|
+
root.sign!(ca_root_signing_profile)
|
180
|
+
|
181
|
+
# save
|
182
|
+
write_file!(key_file, root.key_material.private_key.to_pem)
|
183
|
+
write_file!(cert_file, root.to_pem)
|
184
|
+
end
|
185
|
+
|
186
|
+
#
|
187
|
+
# returns true if the certs associated with +node+ need to be regenerated.
|
188
|
+
#
|
189
|
+
def cert_needs_updating?(node)
|
190
|
+
if !file_exists?([:node_x509_cert, node.name], [:node_x509_key, node.name])
|
191
|
+
return true
|
192
|
+
else
|
193
|
+
cert = load_certificate_file([:node_x509_cert, node.name])
|
194
|
+
if cert.not_after < months_from_yesterday(1)
|
195
|
+
log :updating, "cert for node '#{node.name}' because it will expire soon"
|
196
|
+
return true
|
197
|
+
end
|
198
|
+
if cert.subject.common_name != node.domain.full
|
199
|
+
log :updating, "cert for node '#{node.name}' because domain.full has changed"
|
200
|
+
return true
|
201
|
+
end
|
202
|
+
cert.openssl_body.extensions.each do |ext|
|
203
|
+
if ext.oid == "subjectAltName"
|
204
|
+
ips = []
|
205
|
+
dns_names = []
|
206
|
+
ext.value.split(",").each do |value|
|
207
|
+
value.strip!
|
208
|
+
ips << $1 if value =~ /^IP Address:(.*)$/
|
209
|
+
dns_names << $1 if value =~ /^DNS:(.*)$/
|
210
|
+
end
|
211
|
+
if ips.first != node.ip_address
|
212
|
+
log :updating, "cert for node '#{node.name}' because ip_address has changed (from #{ips.first} to #{node.ip_address})"
|
213
|
+
return true
|
214
|
+
elsif dns_names != dns_names_for_node(node)
|
215
|
+
log :updating, "cert for node '#{node.name}' because domain name aliases have changed (from #{dns_names.inspect} to #{dns_names_for_node(node).inspect})"
|
216
|
+
return true
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
return false
|
222
|
+
end
|
223
|
+
|
224
|
+
def generate_cert_for_node(node)
|
225
|
+
return if node.x509.use == false
|
226
|
+
|
227
|
+
cert = CertificateAuthority::Certificate.new
|
228
|
+
|
229
|
+
# set subject
|
230
|
+
cert.subject.common_name = node.domain.full
|
231
|
+
cert.serial_number.number = cert_serial_number(node.domain.full)
|
232
|
+
|
233
|
+
# set expiration
|
234
|
+
cert.not_before = yesterday
|
235
|
+
cert.not_after = years_from_yesterday(provider.ca.server_certificates.life_span.to_i)
|
236
|
+
|
237
|
+
# generate key
|
238
|
+
cert.key_material.generate_key(provider.ca.server_certificates.bit_size)
|
239
|
+
|
240
|
+
# sign
|
241
|
+
cert.parent = ca_root
|
242
|
+
cert.sign!(server_signing_profile(node))
|
243
|
+
|
244
|
+
# save
|
245
|
+
write_file!([:node_x509_key, node.name], cert.key_material.private_key.to_pem)
|
246
|
+
write_file!([:node_x509_cert, node.name], cert.to_pem)
|
247
|
+
end
|
248
|
+
|
249
|
+
#
|
250
|
+
# yields client key and cert suitable for testing
|
251
|
+
#
|
252
|
+
def generate_test_client_cert(prefix=nil)
|
253
|
+
cert = CertificateAuthority::Certificate.new
|
254
|
+
cert.serial_number.number = cert_serial_number(provider.domain)
|
255
|
+
cert.subject.common_name = [prefix, random_common_name(provider.domain)].join
|
256
|
+
cert.not_before = yesterday
|
257
|
+
cert.not_after = years_from_yesterday(1)
|
258
|
+
cert.key_material.generate_key(1024) # just for testing, remember!
|
259
|
+
cert.parent = client_ca_root
|
260
|
+
cert.sign! client_test_signing_profile
|
261
|
+
yield cert.key_material.private_key.to_pem, cert.to_pem
|
262
|
+
end
|
263
|
+
|
264
|
+
def ca_root
|
265
|
+
@ca_root ||= begin
|
266
|
+
load_certificate_file(:ca_cert, :ca_key)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def client_ca_root
|
271
|
+
@client_ca_root ||= begin
|
272
|
+
load_certificate_file(:client_ca_cert, :client_ca_key)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def load_certificate_file(crt_file, key_file=nil, password=nil)
|
277
|
+
crt = read_file!(crt_file)
|
278
|
+
openssl_cert = OpenSSL::X509::Certificate.new(crt)
|
279
|
+
cert = CertificateAuthority::Certificate.from_openssl(openssl_cert)
|
280
|
+
if key_file
|
281
|
+
key = read_file!(key_file)
|
282
|
+
cert.key_material.private_key = OpenSSL::PKey::RSA.new(key, password)
|
283
|
+
end
|
284
|
+
return cert
|
285
|
+
end
|
286
|
+
|
287
|
+
def ca_root_signing_profile
|
288
|
+
{
|
289
|
+
"extensions" => {
|
290
|
+
"basicConstraints" => {"ca" => true},
|
291
|
+
"keyUsage" => {
|
292
|
+
"usage" => ["critical", "keyCertSign"]
|
293
|
+
},
|
294
|
+
"extendedKeyUsage" => {
|
295
|
+
"usage" => []
|
296
|
+
}
|
297
|
+
}
|
298
|
+
}
|
299
|
+
end
|
300
|
+
|
301
|
+
#
|
302
|
+
# For keyusage, openvpn server certs can have keyEncipherment or keyAgreement.
|
303
|
+
# Web browsers seem to break without keyEncipherment.
|
304
|
+
# For now, I am using digitalSignature + keyEncipherment
|
305
|
+
#
|
306
|
+
# * digitalSignature -- for (EC)DHE cipher suites
|
307
|
+
# "The digitalSignature bit is asserted when the subject public key is used
|
308
|
+
# with a digital signature mechanism to support security services other
|
309
|
+
# than certificate signing (bit 5), or CRL signing (bit 6). Digital
|
310
|
+
# signature mechanisms are often used for entity authentication and data
|
311
|
+
# origin authentication with integrity."
|
312
|
+
#
|
313
|
+
# * keyEncipherment ==> for plain RSA cipher suites
|
314
|
+
# "The keyEncipherment bit is asserted when the subject public key is used for
|
315
|
+
# key transport. For example, when an RSA key is to be used for key management,
|
316
|
+
# then this bit is set."
|
317
|
+
#
|
318
|
+
# * keyAgreement ==> for used with DH, not RSA.
|
319
|
+
# "The keyAgreement bit is asserted when the subject public key is used for key
|
320
|
+
# agreement. For example, when a Diffie-Hellman key is to be used for key
|
321
|
+
# management, then this bit is set."
|
322
|
+
#
|
323
|
+
# digest options: SHA512, SHA256, SHA1
|
324
|
+
#
|
325
|
+
def server_signing_profile(node)
|
326
|
+
{
|
327
|
+
"digest" => provider.ca.server_certificates.digest,
|
328
|
+
"extensions" => {
|
329
|
+
"keyUsage" => {
|
330
|
+
"usage" => ["digitalSignature", "keyEncipherment"]
|
331
|
+
},
|
332
|
+
"extendedKeyUsage" => {
|
333
|
+
"usage" => ["serverAuth", "clientAuth"]
|
334
|
+
},
|
335
|
+
"subjectAltName" => {
|
336
|
+
"ips" => [node.ip_address],
|
337
|
+
"dns_names" => dns_names_for_node(node)
|
338
|
+
}
|
339
|
+
}
|
340
|
+
}
|
341
|
+
end
|
342
|
+
|
343
|
+
#
|
344
|
+
# This is used when signing the main cert for the provider's domain
|
345
|
+
# with our own CA (for testing purposes). Typically, this cert would
|
346
|
+
# be purchased from a commercial CA, and not signed this way.
|
347
|
+
#
|
348
|
+
def domain_test_signing_profile
|
349
|
+
{
|
350
|
+
"digest" => "SHA256",
|
351
|
+
"extensions" => {
|
352
|
+
"keyUsage" => {
|
353
|
+
"usage" => ["digitalSignature", "keyEncipherment"]
|
354
|
+
},
|
355
|
+
"extendedKeyUsage" => {
|
356
|
+
"usage" => ["serverAuth"]
|
357
|
+
}
|
358
|
+
}
|
359
|
+
}
|
360
|
+
end
|
361
|
+
|
362
|
+
#
|
363
|
+
# This is used when signing a dummy client certificate that is only to be
|
364
|
+
# used for testing.
|
365
|
+
#
|
366
|
+
def client_test_signing_profile
|
367
|
+
{
|
368
|
+
"digest" => "SHA256",
|
369
|
+
"extensions" => {
|
370
|
+
"keyUsage" => {
|
371
|
+
"usage" => ["digitalSignature"]
|
372
|
+
},
|
373
|
+
"extendedKeyUsage" => {
|
374
|
+
"usage" => ["clientAuth"]
|
375
|
+
}
|
376
|
+
}
|
377
|
+
}
|
378
|
+
end
|
379
|
+
|
380
|
+
def dns_names_for_node(node)
|
381
|
+
names = [node.domain.internal, node.domain.full]
|
382
|
+
if node['dns'] && node.dns['aliases'] && node.dns.aliases.any?
|
383
|
+
names += node.dns.aliases
|
384
|
+
names.compact!
|
385
|
+
end
|
386
|
+
return names
|
387
|
+
end
|
388
|
+
|
389
|
+
#
|
390
|
+
# For cert serial numbers, we need a non-colliding number less than 160 bits.
|
391
|
+
# md5 will do nicely, since there is no need for a secure hash, just a short one.
|
392
|
+
# (md5 is 128 bits)
|
393
|
+
#
|
394
|
+
def cert_serial_number(domain_name)
|
395
|
+
Digest::MD5.hexdigest("#{domain_name} -- #{Time.now}").to_i(16)
|
396
|
+
end
|
397
|
+
|
398
|
+
#
|
399
|
+
# for the random common name, we need a text string that will be unique across all certs.
|
400
|
+
# ruby 1.8 doesn't have a built-in uuid generator, or we would use SecureRandom.uuid
|
401
|
+
#
|
402
|
+
def random_common_name(domain_name)
|
403
|
+
cert_serial_number(domain_name).to_s(36)
|
404
|
+
end
|
405
|
+
|
406
|
+
##
|
407
|
+
## TIME HELPERS
|
408
|
+
##
|
409
|
+
## note: we use 'yesterday' instead of 'today', because times are in UTC, and some people on the planet
|
410
|
+
## are behind UTC.
|
411
|
+
##
|
412
|
+
|
413
|
+
def yesterday
|
414
|
+
t = Time.now - 24*24*60
|
415
|
+
Time.utc t.year, t.month, t.day
|
416
|
+
end
|
417
|
+
|
418
|
+
def years_from_yesterday(num)
|
419
|
+
t = yesterday
|
420
|
+
Time.utc t.year + num, t.month, t.day
|
421
|
+
end
|
422
|
+
|
423
|
+
def months_from_yesterday(num)
|
424
|
+
t = yesterday
|
425
|
+
date = Date.new t.year, t.month, t.day
|
426
|
+
date = date >> num # >> is months in the future operator
|
427
|
+
Time.utc date.year, date.month, date.day
|
428
|
+
end
|
429
|
+
|
430
|
+
end; end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module LeapCli
|
2
|
+
module Commands
|
3
|
+
|
4
|
+
desc 'Removes all files generated with the "compile" command.'
|
5
|
+
command :clean do |c|
|
6
|
+
c.action do |global_options,options,args|
|
7
|
+
Dir.glob(path([:hiera, '*'])).each do |file|
|
8
|
+
remove_file! file
|
9
|
+
end
|
10
|
+
remove_file! path(:authorized_keys)
|
11
|
+
remove_file! path(:known_hosts)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|