]> git.proxmox.com Git - pve-manager.git/blame - PVE/API2/ACME.pm
bump version to 5.4-15
[pve-manager.git] / PVE / API2 / ACME.pm
CommitLineData
9945a501
FG
1package PVE::API2::ACME;
2
3use strict;
4use warnings;
5
6use PVE::ACME;
7use PVE::ACME::StandAlone;
8use PVE::CertHelpers;
9use PVE::Certificate;
10use PVE::Exception qw(raise raise_param_exc);
11use PVE::JSONSchema qw(get_standard_option);
12use PVE::NodeConfig;
13use PVE::Tools qw(extract_param);
14
15use IO::Handle;
16
17use base qw(PVE::RESTHandler);
18
19my $acme_account_dir = PVE::CertHelpers::acme_account_dir();
20
21__PACKAGE__->register_method ({
22 name => 'index',
23 path => '',
24 method => 'GET',
25 permissions => { user => 'all' },
26 description => "ACME index.",
27 parameters => {
28 additionalProperties => 0,
29 properties => {
30 node => get_standard_option('pve-node'),
31 },
32 },
33 returns => {
34 type => 'array',
35 items => {
36 type => "object",
37 properties => {},
38 },
39 links => [ { rel => 'child', href => "{name}" } ],
40 },
41 code => sub {
42 my ($param) = @_;
43
44 return [
45 { name => 'certificate' },
46 ];
47 }});
48
49my $order_certificate = sub {
50 my ($acme, $domains) = @_;
51 print "Placing ACME order\n";
52 my ($order_url, $order) = $acme->new_order($domains);
53 print "Order URL: $order_url\n";
54 for my $auth_url (@{$order->{authorizations}}) {
55 print "\nGetting authorization details from '$auth_url'\n";
56 my $auth = $acme->get_authorization($auth_url);
57 if ($auth->{status} eq 'valid') {
58 print "... already validated!\n";
59 } else {
60 print "... pending!\n";
61 print "Setting up webserver\n";
62 my $validation = eval { PVE::ACME::StandAlone->setup($acme, $auth) };
63 die "failed setting up webserver - $@\n" if $@;
64
65 print "Triggering validation\n";
66 eval {
67 $acme->request_challenge_validation($validation->{url}, $validation->{key_auth});
68 print "Sleeping for 5 seconds\n";
69 sleep 5;
70 while (1) {
71 $auth = $acme->get_authorization($auth_url);
72 if ($auth->{status} eq 'pending') {
73 print "Status is still 'pending', trying again in 30 seconds\n";
74 sleep 30;
75 next;
76 } elsif ($auth->{status} eq 'valid') {
77 print "Status is 'valid'!\n";
78 last;
79 }
80 die "validating challenge '$auth_url' failed\n";
81 }
82 };
83 my $err = $@;
84 eval { $validation->teardown() };
85 warn "$@\n" if $@;
86 die $err if $err;
87 }
88 }
89 print "\nAll domains validated!\n";
90 print "\nCreating CSR\n";
91 my ($csr, $key) = PVE::Certificate::generate_csr(identifiers => $order->{identifiers});
92
c93bc1a1 93 my $finalize_error_cnt = 0;
9945a501
FG
94 print "Checking order status\n";
95 while (1) {
96 $order = $acme->get_order($order_url);
97 if ($order->{status} eq 'pending') {
c93bc1a1
DC
98 print "still pending, trying to finalize order\n";
99 # FIXME
100 # to be compatible with and without the order ready state
101 # we try to finalize even at the 'pending' state
102 # and give up after 5 unsuccessful tries
103 # this can be removed when the letsencrypt api
104 # definitely has implemented the 'ready' state
105 eval {
106 $acme->finalize_order($order, PVE::Certificate::pem_to_der($csr));
107 };
108 if (my $err = $@) {
109 die $err if $finalize_error_cnt >= 5;
110
111 $finalize_error_cnt++;
112 warn $err;
113 }
114 sleep 5;
115 next;
116 } elsif ($order->{status} eq 'ready') {
117 print "Order is ready, finalizing order\n";
118 $acme->finalize_order($order, PVE::Certificate::pem_to_der($csr));
119 sleep 5;
120 next;
121 } elsif ($order->{status} eq 'processing') {
122 print "still processing, trying again in 30 seconds\n";
9945a501
FG
123 sleep 30;
124 next;
125 } elsif ($order->{status} eq 'valid') {
126 print "valid!\n";
127 last;
128 }
129 die "order status: $order->{status}\n";
130 }
131
132 print "\nDownloading certificate\n";
133 my $cert = $acme->get_certificate($order);
134
135 return ($cert, $key);
136};
137
138__PACKAGE__->register_method ({
139 name => 'new_certificate',
140 path => 'certificate',
141 method => 'POST',
142 description => "Order a new certificate from ACME-compatible CA.",
143 protected => 1,
144 proxyto => 'node',
145 parameters => {
146 additionalProperties => 0,
147 properties => {
148 node => get_standard_option('pve-node'),
149 force => {
150 type => 'boolean',
151 description => 'Overwrite existing custom certificate.',
152 optional => 1,
153 default => 0,
154 },
155 },
156 },
157 returns => {
158 type => 'string',
159 },
160 code => sub {
161 my ($param) = @_;
162
163 my $node = extract_param($param, 'node');
164 my $cert_prefix = PVE::CertHelpers::cert_path_prefix($node);
165
166 raise_param_exc({'force' => "Custom certificate exists but 'force' is not set."})
167 if !$param->{force} && -e "${cert_prefix}.pem";
168
169 my $node_config = PVE::NodeConfig::load_config($node);
170 raise("ACME settings in node configuration are missing!", 400)
171 if !$node_config || !$node_config->{acme};
172 my $acme_node_config = PVE::NodeConfig::parse_acme($node_config->{acme});
173 raise("ACME domain list in node configuration is missing!", 400)
174 if !$acme_node_config;
175
176 my $rpcenv = PVE::RPCEnvironment::get();
177
178 my $authuser = $rpcenv->get_user();
179
180 my $realcmd = sub {
181 STDOUT->autoflush(1);
182 my $account = $acme_node_config->{account} // 'default';
183 my $account_file = "${acme_account_dir}/${account}";
184 die "ACME account config file '$account' does not exist.\n"
185 if ! -e $account_file;
186
187 my $acme = PVE::ACME->new($account_file);
188
189 print "Loading ACME account details\n";
190 $acme->load();
191
192 my ($cert, $key) = $order_certificate->($acme, $acme_node_config->{domains});
193
194 my $code = sub {
195 print "Setting pveproxy certificate and key\n";
196 PVE::CertHelpers::set_cert_files($cert, $key, $cert_prefix, $param->{force});
197
198 print "Restarting pveproxy\n";
199 PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pveproxy']);
200 };
201 PVE::CertHelpers::cert_lock(10, $code);
202 die "$@\n" if $@;
203 };
204
205 return $rpcenv->fork_worker("acmenewcert", undef, $authuser, $realcmd);
206 }});
207
208__PACKAGE__->register_method ({
209 name => 'renew_certificate',
210 path => 'certificate',
211 method => 'PUT',
212 description => "Renew existing certificate from CA.",
213 protected => 1,
214 proxyto => 'node',
215 parameters => {
216 additionalProperties => 0,
217 properties => {
218 node => get_standard_option('pve-node'),
219 force => {
220 type => 'boolean',
221 description => 'Force renewal even if expiry is more than 30 days away.',
222 optional => 1,
223 default => 0,
224 },
225 },
226 },
227 returns => {
228 type => 'string',
229 },
230 code => sub {
231 my ($param) = @_;
232
233 my $node = extract_param($param, 'node');
234 my $cert_prefix = PVE::CertHelpers::cert_path_prefix($node);
235
236 raise("No current (custom) certificate found, please order a new certificate!\n")
237 if ! -e "${cert_prefix}.pem";
238
239 my $expires_soon = PVE::Certificate::check_expiry("${cert_prefix}.pem", time() + 30*24*60*60);
240 raise_param_exc({'force' => "Certificate does not expire within the next 30 days, and 'force' is not set."})
241 if !$expires_soon && !$param->{force};
242
243 my $node_config = PVE::NodeConfig::load_config($node);
244 raise("ACME settings in node configuration are missing!", 400)
245 if !$node_config || !$node_config->{acme};
246 my $acme_node_config = PVE::NodeConfig::parse_acme($node_config->{acme});
247 raise("ACME domain list in node configuration is missing!", 400)
248 if !$acme_node_config;
249
250 my $rpcenv = PVE::RPCEnvironment::get();
251
252 my $authuser = $rpcenv->get_user();
253
254 my $old_cert = PVE::Tools::file_get_contents("${cert_prefix}.pem");
255
256 my $realcmd = sub {
257 STDOUT->autoflush(1);
258 my $account = $acme_node_config->{account} // 'default';
259 my $account_file = "${acme_account_dir}/${account}";
260 die "ACME account config file '$account' does not exist.\n"
261 if ! -e $account_file;
262
263 my $acme = PVE::ACME->new($account_file);
264
265 print "Loading ACME account details\n";
266 $acme->load();
267
268 my ($cert, $key) = $order_certificate->($acme, $acme_node_config->{domains});
269
270 my $code = sub {
271 print "Setting pveproxy certificate and key\n";
272 PVE::CertHelpers::set_cert_files($cert, $key, $cert_prefix, 1);
273
274 print "Restarting pveproxy\n";
275 PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pveproxy']);
276 };
277 PVE::CertHelpers::cert_lock(10, $code);
278 die "$@\n" if $@;
279
280 print "Revoking old certificate\n";
281 $acme->revoke_certificate($old_cert);
282 };
283
284 return $rpcenv->fork_worker("acmerenew", undef, $authuser, $realcmd);
285 }});
286
287__PACKAGE__->register_method ({
288 name => 'revoke_certificate',
289 path => 'certificate',
290 method => 'DELETE',
291 description => "Revoke existing certificate from CA.",
292 protected => 1,
293 proxyto => 'node',
294 parameters => {
295 additionalProperties => 0,
296 properties => {
297 node => get_standard_option('pve-node'),
298 },
299 },
300 returns => {
301 type => 'string',
302 },
303 code => sub {
304 my ($param) = @_;
305
306 my $node = extract_param($param, 'node');
307 my $cert_prefix = PVE::CertHelpers::cert_path_prefix($node);
308
309 my $node_config = PVE::NodeConfig::load_config($node);
310 raise("ACME settings in node configuration are missing!", 400)
311 if !$node_config || !$node_config->{acme};
312 my $acme_node_config = PVE::NodeConfig::parse_acme($node_config->{acme});
313 raise("ACME domain list in node configuration is missing!", 400)
314 if !$acme_node_config;
315
316 my $rpcenv = PVE::RPCEnvironment::get();
317
318 my $authuser = $rpcenv->get_user();
319
320 my $cert = PVE::Tools::file_get_contents("${cert_prefix}.pem");
321
322 my $realcmd = sub {
323 STDOUT->autoflush(1);
324 my $account = $acme_node_config->{account} // 'default';
325 my $account_file = "${acme_account_dir}/${account}";
326 die "ACME account config file '$account' does not exist.\n"
327 if ! -e $account_file;
328
329 my $acme = PVE::ACME->new($account_file);
330
331 print "Loading ACME account details\n";
332 $acme->load();
333
334 print "Revoking old certificate\n";
335 $acme->revoke_certificate($cert);
336
337 my $code = sub {
338 print "Deleting certificate files\n";
339 unlink "${cert_prefix}.pem";
340 unlink "${cert_prefix}.key";
341
342 print "Restarting pveproxy to revert to self-signed certificates\n";
343 PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pveproxy']);
344 };
345
346 PVE::CertHelpers::cert_lock(10, $code);
347 die "$@\n" if $@;
348 };
349
350 return $rpcenv->fork_worker("acmerevoke", undef, $authuser, $realcmd);
351 }});
352
3531;