]>
Commit | Line | Data |
---|---|---|
9945a501 FG |
1 | package PVE::API2::ACME; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
6 | use PVE::ACME; | |
7 | use PVE::ACME::StandAlone; | |
8 | use PVE::CertHelpers; | |
9 | use PVE::Certificate; | |
10 | use PVE::Exception qw(raise raise_param_exc); | |
11 | use PVE::JSONSchema qw(get_standard_option); | |
12 | use PVE::NodeConfig; | |
13 | use PVE::Tools qw(extract_param); | |
14 | ||
15 | use IO::Handle; | |
16 | ||
17 | use base qw(PVE::RESTHandler); | |
18 | ||
19 | my $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 | ||
49 | my $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 | ||
353 | 1; |