for creating/ordering a new certificate and renewing respectively revoking an existing one.
Signed-off-by: Fabian Grünbichler <f.gruenbich...@proxmox.com> --- PVE/API2/Makefile | 1 + PVE/API2/ACME.pm | 319 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 320 insertions(+) create mode 100644 PVE/API2/ACME.pm diff --git a/PVE/API2/Makefile b/PVE/API2/Makefile index d72ddd9b..44b9cf7c 100644 --- a/PVE/API2/Makefile +++ b/PVE/API2/Makefile @@ -14,6 +14,7 @@ PERLSOURCE = \ Pool.pm \ Tasks.pm \ Network.pm \ + ACME.pm \ ACMEAccount.pm \ NodeConfig.pm \ Services.pm diff --git a/PVE/API2/ACME.pm b/PVE/API2/ACME.pm new file mode 100644 index 00000000..23db9c94 --- /dev/null +++ b/PVE/API2/ACME.pm @@ -0,0 +1,319 @@ +package PVE::API2::ACME; + +use strict; +use warnings; + +use PVE::ACME; +use PVE::ACME::StandAlone; +use PVE::CertHelpers; +use PVE::Certificate; +use PVE::Exception qw(raise raise_param_exc); +use PVE::JSONSchema qw(get_standard_option); +use PVE::NodeConfig; +use PVE::Tools qw(extract_param); + +use IO::Handle; + +use base qw(PVE::RESTHandler); + +__PACKAGE__->register_method ({ + name => 'index', + path => '', + method => 'GET', + permissions => { user => 'all' }, + description => "ACME index.", + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => {}, + }, + links => [ { rel => 'child', href => "{name}" } ], + }, + code => sub { + my ($param) = @_; + + return [ + { name => 'certificate' }, + ]; + }, +}); + +my $order_certificate = sub { + my ($acme, $domains) = @_; + print "Placing ACME order\n"; + my ($order_url, $order) = $acme->new_order($domains); + print "Order URL: $order_url\n"; + for my $auth_url (@{$order->{authorizations}}) { + print "\nGetting authorization details from '$auth_url'\n"; + my $auth = $acme->get_authorization($auth_url); + if ($auth->{status} eq 'valid') { + print "... already validated!\n"; + } else { + print "... pending!\n"; + print "Setting up webserver\n"; + my $validation = eval { PVE::ACME::StandAlone->setup($acme, $auth) }; + die "failed setting up webserver - $@\n" if $@; + + print "Triggering validation\n"; + eval { + $acme->request_challenge_validation($validation->{url}, $validation->{key_auth}); + while (1) { + $auth = $acme->get_authorization($auth_url); + if ($auth->{status} eq 'pending') { + print "still pending, trying again in 30 seconds\n"; + sleep 30; + next; + } elsif ($auth->{status} eq 'valid') { + last; + } + die "validating challenge '$auth' failed\n"; + } + }; + my $err = $@; + eval { $validation->teardown() }; + warn "$@\n" if $@; + die $err if $err; + } + } + print "\nAll domains validated!\n"; + print "\nCreating CSR\n"; + # TODO use proper tmp path + my ($csr, $key) = $acme->new_csr($order, path => '/tmp/'); + my $csr_content = PVE::Tools::file_get_contents($csr); + my $key_content = PVE::Tools::file_get_contents($key); + unlink $csr; + unlink $key; + + print "Finalizing order\n"; + $acme->finalize_order($order, $csr_content); + + print "Checking order status\n"; + while (1) { + $order = $acme->get_order($order_url); + if ($order->{status} eq 'pending') { + print "still pending, trying again in 30 seconds\n"; + sleep 30; + next; + } elsif ($order->{status} eq 'valid') { + print "valid!\n"; + last; + } + die "order status: $order->{status}\n"; + } + + print "\nDownloading certificate\n"; + my $cert = $acme->get_certificate($order); + + return ($cert, $key_content); +}; + +__PACKAGE__->register_method ({ + name => 'new_certificate', + path => 'certificate', + method => 'POST', + description => "Order a new certificate from ACME-compatible CA.", + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + force => { + type => 'boolean', + description => 'Overwrite existing custom certificate.', + optional => 1, + default => 0, + }, + }, + }, + returns => { + type => 'string', + }, + code => sub { + my ($param) = @_; + + my $node = extract_param($param, 'node'); + my $cert_prefix = PVE::CertHelpers::cert_path_prefix($node); + + raise_param_exc({'force' => "Custom certificate exists but 'force' is not set."}) + if !$param->{force} && -e "${cert_prefix}.pem"; + + my $node_config = PVE::NodeConfig::load_config($node); + raise("ACME settings in node configuration are missing!", 400) + if !$node_config || !$node_config->{acme}; + my $acme_node_config = PVE::NodeConfig::parse_acme($node_config->{acme}); + raise("ACME domain list in node configuration is missing!", 400) + if !$acme_node_config; + + my $rpcenv = PVE::RPCEnvironment::get(); + + my $authuser = $rpcenv->get_user(); + + my $realcmd = sub { + STDOUT->autoflush(1); + my $account = $acme_node_config->{account} // 'default'; + my $account_file = PVE::CertHelpers::get_acme_account_file($account); + die "ACME account config file '$account' does not exist.\n" + if ! -e $account_file; + + my $acme = PVE::ACME->new($account_file); + + print "Loading ACME account details\n"; + $acme->load(); + + my ($cert, $key) = $order_certificate->($acme, $acme_node_config->{domains}); + + my $code = sub { + print "Setting pveproxy certificate and key\n"; + PVE::CertHelper::set_cert_files($cert, $key, $cert_prefix, $param->{force}); + + print "Restarting pveproxy\n"; + PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pveproxy']); + }; + PVE::CertHelpers::cert_lock(10, $code); + die "$@\n" if $@; + }; + + return $rpcenv->fork_worker("acmenewcert", undef, $authuser, $realcmd); + }, +}); + +__PACKAGE__->register_method ({ + name => 'renew_certificate', + path => 'certificate', + method => 'PUT', + description => "Renew existing certificate from CA.", + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + }, + }, + returns => { + type => 'string', + }, + code => sub { + my ($param) = @_; + + my $node = extract_param($param, 'node'); + my $cert_prefix = PVE::CertHelpers::cert_path_prefix($node); + + my $node_config = PVE::NodeConfig::load_config($node); + raise("ACME settings in node configuration are missing!", 400) + if !$node_config || !$node_config->{acme}; + my $acme_node_config = PVE::NodeConfig::parse_acme($node_config->{acme}); + raise("ACME domain list in node configuration is missing!", 400) + if !$acme_node_config; + + my $rpcenv = PVE::RPCEnvironment::get(); + + my $authuser = $rpcenv->get_user(); + + my $old_cert = PVE::Tools::file_get_contents("${cert_prefix}.pem"); + + my $realcmd = sub { + STDOUT->autoflush(1); + my $account = $acme_node_config->{account} // 'default'; + my $account_file = PVE::CertHelpers::get_acme_account_file($account); + die "ACME account config file '$account' does not exist.\n" + if ! -e $account_file; + + my $acme = PVE::ACME->new($account_file); + + print "Loading ACME account details\n"; + $acme->load(); + + my ($cert, $key) = $order_certificate->($acme, $acme_node_config->{domains}); + + my $code = sub { + print "Setting pveproxy certificate and key\n"; + PVE::CertHelper::set_cert_files($cert, $key, $cert_prefix, 1); + + print "Restarting pveproxy\n"; + PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pveproxy']); + }; + PVE::CertHelpers::cert_lock(10, $code); + die "$@\n" if $@; + + print "Revoking old certificate\n"; + $acme->revoke_certificate($old_cert); + }; + + return $rpcenv->fork_worker("acmerenew", undef, $authuser, $realcmd); + }, +}); + +__PACKAGE__->register_method ({ + name => 'revoke_certificate', + path => 'certificate', + method => 'DELETE', + description => "Revoke existing certificate from CA.", + protected => 1, + parameters => { + additionalProperties => 0, + properties => { + node => get_standard_option('pve-node'), + }, + }, + returns => { + type => 'string', + }, + code => sub { + my ($param) = @_; + + my $node = extract_param($param, 'node'); + my $cert_prefix = PVE::CertHelpers::cert_path_prefix($node); + + my $node_config = PVE::NodeConfig::load_config($node); + raise("ACME settings in node configuration are missing!", 400) + if !$node_config || !$node_config->{acme}; + my $acme_node_config = PVE::NodeConfig::parse_acme($node_config->{acme}); + raise("ACME domain list in node configuration is missing!", 400) + if !$acme_node_config; + + my $rpcenv = PVE::RPCEnvironment::get(); + + my $authuser = $rpcenv->get_user(); + + my $cert = PVE::Tools::file_get_contents("${cert_prefix}.pem"); + + my $realcmd = sub { + STDOUT->autoflush(1); + my $account = $acme_node_config->{account} // 'default'; + my $account_file = PVE::CertHelpers::get_acme_account_file($account); + die "ACME account config file '$account' does not exist.\n" + if ! -e $account_file; + + my $acme = PVE::ACME->new($account_file); + + print "Loading ACME account details\n"; + $acme->load(); + + print "Revoking old certificate\n"; + $acme->revoke_certificate($cert); + + my $code = sub { + print "Deleting certificate files\n"; + unlink "${cert_prefix}.pem"; + unlink "${cert_prefix}.key"; + + print "Restarting pveproxy to revert to self-signed certificates\n"; + PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pveproxy']); + }; + + PVE::CertHelpers::cert_lock(10, $code); + die "$@\n" if $@; + }; + + return $rpcenv->fork_worker("acmerevoke", undef, $authuser, $realcmd); + }, +}); + +1; -- 2.14.2 _______________________________________________ pve-devel mailing list pve-devel@pve.proxmox.com https://pve.proxmox.com/cgi-bin/mailman/listinfo/pve-devel