puppetserver-ca 1.10.0 → 2.0.0
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 +4 -4
- data/README.md +5 -10
- data/lib/puppetserver/ca/action/clean.rb +3 -3
- data/lib/puppetserver/ca/action/enable.rb +1 -1
- data/lib/puppetserver/ca/action/generate.rb +5 -7
- data/lib/puppetserver/ca/action/import.rb +15 -12
- data/lib/puppetserver/ca/action/list.rb +15 -69
- data/lib/puppetserver/ca/action/migrate.rb +1 -9
- data/lib/puppetserver/ca/action/revoke.rb +3 -3
- data/lib/puppetserver/ca/action/setup.rb +16 -13
- data/lib/puppetserver/ca/action/sign.rb +1 -1
- data/lib/puppetserver/ca/certificate_authority.rb +1 -1
- data/lib/puppetserver/ca/cli.rb +1 -6
- data/lib/puppetserver/ca/config/puppet.rb +45 -40
- data/lib/puppetserver/ca/host.rb +2 -2
- data/lib/puppetserver/ca/local_certificate_authority.rb +9 -9
- data/lib/puppetserver/ca/logger.rb +0 -4
- data/lib/puppetserver/ca/utils/config.rb +17 -0
- data/lib/puppetserver/ca/utils/file_system.rb +5 -0
- data/lib/puppetserver/ca/utils/http_client.rb +7 -16
- data/lib/puppetserver/ca/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6523b5628cc4d83aa2627326400a2fb493a18f28d7e4da4b8046eac41e09c555
|
4
|
+
data.tar.gz: 6e8cfbeb2a63ad443f22b196d9cdf2749242ef552c6376fd55b723aa699ceefd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 82c62889b706bad66349d5efd8469969b919d8d90741c57c12827eccdedf2de80597ea923509c66de6d6f317da365d860705d556441ce817167d323ad6e80325
|
7
|
+
data.tar.gz: 72cca87e22e38e8c6b2b7975d4920057f5364c7a9499bcf30553894b9285468a0cb1d3fd93aace8974a889a0e0a0ed35f348d902139a07d0f588957e61f479f4
|
data/README.md
CHANGED
@@ -55,25 +55,20 @@ To create a new keypair and certificate for a certname:
|
|
55
55
|
puppetserver ca generate --certname foo.example.com
|
56
56
|
```
|
57
57
|
|
58
|
-
To enable verbose mode:
|
59
|
-
```
|
60
|
-
puppetserver ca --verbose <action>
|
61
|
-
```
|
62
|
-
|
63
58
|
For more details, see the help output:
|
64
59
|
```
|
65
60
|
puppetserver ca --help
|
66
61
|
```
|
67
62
|
|
68
63
|
This code in this project is licensed under the Apache Software License v2,
|
69
|
-
please see the included [License](https://github.com/puppetlabs/puppetserver-ca-cli/blob/
|
64
|
+
please see the included [License](https://github.com/puppetlabs/puppetserver-ca-cli/blob/master/LICENSE.md)
|
70
65
|
for more details.
|
71
66
|
|
72
67
|
|
73
68
|
## Development
|
74
69
|
|
75
70
|
After checking out the repo, run `bin/setup` to install dependencies. Then,
|
76
|
-
run `
|
71
|
+
run `rake spec` to run the tests. You can also run `bin/console` for an
|
77
72
|
interactive prompt that will allow you to experiment.
|
78
73
|
|
79
74
|
To install this gem onto your local machine, run `bundle exec rake install`.
|
@@ -97,7 +92,7 @@ To test your changes on a VM:
|
|
97
92
|
1. To confirm that installation was successful, run `puppetserver ca --help`
|
98
93
|
|
99
94
|
### Releasing
|
100
|
-
To release a new version, run the [release pipeline](https://jenkins-
|
95
|
+
To release a new version, run the [release pipeline](https://jenkins-master-prod-1.delivery.puppetlabs.net/job/platform_puppetserver-ca_init-multijob_main/), which will bump the version, tag, build, and release the gem.
|
101
96
|
|
102
97
|
|
103
98
|
## Contributing & Support
|
@@ -110,9 +105,9 @@ Freenode, or the Puppet Community Slack channel.
|
|
110
105
|
|
111
106
|
Contributions are welcome at https://github.com/puppetlabs/puppetserver-ca-cli/pulls.
|
112
107
|
Contributors should both be sure to read the
|
113
|
-
[contributing document](https://github.com/puppetlabs/puppetserver-ca-cli/blob/
|
108
|
+
[contributing document](https://github.com/puppetlabs/puppetserver-ca-cli/blob/master/CONTRIBUTING.md)
|
114
109
|
and sign the [contributor license agreement](https://cla.puppet.com/).
|
115
110
|
|
116
111
|
Everyone interacting with the project’s codebase, issue tracker, etc is expected
|
117
112
|
to follow the
|
118
|
-
[code of conduct](https://github.com/puppetlabs/puppetserver-ca-cli/blob/
|
113
|
+
[code of conduct](https://github.com/puppetlabs/puppetserver-ca-cli/blob/master/CODE_OF_CONDUCT.md).
|
@@ -14,7 +14,7 @@ module Puppetserver
|
|
14
14
|
|
15
15
|
include Puppetserver::Ca::Utils
|
16
16
|
|
17
|
-
|
17
|
+
CERTNAME_BLACKLIST = %w{--all --config}
|
18
18
|
|
19
19
|
SUMMARY = 'Revoke cert(s) and remove related files from CA'
|
20
20
|
BANNER = <<-BANNER
|
@@ -59,7 +59,7 @@ BANNER
|
|
59
59
|
errors = CliParsing.parse_with_errors(parser, args)
|
60
60
|
|
61
61
|
results['certnames'].each do |certname|
|
62
|
-
if
|
62
|
+
if CERTNAME_BLACKLIST.include?(certname)
|
63
63
|
errors << " Cannot manage cert named `#{certname}` from " +
|
64
64
|
"the CLI, if needed use the HTTP API directly"
|
65
65
|
end
|
@@ -85,7 +85,7 @@ BANNER
|
|
85
85
|
return 1 if Errors.handle_with_usage(@logger, errors)
|
86
86
|
end
|
87
87
|
|
88
|
-
puppet = Config::Puppet.parse(config)
|
88
|
+
puppet = Config::Puppet.parse(config, @logger)
|
89
89
|
return 1 if Errors.handle_with_usage(@logger, puppet.errors)
|
90
90
|
|
91
91
|
result = clean_certs(certnames, puppet.settings)
|
@@ -18,7 +18,7 @@ module Puppetserver
|
|
18
18
|
|
19
19
|
# Only allow printing ascii characters, excluding /
|
20
20
|
VALID_CERTNAME = /\A[ -.0-~]+\Z/
|
21
|
-
|
21
|
+
CERTNAME_BLACKLIST = %w{--all --config}
|
22
22
|
|
23
23
|
SUMMARY = "Generate a new certificate signed by the CA"
|
24
24
|
BANNER = <<-BANNER
|
@@ -35,7 +35,7 @@ Description:
|
|
35
35
|
If the `--ca-client` flag is passed, the cert will be generated
|
36
36
|
offline, without using Puppet Server's signing code, and will add
|
37
37
|
a special extension authorizing it to talk to the CA API. This can
|
38
|
-
be used for regenerating the
|
38
|
+
be used for regenerating the master's host cert, or for manually
|
39
39
|
setting up other nodes to be CA clients. Do not distribute certs
|
40
40
|
generated this way to any node that you do not intend to have
|
41
41
|
administrative access to the CA (e.g. the ability to sign a cert).
|
@@ -91,7 +91,7 @@ BANNER
|
|
91
91
|
errors << ' At least one certname is required to generate'
|
92
92
|
else
|
93
93
|
results['certnames'].each do |certname|
|
94
|
-
if
|
94
|
+
if CERTNAME_BLACKLIST.include?(certname)
|
95
95
|
errors << " Cannot manage cert named `#{certname}` from " +
|
96
96
|
"the CLI, if needed use the HTTP API directly"
|
97
97
|
end
|
@@ -126,7 +126,7 @@ BANNER
|
|
126
126
|
# Load, resolve, and validate puppet config settings
|
127
127
|
settings_overrides = {}
|
128
128
|
puppet = Config::Puppet.new(config_path)
|
129
|
-
puppet.load(settings_overrides)
|
129
|
+
puppet.load(settings_overrides, @logger)
|
130
130
|
return 1 if Errors.handle_with_usage(@logger, puppet.errors)
|
131
131
|
|
132
132
|
# We don't want generate to respect the alt names setting, since it is usually
|
@@ -296,9 +296,7 @@ BANNER
|
|
296
296
|
end
|
297
297
|
|
298
298
|
def process_alt_names(alt_names, certname)
|
299
|
-
|
300
|
-
# the certname as a SAN, see RFC 2818 https://tools.ietf.org/html/rfc2818#section-3.1.
|
301
|
-
return "DNS:#{certname}" if alt_names.empty?
|
299
|
+
return '' if alt_names.empty?
|
302
300
|
|
303
301
|
current_alt_names = alt_names.dup
|
304
302
|
# When validating the cert, OpenSSL will ignore the CN field if
|
@@ -4,6 +4,7 @@ require 'puppetserver/ca/config/puppet'
|
|
4
4
|
require 'puppetserver/ca/errors'
|
5
5
|
require 'puppetserver/ca/local_certificate_authority'
|
6
6
|
require 'puppetserver/ca/utils/cli_parsing'
|
7
|
+
require 'puppetserver/ca/utils/config'
|
7
8
|
require 'puppetserver/ca/utils/file_system'
|
8
9
|
require 'puppetserver/ca/utils/signing_digest'
|
9
10
|
require 'puppetserver/ca/x509_loader'
|
@@ -14,7 +15,7 @@ module Puppetserver
|
|
14
15
|
class Import
|
15
16
|
include Puppetserver::Ca::Utils
|
16
17
|
|
17
|
-
SUMMARY = "Import an external CA chain and generate
|
18
|
+
SUMMARY = "Import an external CA chain and generate master PKI"
|
18
19
|
BANNER = <<-BANNER
|
19
20
|
Usage:
|
20
21
|
puppetserver ca import [--help]
|
@@ -55,7 +56,7 @@ BANNER
|
|
55
56
|
settings_overrides[:dns_alt_names] = input['subject-alt-names'] unless input['subject-alt-names'].empty?
|
56
57
|
|
57
58
|
puppet = Config::Puppet.new(config_path)
|
58
|
-
puppet.load(settings_overrides)
|
59
|
+
puppet.load(settings_overrides, @logger)
|
59
60
|
return 1 if Errors.handle_with_usage(@logger, puppet.errors)
|
60
61
|
|
61
62
|
# Load most secure signing digest we can for cers/crl/csr signing.
|
@@ -72,7 +73,7 @@ BANNER
|
|
72
73
|
def import(loader, settings, signing_digest)
|
73
74
|
ca = Puppetserver::Ca::LocalCertificateAuthority.new(signing_digest, settings)
|
74
75
|
ca.initialize_ssl_components(loader)
|
75
|
-
|
76
|
+
master_key, master_cert = ca.create_master_cert
|
76
77
|
return ca.errors if ca.errors.any?
|
77
78
|
|
78
79
|
FileSystem.ensure_dirs([settings[:ssldir],
|
@@ -88,25 +89,25 @@ BANNER
|
|
88
89
|
[settings[:cadir] + '/infra_crl.pem', loader.crls],
|
89
90
|
[settings[:localcacert], loader.certs],
|
90
91
|
[settings[:hostcrl], loader.crls],
|
91
|
-
[settings[:hostpubkey],
|
92
|
-
[settings[:hostcert],
|
93
|
-
[settings[:cert_inventory], ca.inventory_entry(
|
92
|
+
[settings[:hostpubkey], master_key.public_key],
|
93
|
+
[settings[:hostcert], master_cert],
|
94
|
+
[settings[:cert_inventory], ca.inventory_entry(master_cert)],
|
94
95
|
[settings[:capub], loader.key.public_key],
|
95
96
|
[settings[:cadir] + '/infra_inventory.txt', ''],
|
96
97
|
[settings[:cadir] + '/infra_serials', ''],
|
97
98
|
[settings[:serial], "002"],
|
98
|
-
[File.join(settings[:signeddir], "#{settings[:certname]}.pem"),
|
99
|
+
[File.join(settings[:signeddir], "#{settings[:certname]}.pem"), master_cert]
|
99
100
|
]
|
100
101
|
|
101
102
|
private_files = [
|
102
|
-
[settings[:hostprivkey],
|
103
|
+
[settings[:hostprivkey], master_key],
|
103
104
|
[settings[:cakey], loader.key],
|
104
105
|
]
|
105
106
|
|
106
107
|
files_to_check = public_files + private_files
|
107
|
-
# We don't want to error if
|
108
|
+
# We don't want to error if master's keys exist. Certain workflows
|
108
109
|
# allow the agent to have already be installed with keys and then
|
109
|
-
# upgraded to be a
|
110
|
+
# upgraded to be a master. The host class will honor keys, if both
|
110
111
|
# public and private exist, and error if only one exists - as is
|
111
112
|
# previous behavior.
|
112
113
|
files_to_check = files_to_check.map(&:first) - [settings[:hostpubkey], settings[:hostprivkey]]
|
@@ -130,6 +131,8 @@ ERR
|
|
130
131
|
FileSystem.write_file(location, content, 0640)
|
131
132
|
end
|
132
133
|
|
134
|
+
Puppetserver::Ca::Utils::Config.symlink_to_old_cadir(settings[:cadir], settings[:confdir])
|
135
|
+
|
133
136
|
return []
|
134
137
|
end
|
135
138
|
|
@@ -178,11 +181,11 @@ ERR
|
|
178
181
|
parsed['crl-chain'] = chain
|
179
182
|
end
|
180
183
|
opts.on('--certname NAME',
|
181
|
-
'Common name to use for the
|
184
|
+
'Common name to use for the master cert') do |name|
|
182
185
|
parsed['certname'] = name
|
183
186
|
end
|
184
187
|
opts.on('--subject-alt-names NAME[,NAME]',
|
185
|
-
'Subject alternative names for the
|
188
|
+
'Subject alternative names for the master cert') do |sans|
|
186
189
|
parsed['subject-alt-names'] = sans
|
187
190
|
end
|
188
191
|
end
|
@@ -30,7 +30,6 @@ Options:
|
|
30
30
|
BANNER
|
31
31
|
|
32
32
|
BODY = JSON.dump({desired_state: 'signed'})
|
33
|
-
VALID_FORMAT = ['text', 'json']
|
34
33
|
|
35
34
|
def initialize(logger)
|
36
35
|
@logger = logger
|
@@ -48,9 +47,6 @@ Options:
|
|
48
47
|
opts.on('--all', 'List all certificates') do |a|
|
49
48
|
parsed['all'] = true
|
50
49
|
end
|
51
|
-
opts.on('--format FORMAT', "Valid formats are: 'text' (default), 'json'") do |f|
|
52
|
-
parsed['format'] = f
|
53
|
-
end
|
54
50
|
opts.on('--certname NAME[,NAME]', Array, 'List the specified cert(s)') do |cert|
|
55
51
|
parsed['certname'] = cert
|
56
52
|
end
|
@@ -61,15 +57,9 @@ Options:
|
|
61
57
|
config = input['config']
|
62
58
|
certnames = input['certname'] || []
|
63
59
|
all = input['all']
|
64
|
-
output_format = input['format'] || "text"
|
65
|
-
|
66
|
-
unless VALID_FORMAT.include?(output_format)
|
67
|
-
Errors.handle_with_usage(@logger, ["Unknown format flag '#{output_format}'. Valid formats are '#{VALID_FORMAT.join("', '")}'."])
|
68
|
-
return 1
|
69
|
-
end
|
70
60
|
|
71
61
|
if all && certnames.any?
|
72
|
-
Errors.handle_with_usage(@logger, ['Cannot combine use of --all and --certname
|
62
|
+
Errors.handle_with_usage(@logger, ['Cannot combine use of --all and --certname'])
|
73
63
|
return 1
|
74
64
|
end
|
75
65
|
|
@@ -78,63 +68,27 @@ Options:
|
|
78
68
|
return 1 if Errors.handle_with_usage(@logger, errors)
|
79
69
|
end
|
80
70
|
|
81
|
-
puppet = Config::Puppet.parse(config)
|
71
|
+
puppet = Config::Puppet.parse(config, @logger)
|
82
72
|
return 1 if Errors.handle_with_usage(@logger, puppet.errors)
|
83
73
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
filter_names = lambda { |x| true }
|
88
|
-
end
|
74
|
+
filter_names = certnames.any? \
|
75
|
+
? lambda { |x| certnames.include?(x['name']) }
|
76
|
+
: lambda { |x| true }
|
89
77
|
|
90
78
|
all_certs = get_all_certs(puppet.settings).select { |cert| filter_names.call(cert) }
|
91
79
|
requested, signed, revoked = separate_certs(all_certs)
|
92
80
|
missing = certnames - all_certs.map { |cert| cert['name'] }
|
93
81
|
|
94
|
-
|
95
|
-
output_certs_by_state(
|
96
|
-
|
97
|
-
output_certs_by_state(all, output_format, requested)
|
98
|
-
end
|
99
|
-
|
100
|
-
return missing.any? ? 1 : 0
|
101
|
-
end
|
82
|
+
(all || certnames.any?) \
|
83
|
+
? output_certs_by_state(requested, signed, revoked, missing)
|
84
|
+
: output_certs_by_state(requested)
|
102
85
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
else
|
107
|
-
output_certs_text_format(requested, signed, revoked, missing)
|
108
|
-
end
|
86
|
+
return missing.any? \
|
87
|
+
? 1
|
88
|
+
: 0
|
109
89
|
end
|
110
90
|
|
111
|
-
def
|
112
|
-
grouped_cert = {}
|
113
|
-
|
114
|
-
if all
|
115
|
-
grouped_cert = { "requested" => requested,
|
116
|
-
"signed" => signed,
|
117
|
-
"revoked" => revoked }.to_json
|
118
|
-
@logger.inform(grouped_cert)
|
119
|
-
else
|
120
|
-
grouped_cert["requested"] = requested unless requested.empty?
|
121
|
-
grouped_cert["signed"] = signed unless signed.empty?
|
122
|
-
grouped_cert["revoked"] = revoked unless revoked.empty?
|
123
|
-
grouped_cert["missing"] = missing unless missing.empty?
|
124
|
-
|
125
|
-
# If neither the '--all' flag or the '--certname' flag was passed in
|
126
|
-
# and the requested cert array is empty, we output a JSON object
|
127
|
-
# with an empty 'requested' key. Otherwise, we display
|
128
|
-
# any of the classes that are currently in grouped_cert
|
129
|
-
if grouped_cert.empty?
|
130
|
-
@logger.inform({ "requested" => requested }.to_json)
|
131
|
-
else
|
132
|
-
@logger.inform(grouped_cert.to_json)
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
def output_certs_text_format(requested, signed, revoked, missing)
|
91
|
+
def output_certs_by_state(requested, signed = [], revoked = [], missing = [])
|
138
92
|
if revoked.empty? && signed.empty? && requested.empty? && missing.empty?
|
139
93
|
@logger.inform "No certificates to list"
|
140
94
|
return
|
@@ -211,12 +165,7 @@ Options:
|
|
211
165
|
|
212
166
|
def get_all_certs(settings)
|
213
167
|
result = Puppetserver::Ca::CertificateAuthority.new(@logger, settings).get_certificate_statuses
|
214
|
-
|
215
|
-
if result
|
216
|
-
return JSON.parse(result.body)
|
217
|
-
else
|
218
|
-
return []
|
219
|
-
end
|
168
|
+
result ? JSON.parse(result.body) : []
|
220
169
|
end
|
221
170
|
|
222
171
|
def parse(args)
|
@@ -227,11 +176,8 @@ Options:
|
|
227
176
|
|
228
177
|
errors_were_handled = Errors.handle_with_usage(@logger, errors, parser.help)
|
229
178
|
|
230
|
-
|
231
|
-
|
232
|
-
else
|
233
|
-
exit_code = nil
|
234
|
-
end
|
179
|
+
exit_code = errors_were_handled ? 1 : nil
|
180
|
+
|
235
181
|
return results, exit_code
|
236
182
|
end
|
237
183
|
end
|
@@ -29,7 +29,7 @@ BANNER
|
|
29
29
|
def run(input)
|
30
30
|
config_path = input['config']
|
31
31
|
puppet = Config::Puppet.new(config_path)
|
32
|
-
puppet.load
|
32
|
+
puppet.load({}, @logger)
|
33
33
|
return 1 if HttpClient.check_server_online(puppet.settings, @logger)
|
34
34
|
|
35
35
|
errors = FileSystem.check_for_existing_files(PUPPETSERVER_CA_DIR)
|
@@ -66,14 +66,6 @@ SUCCESS_MESSAGE
|
|
66
66
|
def migrate(old_cadir, new_cadir=PUPPETSERVER_CA_DIR)
|
67
67
|
FileUtils.mv(old_cadir, new_cadir)
|
68
68
|
FileUtils.symlink(new_cadir, old_cadir)
|
69
|
-
# Ensure the symlink has the same ownership as the actual cadir.
|
70
|
-
# This requires using `FileUtils.chown` rather than `File.chown`, as
|
71
|
-
# the latter will update the ownership of the target rather than the
|
72
|
-
# link itself.
|
73
|
-
# Symlink permissions are ignored in favor of the target's permissions,
|
74
|
-
# so we don't have to change those.
|
75
|
-
cadir = File.stat(new_cadir)
|
76
|
-
FileUtils.chown(cadir.uid, cadir.gid, old_cadir)
|
77
69
|
end
|
78
70
|
|
79
71
|
def parse(args)
|
@@ -12,7 +12,7 @@ module Puppetserver
|
|
12
12
|
|
13
13
|
include Puppetserver::Ca::Utils
|
14
14
|
|
15
|
-
|
15
|
+
CERTNAME_BLACKLIST = %w{--all --config}
|
16
16
|
|
17
17
|
SUMMARY = 'Revoke certificate(s)'
|
18
18
|
BANNER = <<-BANNER
|
@@ -55,7 +55,7 @@ BANNER
|
|
55
55
|
errors = CliParsing.parse_with_errors(parser, args)
|
56
56
|
|
57
57
|
results['certnames'].each do |certname|
|
58
|
-
if
|
58
|
+
if CERTNAME_BLACKLIST.include?(certname)
|
59
59
|
errors << " Cannot manage cert named `#{certname}` from " +
|
60
60
|
"the CLI, if needed use the HTTP API directly"
|
61
61
|
end
|
@@ -83,7 +83,7 @@ BANNER
|
|
83
83
|
return 1 if Errors.handle_with_usage(@logger, errors)
|
84
84
|
end
|
85
85
|
|
86
|
-
puppet = Config::Puppet.parse(config)
|
86
|
+
puppet = Config::Puppet.parse(config, @logger)
|
87
87
|
return 1 if Errors.handle_with_usage(@logger, puppet.errors)
|
88
88
|
|
89
89
|
result = revoke_certs(certnames, puppet.settings)
|
@@ -3,6 +3,7 @@ require 'optparse'
|
|
3
3
|
require 'puppetserver/ca/config/puppet'
|
4
4
|
require 'puppetserver/ca/errors'
|
5
5
|
require 'puppetserver/ca/local_certificate_authority'
|
6
|
+
require 'puppetserver/ca/utils/config'
|
6
7
|
require 'puppetserver/ca/utils/cli_parsing'
|
7
8
|
require 'puppetserver/ca/utils/file_system'
|
8
9
|
require 'puppetserver/ca/utils/signing_digest'
|
@@ -23,10 +24,10 @@ Usage:
|
|
23
24
|
Description:
|
24
25
|
Setup a root and intermediate signing CA for Puppet Server
|
25
26
|
and store generated CA keys, certs, crls, and associated
|
26
|
-
|
27
|
+
master related files on disk.
|
27
28
|
|
28
29
|
The `--subject-alt-names` flag can be used to add SANs to the
|
29
|
-
certificate generated for the Puppet
|
30
|
+
certificate generated for the Puppet master. Multiple names can be
|
30
31
|
listed as a comma separated string. These can be either DNS names or
|
31
32
|
IP addresses, differentiated by prefixes: `DNS:foo.bar.com,IP:123.456.789`.
|
32
33
|
Names with no prefix will be treated as DNS names.
|
@@ -55,7 +56,7 @@ BANNER
|
|
55
56
|
settings_overrides[:dns_alt_names] = input['subject-alt-names'] unless input['subject-alt-names'].empty?
|
56
57
|
|
57
58
|
puppet = Config::Puppet.new(config_path)
|
58
|
-
puppet.load(settings_overrides)
|
59
|
+
puppet.load(settings_overrides, @logger)
|
59
60
|
return 1 if Errors.handle_with_usage(@logger, puppet.errors)
|
60
61
|
|
61
62
|
# Load most secure signing digest we can for cers/crl/csr signing.
|
@@ -76,7 +77,7 @@ BANNER
|
|
76
77
|
|
77
78
|
root_key, root_cert, root_crl = ca.create_root_cert
|
78
79
|
ca.create_intermediate_cert(root_key, root_cert)
|
79
|
-
|
80
|
+
master_key, master_cert = ca.create_master_cert
|
80
81
|
return ca.errors if ca.errors.any?
|
81
82
|
|
82
83
|
FileSystem.ensure_dirs([settings[:ssldir],
|
@@ -90,28 +91,28 @@ BANNER
|
|
90
91
|
[settings[:cacert], [ca.cert, root_cert]],
|
91
92
|
[settings[:cacrl], [ca.crl, root_crl]],
|
92
93
|
[settings[:cadir] + '/infra_crl.pem', [ca.crl, root_crl]],
|
93
|
-
[settings[:hostcert],
|
94
|
+
[settings[:hostcert], master_cert],
|
94
95
|
[settings[:localcacert], [ca.cert, root_cert]],
|
95
96
|
[settings[:hostcrl], [ca.crl, root_crl]],
|
96
|
-
[settings[:hostpubkey],
|
97
|
+
[settings[:hostpubkey], master_key.public_key],
|
97
98
|
[settings[:capub], ca.key.public_key],
|
98
|
-
[settings[:cert_inventory], ca.inventory_entry(
|
99
|
+
[settings[:cert_inventory], ca.inventory_entry(master_cert)],
|
99
100
|
[settings[:cadir] + '/infra_inventory.txt', ''],
|
100
101
|
[settings[:cadir] + '/infra_serials', ''],
|
101
102
|
[settings[:serial], "002"],
|
102
|
-
[File.join(settings[:signeddir], "#{settings[:certname]}.pem"),
|
103
|
+
[File.join(settings[:signeddir], "#{settings[:certname]}.pem"), master_cert],
|
103
104
|
]
|
104
105
|
|
105
106
|
private_files = [
|
106
|
-
[settings[:hostprivkey],
|
107
|
+
[settings[:hostprivkey], master_key],
|
107
108
|
[settings[:rootkey], root_key],
|
108
109
|
[settings[:cakey], ca.key],
|
109
110
|
]
|
110
111
|
|
111
112
|
files_to_check = public_files + private_files
|
112
|
-
# We don't want to error if
|
113
|
+
# We don't want to error if master's keys exist. Certain workflows
|
113
114
|
# allow the agent to have already be installed with keys and then
|
114
|
-
# upgraded to be a
|
115
|
+
# upgraded to be a master. The host class will honor keys, if both
|
115
116
|
# public and private exist, and error if only one exists - as is
|
116
117
|
# previous behavior.
|
117
118
|
files_to_check = files_to_check.map(&:first) - [settings[:hostpubkey], settings[:hostprivkey]]
|
@@ -135,6 +136,8 @@ ERR
|
|
135
136
|
FileSystem.write_file(location, content, 0640)
|
136
137
|
end
|
137
138
|
|
139
|
+
Puppetserver::Ca::Utils::Config.symlink_to_old_cadir(settings[:cadir], settings[:confdir])
|
140
|
+
|
138
141
|
return []
|
139
142
|
end
|
140
143
|
|
@@ -160,7 +163,7 @@ ERR
|
|
160
163
|
parsed['config'] = conf
|
161
164
|
end
|
162
165
|
opts.on('--subject-alt-names NAME[,NAME]',
|
163
|
-
'Subject alternative names for the
|
166
|
+
'Subject alternative names for the master cert') do |sans|
|
164
167
|
parsed['subject-alt-names'] = sans
|
165
168
|
end
|
166
169
|
opts.on('--ca-name NAME',
|
@@ -168,7 +171,7 @@ ERR
|
|
168
171
|
parsed['ca-name'] = name
|
169
172
|
end
|
170
173
|
opts.on('--certname NAME',
|
171
|
-
'Common name to use for the
|
174
|
+
'Common name to use for the master cert') do |name|
|
172
175
|
parsed['certname'] = name
|
173
176
|
end
|
174
177
|
end
|
@@ -62,7 +62,7 @@ Options:
|
|
62
62
|
return 1 if Errors.handle_with_usage(@logger, errors)
|
63
63
|
end
|
64
64
|
|
65
|
-
puppet = Config::Puppet.parse(config)
|
65
|
+
puppet = Config::Puppet.parse(config, @logger)
|
66
66
|
return 1 if Errors.handle_with_usage(@logger, puppet.errors)
|
67
67
|
|
68
68
|
ca = Puppetserver::Ca::CertificateAuthority.new(@logger, puppet.settings)
|
data/lib/puppetserver/ca/cli.rb
CHANGED
@@ -64,10 +64,8 @@ BANNER
|
|
64
64
|
|
65
65
|
|
66
66
|
def self.run(cli_args = ARGV, out = STDOUT, err = STDERR)
|
67
|
+
logger = Puppetserver::Ca::Logger.new(:info, out, err)
|
67
68
|
parser, general_options, unparsed = parse_general_inputs(cli_args)
|
68
|
-
level = general_options.delete('verbose') ? :debug : :info
|
69
|
-
|
70
|
-
logger = Puppetserver::Ca::Logger.new(level, out, err)
|
71
69
|
|
72
70
|
if general_options['version']
|
73
71
|
logger.inform Puppetserver::Ca::VERSION
|
@@ -123,9 +121,6 @@ BANNER
|
|
123
121
|
opts.on('--version', 'Display the version') do |v|
|
124
122
|
parsed['version'] = true
|
125
123
|
end
|
126
|
-
opts.on('--verbose', 'Display low-level information') do |verbose|
|
127
|
-
parsed['verbose'] = true
|
128
|
-
end
|
129
124
|
|
130
125
|
opts.separator ACTION_OPTIONS
|
131
126
|
opts.separator "\nSee `puppetserver ca <action> --help` for detailed info"
|
@@ -23,9 +23,9 @@ module Puppetserver
|
|
23
23
|
# A regex describing valid formats with groups for capturing the value and units
|
24
24
|
TTL_FORMAT = /^(\d+)(y|d|h|m|s)?$/
|
25
25
|
|
26
|
-
def self.parse(config_path)
|
26
|
+
def self.parse(config_path, logger)
|
27
27
|
instance = new(config_path)
|
28
|
-
instance.load
|
28
|
+
instance.load({}, logger)
|
29
29
|
|
30
30
|
return instance
|
31
31
|
end
|
@@ -34,7 +34,7 @@ module Puppetserver
|
|
34
34
|
|
35
35
|
def initialize(supplied_config_path = nil)
|
36
36
|
@using_default_location = !supplied_config_path
|
37
|
-
@config_path = supplied_config_path ||
|
37
|
+
@config_path = supplied_config_path || user_specific_puppet_config
|
38
38
|
|
39
39
|
@settings = nil
|
40
40
|
@errors = []
|
@@ -46,55 +46,45 @@ module Puppetserver
|
|
46
46
|
# on Windows are unsupported.
|
47
47
|
# Note that Puppet Server runs as the [pe-]puppet user but to
|
48
48
|
# start/stop it you must be root.
|
49
|
-
def
|
50
|
-
@
|
51
|
-
if Puppetserver::Ca::Utils::Config.running_as_root?
|
52
|
-
'/etc/puppetlabs/puppet'
|
53
|
-
else
|
54
|
-
"#{ENV['HOME']}/.puppetlabs/etc/puppet"
|
55
|
-
end
|
49
|
+
def user_specific_puppet_confdir
|
50
|
+
@user_specific_puppet_confdir ||= Puppetserver::Ca::Utils::Config.puppet_confdir
|
56
51
|
end
|
57
52
|
|
58
|
-
def
|
59
|
-
|
53
|
+
def user_specific_puppet_config
|
54
|
+
user_specific_puppet_confdir + '/puppet.conf'
|
60
55
|
end
|
61
56
|
|
62
|
-
def load(cli_overrides = {})
|
57
|
+
def load(cli_overrides = {}, logger)
|
63
58
|
if explicitly_given_config_file_or_default_config_exists?
|
64
59
|
results = parse_text(File.read(@config_path))
|
65
60
|
end
|
66
61
|
|
67
62
|
results ||= {}
|
68
63
|
results[:main] ||= {}
|
69
|
-
# The [master] config section is deprecated
|
70
|
-
# We now favor [server], but support both for backwards compatibility
|
71
64
|
results[:master] ||= {}
|
72
|
-
results[:server] ||= {}
|
73
65
|
results[:agent] ||= {}
|
74
66
|
|
75
|
-
overrides = results[:agent].merge(results[:main]).merge(results[:master])
|
67
|
+
overrides = results[:agent].merge(results[:main]).merge(results[:master])
|
76
68
|
overrides.merge!(cli_overrides)
|
77
|
-
if overrides[:masterport]
|
78
|
-
overrides[:serverport] ||= overrides.delete(:masterport)
|
79
|
-
end
|
80
69
|
|
81
|
-
@settings = resolve_settings(overrides).freeze
|
70
|
+
@settings = resolve_settings(overrides, logger).freeze
|
82
71
|
end
|
83
72
|
|
84
73
|
def default_certname
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
74
|
+
@certname ||=
|
75
|
+
hostname = Facter.value(:hostname)
|
76
|
+
domain = Facter.value(:domain)
|
77
|
+
if domain and domain != ''
|
78
|
+
fqdn = [hostname, domain].join('.')
|
79
|
+
else
|
80
|
+
fqdn = hostname
|
81
|
+
end
|
82
|
+
fqdn.chomp('.')
|
93
83
|
end
|
94
84
|
|
95
85
|
# Resolve settings from default values, with any overrides for the
|
96
86
|
# specific settings or their dependent settings (ssldir, cadir) taken into account.
|
97
|
-
def resolve_settings(overrides = {})
|
87
|
+
def resolve_settings(overrides = {}, logger)
|
98
88
|
unresolved_setting = /\$[a-z_]+/
|
99
89
|
|
100
90
|
# Returning the key for unknown keys (rather than nil) is required to
|
@@ -106,12 +96,12 @@ module Puppetserver
|
|
106
96
|
# These need to be evaluated before we can construct their dependent
|
107
97
|
# defaults below
|
108
98
|
base_defaults = [
|
109
|
-
[:confdir,
|
99
|
+
[:confdir, user_specific_puppet_confdir],
|
110
100
|
[:ssldir,'$confdir/ssl'],
|
111
101
|
[:certdir, '$ssldir/certs'],
|
112
102
|
[:certname, default_certname],
|
113
103
|
[:server, 'puppet'],
|
114
|
-
[:
|
104
|
+
[:masterport, '8140'],
|
115
105
|
[:privatekeydir, '$ssldir/private_keys'],
|
116
106
|
[:publickeydir, '$ssldir/public_keys'],
|
117
107
|
]
|
@@ -129,7 +119,7 @@ module Puppetserver
|
|
129
119
|
:serial => '$cadir/serial',
|
130
120
|
:cert_inventory => '$cadir/inventory.txt',
|
131
121
|
:ca_server => '$server',
|
132
|
-
:ca_port => '$
|
122
|
+
:ca_port => '$masterport',
|
133
123
|
:localcacert => '$certdir/ca.pem',
|
134
124
|
:hostcrl => '$ssldir/crl.pem',
|
135
125
|
:hostcert => '$certdir/$certname.pem',
|
@@ -153,9 +143,12 @@ module Puppetserver
|
|
153
143
|
end
|
154
144
|
|
155
145
|
cadir = find_cadir(overrides.fetch(:cadir, false),
|
156
|
-
settings[:confdir]
|
146
|
+
settings[:confdir],
|
147
|
+
settings[:ssldir],
|
148
|
+
logger)
|
157
149
|
settings[:cadir] = substitutions['$cadir'] = cadir
|
158
150
|
|
151
|
+
|
159
152
|
dependent_defaults.each do |setting_name, default_value|
|
160
153
|
setting_value = overrides.fetch(setting_name, default_value)
|
161
154
|
settings[setting_name] = setting_value
|
@@ -218,17 +211,29 @@ module Puppetserver
|
|
218
211
|
|
219
212
|
private
|
220
213
|
|
221
|
-
|
214
|
+
|
215
|
+
def find_cadir(configured_cadir, confdir, ssldir, logger)
|
216
|
+
warning = 'The cadir is currently configured to be inside the ' +
|
217
|
+
'%{ssldir} directory. This config setting and the directory ' +
|
218
|
+
'location will not be used in a future version of puppet. ' +
|
219
|
+
'Please run the puppetserver ca tool to migrate out from the ' +
|
220
|
+
'puppet confdir to the /etc/puppetlabs/puppetserver/ca directory. ' +
|
221
|
+
'Use `puppetserver ca migrate --help` for more info.'
|
222
|
+
|
222
223
|
if configured_cadir
|
224
|
+
if configured_cadir.start_with?(ssldir)
|
225
|
+
logger.warn(warning % {ssldir: ssldir})
|
226
|
+
end
|
223
227
|
configured_cadir
|
228
|
+
|
224
229
|
else
|
225
230
|
old_cadir = Puppetserver::Ca::Utils::Config.old_default_cadir(confdir)
|
226
231
|
new_cadir = Puppetserver::Ca::Utils::Config.new_default_cadir(confdir)
|
227
|
-
|
228
|
-
|
229
|
-
new_cadir
|
230
|
-
else
|
232
|
+
if File.exist?(old_cadir) && !File.symlink?(old_cadir)
|
233
|
+
logger.warn(warning % {ssldir: ssldir})
|
231
234
|
old_cadir
|
235
|
+
else
|
236
|
+
new_cadir
|
232
237
|
end
|
233
238
|
end
|
234
239
|
end
|
@@ -279,7 +284,7 @@ module Puppetserver
|
|
279
284
|
end
|
280
285
|
|
281
286
|
if settings.dig(:server_list, 0, 1) &&
|
282
|
-
settings[:ca_port] == '$
|
287
|
+
settings[:ca_port] == '$masterport'
|
283
288
|
|
284
289
|
settings[:ca_port] = settings.dig(:server_list, 0, 1)
|
285
290
|
end
|
data/lib/puppetserver/ca/host.rb
CHANGED
@@ -58,10 +58,10 @@ module Puppetserver
|
|
58
58
|
@errors = []
|
59
59
|
end
|
60
60
|
|
61
|
-
# If both the private and public keys exist for a
|
61
|
+
# If both the private and public keys exist for a master then we want
|
62
62
|
# to honor them here, if only one key exists we want to surface an error,
|
63
63
|
# and if neither exist we generate a new key. This logic is necessary for
|
64
|
-
# proper bootstrapping for certain
|
64
|
+
# proper bootstrapping for certain master workflows.
|
65
65
|
def create_private_key(keylength, private_path = '', public_path = '')
|
66
66
|
if File.exists?(private_path) && File.exists?(public_path)
|
67
67
|
return OpenSSL::PKey.read(File.read(private_path))
|
@@ -20,7 +20,7 @@ module Puppetserver
|
|
20
20
|
|
21
21
|
CLI_AUTH_EXT_OID = "1.3.6.1.4.1.34380.1.3.39"
|
22
22
|
|
23
|
-
|
23
|
+
MASTER_EXTENSIONS = [
|
24
24
|
["basicConstraints", "CA:FALSE", true],
|
25
25
|
["nsComment", "Puppet Server Internal Certificate", false],
|
26
26
|
["authorityKeyIdentifier", "keyid:always", false],
|
@@ -132,23 +132,23 @@ module Puppetserver
|
|
132
132
|
time.strftime('%Y-%m-%dT%H:%M:%S%Z')
|
133
133
|
end
|
134
134
|
|
135
|
-
def
|
136
|
-
|
137
|
-
|
135
|
+
def create_master_cert
|
136
|
+
master_cert = nil
|
137
|
+
master_key = @host.create_private_key(@settings[:keylength],
|
138
138
|
@settings[:hostprivkey],
|
139
139
|
@settings[:hostpubkey])
|
140
|
-
if
|
141
|
-
|
140
|
+
if master_key
|
141
|
+
master_csr = @host.create_csr(name: @settings[:certname], key: master_key)
|
142
142
|
if @settings[:subject_alt_names].empty?
|
143
143
|
alt_names = "DNS:puppet, DNS:#{@settings[:certname]}"
|
144
144
|
else
|
145
145
|
alt_names = @settings[:subject_alt_names]
|
146
146
|
end
|
147
147
|
|
148
|
-
|
148
|
+
master_cert = sign_authorized_cert(master_csr, alt_names)
|
149
149
|
end
|
150
150
|
|
151
|
-
return
|
151
|
+
return master_key, master_cert
|
152
152
|
end
|
153
153
|
|
154
154
|
def sign_authorized_cert(csr, alt_names = '')
|
@@ -176,7 +176,7 @@ module Puppetserver
|
|
176
176
|
end
|
177
177
|
|
178
178
|
def add_authorized_extensions(cert, ef)
|
179
|
-
|
179
|
+
MASTER_EXTENSIONS.each do |ext|
|
180
180
|
extension = ef.create_extension(*ext)
|
181
181
|
cert.add_extension(extension)
|
182
182
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'puppetserver/ca/utils/file_system'
|
2
|
+
|
1
3
|
module Puppetserver
|
2
4
|
module Ca
|
3
5
|
module Utils
|
@@ -31,6 +33,10 @@ module Puppetserver
|
|
31
33
|
File.join(File.dirname(puppet_confdir), 'puppetserver')
|
32
34
|
end
|
33
35
|
|
36
|
+
def self.default_ssldir(confdir = puppet_confdir)
|
37
|
+
File.join(confdir, 'ssl')
|
38
|
+
end
|
39
|
+
|
34
40
|
def self.old_default_cadir(confdir = puppet_confdir)
|
35
41
|
File.join(confdir, 'ssl', 'ca')
|
36
42
|
end
|
@@ -38,6 +44,17 @@ module Puppetserver
|
|
38
44
|
def self.new_default_cadir(confdir = puppet_confdir)
|
39
45
|
File.join(puppetserver_confdir(confdir), 'ca')
|
40
46
|
end
|
47
|
+
|
48
|
+
def self.symlink_to_old_cadir(current_cadir, puppet_confdir)
|
49
|
+
old_cadir = old_default_cadir(puppet_confdir)
|
50
|
+
new_cadir = new_default_cadir(puppet_confdir)
|
51
|
+
return if current_cadir != new_cadir
|
52
|
+
# This is only run on setup/import, so there should be no files in the
|
53
|
+
# old cadir, so it should be safe to forcibly remove it (which we need
|
54
|
+
# to do in order to create a symlink).
|
55
|
+
Puppetserver::Ca::Utils::FileSystem.forcibly_symlink(new_cadir, old_cadir)
|
56
|
+
end
|
57
|
+
|
41
58
|
end
|
42
59
|
end
|
43
60
|
end
|
@@ -19,8 +19,7 @@ module Puppetserver
|
|
19
19
|
|
20
20
|
# Not all connections require a client cert to be present.
|
21
21
|
# For example, when querying the status endpoint.
|
22
|
-
def initialize(
|
23
|
-
@logger = logger
|
22
|
+
def initialize(settings, with_client_cert: true)
|
24
23
|
@store = make_store(settings[:localcacert],
|
25
24
|
settings[:certificate_revocation],
|
26
25
|
settings[:hostcrl])
|
@@ -51,7 +50,7 @@ module Puppetserver
|
|
51
50
|
# The Connection object should have HTTP verbs defined on it that take
|
52
51
|
# a body (and optional overrides). Returns whatever the block given returned.
|
53
52
|
def with_connection(url, &block)
|
54
|
-
request = ->(conn) { block.call(Connection.new(conn, url
|
53
|
+
request = ->(conn) { block.call(Connection.new(conn, url)) }
|
55
54
|
|
56
55
|
begin
|
57
56
|
Net::HTTP.start(url.host, url.port,
|
@@ -86,35 +85,29 @@ module Puppetserver
|
|
86
85
|
# and defines methods named after HTTP verbs that are called on the
|
87
86
|
# saved connection, returning a Result.
|
88
87
|
class Connection
|
89
|
-
def initialize(net_http_connection, url_struct
|
88
|
+
def initialize(net_http_connection, url_struct)
|
90
89
|
@conn = net_http_connection
|
91
90
|
@url = url_struct
|
92
|
-
@logger = logger
|
93
91
|
end
|
94
92
|
|
95
93
|
def get(url_overide = nil, headers = {})
|
96
94
|
url = url_overide || @url
|
97
95
|
headers = DEFAULT_HEADERS.merge(headers)
|
98
96
|
|
99
|
-
@logger.debug("Making a GET request at #{url.full_url}")
|
100
|
-
|
101
97
|
request = Net::HTTP::Get.new(url.to_uri, headers)
|
102
98
|
result = @conn.request(request)
|
103
|
-
Result.new(result.code, result.body)
|
104
99
|
|
100
|
+
Result.new(result.code, result.body)
|
105
101
|
end
|
106
102
|
|
107
103
|
def put(body, url_override = nil, headers = {})
|
108
104
|
url = url_override || @url
|
109
105
|
headers = DEFAULT_HEADERS.merge(headers)
|
110
106
|
|
111
|
-
@logger.debug("Making a PUT request at #{url.full_url}")
|
112
|
-
|
113
107
|
request = Net::HTTP::Put.new(url.to_uri, headers)
|
114
108
|
request.body = body
|
115
109
|
result = @conn.request(request)
|
116
110
|
|
117
|
-
|
118
111
|
Result.new(result.code, result.body)
|
119
112
|
end
|
120
113
|
|
@@ -122,8 +115,6 @@ module Puppetserver
|
|
122
115
|
url = url_override || @url
|
123
116
|
headers = DEFAULT_HEADERS.merge(headers)
|
124
117
|
|
125
|
-
@logger.debug("Making a DELETE request at #{url.full_url}")
|
126
|
-
|
127
118
|
result = @conn.request(Net::HTTP::Delete.new(url.to_uri, headers))
|
128
119
|
|
129
120
|
Result.new(result.code, result.body)
|
@@ -175,15 +166,15 @@ module Puppetserver
|
|
175
166
|
def self.check_server_online(settings, logger)
|
176
167
|
status_url = URL.new('https', settings[:ca_server], settings[:ca_port], 'status', 'v1', 'simple', 'ca')
|
177
168
|
begin
|
178
|
-
# Generating certs offline is necessary if the
|
169
|
+
# Generating certs offline is necessary if the master cert has been destroyed
|
179
170
|
# or compromised. Since querying the status endpoint does not require a client cert, and
|
180
171
|
# we commonly won't have one, don't require one for creating the connection.
|
181
172
|
# Additionally, we want to ensure the server is stopped before migrating the CA dir to
|
182
173
|
# avoid issues with writing to the CA dir and moving it.
|
183
|
-
self.new(
|
174
|
+
self.new(settings, with_client_cert: false).with_connection(status_url) do |conn|
|
184
175
|
result = conn.get
|
185
176
|
if result.body == "running"
|
186
|
-
logger.err "
|
177
|
+
logger.err "CA service is running. Please stop it before attempting to run this command."
|
187
178
|
true
|
188
179
|
else
|
189
180
|
false
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: puppetserver-ca
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Puppet, Inc.
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-11-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: facter
|