]>
git.proxmox.com Git - pve-manager.git/blob - PVE/API2/ACME.pm
1 package PVE
::API2
::ACME
;
9 use PVE
::Exception
qw(raise raise_param_exc);
10 use PVE
::JSONSchema
qw(get_standard_option);
12 use PVE
::Tools
qw(extract_param);
16 use base
qw(PVE::RESTHandler);
18 my $acme_account_dir = PVE
::CertHelpers
::acme_account_dir
();
20 __PACKAGE__-
>register_method ({
24 permissions
=> { user
=> 'all' },
25 description
=> "ACME index.",
27 additionalProperties
=> 0,
29 node
=> get_standard_option
('pve-node'),
38 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
44 { name
=> 'certificate' },
48 my $order_certificate = sub {
49 my ($acme, $acme_node_config) = @_;
51 my $plugins = PVE
::API2
::ACMEPlugin
::load_config
();
53 print "Placing ACME order\n";
54 my ($order_url, $order) = $acme->new_order([ keys %{$acme_node_config->{domains
}} ]);
55 print "Order URL: $order_url\n";
56 for my $auth_url (@{$order->{authorizations
}}) {
57 print "\nGetting authorization details from '$auth_url'\n";
58 my $auth = $acme->get_authorization($auth_url);
60 # force lower case, like get_acme_conf does
61 my $domain = lc($auth->{identifier
}->{value
});
62 if ($auth->{status
} eq 'valid') {
63 print "$domain is already validated!\n";
65 print "The validation for $domain is pending!\n";
67 my $domain_config = $acme_node_config->{domains
}->{$domain};
68 die "no config for domain '$domain'\n" if !$domain_config;
70 my $plugin_id = $domain_config->{plugin
};
72 my $plugin_cfg = $plugins->{ids
}->{$plugin_id};
73 die "plugin '$plugin_id' for domain '$domain' not found!\n"
77 plugin
=> $plugin_cfg,
78 alias
=> $domain_config->{alias
},
81 my $plugin = PVE
::ACME
::Challenge-
>lookup($plugin_cfg->{type
});
82 $plugin->setup($acme, $auth, $data);
84 print "Triggering validation\n";
86 die "no validation URL returned by plugin '$plugin_id' for domain '$domain'\n"
87 if !defined($data->{url
});
89 $acme->request_challenge_validation($data->{url
});
90 print "Sleeping for 5 seconds\n";
93 $auth = $acme->get_authorization($auth_url);
94 if ($auth->{status
} eq 'pending') {
95 print "Status is still 'pending', trying again in 10 seconds\n";
98 } elsif ($auth->{status
} eq 'valid') {
99 print "Status is 'valid', domain '$domain' OK!\n";
102 die "validating challenge '$auth_url' failed - status: $auth->{status}\n";
106 eval { $plugin->teardown($acme, $auth, $data) };
111 print "\nAll domains validated!\n";
112 print "\nCreating CSR\n";
113 my ($csr, $key) = PVE
::Certificate
::generate_csr
(identifiers
=> $order->{identifiers
});
115 my $finalize_error_cnt = 0;
116 print "Checking order status\n";
118 $order = $acme->get_order($order_url);
119 if ($order->{status
} eq 'pending') {
120 print "still pending, trying to finalize order\n";
122 # to be compatible with and without the order ready state we try to
123 # finalize even at the 'pending' state and give up after 5
124 # unsuccessful tries this can be removed when the letsencrypt api
125 # definitely has implemented the 'ready' state
127 $acme->finalize_order($order, PVE
::Certificate
::pem_to_der
($csr));
130 die $err if $finalize_error_cnt >= 5;
132 $finalize_error_cnt++;
137 } elsif ($order->{status
} eq 'ready') {
138 print "Order is ready, finalizing order\n";
139 $acme->finalize_order($order, PVE
::Certificate
::pem_to_der
($csr));
142 } elsif ($order->{status
} eq 'processing') {
143 print "still processing, trying again in 30 seconds\n";
146 } elsif ($order->{status
} eq 'valid') {
150 die "order status: $order->{status}\n";
153 print "\nDownloading certificate\n";
154 my $cert = $acme->get_certificate($order);
156 return ($cert, $key);
159 __PACKAGE__-
>register_method ({
160 name
=> 'new_certificate',
161 path
=> 'certificate',
164 check
=> ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
166 description
=> "Order a new certificate from ACME-compatible CA.",
170 additionalProperties
=> 0,
172 node
=> get_standard_option
('pve-node'),
175 description
=> 'Overwrite existing custom certificate.',
187 my $node = extract_param
($param, 'node');
188 my $cert_prefix = PVE
::CertHelpers
::cert_path_prefix
($node);
190 raise_param_exc
({'force' => "Custom certificate exists but 'force' is not set."})
191 if !$param->{force
} && -e
"${cert_prefix}.pem";
193 my $node_config = PVE
::NodeConfig
::load_config
($node);
194 my $acme_node_config = PVE
::NodeConfig
::get_acme_conf
($node_config);
195 raise
("ACME domain list in node configuration is missing!", 400)
196 if !$acme_node_config || !%{$acme_node_config->{domains
}};
198 my $rpcenv = PVE
::RPCEnvironment
::get
();
200 my $authuser = $rpcenv->get_user();
203 STDOUT-
>autoflush(1);
204 my $account = $acme_node_config->{account
};
205 my $account_file = "${acme_account_dir}/${account}";
206 die "ACME account config file '$account' does not exist.\n"
207 if ! -e
$account_file;
209 my $acme = PVE
::ACME-
>new($account_file);
211 print "Loading ACME account details\n";
214 my ($cert, $key) = $order_certificate->($acme, $acme_node_config);
217 print "Setting pveproxy certificate and key\n";
218 PVE
::CertHelpers
::set_cert_files
($cert, $key, $cert_prefix, $param->{force
});
220 print "Restarting pveproxy\n";
221 PVE
::Tools
::run_command
(['systemctl', 'reload-or-restart', 'pveproxy']);
223 PVE
::CertHelpers
::cert_lock
(10, $code);
227 return $rpcenv->fork_worker("acmenewcert", undef, $authuser, $realcmd);
230 __PACKAGE__-
>register_method ({
231 name
=> 'renew_certificate',
232 path
=> 'certificate',
235 check
=> ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
237 description
=> "Renew existing certificate from CA.",
241 additionalProperties
=> 0,
243 node
=> get_standard_option
('pve-node'),
246 description
=> 'Force renewal even if expiry is more than 30 days away.',
258 my $node = extract_param
($param, 'node');
259 my $cert_prefix = PVE
::CertHelpers
::cert_path_prefix
($node);
261 raise
("No current (custom) certificate found, please order a new certificate!\n")
262 if ! -e
"${cert_prefix}.pem";
264 my $expires_soon = PVE
::Certificate
::check_expiry
("${cert_prefix}.pem", time() + 30*24*60*60);
265 raise_param_exc
({'force' => "Certificate does not expire within the next 30 days, and 'force' is not set."})
266 if !$expires_soon && !$param->{force
};
268 my $node_config = PVE
::NodeConfig
::load_config
($node);
269 my $acme_node_config = PVE
::NodeConfig
::get_acme_conf
($node_config);
270 raise
("ACME domain list in node configuration is missing!", 400)
271 if !$acme_node_config || !%{$acme_node_config->{domains
}};
273 my $rpcenv = PVE
::RPCEnvironment
::get
();
275 my $authuser = $rpcenv->get_user();
277 my $old_cert = PVE
::Tools
::file_get_contents
("${cert_prefix}.pem");
280 STDOUT-
>autoflush(1);
281 my $account = $acme_node_config->{account
};
282 my $account_file = "${acme_account_dir}/${account}";
283 die "ACME account config file '$account' does not exist.\n"
284 if ! -e
$account_file;
286 my $acme = PVE
::ACME-
>new($account_file);
288 print "Loading ACME account details\n";
291 my ($cert, $key) = $order_certificate->($acme, $acme_node_config);
294 print "Setting pveproxy certificate and key\n";
295 PVE
::CertHelpers
::set_cert_files
($cert, $key, $cert_prefix, 1);
297 print "Restarting pveproxy\n";
298 PVE
::Tools
::run_command
(['systemctl', 'reload-or-restart', 'pveproxy']);
300 PVE
::CertHelpers
::cert_lock
(10, $code);
303 print "Revoking old certificate\n";
304 eval { $acme->revoke_certificate($old_cert) };
305 warn "Revoke request to CA failed: $@" if $@;
308 return $rpcenv->fork_worker("acmerenew", undef, $authuser, $realcmd);
311 __PACKAGE__-
>register_method ({
312 name
=> 'revoke_certificate',
313 path
=> 'certificate',
316 check
=> ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
318 description
=> "Revoke existing certificate from CA.",
322 additionalProperties
=> 0,
324 node
=> get_standard_option
('pve-node'),
333 my $node = extract_param
($param, 'node');
334 my $cert_prefix = PVE
::CertHelpers
::cert_path_prefix
($node);
336 my $node_config = PVE
::NodeConfig
::load_config
($node);
337 my $acme_node_config = PVE
::NodeConfig
::get_acme_conf
($node_config);
338 raise
("ACME domain list in node configuration is missing!", 400)
339 if !$acme_node_config || !%{$acme_node_config->{domains
}};
341 my $rpcenv = PVE
::RPCEnvironment
::get
();
343 my $authuser = $rpcenv->get_user();
345 my $cert = PVE
::Tools
::file_get_contents
("${cert_prefix}.pem");
348 STDOUT-
>autoflush(1);
349 my $account = $acme_node_config->{account
};
350 my $account_file = "${acme_account_dir}/${account}";
351 die "ACME account config file '$account' does not exist.\n"
352 if ! -e
$account_file;
354 my $acme = PVE
::ACME-
>new($account_file);
356 print "Loading ACME account details\n";
359 print "Revoking old certificate\n";
360 eval { $acme->revoke_certificate($cert) };
362 # is there a better check?
363 die "Revoke request to CA failed: $err" if $err !~ /"Certificate is expired"/;
367 print "Deleting certificate files\n";
368 unlink "${cert_prefix}.pem";
369 unlink "${cert_prefix}.key";
371 print "Restarting pveproxy to revert to self-signed certificates\n";
372 PVE
::Tools
::run_command
(['systemctl', 'reload-or-restart', 'pveproxy']);
375 PVE
::CertHelpers
::cert_lock
(10, $code);
379 return $rpcenv->fork_worker("acmerevoke", undef, $authuser, $realcmd);