]>
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, $domains) = @_;
50 print "Placing ACME order\n";
51 my ($order_url, $order) = $acme->new_order($domains);
52 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 my $domain = $auth->{identifier
}->{value
};
58 if ($auth->{status
} eq 'valid') {
59 $domain = %{@{$order->{identifiers
}}[$index]}{value
};
60 print "$domain is already validated!\n";
62 print "The validation for $domain is pending!\n";
64 my ($plugin_type, $plugin_config) = &$get_plugin_type($domain, $acme_node_config);
66 my $plugin = PVE
::ACME
::Challenge-
>lookup($plugin_type);
68 my $challenge = $plugin->extract_challenge($auth->{challenges
});
69 my $key_auth = $acme->key_authorization($challenge->{token
});
71 key_authorization
=> $key_auth,
72 token
=> $challenge->{token
},
73 url
=> $challenge->{url
},
77 foreach my $key (keys %$plugin_config) {
78 $data->{plugin
}->{$key} = $plugin_config->{$key};
81 $plugin->setup($data);
83 print "Triggering validation\n";
85 $acme->request_challenge_validation($data->{url
}, $data->{key_authorization
});
86 print "Sleeping for 5 seconds\n";
89 $auth = $acme->get_authorization($auth_url);
90 if ($auth->{status
} eq 'pending') {
91 print "Status is still 'pending', trying again in 30 seconds\n";
94 } elsif ($auth->{status
} eq 'valid') {
95 print "Status is 'valid'!\n";
98 die "validating challenge '$auth_url' failed\n";
102 eval { $plugin->teardown($data) };
108 print "\nAll domains validated!\n";
109 print "\nCreating CSR\n";
110 my ($csr, $key) = PVE
::Certificate
::generate_csr
(identifiers
=> $order->{identifiers
});
112 my $finalize_error_cnt = 0;
113 print "Checking order status\n";
115 $order = $acme->get_order($order_url);
116 if ($order->{status
} eq 'pending') {
117 print "still pending, trying to finalize order\n";
119 # to be compatible with and without the order ready state
120 # we try to finalize even at the 'pending' state
121 # and give up after 5 unsuccessful tries
122 # this can be removed when the letsencrypt api
123 # definitely has implemented the 'ready' state
125 $acme->finalize_order($order, PVE
::Certificate
::pem_to_der
($csr));
128 die $err if $finalize_error_cnt >= 5;
130 $finalize_error_cnt++;
135 } elsif ($order->{status
} eq 'ready') {
136 print "Order is ready, finalizing order\n";
137 $acme->finalize_order($order, PVE
::Certificate
::pem_to_der
($csr));
140 } elsif ($order->{status
} eq 'processing') {
141 print "still processing, trying again in 30 seconds\n";
144 } elsif ($order->{status
} eq 'valid') {
148 die "order status: $order->{status}\n";
151 print "\nDownloading certificate\n";
152 my $cert = $acme->get_certificate($order);
154 return ($cert, $key);
157 __PACKAGE__-
>register_method ({
158 name
=> 'new_certificate',
159 path
=> 'certificate',
161 description
=> "Order a new certificate from ACME-compatible CA.",
165 additionalProperties
=> 0,
167 node
=> get_standard_option
('pve-node'),
170 description
=> 'Overwrite existing custom certificate.',
182 my $node = extract_param
($param, 'node');
183 my $cert_prefix = PVE
::CertHelpers
::cert_path_prefix
($node);
185 raise_param_exc
({'force' => "Custom certificate exists but 'force' is not set."})
186 if !$param->{force
} && -e
"${cert_prefix}.pem";
188 my $node_config = PVE
::NodeConfig
::load_config
($node);
189 raise
("ACME settings in node configuration are missing!", 400)
190 if !$node_config || !$node_config->{acme
};
191 my $acme_node_config = PVE
::NodeConfig
::parse_acme
($node_config->{acme
});
192 raise
("ACME domain list in node configuration is missing!", 400)
193 if !$acme_node_config;
195 my $rpcenv = PVE
::RPCEnvironment
::get
();
197 my $authuser = $rpcenv->get_user();
200 STDOUT-
>autoflush(1);
201 my $account = $acme_node_config->{account
} // 'default';
202 my $account_file = "${acme_account_dir}/${account}";
203 die "ACME account config file '$account' does not exist.\n"
204 if ! -e
$account_file;
206 my $acme = PVE
::ACME-
>new($account_file);
208 print "Loading ACME account details\n";
211 my ($cert, $key) = $order_certificate->($acme, $acme_node_config->{domains
});
214 print "Setting pveproxy certificate and key\n";
215 PVE
::CertHelpers
::set_cert_files
($cert, $key, $cert_prefix, $param->{force
});
217 print "Restarting pveproxy\n";
218 PVE
::Tools
::run_command
(['systemctl', 'reload-or-restart', 'pveproxy']);
220 PVE
::CertHelpers
::cert_lock
(10, $code);
224 return $rpcenv->fork_worker("acmenewcert", undef, $authuser, $realcmd);
227 __PACKAGE__-
>register_method ({
228 name
=> 'renew_certificate',
229 path
=> 'certificate',
231 description
=> "Renew existing certificate from CA.",
235 additionalProperties
=> 0,
237 node
=> get_standard_option
('pve-node'),
240 description
=> 'Force renewal even if expiry is more than 30 days away.',
252 my $node = extract_param
($param, 'node');
253 my $cert_prefix = PVE
::CertHelpers
::cert_path_prefix
($node);
255 raise
("No current (custom) certificate found, please order a new certificate!\n")
256 if ! -e
"${cert_prefix}.pem";
258 my $expires_soon = PVE
::Certificate
::check_expiry
("${cert_prefix}.pem", time() + 30*24*60*60);
259 raise_param_exc
({'force' => "Certificate does not expire within the next 30 days, and 'force' is not set."})
260 if !$expires_soon && !$param->{force
};
262 my $node_config = PVE
::NodeConfig
::load_config
($node);
263 raise
("ACME settings in node configuration are missing!", 400)
264 if !$node_config || !$node_config->{acme
};
265 my $acme_node_config = PVE
::NodeConfig
::parse_acme
($node_config->{acme
});
266 raise
("ACME domain list in node configuration is missing!", 400)
267 if !$acme_node_config;
269 my $rpcenv = PVE
::RPCEnvironment
::get
();
271 my $authuser = $rpcenv->get_user();
273 my $old_cert = PVE
::Tools
::file_get_contents
("${cert_prefix}.pem");
276 STDOUT-
>autoflush(1);
277 my $account = $acme_node_config->{account
} // 'default';
278 my $account_file = "${acme_account_dir}/${account}";
279 die "ACME account config file '$account' does not exist.\n"
280 if ! -e
$account_file;
282 my $acme = PVE
::ACME-
>new($account_file);
284 print "Loading ACME account details\n";
287 my ($cert, $key) = $order_certificate->($acme, $acme_node_config->{domains
});
290 print "Setting pveproxy certificate and key\n";
291 PVE
::CertHelpers
::set_cert_files
($cert, $key, $cert_prefix, 1);
293 print "Restarting pveproxy\n";
294 PVE
::Tools
::run_command
(['systemctl', 'reload-or-restart', 'pveproxy']);
296 PVE
::CertHelpers
::cert_lock
(10, $code);
299 print "Revoking old certificate\n";
300 $acme->revoke_certificate($old_cert);
303 return $rpcenv->fork_worker("acmerenew", undef, $authuser, $realcmd);
306 __PACKAGE__-
>register_method ({
307 name
=> 'revoke_certificate',
308 path
=> 'certificate',
310 description
=> "Revoke existing certificate from CA.",
314 additionalProperties
=> 0,
316 node
=> get_standard_option
('pve-node'),
325 my $node = extract_param
($param, 'node');
326 my $cert_prefix = PVE
::CertHelpers
::cert_path_prefix
($node);
328 my $node_config = PVE
::NodeConfig
::load_config
($node);
329 raise
("ACME settings in node configuration are missing!", 400)
330 if !$node_config || !$node_config->{acme
};
331 my $acme_node_config = PVE
::NodeConfig
::parse_acme
($node_config->{acme
});
332 raise
("ACME domain list in node configuration is missing!", 400)
333 if !$acme_node_config;
335 my $rpcenv = PVE
::RPCEnvironment
::get
();
337 my $authuser = $rpcenv->get_user();
339 my $cert = PVE
::Tools
::file_get_contents
("${cert_prefix}.pem");
342 STDOUT-
>autoflush(1);
343 my $account = $acme_node_config->{account
} // 'default';
344 my $account_file = "${acme_account_dir}/${account}";
345 die "ACME account config file '$account' does not exist.\n"
346 if ! -e
$account_file;
348 my $acme = PVE
::ACME-
>new($account_file);
350 print "Loading ACME account details\n";
353 print "Revoking old certificate\n";
354 $acme->revoke_certificate($cert);
357 print "Deleting certificate files\n";
358 unlink "${cert_prefix}.pem";
359 unlink "${cert_prefix}.key";
361 print "Restarting pveproxy to revert to self-signed certificates\n";
362 PVE
::Tools
::run_command
(['systemctl', 'reload-or-restart', 'pveproxy']);
365 PVE
::CertHelpers
::cert_lock
(10, $code);
369 return $rpcenv->fork_worker("acmerevoke", undef, $authuser, $realcmd);