puppetserver-ca 2.4.0 → 2.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/mend.yaml +37 -0
- data/.travis.yml +2 -0
- data/lib/puppetserver/ca/action/delete.rb +286 -0
- data/lib/puppetserver/ca/action/prune.rb +146 -9
- data/lib/puppetserver/ca/cli.rb +2 -0
- data/lib/puppetserver/ca/utils/http_client.rb +28 -16
- data/lib/puppetserver/ca/utils/inventory.rb +84 -0
- data/lib/puppetserver/ca/version.rb +1 -1
- metadata +6 -4
- data/.github/workflows/snyk.yaml +0 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 78b8703a01de06ec5c4a6202f8341662452f4d33d37c1390b558556ce817253d
|
4
|
+
data.tar.gz: eea0c603eee53c14a82de6b80f263014ca76261eb39fa2b679031ad6ecc69b1f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 64ff93865e33b2dccd3f4308580278315d007b450611acdbb36631aabc9794d82b5f8973aa5872384f05146abe62dfa3290b5e3774f8816bafa4b70db7bf3fbc
|
7
|
+
data.tar.gz: 734dc57fa4e9f8ce390a966502ea8104cb7640aec4564759ca39c377fdaeb06ed8992fbab88dfa3ef5a2c24921e6400223e4bee2c902d0f4f92f676537f779c5
|
@@ -0,0 +1,37 @@
|
|
1
|
+
name: mend_scan
|
2
|
+
|
3
|
+
on:
|
4
|
+
workflow_dispatch:
|
5
|
+
push:
|
6
|
+
branches:
|
7
|
+
- main
|
8
|
+
jobs:
|
9
|
+
build:
|
10
|
+
runs-on: ubuntu-latest
|
11
|
+
steps:
|
12
|
+
- name: checkout repo content
|
13
|
+
uses: actions/checkout@v2 # checkout the repository content to github runner.
|
14
|
+
with:
|
15
|
+
fetch-depth: 1
|
16
|
+
- name: setup ruby
|
17
|
+
uses: ruby/setup-ruby@v1
|
18
|
+
with:
|
19
|
+
ruby-version: 2.7
|
20
|
+
- name: create lock
|
21
|
+
run: bundle lock
|
22
|
+
# install java
|
23
|
+
- uses: actions/setup-java@v3
|
24
|
+
with:
|
25
|
+
distribution: 'temurin' # See 'Supported distributions' for available options
|
26
|
+
java-version: '17'
|
27
|
+
# download mend
|
28
|
+
- name: download_mend
|
29
|
+
run: curl -o wss-unified-agent.jar https://unified-agent.s3.amazonaws.com/wss-unified-agent.jar
|
30
|
+
- name: run mend
|
31
|
+
run: java -jar wss-unified-agent.jar
|
32
|
+
env:
|
33
|
+
WS_APIKEY: ${{ secrets.MEND_API_KEY }}
|
34
|
+
WS_WSS_URL: https://saas-eu.whitesourcesoftware.com/agent
|
35
|
+
WS_USERKEY: ${{ secrets.MEND_TOKEN }}
|
36
|
+
WS_PRODUCTNAME: Puppet Enterprise
|
37
|
+
WS_PROJECTNAME: ${{ github.event.repository.name }}
|
data/.travis.yml
CHANGED
@@ -0,0 +1,286 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'optparse'
|
3
|
+
require 'time'
|
4
|
+
require 'puppetserver/ca/certificate_authority'
|
5
|
+
require 'puppetserver/ca/config/puppet'
|
6
|
+
require 'puppetserver/ca/errors'
|
7
|
+
require 'puppetserver/ca/utils/cli_parsing'
|
8
|
+
require 'puppetserver/ca/utils/file_system'
|
9
|
+
require 'puppetserver/ca/utils/inventory'
|
10
|
+
require 'puppetserver/ca/x509_loader'
|
11
|
+
|
12
|
+
module Puppetserver
|
13
|
+
module Ca
|
14
|
+
module Action
|
15
|
+
class Delete
|
16
|
+
include Puppetserver::Ca::Utils
|
17
|
+
|
18
|
+
CERTNAME_BLOCKLIST = %w{--config --expired --revoked --all}
|
19
|
+
|
20
|
+
SUMMARY = 'Delete signed certificate(s) from disk'
|
21
|
+
BANNER = <<-BANNER
|
22
|
+
Usage:
|
23
|
+
puppetserver ca delete [--help]
|
24
|
+
puppetserver ca delete [--config CONF] [--expired] [--revoked]
|
25
|
+
[--certname NAME[,NAME]] [--all]
|
26
|
+
|
27
|
+
Description:
|
28
|
+
Deletes signed certificates from disk. Once a certificate is
|
29
|
+
signed and delivered to a node, it no longer necessarily needs
|
30
|
+
to be stored on disk.
|
31
|
+
|
32
|
+
Options:
|
33
|
+
BANNER
|
34
|
+
|
35
|
+
def initialize(logger)
|
36
|
+
@logger = logger
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.parser(parsed = {})
|
40
|
+
OptionParser.new do |opts|
|
41
|
+
opts.banner = BANNER
|
42
|
+
opts.on('--help', 'Display this command-specific help output') do |help|
|
43
|
+
parsed['help'] = true
|
44
|
+
end
|
45
|
+
opts.on('--config CONF', 'Path to puppet.conf') do |conf|
|
46
|
+
parsed['config'] = conf
|
47
|
+
end
|
48
|
+
opts.on('--expired', 'Delete expired signed certificates') do |expired|
|
49
|
+
parsed['expired'] = true
|
50
|
+
end
|
51
|
+
opts.on('--revoked', 'Delete signed certificates that have already been revoked') do |revoked|
|
52
|
+
parsed['revoked'] = true
|
53
|
+
end
|
54
|
+
opts.on('--certname NAME[,NAME]', Array,
|
55
|
+
'One or more comma-separated certnames for which to delete signed certificates') do |certs|
|
56
|
+
parsed['certname'] = [certs].flatten
|
57
|
+
end
|
58
|
+
opts.on('--all', 'Delete all signed certificates on disk') do |all|
|
59
|
+
parsed['all'] = true
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse(args)
|
65
|
+
results = {}
|
66
|
+
parser = self.class.parser(results)
|
67
|
+
|
68
|
+
errors = CliParsing.parse_with_errors(parser, args)
|
69
|
+
|
70
|
+
if results['certname']
|
71
|
+
results['certname'].each do |certname|
|
72
|
+
if CERTNAME_BLOCKLIST.include?(certname)
|
73
|
+
errors << " Cannot manage cert named `#{certname}` from "+
|
74
|
+
"the CLI. If needed, use the HTTP API directly."
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
unless results['help'] || results['expired'] || results['revoked'] || results['certname'] || results['all']
|
80
|
+
errors << ' Must pass one of the valid flags to determine which certs to delete'
|
81
|
+
end
|
82
|
+
|
83
|
+
if results['all'] && (results['expired'] || results['revoked'] || results['certname'])
|
84
|
+
errors << ' The --all flag must not be used with --expired, --revoked, or --certname'
|
85
|
+
end
|
86
|
+
|
87
|
+
errors_were_handled = Errors.handle_with_usage(@logger, errors, parser.help)
|
88
|
+
|
89
|
+
exit_code = errors_were_handled ? 1 : nil
|
90
|
+
return results, exit_code
|
91
|
+
end
|
92
|
+
|
93
|
+
def run(args)
|
94
|
+
config = args['config']
|
95
|
+
|
96
|
+
# Validate the config path
|
97
|
+
if config
|
98
|
+
errors = FileSystem.validate_file_paths(config)
|
99
|
+
return 1 if Errors.handle_with_usage(@logger, errors)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Validate puppet config setting
|
103
|
+
puppet = Config::Puppet.parse(config, @logger)
|
104
|
+
settings = puppet.settings
|
105
|
+
return 1 if Errors.handle_with_usage(@logger, puppet.errors)
|
106
|
+
|
107
|
+
# Validate that we are offline
|
108
|
+
return 1 if HttpClient.check_server_online(settings, @logger)
|
109
|
+
|
110
|
+
# Perform the desired action, keeping track if any errors occurred
|
111
|
+
errored = false
|
112
|
+
deleted_count = 0
|
113
|
+
cadir = settings[:cadir]
|
114
|
+
inventory_file_path = File.join(cadir, 'inventory.txt')
|
115
|
+
|
116
|
+
# Because --revoke has a potentially fatal error it can throw,
|
117
|
+
# process it first.
|
118
|
+
if args['revoked']
|
119
|
+
loader = X509Loader.new(settings[:cacert], settings[:cakey], settings[:cacrl])
|
120
|
+
verified_crls = loader.crls.select { |crl| crl.verify(loader.key) }
|
121
|
+
unless verified_crls.length == 1
|
122
|
+
@logger.err("Could not identify Puppet's CRL. Aborting delete action.")
|
123
|
+
return 1
|
124
|
+
end
|
125
|
+
crl = verified_crls.first
|
126
|
+
|
127
|
+
# First, search the inventory for the revoked serial.
|
128
|
+
# If it matches the current serial for the cert, delete the cert.
|
129
|
+
# If it is an old serial for the certname, verify the file on disk
|
130
|
+
# is not the serial that was revoked, and then ignore it. If the
|
131
|
+
# file on disk does match that serial, delete it.
|
132
|
+
# If it isn't in the inventory, fall back to searching every cert on
|
133
|
+
# disk for the given serial.
|
134
|
+
inventory, err = Inventory.parse_inventory_file(inventory_file_path, @logger)
|
135
|
+
revoked_serials = crl.revoked.map { |r| r.serial.to_i }
|
136
|
+
to_delete = []
|
137
|
+
revoked_serials.each do |revoked_serial|
|
138
|
+
current_serial = inventory.find { |k,v| v[:serial] == revoked_serial }
|
139
|
+
old_serial = inventory.find { |k,v| v[:old_serials].include?(revoked_serial) }
|
140
|
+
if current_serial
|
141
|
+
@logger.debug("#{revoked_serial} is the current serial for #{current_serial.first}")
|
142
|
+
to_delete << current_serial.first
|
143
|
+
elsif old_serial
|
144
|
+
@logger.debug("#{revoked_serial} appears to be an old serial for #{old_serial.first}. Verifying cert on disk is not the revoked serial.")
|
145
|
+
begin
|
146
|
+
serial = get_cert_serial("#{cadir}/signed/#{old_serial.first}.pem")
|
147
|
+
# This should never happen unless someone has messed with
|
148
|
+
# the inventory.txt file or replaced the cert on disk with
|
149
|
+
# an old one.
|
150
|
+
to_delete << old_serial.first if serial == revoked_serial
|
151
|
+
rescue Exception => e
|
152
|
+
@logger.err("Error reading serial from certificate for #{old_serial.first} with exception #{e}")
|
153
|
+
errored = true
|
154
|
+
end
|
155
|
+
else
|
156
|
+
@logger.debug("Could not find #{revoked_serial} in inventory.txt. Searching certs on disk for this serial.")
|
157
|
+
begin
|
158
|
+
certname = find_cert_with_serial(cadir, revoked_serial)
|
159
|
+
if certname
|
160
|
+
to_delete << certname
|
161
|
+
else
|
162
|
+
@logger.err("Could not find serial #{revoked_serial} in inventory.txt or in any certificate file currently on disk.")
|
163
|
+
errored = true
|
164
|
+
end
|
165
|
+
rescue Exception => e
|
166
|
+
@logger.err("Error reading serial from certificates when trying to find certificate with serial #{revoked_serial} with exception #{e}")
|
167
|
+
errored = true
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
# Because the CRL will likely contain certs that no longer exist on disk,
|
172
|
+
# don't show an error if we can't find the file.
|
173
|
+
count, err = delete_certs(cadir, to_delete, false)
|
174
|
+
errored ||= err
|
175
|
+
deleted_count += count
|
176
|
+
end
|
177
|
+
|
178
|
+
if args['expired']
|
179
|
+
# Delete expired certs found in inventory first since this is cheaper.
|
180
|
+
# Then, look for any certs not in the inventory, check if they
|
181
|
+
# are expired, then delete those.
|
182
|
+
inventory, err = Inventory.parse_inventory_file(inventory_file_path, @logger)
|
183
|
+
errored ||= err
|
184
|
+
expired_in_inventory = inventory.select { |k,v| v[:not_after] < Time.now }.map(&:first)
|
185
|
+
# Don't print errors if the cert is not found, since the inventory
|
186
|
+
# file can contain old entries that have already been deleted.
|
187
|
+
count, err = delete_certs(cadir, expired_in_inventory, false)
|
188
|
+
deleted_count += count
|
189
|
+
errored ||= err
|
190
|
+
other_certs_to_check = find_certs_not_in_inventory(cadir, inventory.map(&:first))
|
191
|
+
count, err = delete_expired_certs(cadir, other_certs_to_check)
|
192
|
+
deleted_count += count
|
193
|
+
errored ||= err
|
194
|
+
end
|
195
|
+
|
196
|
+
if args['certname']
|
197
|
+
count, errored = delete_certs(cadir, args['certname'])
|
198
|
+
deleted_count += count
|
199
|
+
end
|
200
|
+
|
201
|
+
if args['all']
|
202
|
+
certnames = Dir.glob("#{cadir}/signed/*.pem").map{ |c| File.basename(c, '.pem') }
|
203
|
+
# Since we don't run this with any other flags, we can set these variables directly
|
204
|
+
deleted_count, errored = delete_certs(cadir, certnames)
|
205
|
+
end
|
206
|
+
|
207
|
+
plural = deleted_count == 1 ? "" : "s"
|
208
|
+
@logger.inform("#{deleted_count} certificate#{plural} deleted.")
|
209
|
+
# If encountered non-fatal errors (an invalid entry in inventory.txt, cert not existing on disk)
|
210
|
+
# return 24. Returning 1 should be for fatal errors where we could not do any part of the action.
|
211
|
+
return errored ? 24 : 0
|
212
|
+
end
|
213
|
+
|
214
|
+
def find_certs_not_in_inventory(cadir, inventory_certnames)
|
215
|
+
all_cert_files = Dir.glob("#{cadir}/signed/*.pem").map { |f| File.basename(f, '.pem') }
|
216
|
+
all_cert_files - inventory_certnames
|
217
|
+
end
|
218
|
+
|
219
|
+
def delete_certs(cadir, certnames, error_on_not_found = true)
|
220
|
+
deleted = 0
|
221
|
+
errored = false
|
222
|
+
certnames.each do |cert|
|
223
|
+
path = "#{cadir}/signed/#{cert}.pem"
|
224
|
+
if File.exist?(path)
|
225
|
+
@logger.inform("Deleting certificate at #{path}")
|
226
|
+
File.delete(path)
|
227
|
+
deleted += 1
|
228
|
+
else
|
229
|
+
if error_on_not_found
|
230
|
+
@logger.err("Could not find certificate file at #{path}")
|
231
|
+
errored = true
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
[deleted, errored]
|
236
|
+
end
|
237
|
+
|
238
|
+
def delete_expired_certs(cadir, certnames)
|
239
|
+
deleted = 0
|
240
|
+
errored = false
|
241
|
+
files = certnames.map { |c| "#{cadir}/signed/#{c}.pem" }
|
242
|
+
files.each do |f|
|
243
|
+
# Shouldn't really be possible since we look for certs on disk
|
244
|
+
# before calling this function, but just in case.
|
245
|
+
unless File.exist?(f)
|
246
|
+
@logger.err("Could not find certificate file at #{f}")
|
247
|
+
errored = true
|
248
|
+
next
|
249
|
+
end
|
250
|
+
begin
|
251
|
+
cert = OpenSSL::X509::Certificate.new(File.read(f))
|
252
|
+
rescue OpenSSL::X509::CertificateError
|
253
|
+
@logger.err("Error reading certificate at #{f}")
|
254
|
+
errored = true
|
255
|
+
next
|
256
|
+
end
|
257
|
+
if cert.not_after < Time.now
|
258
|
+
@logger.inform("Deleting certificate at #{f}")
|
259
|
+
File.delete(f)
|
260
|
+
deleted += 1
|
261
|
+
end
|
262
|
+
end
|
263
|
+
[deleted, errored]
|
264
|
+
end
|
265
|
+
|
266
|
+
def get_cert_serial(file)
|
267
|
+
cert = OpenSSL::X509::Certificate.new(File.read(file))
|
268
|
+
cert.serial.to_i
|
269
|
+
end
|
270
|
+
|
271
|
+
def find_cert_with_serial(cadir, serial)
|
272
|
+
files = Dir.glob("#{cadir}/signed/*.pem")
|
273
|
+
files.each do |f|
|
274
|
+
begin
|
275
|
+
s = get_cert_serial(f)
|
276
|
+
return File.basename(f, '.pem') if s == serial # Remove .pem
|
277
|
+
rescue Exception => e
|
278
|
+
@logger.debug("Error reading certificate at #{f} with exception #{e}. Skipping this file.")
|
279
|
+
end
|
280
|
+
end
|
281
|
+
return nil
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
@@ -6,6 +6,7 @@ require 'puppetserver/ca/utils/cli_parsing'
|
|
6
6
|
require 'puppetserver/ca/utils/file_system'
|
7
7
|
require 'puppetserver/ca/utils/config'
|
8
8
|
require 'puppetserver/ca/x509_loader'
|
9
|
+
require 'puppetserver/ca/config/puppet'
|
9
10
|
|
10
11
|
module Puppetserver
|
11
12
|
module Ca
|
@@ -13,15 +14,22 @@ module Puppetserver
|
|
13
14
|
class Prune
|
14
15
|
include Puppetserver::Ca::Utils
|
15
16
|
|
16
|
-
SUMMARY = "Prune the local CRL on disk to remove
|
17
|
+
SUMMARY = "Prune the local CRL on disk to remove certificate entries"
|
17
18
|
BANNER = <<-BANNER
|
18
19
|
Usage:
|
19
20
|
puppetserver ca prune [--help]
|
20
21
|
puppetserver ca prune [--config]
|
22
|
+
puppetserver ca prune [--config] [--remove-duplicates]
|
23
|
+
puppetserver ca prune [--config] [--remove-expired]
|
24
|
+
puppetserver ca prune [--config] [--remove-entries] [--serial NUMBER[,NUMBER]] [--certname NAME[,NAME]]
|
21
25
|
|
22
26
|
Description:
|
23
|
-
Prune the list of revoked certificates
|
24
|
-
|
27
|
+
Prune the list of revoked certificates. If no options are provided or
|
28
|
+
--remove-duplicates is specified, prune CRL of any duplicate entries.
|
29
|
+
If --remove-expired is specified, remove expired entries from CRL.
|
30
|
+
If --remove-entries is specified, remove matching entries provided by
|
31
|
+
--serial and/or --certname values. This command will only prune the CRL
|
32
|
+
issued by Puppet's CA cert.
|
25
33
|
|
26
34
|
Options:
|
27
35
|
BANNER
|
@@ -32,6 +40,11 @@ BANNER
|
|
32
40
|
|
33
41
|
def run(inputs)
|
34
42
|
config_path = inputs['config']
|
43
|
+
remove_duplicates = inputs['remove-duplicates']
|
44
|
+
remove_expired = inputs['remove-expired']
|
45
|
+
remove_entries = inputs['remove-entries']
|
46
|
+
serialnumbers = inputs['serial']
|
47
|
+
certnames = inputs['certname']
|
35
48
|
exit_code = 0
|
36
49
|
|
37
50
|
# Validate the config path.
|
@@ -45,31 +58,57 @@ BANNER
|
|
45
58
|
puppet.load(logger: @logger)
|
46
59
|
return 1 if Errors.handle_with_usage(@logger, puppet.errors)
|
47
60
|
|
61
|
+
# Validate arguments
|
62
|
+
if (remove_entries && (!serialnumbers && !certnames))
|
63
|
+
return 1 if Errors.handle_with_usage(@logger,["--remove-entries option require --serial or --certname values"])
|
64
|
+
end
|
65
|
+
|
48
66
|
# Validate that we are offline
|
49
67
|
return 1 if HttpClient.check_server_online(puppet.settings, @logger)
|
50
68
|
|
51
69
|
# Getting the CRL(s)
|
52
70
|
loader = X509Loader.new(puppet.settings[:cacert], puppet.settings[:cakey], puppet.settings[:cacrl])
|
71
|
+
inventory_file = puppet.settings[:cert_inventory]
|
72
|
+
cadir = puppet.settings[:cadir]
|
53
73
|
|
54
74
|
verified_crls = loader.crls.select { |crl| crl.verify(loader.key) }
|
75
|
+
number_of_removed_duplicates = 0
|
76
|
+
number_of_removed_crl_entries = 0
|
55
77
|
|
56
78
|
if verified_crls.length == 1
|
57
79
|
puppet_crl = verified_crls.first
|
58
80
|
@logger.inform("Total number of certificates found in Puppet's CRL is: #{puppet_crl.revoked.length}.")
|
59
|
-
number_of_removed_duplicates = prune_CRL(puppet_crl)
|
60
81
|
|
61
|
-
if
|
82
|
+
if remove_entries
|
83
|
+
if serialnumbers
|
84
|
+
number_of_removed_crl_entries += prune_using_serial(puppet_crl, loader.key, serialnumbers)
|
85
|
+
end
|
86
|
+
if certnames
|
87
|
+
number_of_removed_crl_entries += prune_using_certname(puppet_crl, loader.key, inventory_file, cadir, certnames)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
if remove_expired
|
92
|
+
number_of_removed_crl_entries += prune_expired(puppet_crl, loader.key, inventory_file, cadir)
|
93
|
+
end
|
94
|
+
|
95
|
+
if (remove_duplicates || (!remove_entries && !remove_expired))
|
96
|
+
number_of_removed_duplicates += prune_CRL(puppet_crl)
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
if (number_of_removed_duplicates > 0 || number_of_removed_crl_entries > 0)
|
62
101
|
update_pruned_CRL(puppet_crl, loader.key)
|
63
102
|
FileSystem.write_file(puppet.settings[:cacrl], loader.crls, 0644)
|
64
|
-
@logger.inform("Removed #{number_of_removed_duplicates} duplicated certs from Puppet's CRL.")
|
103
|
+
@logger.inform("Removed #{number_of_removed_duplicates} duplicated certs from Puppet's CRL.") if number_of_removed_duplicates > 0
|
104
|
+
@logger.inform("Removed #{number_of_removed_crl_entries} certs from Puppet's CRL.") if number_of_removed_crl_entries > 0
|
65
105
|
else
|
66
|
-
@logger.inform("No
|
106
|
+
@logger.inform("No matching revocations found in the CRL for pruning")
|
67
107
|
end
|
68
108
|
else
|
69
109
|
@logger.err("Could not identify Puppet's CRL. Aborting prune action.")
|
70
110
|
exit_code = 1
|
71
111
|
end
|
72
|
-
|
73
112
|
return exit_code
|
74
113
|
end
|
75
114
|
|
@@ -106,6 +145,89 @@ BANNER
|
|
106
145
|
crl.sign(pkey, OpenSSL::Digest::SHA256.new)
|
107
146
|
end
|
108
147
|
|
148
|
+
def prune_using_serial(crl, key, serialnumbers)
|
149
|
+
removed_serials = []
|
150
|
+
revoked_list = crl.revoked
|
151
|
+
@logger.debug("Removing entries in CRL for issuer " \
|
152
|
+
"#{crl.issuer.to_s(OpenSSL::X509::Name::RFC2253)}") if @logger.debug?
|
153
|
+
serialnumbers.each do |serial|
|
154
|
+
if serial.match(/^(?:0[xX])?[A-Fa-f0-9]+$/)
|
155
|
+
revoked_list.delete_if do |revoked|
|
156
|
+
if revoked.serial == OpenSSL::BN.new(serial.hex)
|
157
|
+
removed_serials.push(serial)
|
158
|
+
true
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
crl.revoked = (revoked_list)
|
164
|
+
@logger.debug("Removed these CRL entries : #{removed_serials}") if @logger.debug?
|
165
|
+
return removed_serials.length
|
166
|
+
end
|
167
|
+
|
168
|
+
def prune_using_certname(crl, key, inventory_file, cadir, certnames)
|
169
|
+
serialnumbers = []
|
170
|
+
@logger.debug("Checking inventory file #{inventory_file} for matching cert names") if @logger.debug?
|
171
|
+
errors = FileSystem.validate_file_paths(inventory_file)
|
172
|
+
if errors.empty?
|
173
|
+
File.open(inventory_file).each_line do |line|
|
174
|
+
certnames.each do |certname|
|
175
|
+
if line.match(/\/CN=#{certname}$/) && line.split.length == 4
|
176
|
+
serialnumbers.push(line.split.first)
|
177
|
+
certnames.delete(certname)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
else
|
182
|
+
@logger.warn "Reading inventory file at #{inventory_file} failed with error #{errors}"
|
183
|
+
end
|
184
|
+
if certnames
|
185
|
+
@logger.debug("Checking CA dir #{cadir} for matching cert names")
|
186
|
+
certnames.each do |certname|
|
187
|
+
cert_file = "#{cadir}/signed/#{certname}.pem"
|
188
|
+
if File.file?(cert_file)
|
189
|
+
raw = File.read(cert_file)
|
190
|
+
certificate = OpenSSL::X509::Certificate.new(raw)
|
191
|
+
serial = certificate.serial
|
192
|
+
serialnumbers.push(serial.to_s(16))
|
193
|
+
end
|
194
|
+
end
|
195
|
+
prune_using_serial(crl, key, serialnumbers)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def prune_expired (crl, key, inventory_file, cadir)
|
200
|
+
serialnumbers = []
|
201
|
+
signed_dir = "#{cadir}/signed"
|
202
|
+
@logger.debug("Checking inventory file #{inventory_file} for expired entries") if @logger.debug?
|
203
|
+
errors = FileSystem.validate_file_paths(inventory_file)
|
204
|
+
if errors.empty?
|
205
|
+
File.open(inventory_file).each_line do |line|
|
206
|
+
if line.match(/\/CN=.*$/) && line.split.length == 4
|
207
|
+
not_after = line.split[2]
|
208
|
+
begin
|
209
|
+
not_after = Time.parse(line.split[2])
|
210
|
+
serialnumbers.push(line.split.first) if not_after < Time.now
|
211
|
+
rescue ArgumentError
|
212
|
+
@logger.warn "Invalid not_after time found in inventory.txt file at #{line}"
|
213
|
+
next
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
else
|
218
|
+
@logger.warn "Reading inventory file at #{inventory_file} failed with error #{errors}"
|
219
|
+
end
|
220
|
+
@logger.debug("Checking CA dir #{cadir} for expired certs")
|
221
|
+
Dir.foreach(signed_dir) do |filename|
|
222
|
+
if File.extname(filename) == '.pem'
|
223
|
+
raw = File.read("#{signed_dir}/#{filename}")
|
224
|
+
certificate = OpenSSL::X509::Certificate.new(raw)
|
225
|
+
serialnumbers.push(certificate.serial.to_s(16)) if certificate.not_after < Time.now
|
226
|
+
end
|
227
|
+
end
|
228
|
+
prune_using_serial(crl, key, serialnumbers)
|
229
|
+
end
|
230
|
+
|
109
231
|
def self.parser(parsed = {})
|
110
232
|
OptionParser.new do |opts|
|
111
233
|
opts.banner = BANNER
|
@@ -115,6 +237,21 @@ BANNER
|
|
115
237
|
opts.on('--config CONF', 'Path to the puppet.conf file on disk') do |conf|
|
116
238
|
parsed['config'] = conf
|
117
239
|
end
|
240
|
+
opts.on('--remove-duplicates', 'Remove duplicate entries from CRL(default)') do |remove_duplicates|
|
241
|
+
parsed['remove-duplicates'] = true
|
242
|
+
end
|
243
|
+
opts.on('--remove-expired', 'Remove expired entries from CRL') do |remove_expired|
|
244
|
+
parsed['remove-expired'] = true
|
245
|
+
end
|
246
|
+
opts.on('--remove-entries', 'Remove entries from CRL') do |remove_entries|
|
247
|
+
parsed['remove-entries'] = true
|
248
|
+
end
|
249
|
+
opts.on('--serial NUMBER[,NUMBER]', Array, 'Serial numbers(s) in HEX to be removed from CRL') do |serialnumbers|
|
250
|
+
parsed['serial'] = serialnumbers
|
251
|
+
end
|
252
|
+
opts.on('--certname NAME[,NAME]', Array, 'Name(s) of the cert(s) to be removed from CRL') do |certnames|
|
253
|
+
parsed['certname'] = certnames
|
254
|
+
end
|
118
255
|
end
|
119
256
|
end
|
120
257
|
|
@@ -134,4 +271,4 @@ BANNER
|
|
134
271
|
end
|
135
272
|
end
|
136
273
|
end
|
137
|
-
end
|
274
|
+
end
|
data/lib/puppetserver/ca/cli.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'optparse'
|
2
2
|
|
3
3
|
require 'puppetserver/ca/action/clean'
|
4
|
+
require 'puppetserver/ca/action/delete'
|
4
5
|
require 'puppetserver/ca/action/generate'
|
5
6
|
require 'puppetserver/ca/action/import'
|
6
7
|
require 'puppetserver/ca/action/enable'
|
@@ -27,6 +28,7 @@ Puppet Server's built-in Certificate Authority
|
|
27
28
|
BANNER
|
28
29
|
|
29
30
|
ADMIN_ACTIONS = {
|
31
|
+
'delete' => Action::Delete,
|
30
32
|
'import' => Action::Import,
|
31
33
|
'setup' => Action::Setup,
|
32
34
|
'enable' => Action::Enable,
|
@@ -10,17 +10,12 @@ module Puppetserver
|
|
10
10
|
# Utilities for doing HTTPS against the CA that wraps Net::HTTP constructs
|
11
11
|
class HttpClient
|
12
12
|
|
13
|
-
DEFAULT_HEADERS = {
|
14
|
-
'User-Agent' => 'PuppetserverCaCli',
|
15
|
-
'Content-Type' => 'application/json',
|
16
|
-
'Accept' => 'application/json'
|
17
|
-
}
|
18
|
-
|
19
13
|
attr_reader :store
|
20
14
|
|
21
15
|
# Not all connections require a client cert to be present.
|
22
16
|
# For example, when querying the status endpoint.
|
23
17
|
def initialize(logger, settings, with_client_cert: true)
|
18
|
+
@default_headers = make_headers(ENV['HOME'])
|
24
19
|
@logger = logger
|
25
20
|
@store = make_store(settings[:localcacert],
|
26
21
|
settings[:certificate_revocation],
|
@@ -52,7 +47,7 @@ module Puppetserver
|
|
52
47
|
# The Connection object should have HTTP verbs defined on it that take
|
53
48
|
# a body (and optional overrides). Returns whatever the block given returned.
|
54
49
|
def with_connection(url, &block)
|
55
|
-
request = ->(conn) { block.call(Connection.new(conn, url, @logger)) }
|
50
|
+
request = ->(conn) { block.call(Connection.new(conn, url, @logger, @default_headers)) }
|
56
51
|
|
57
52
|
begin
|
58
53
|
Net::HTTP.start(url.host, url.port,
|
@@ -68,7 +63,22 @@ module Puppetserver
|
|
68
63
|
|
69
64
|
private
|
70
65
|
|
71
|
-
|
66
|
+
def make_headers(home)
|
67
|
+
headers = {
|
68
|
+
'User-Agent' => 'PuppetserverCaCli',
|
69
|
+
'Content-Type' => 'application/json',
|
70
|
+
'Accept' => 'application/json'
|
71
|
+
}
|
72
|
+
|
73
|
+
token_path = "#{home}/.puppetlabs/token"
|
74
|
+
if File.exist?(token_path)
|
75
|
+
headers['X-Authentication'] = File.read(token_path)
|
76
|
+
end
|
77
|
+
|
78
|
+
headers
|
79
|
+
end
|
80
|
+
|
81
|
+
def load_with_errors(path, setting, &block)
|
72
82
|
begin
|
73
83
|
content = File.read(path)
|
74
84
|
block.call(content)
|
@@ -81,21 +91,23 @@ module Puppetserver
|
|
81
91
|
"Could not parse '#{setting}' at '#{path}'.\n" +
|
82
92
|
" OpenSSL returned: #{e.message}")
|
83
93
|
end
|
84
|
-
|
94
|
+
end
|
85
95
|
|
86
96
|
# Helper class that wraps a Net::HTTP connection, a HttpClient::URL
|
87
97
|
# and defines methods named after HTTP verbs that are called on the
|
88
98
|
# saved connection, returning a Result.
|
89
99
|
class Connection
|
90
|
-
|
100
|
+
|
101
|
+
def initialize(net_http_connection, url_struct, logger, default_headers)
|
91
102
|
@conn = net_http_connection
|
92
103
|
@url = url_struct
|
93
104
|
@logger = logger
|
105
|
+
@default_headers = default_headers
|
94
106
|
end
|
95
107
|
|
96
|
-
def get(url_overide = nil,
|
108
|
+
def get(url_overide = nil, header_overrides = {})
|
97
109
|
url = url_overide || @url
|
98
|
-
headers =
|
110
|
+
headers = @default_headers.merge(header_overrides)
|
99
111
|
|
100
112
|
@logger.debug("Making a GET request at #{url.full_url}")
|
101
113
|
|
@@ -105,9 +117,9 @@ module Puppetserver
|
|
105
117
|
|
106
118
|
end
|
107
119
|
|
108
|
-
def put(body, url_override = nil,
|
120
|
+
def put(body, url_override = nil, header_overrides = {})
|
109
121
|
url = url_override || @url
|
110
|
-
headers =
|
122
|
+
headers = @default_headers.merge(header_overrides)
|
111
123
|
|
112
124
|
@logger.debug("Making a PUT request at #{url.full_url}")
|
113
125
|
|
@@ -118,9 +130,9 @@ module Puppetserver
|
|
118
130
|
Result.new(result.code, result.body)
|
119
131
|
end
|
120
132
|
|
121
|
-
def delete(url_override = nil,
|
133
|
+
def delete(url_override = nil, header_overrides = {})
|
122
134
|
url = url_override || @url
|
123
|
-
headers =
|
135
|
+
headers = @default_headers.merge(header_overrides)
|
124
136
|
|
125
137
|
@logger.debug("Making a DELETE request at #{url.full_url}")
|
126
138
|
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Puppetserver
|
4
|
+
module Ca
|
5
|
+
module Utils
|
6
|
+
module Inventory
|
7
|
+
|
8
|
+
# Note that the inventory file may have multiple entries for the same certname,
|
9
|
+
# so it should only provide the latest cert for the given certname.
|
10
|
+
def self.parse_inventory_file(path, logger)
|
11
|
+
unless File.exist?(path)
|
12
|
+
logger.err("Could not find inventory at #{path}")
|
13
|
+
return [{}, true]
|
14
|
+
end
|
15
|
+
inventory = {}
|
16
|
+
errored = false
|
17
|
+
File.readlines(path).each do |line|
|
18
|
+
# Shouldn't be any blank lines, but skip them if there are
|
19
|
+
next if line.strip.empty?
|
20
|
+
|
21
|
+
items = line.strip.split
|
22
|
+
if items.count != 4
|
23
|
+
logger.err("Invalid entry found in inventory.txt: #{line}")
|
24
|
+
errored = true
|
25
|
+
next
|
26
|
+
end
|
27
|
+
unless items[0].match(/^(?:0x)?[A-Fa-f0-9]+$/)
|
28
|
+
logger.err("Invalid serial found in inventory.txt line: #{line}")
|
29
|
+
errored = true
|
30
|
+
next
|
31
|
+
end
|
32
|
+
serial = items[0].hex
|
33
|
+
not_before = nil
|
34
|
+
not_after = nil
|
35
|
+
begin
|
36
|
+
not_before = Time.parse(items[1])
|
37
|
+
rescue ArgumentError
|
38
|
+
logger.err("Invalid not_before time found in inventory.txt line: #{line}")
|
39
|
+
errored = true
|
40
|
+
next
|
41
|
+
end
|
42
|
+
begin
|
43
|
+
not_after = Time.parse(items[2])
|
44
|
+
rescue ArgumentError
|
45
|
+
logger.err("Invalid not_after time found in inventory.txt line: #{line}")
|
46
|
+
errored = true
|
47
|
+
next
|
48
|
+
end
|
49
|
+
unless items[3].start_with?('/CN=')
|
50
|
+
logger.err("Invalid certname found in inventory.txt line: #{line}")
|
51
|
+
errored = true
|
52
|
+
next
|
53
|
+
end
|
54
|
+
certname = items[3][4..-1]
|
55
|
+
|
56
|
+
if !inventory.keys.include?(certname)
|
57
|
+
inventory[certname] = {
|
58
|
+
:serial => serial,
|
59
|
+
:old_serials => [],
|
60
|
+
:not_before => not_before,
|
61
|
+
:not_after => not_after,
|
62
|
+
}
|
63
|
+
else
|
64
|
+
if not_after >= inventory[certname][:not_after]
|
65
|
+
# This is a newer cert than the one we currently have recorded,
|
66
|
+
# so save the previous serial in :old_serials
|
67
|
+
inventory[certname][:old_serials] << inventory[certname][:serial]
|
68
|
+
inventory[certname][:serial] = serial
|
69
|
+
inventory[certname][:not_before] = not_before
|
70
|
+
inventory[certname][:not_after] = not_after
|
71
|
+
else
|
72
|
+
# This somehow is an older cert (shouldn't really be possible as we just append
|
73
|
+
# to the file with each new cert and we are reading it order)
|
74
|
+
inventory[certname][:old_serials] << serial
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
[inventory, errored]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
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: 2.
|
4
|
+
version: 2.6.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: 2023-
|
11
|
+
date: 2023-05-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: facter
|
@@ -80,7 +80,7 @@ executables:
|
|
80
80
|
extensions: []
|
81
81
|
extra_rdoc_files: []
|
82
82
|
files:
|
83
|
-
- ".github/workflows/
|
83
|
+
- ".github/workflows/mend.yaml"
|
84
84
|
- ".gitignore"
|
85
85
|
- ".rspec"
|
86
86
|
- ".travis.yml"
|
@@ -96,6 +96,7 @@ files:
|
|
96
96
|
- exe/puppetserver-ca
|
97
97
|
- lib/puppetserver/ca.rb
|
98
98
|
- lib/puppetserver/ca/action/clean.rb
|
99
|
+
- lib/puppetserver/ca/action/delete.rb
|
99
100
|
- lib/puppetserver/ca/action/enable.rb
|
100
101
|
- lib/puppetserver/ca/action/generate.rb
|
101
102
|
- lib/puppetserver/ca/action/import.rb
|
@@ -118,6 +119,7 @@ files:
|
|
118
119
|
- lib/puppetserver/ca/utils/config.rb
|
119
120
|
- lib/puppetserver/ca/utils/file_system.rb
|
120
121
|
- lib/puppetserver/ca/utils/http_client.rb
|
122
|
+
- lib/puppetserver/ca/utils/inventory.rb
|
121
123
|
- lib/puppetserver/ca/utils/signing_digest.rb
|
122
124
|
- lib/puppetserver/ca/version.rb
|
123
125
|
- lib/puppetserver/ca/x509_loader.rb
|
@@ -141,7 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
141
143
|
- !ruby/object:Gem::Version
|
142
144
|
version: '0'
|
143
145
|
requirements: []
|
144
|
-
rubygems_version: 3.
|
146
|
+
rubygems_version: 3.4.12
|
145
147
|
signing_key:
|
146
148
|
specification_version: 4
|
147
149
|
summary: A simple CLI tool for interacting with Puppet Server's Certificate Authority
|
data/.github/workflows/snyk.yaml
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
---
|
2
|
-
name: Snyk Monitor
|
3
|
-
on:
|
4
|
-
push:
|
5
|
-
branches:
|
6
|
-
- main
|
7
|
-
jobs:
|
8
|
-
snyk_monitor:
|
9
|
-
if: ${{ github.repository_owner == 'puppetlabs' }}
|
10
|
-
runs-on: ubuntu-latest
|
11
|
-
name: Snyk Monitor
|
12
|
-
steps:
|
13
|
-
- name: Checkout current PR
|
14
|
-
uses: actions/checkout@v2
|
15
|
-
- name: Setup Ruby
|
16
|
-
uses: ruby/setup-ruby@v1
|
17
|
-
with:
|
18
|
-
ruby-version: 2.7
|
19
|
-
- name: Install dependencies
|
20
|
-
run: bundle install --jobs 3 --retry 3
|
21
|
-
- name: Extract branch name
|
22
|
-
shell: bash
|
23
|
-
run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
|
24
|
-
id: extract_branch
|
25
|
-
- name: Run Snyk to check for vulnerabilities
|
26
|
-
uses: snyk/actions/ruby@master
|
27
|
-
env:
|
28
|
-
SNYK_TOKEN: ${{ secrets.SNYK_FOSS_KEY }}
|
29
|
-
with:
|
30
|
-
command: monitor
|
31
|
-
args: --org=puppet-foss --project-name=${{ github.repository }}#${{ steps.extract_branch.outputs.branch }}
|