leap_cli 1.2.5
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.
- 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
|