puppetserver-ca 2.4.0 → 2.6.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/.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 }}
|