]>
git.proxmox.com Git - pve-manager.git/blob - PVE/API2/ACME.pm
1 package PVE
::API2
::ACME
;
7 use PVE
::ACME
::StandAlone
;
10 use PVE
::Exception
qw(raise raise_param_exc);
11 use PVE
::JSONSchema
qw(get_standard_option);
13 use PVE
::Tools
qw(extract_param);
17 use base
qw(PVE::RESTHandler);
19 my $acme_account_dir = PVE
::CertHelpers
::acme_account_dir
();
21 __PACKAGE__-
>register_method ({
25 permissions
=> { user
=> 'all' },
26 description
=> "ACME index.",
28 additionalProperties
=> 0,
30 node
=> get_standard_option
('pve-node'),
39 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
45 { name
=> 'certificate' },
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";
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 $@;
65 print "Triggering validation\n";
67 $acme->request_challenge_validation($validation->{url
}, $validation->{key_auth
});
68 print "Sleeping for 5 seconds\n";
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";
76 } elsif ($auth->{status
} eq 'valid') {
77 print "Status is 'valid'!\n";
80 die "validating challenge '$auth_url' failed\n";
84 eval { $validation->teardown() };
89 print "\nAll domains validated!\n";
90 print "\nCreating CSR\n";
91 my ($csr, $key) = PVE
::Certificate
::generate_csr
(identifiers
=> $order->{identifiers
});
93 my $finalize_error_cnt = 0;
94 print "Checking order status\n";
96 $order = $acme->get_order($order_url);
97 if ($order->{status
} eq 'pending') {
98 print "still pending, trying to finalize order\n";
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
106 $acme->finalize_order($order, PVE
::Certificate
::pem_to_der
($csr));
109 die $err if $finalize_error_cnt >= 5;
111 $finalize_error_cnt++;
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));
121 } elsif ($order->{status
} eq 'processing') {
122 print "still processing, trying again in 30 seconds\n";
125 } elsif ($order->{status
} eq 'valid') {
129 die "order status: $order->{status}\n";
132 print "\nDownloading certificate\n";
133 my $cert = $acme->get_certificate($order);
135 return ($cert, $key);
138 __PACKAGE__-
>register_method ({
139 name
=> 'new_certificate',
140 path
=> 'certificate',
142 description
=> "Order a new certificate from ACME-compatible CA.",
146 additionalProperties
=> 0,
148 node
=> get_standard_option
('pve-node'),
151 description
=> 'Overwrite existing custom certificate.',
163 my $node = extract_param
($param, 'node');
164 my $cert_prefix = PVE
::CertHelpers
::cert_path_prefix
($node);
166 raise_param_exc
({'force' => "Custom certificate exists but 'force' is not set."})
167 if !$param->{force
} && -e
"${cert_prefix}.pem";
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;
176 my $rpcenv = PVE
::RPCEnvironment
::get
();
178 my $authuser = $rpcenv->get_user();
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;
187 my $acme = PVE
::ACME-
>new($account_file);
189 print "Loading ACME account details\n";
192 my ($cert, $key) = $order_certificate->($acme, $acme_node_config->{domains
});
195 print "Setting pveproxy certificate and key\n";
196 PVE
::CertHelpers
::set_cert_files
($cert, $key, $cert_prefix, $param->{force
});
198 print "Restarting pveproxy\n";
199 PVE
::Tools
::run_command
(['systemctl', 'reload-or-restart', 'pveproxy']);
201 PVE
::CertHelpers
::cert_lock
(10, $code);
205 return $rpcenv->fork_worker("acmenewcert", undef, $authuser, $realcmd);
208 __PACKAGE__-
>register_method ({
209 name
=> 'renew_certificate',
210 path
=> 'certificate',
212 description
=> "Renew existing certificate from CA.",
216 additionalProperties
=> 0,
218 node
=> get_standard_option
('pve-node'),
221 description
=> 'Force renewal even if expiry is more than 30 days away.',
233 my $node = extract_param
($param, 'node');
234 my $cert_prefix = PVE
::CertHelpers
::cert_path_prefix
($node);
236 raise
("No current (custom) certificate found, please order a new certificate!\n")
237 if ! -e
"${cert_prefix}.pem";
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
};
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;
250 my $rpcenv = PVE
::RPCEnvironment
::get
();
252 my $authuser = $rpcenv->get_user();
254 my $old_cert = PVE
::Tools
::file_get_contents
("${cert_prefix}.pem");
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;
263 my $acme = PVE
::ACME-
>new($account_file);
265 print "Loading ACME account details\n";
268 my ($cert, $key) = $order_certificate->($acme, $acme_node_config->{domains
});
271 print "Setting pveproxy certificate and key\n";
272 PVE
::CertHelpers
::set_cert_files
($cert, $key, $cert_prefix, 1);
274 print "Restarting pveproxy\n";
275 PVE
::Tools
::run_command
(['systemctl', 'reload-or-restart', 'pveproxy']);
277 PVE
::CertHelpers
::cert_lock
(10, $code);
280 print "Revoking old certificate\n";
281 $acme->revoke_certificate($old_cert);
284 return $rpcenv->fork_worker("acmerenew", undef, $authuser, $realcmd);
287 __PACKAGE__-
>register_method ({
288 name
=> 'revoke_certificate',
289 path
=> 'certificate',
291 description
=> "Revoke existing certificate from CA.",
295 additionalProperties
=> 0,
297 node
=> get_standard_option
('pve-node'),
306 my $node = extract_param
($param, 'node');
307 my $cert_prefix = PVE
::CertHelpers
::cert_path_prefix
($node);
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;
316 my $rpcenv = PVE
::RPCEnvironment
::get
();
318 my $authuser = $rpcenv->get_user();
320 my $cert = PVE
::Tools
::file_get_contents
("${cert_prefix}.pem");
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;
329 my $acme = PVE
::ACME-
>new($account_file);
331 print "Loading ACME account details\n";
334 print "Revoking old certificate\n";
335 $acme->revoke_certificate($cert);
338 print "Deleting certificate files\n";
339 unlink "${cert_prefix}.pem";
340 unlink "${cert_prefix}.key";
342 print "Restarting pveproxy to revert to self-signed certificates\n";
343 PVE
::Tools
::run_command
(['systemctl', 'reload-or-restart', 'pveproxy']);
346 PVE
::CertHelpers
::cert_lock
(10, $code);
350 return $rpcenv->fork_worker("acmerevoke", undef, $authuser, $realcmd);