]>
Commit | Line | Data |
---|---|---|
ac78733c WB |
1 | package PMG::API2::Certificates; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
6 | use PVE::Certificate; | |
7 | use PVE::Exception qw(raise raise_param_exc); | |
8 | use PVE::JSONSchema qw(get_standard_option); | |
9 | use PVE::Tools qw(extract_param file_get_contents file_set_contents); | |
10 | ||
11 | use PMG::CertHelpers; | |
12 | use PMG::NodeConfig; | |
a0822f48 | 13 | use PMG::RS::Acme; |
ac78733c WB |
14 | use PMG::RS::CSR; |
15 | ||
16 | use PMG::API2::ACMEPlugin; | |
17 | ||
18 | use base qw(PVE::RESTHandler); | |
19 | ||
20 | my $acme_account_dir = PMG::CertHelpers::acme_account_dir(); | |
21 | ||
22 | sub first_typed_pem_entry : prototype($$) { | |
23 | my ($label, $data) = @_; | |
24 | ||
25 | if ($data =~ /^(-----BEGIN \Q$label\E-----\n.*?\n-----END \Q$label\E-----)$/ms) { | |
26 | return $1; | |
27 | } | |
28 | return undef; | |
29 | } | |
30 | ||
31 | sub pem_private_key : prototype($) { | |
32 | my ($data) = @_; | |
33 | return first_typed_pem_entry('PRIVATE KEY', $data); | |
34 | } | |
35 | ||
36 | sub pem_certificate : prototype($) { | |
37 | my ($data) = @_; | |
38 | return first_typed_pem_entry('CERTIFICATE', $data); | |
39 | } | |
40 | ||
41 | my sub restart_after_cert_update : prototype($) { | |
42 | my ($type) = @_; | |
43 | ||
44 | if ($type eq 'api') { | |
45 | print "Restarting pmgproxy\n"; | |
46 | PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pmgproxy']); | |
d7ce72c8 SI |
47 | |
48 | my $cinfo = PMG::ClusterConfig->new(); | |
49 | if (scalar(keys %{$cinfo->{ids}})) { | |
50 | print "Notify cluster about new fingerprint\n"; | |
51 | PMG::Cluster::trigger_update_fingerprints($cinfo); | |
52 | } | |
ac78733c WB |
53 | } |
54 | }; | |
55 | ||
56 | my sub update_cert : prototype($$$$$) { | |
57 | my ($type, $cert_path, $certificate, $force, $restart) = @_; | |
58 | my $code = sub { | |
59 | print "Setting custom certificate file $cert_path\n"; | |
60 | PMG::CertHelpers::set_cert_file($certificate, $cert_path, $force); | |
61 | ||
62 | restart_after_cert_update($type) if $restart; | |
63 | }; | |
64 | PMG::CertHelpers::cert_lock(10, $code); | |
65 | }; | |
66 | ||
67 | my sub set_smtp : prototype($$) { | |
68 | my ($on, $reload) = @_; | |
69 | ||
70 | my $code = sub { | |
71 | my $cfg = PMG::Config->new(); | |
6694a159 | 72 | # check if value actually would change |
1ecf138e SI |
73 | if (!$cfg->get('mail', 'tls') != !$on) { |
74 | print "Rewriting postfix config\n"; | |
75 | $cfg->set('mail', 'tls', $on); | |
76 | $cfg->write(); | |
6694a159 | 77 | $cfg->rewrite_config_postfix(); |
ac78733c WB |
78 | } |
79 | ||
1ecf138e | 80 | if ($reload) { |
ac78733c WB |
81 | print "Reloading postfix\n"; |
82 | PMG::Utils::service_cmd('postfix', 'reload'); | |
83 | } | |
84 | }; | |
85 | PMG::Config::lock_config($code, "failed to reload postfix"); | |
86 | } | |
87 | ||
88 | __PACKAGE__->register_method ({ | |
89 | name => 'index', | |
90 | path => '', | |
91 | method => 'GET', | |
92 | permissions => { user => 'all' }, | |
93 | description => "Node index.", | |
94 | parameters => { | |
95 | additionalProperties => 0, | |
96 | properties => { | |
97 | node => get_standard_option('pve-node'), | |
98 | }, | |
99 | }, | |
100 | returns => { | |
101 | type => 'array', | |
102 | items => { | |
103 | type => "object", | |
104 | properties => {}, | |
105 | }, | |
106 | links => [ { rel => 'child', href => "{name}" } ], | |
107 | }, | |
108 | code => sub { | |
109 | my ($param) = @_; | |
110 | ||
111 | return [ | |
112 | { name => 'acme' }, | |
113 | { name => 'custom' }, | |
114 | { name => 'info' }, | |
115 | { name => 'config' }, | |
116 | ]; | |
117 | }, | |
118 | }); | |
119 | ||
120 | __PACKAGE__->register_method ({ | |
121 | name => 'info', | |
122 | path => 'info', | |
123 | method => 'GET', | |
124 | permissions => { user => 'all' }, | |
125 | proxyto => 'node', | |
126 | protected => 1, | |
127 | description => "Get information about the node's certificates.", | |
128 | parameters => { | |
129 | additionalProperties => 0, | |
130 | properties => { | |
131 | node => get_standard_option('pve-node'), | |
132 | }, | |
133 | }, | |
134 | returns => { | |
135 | type => 'array', | |
136 | items => get_standard_option('pve-certificate-info'), | |
137 | }, | |
138 | code => sub { | |
139 | my ($param) = @_; | |
140 | ||
141 | my $res = []; | |
142 | for my $path (&PMG::CertHelpers::API_CERT, &PMG::CertHelpers::SMTP_CERT) { | |
143 | eval { | |
144 | my $info = PVE::Certificate::get_certificate_info($path); | |
145 | push @$res, $info if $info; | |
146 | }; | |
147 | } | |
148 | return $res; | |
149 | }, | |
150 | }); | |
151 | ||
152 | __PACKAGE__->register_method ({ | |
153 | name => 'custom_cert_index', | |
154 | path => 'custom', | |
155 | method => 'GET', | |
156 | permissions => { user => 'all' }, | |
157 | description => "Certificate index.", | |
158 | parameters => { | |
159 | additionalProperties => 0, | |
160 | properties => { | |
161 | node => get_standard_option('pve-node'), | |
162 | }, | |
163 | }, | |
164 | returns => { | |
165 | type => 'array', | |
166 | items => { | |
167 | type => "object", | |
168 | properties => {}, | |
169 | }, | |
170 | links => [ { rel => 'child', href => "{type}" } ], | |
171 | }, | |
172 | code => sub { | |
173 | my ($param) = @_; | |
174 | ||
175 | return [ | |
176 | { type => 'api' }, | |
177 | { type => 'smtp' }, | |
178 | ]; | |
179 | }, | |
180 | }); | |
181 | ||
182 | __PACKAGE__->register_method ({ | |
183 | name => 'upload_custom_cert', | |
184 | path => 'custom/{type}', | |
185 | method => 'POST', | |
186 | permissions => { check => [ 'admin' ] }, | |
187 | description => 'Upload or update custom certificate chain and key.', | |
188 | protected => 1, | |
189 | proxyto => 'node', | |
190 | parameters => { | |
191 | additionalProperties => 0, | |
192 | properties => { | |
193 | node => get_standard_option('pve-node'), | |
194 | certificates => { | |
195 | type => 'string', | |
196 | format => 'pem-certificate-chain', | |
197 | description => 'PEM encoded certificate (chain).', | |
198 | }, | |
199 | key => { | |
200 | type => 'string', | |
201 | description => 'PEM encoded private key.', | |
202 | format => 'pem-string', | |
203 | optional => 0, | |
204 | }, | |
205 | type => get_standard_option('pmg-certificate-type'), | |
206 | force => { | |
207 | type => 'boolean', | |
208 | description => 'Overwrite existing custom or ACME certificate files.', | |
209 | optional => 1, | |
210 | default => 0, | |
211 | }, | |
212 | restart => { | |
213 | type => 'boolean', | |
214 | description => 'Restart services.', | |
215 | optional => 1, | |
216 | default => 0, | |
217 | }, | |
218 | }, | |
219 | }, | |
220 | returns => get_standard_option('pve-certificate-info'), | |
221 | code => sub { | |
222 | my ($param) = @_; | |
223 | ||
224 | my $type = extract_param($param, 'type'); # also used to know which service to restart | |
225 | my $cert_path = PMG::CertHelpers::cert_path($type); | |
226 | ||
227 | my $certs = extract_param($param, 'certificates'); | |
228 | $certs = PVE::Certificate::strip_leading_text($certs); | |
229 | ||
230 | my $key = extract_param($param, 'key'); | |
231 | if ($key) { | |
232 | $key = PVE::Certificate::strip_leading_text($key); | |
233 | $certs = "$key\n$certs"; | |
234 | } else { | |
235 | my $private_key = pem_private_key($certs); | |
236 | if (!defined($private_key)) { | |
237 | my $old = file_get_contents($cert_path); | |
238 | $private_key = pem_private_key($old); | |
239 | if (!defined($private_key)) { | |
240 | raise_param_exc({ | |
241 | 'key' => "Attempted to upload custom certificate without (existing) key." | |
242 | }) | |
243 | } | |
244 | ||
245 | # copy the old certificate's key: | |
246 | $certs = "$key\n$certs"; | |
247 | } | |
248 | } | |
249 | ||
250 | my $info; | |
251 | ||
252 | PMG::CertHelpers::cert_lock(10, sub { | |
253 | update_cert($type, $cert_path, $certs, $param->{force}, $param->{restart}); | |
254 | }); | |
255 | ||
256 | if ($type eq 'smtp') { | |
257 | set_smtp(1, $param->{restart}); | |
258 | } | |
259 | ||
260 | return $info; | |
261 | }}); | |
262 | ||
263 | __PACKAGE__->register_method ({ | |
264 | name => 'remove_custom_cert', | |
265 | path => 'custom/{type}', | |
266 | method => 'DELETE', | |
267 | permissions => { check => [ 'admin' ] }, | |
268 | description => 'DELETE custom certificate chain and key.', | |
269 | protected => 1, | |
270 | proxyto => 'node', | |
271 | parameters => { | |
272 | additionalProperties => 0, | |
273 | properties => { | |
274 | node => get_standard_option('pve-node'), | |
275 | type => get_standard_option('pmg-certificate-type'), | |
276 | restart => { | |
277 | type => 'boolean', | |
278 | description => 'Restart pmgproxy.', | |
279 | optional => 1, | |
280 | default => 0, | |
281 | }, | |
282 | }, | |
283 | }, | |
284 | returns => { | |
285 | type => 'null', | |
286 | }, | |
287 | code => sub { | |
288 | my ($param) = @_; | |
289 | ||
290 | my $type = extract_param($param, 'type'); | |
291 | my $cert_path = PMG::CertHelpers::cert_path($type); | |
292 | ||
293 | my $code = sub { | |
294 | print "Deleting custom certificate files\n"; | |
295 | unlink $cert_path; | |
296 | PMG::Ticket::generate_api_cert(0) if $type eq 'api'; | |
297 | ||
298 | if ($param->{restart}) { | |
299 | restart_after_cert_update($type); | |
300 | } | |
301 | }; | |
302 | ||
303 | PMG::CertHelpers::cert_lock(10, $code); | |
304 | ||
305 | if ($type eq 'smtp') { | |
306 | set_smtp(0, $param->{restart}); | |
307 | } | |
308 | ||
309 | return undef; | |
310 | }}); | |
311 | ||
312 | __PACKAGE__->register_method ({ | |
313 | name => 'acme_cert_index', | |
314 | path => 'acme', | |
315 | method => 'GET', | |
316 | permissions => { user => 'all' }, | |
317 | description => "ACME Certificate index.", | |
318 | parameters => { | |
319 | additionalProperties => 0, | |
320 | properties => { | |
321 | node => get_standard_option('pve-node'), | |
322 | }, | |
323 | }, | |
324 | returns => { | |
325 | type => 'array', | |
326 | items => { | |
327 | type => "object", | |
328 | properties => {}, | |
329 | }, | |
330 | links => [ { rel => 'child', href => "{type}" } ], | |
331 | }, | |
332 | code => sub { | |
333 | my ($param) = @_; | |
334 | ||
335 | return [ | |
336 | { type => 'api' }, | |
337 | { type => 'smtp' }, | |
338 | ]; | |
339 | }, | |
340 | }); | |
341 | ||
342 | my $order_certificate = sub { | |
343 | my ($acme, $acme_node_config) = @_; | |
344 | ||
345 | my $plugins = PMG::API2::ACMEPlugin::load_config(); | |
346 | ||
347 | print "Placing ACME order\n"; | |
af6b569a | 348 | my ($order_url, $order) = $acme->new_order([ sort keys %{$acme_node_config->{domains}} ]); |
ac78733c WB |
349 | print "Order URL: $order_url\n"; |
350 | for my $auth_url (@{$order->{authorizations}}) { | |
351 | print "\nGetting authorization details from '$auth_url'\n"; | |
352 | my $auth = $acme->get_authorization($auth_url); | |
353 | ||
354 | # force lower case, like get_acme_conf does | |
355 | my $domain = lc($auth->{identifier}->{value}); | |
356 | if ($auth->{status} eq 'valid') { | |
357 | print "$domain is already validated!\n"; | |
358 | } else { | |
359 | print "The validation for $domain is pending!\n"; | |
360 | ||
361 | my $domain_config = $acme_node_config->{domains}->{$domain}; | |
362 | die "no config for domain '$domain'\n" if !$domain_config; | |
363 | ||
364 | my $plugin_id = $domain_config->{plugin}; | |
365 | ||
366 | my $plugin_cfg = $plugins->{ids}->{$plugin_id}; | |
367 | die "plugin '$plugin_id' for domain '$domain' not found!\n" | |
368 | if !$plugin_cfg; | |
369 | ||
370 | my $data = { | |
371 | plugin => $plugin_cfg, | |
372 | alias => $domain_config->{alias}, | |
373 | }; | |
374 | ||
375 | my $plugin = PVE::ACME::Challenge->lookup($plugin_cfg->{type}); | |
376 | $plugin->setup($acme, $auth, $data); | |
377 | ||
378 | print "Triggering validation\n"; | |
379 | eval { | |
380 | die "no validation URL returned by plugin '$plugin_id' for domain '$domain'\n" | |
381 | if !defined($data->{url}); | |
382 | ||
383 | $acme->request_challenge_validation($data->{url}); | |
384 | print "Sleeping for 5 seconds\n"; | |
385 | sleep 5; | |
386 | while (1) { | |
387 | $auth = $acme->get_authorization($auth_url); | |
388 | if ($auth->{status} eq 'pending') { | |
389 | print "Status is still 'pending', trying again in 10 seconds\n"; | |
390 | sleep 10; | |
391 | next; | |
392 | } elsif ($auth->{status} eq 'valid') { | |
393 | print "Status is 'valid', domain '$domain' OK!\n"; | |
394 | last; | |
395 | } | |
c58e1065 TL |
396 | my $error = "validating challenge '$auth_url' failed - status: $auth->{status}"; |
397 | for (@{$auth->{challenges}}) { | |
398 | $error .= ", $_->{error}->{detail}" if $_->{error}->{detail}; | |
399 | } | |
400 | die "$error\n"; | |
ac78733c WB |
401 | } |
402 | }; | |
403 | my $err = $@; | |
404 | eval { $plugin->teardown($acme, $auth, $data) }; | |
405 | warn "$@\n" if $@; | |
406 | die $err if $err; | |
407 | } | |
408 | } | |
409 | print "\nAll domains validated!\n"; | |
410 | print "\nCreating CSR\n"; | |
411 | # Currently we only support dns entries, so extract those from the order: | |
412 | my $san = [ | |
413 | map { | |
414 | $_->{value} | |
415 | } grep { | |
416 | $_->{type} eq 'dns' | |
417 | } $order->{identifiers}->@* | |
418 | ]; | |
419 | die "DNS identifiers are required to generate a CSR.\n" if !scalar @$san; | |
420 | my ($csr_der, $key) = PMG::RS::CSR::generate_csr($san, {}); | |
421 | ||
422 | my $finalize_error_cnt = 0; | |
423 | print "Checking order status\n"; | |
424 | while (1) { | |
425 | $order = $acme->get_order($order_url); | |
426 | if ($order->{status} eq 'pending') { | |
427 | print "still pending, trying to finalize order\n"; | |
428 | # FIXME | |
429 | # to be compatible with and without the order ready state we try to | |
430 | # finalize even at the 'pending' state and give up after 5 | |
431 | # unsuccessful tries this can be removed when the letsencrypt api | |
432 | # definitely has implemented the 'ready' state | |
433 | eval { | |
434 | $acme->finalize_order($order->{finalize}, $csr_der); | |
435 | }; | |
436 | if (my $err = $@) { | |
437 | die $err if $finalize_error_cnt >= 5; | |
438 | ||
439 | $finalize_error_cnt++; | |
440 | warn $err; | |
441 | } | |
442 | sleep 5; | |
443 | next; | |
444 | } elsif ($order->{status} eq 'ready') { | |
445 | print "Order is ready, finalizing order\n"; | |
446 | $acme->finalize_order($order->{finalize}, $csr_der); | |
447 | sleep 5; | |
448 | next; | |
449 | } elsif ($order->{status} eq 'processing') { | |
450 | print "still processing, trying again in 30 seconds\n"; | |
451 | sleep 30; | |
452 | next; | |
453 | } elsif ($order->{status} eq 'valid') { | |
454 | print "valid!\n"; | |
455 | last; | |
456 | } | |
457 | die "order status: $order->{status}\n"; | |
458 | } | |
459 | ||
460 | print "\nDownloading certificate\n"; | |
461 | my $cert = $acme->get_certificate($order->{certificate}); | |
462 | ||
463 | return ($cert, $key); | |
464 | }; | |
465 | ||
466 | # Filter domains and raise an error if the list becomes empty. | |
467 | my $filter_domains = sub { | |
468 | my ($acme_config, $type) = @_; | |
469 | ||
4b01992f | 470 | my $domains = PMG::NodeConfig::filter_domains_by_type($acme_config->{domains}, $type); |
ac78733c | 471 | |
4b01992f | 472 | if (!$domains) { |
ac78733c WB |
473 | raise("No domains configured for type '$type'\n", 400); |
474 | } | |
4b01992f WB |
475 | |
476 | $acme_config->{domains} = $domains; | |
ac78733c WB |
477 | }; |
478 | ||
479 | __PACKAGE__->register_method ({ | |
480 | name => 'new_acme_cert', | |
481 | path => 'acme/{type}', | |
482 | method => 'POST', | |
483 | permissions => { check => [ 'admin' ] }, | |
484 | description => 'Order a new certificate from ACME-compatible CA.', | |
485 | protected => 1, | |
486 | proxyto => 'node', | |
487 | parameters => { | |
488 | additionalProperties => 0, | |
489 | properties => { | |
490 | node => get_standard_option('pve-node'), | |
491 | type => get_standard_option('pmg-certificate-type'), | |
492 | force => { | |
493 | type => 'boolean', | |
494 | description => 'Overwrite existing custom certificate.', | |
495 | optional => 1, | |
496 | default => 0, | |
497 | }, | |
498 | }, | |
499 | }, | |
500 | returns => { | |
501 | type => 'string', | |
502 | }, | |
503 | code => sub { | |
504 | my ($param) = @_; | |
505 | ||
506 | my $type = extract_param($param, 'type'); # also used to know which service to restart | |
507 | my $cert_path = PMG::CertHelpers::cert_path($type); | |
508 | raise_param_exc({'force' => "Custom certificate exists but 'force' is not set."}) | |
509 | if !$param->{force} && -e $cert_path; | |
510 | ||
511 | my $node_config = PMG::NodeConfig::load_config(); | |
512 | my $acme_config = PMG::NodeConfig::get_acme_conf($node_config); | |
513 | raise("ACME domain list in configuration is missing!", 400) | |
259f8a41 | 514 | if !($acme_config && $acme_config->{domains} && $acme_config->{domains}->%*); |
ac78733c WB |
515 | |
516 | $filter_domains->($acme_config, $type); | |
517 | ||
518 | my $rpcenv = PMG::RESTEnvironment->get(); | |
519 | my $authuser = $rpcenv->get_user(); | |
520 | ||
521 | my $realcmd = sub { | |
522 | STDOUT->autoflush(1); | |
523 | my $account = $acme_config->{account}; | |
524 | my $account_file = "${acme_account_dir}/${account}"; | |
525 | die "ACME account config file '$account' does not exist.\n" | |
526 | if ! -e $account_file; | |
527 | ||
528 | print "Loading ACME account details\n"; | |
529 | my $acme = PMG::RS::Acme->load($account_file); | |
530 | ||
531 | my ($cert, $key) = $order_certificate->($acme, $acme_config); | |
532 | my $certificate = "$key\n$cert"; | |
533 | ||
534 | update_cert($type, $cert_path, $certificate, $param->{force}, 1); | |
535 | ||
536 | if ($type eq 'smtp') { | |
537 | set_smtp(1, 1); | |
538 | } | |
539 | ||
540 | die "$@\n" if $@; | |
541 | }; | |
542 | ||
543 | return $rpcenv->fork_worker("acmenewcert", undef, $authuser, $realcmd); | |
544 | }}); | |
545 | ||
546 | __PACKAGE__->register_method ({ | |
547 | name => 'renew_acme_cert', | |
548 | path => 'acme/{type}', | |
549 | method => 'PUT', | |
550 | permissions => { check => [ 'admin' ] }, | |
551 | description => "Renew existing certificate from CA.", | |
552 | protected => 1, | |
553 | proxyto => 'node', | |
554 | parameters => { | |
555 | additionalProperties => 0, | |
556 | properties => { | |
557 | node => get_standard_option('pve-node'), | |
558 | type => get_standard_option('pmg-certificate-type'), | |
559 | force => { | |
560 | type => 'boolean', | |
561 | description => 'Force renewal even if expiry is more than 30 days away.', | |
562 | optional => 1, | |
563 | default => 0, | |
564 | }, | |
565 | }, | |
566 | }, | |
567 | returns => { | |
568 | type => 'string', | |
569 | }, | |
570 | code => sub { | |
571 | my ($param) = @_; | |
572 | ||
573 | my $type = extract_param($param, 'type'); # also used to know which service to restart | |
574 | my $cert_path = PMG::CertHelpers::cert_path($type); | |
575 | ||
576 | raise("No current (custom) certificate found, please order a new certificate!\n") | |
577 | if ! -e $cert_path; | |
578 | ||
579 | my $expires_soon = PVE::Certificate::check_expiry($cert_path, time() + 30*24*60*60); | |
580 | raise_param_exc({'force' => "Certificate does not expire within the next 30 days, and 'force' is not set."}) | |
581 | if !$expires_soon && !$param->{force}; | |
582 | ||
583 | my $node_config = PMG::NodeConfig::load_config(); | |
584 | my $acme_config = PMG::NodeConfig::get_acme_conf($node_config); | |
585 | raise("ACME domain list in configuration is missing!", 400) | |
586 | if !$acme_config || !$acme_config->{domains}->%*; | |
587 | ||
588 | $filter_domains->($acme_config, $type); | |
589 | ||
590 | my $rpcenv = PMG::RESTEnvironment->get(); | |
591 | my $authuser = $rpcenv->get_user(); | |
592 | ||
593 | my $old_cert = PVE::Tools::file_get_contents($cert_path); | |
594 | ||
595 | my $realcmd = sub { | |
596 | STDOUT->autoflush(1); | |
597 | my $account = $acme_config->{account}; | |
598 | my $account_file = "${acme_account_dir}/${account}"; | |
599 | die "ACME account config file '$account' does not exist.\n" | |
600 | if ! -e $account_file; | |
601 | ||
602 | print "Loading ACME account details\n"; | |
603 | my $acme = PMG::RS::Acme->load($account_file); | |
604 | ||
605 | my ($cert, $key) = $order_certificate->($acme, $acme_config); | |
606 | my $certificate = "$key\n$cert"; | |
607 | ||
608 | update_cert($type, $cert_path, $certificate, 1, 1); | |
609 | ||
610 | if (defined($old_cert)) { | |
611 | print "Revoking old certificate\n"; | |
58f5c8f1 WB |
612 | eval { |
613 | $old_cert = pem_certificate($old_cert) | |
614 | or die "no certificate section found in '$cert_path'\n"; | |
615 | $acme->revoke_certificate($old_cert, undef); | |
616 | }; | |
ac78733c WB |
617 | warn "Revoke request to CA failed: $@" if $@; |
618 | } | |
619 | }; | |
620 | ||
621 | return $rpcenv->fork_worker("acmerenew", undef, $authuser, $realcmd); | |
622 | }}); | |
623 | ||
624 | __PACKAGE__->register_method ({ | |
625 | name => 'revoke_acme_cert', | |
626 | path => 'acme/{type}', | |
627 | method => 'DELETE', | |
628 | permissions => { check => [ 'admin' ] }, | |
629 | description => "Revoke existing certificate from CA.", | |
630 | protected => 1, | |
631 | proxyto => 'node', | |
632 | parameters => { | |
633 | additionalProperties => 0, | |
634 | properties => { | |
635 | node => get_standard_option('pve-node'), | |
636 | type => get_standard_option('pmg-certificate-type'), | |
637 | }, | |
638 | }, | |
639 | returns => { | |
640 | type => 'string', | |
641 | }, | |
642 | code => sub { | |
643 | my ($param) = @_; | |
644 | ||
645 | my $type = extract_param($param, 'type'); # also used to know which service to restart | |
646 | my $cert_path = PMG::CertHelpers::cert_path($type); | |
647 | ||
648 | my $node_config = PMG::NodeConfig::load_config(); | |
649 | my $acme_config = PMG::NodeConfig::get_acme_conf($node_config); | |
650 | raise("ACME domain list in configuration is missing!", 400) | |
651 | if !$acme_config || !$acme_config->{domains}->%*; | |
652 | ||
653 | $filter_domains->($acme_config, $type); | |
654 | ||
655 | my $rpcenv = PMG::RESTEnvironment->get(); | |
656 | my $authuser = $rpcenv->get_user(); | |
657 | ||
658 | my $cert = PVE::Tools::file_get_contents($cert_path); | |
659 | $cert = pem_certificate($cert) | |
660 | or die "no certificate section found in '$cert_path'\n"; | |
661 | ||
662 | my $realcmd = sub { | |
663 | STDOUT->autoflush(1); | |
664 | my $account = $acme_config->{account}; | |
665 | my $account_file = "${acme_account_dir}/${account}"; | |
666 | die "ACME account config file '$account' does not exist.\n" | |
667 | if ! -e $account_file; | |
668 | ||
669 | print "Loading ACME account details\n"; | |
670 | my $acme = PMG::RS::Acme->load($account_file); | |
671 | ||
672 | print "Revoking old certificate\n"; | |
673 | eval { $acme->revoke_certificate($cert, undef) }; | |
674 | if (my $err = $@) { | |
675 | # is there a better check? | |
676 | die "Revoke request to CA failed: $err" if $err !~ /"Certificate is expired"/; | |
677 | } | |
678 | ||
679 | my $code = sub { | |
680 | print "Deleting certificate files\n"; | |
681 | unlink $cert_path; | |
682 | PMG::Ticket::generate_api_cert(0) if $type eq 'api'; | |
683 | ||
684 | restart_after_cert_update($type); | |
685 | }; | |
686 | ||
687 | PMG::CertHelpers::cert_lock(10, $code); | |
688 | ||
689 | if ($type eq 'smtp') { | |
690 | set_smtp(0, 1); | |
691 | } | |
692 | }; | |
693 | ||
694 | return $rpcenv->fork_worker("acmerevoke", undef, $authuser, $realcmd); | |
695 | }}); | |
696 | ||
697 | 1; |