On Wed, Apr 11, 2018 at 10:08:50AM +0200, Fabian Grünbichler wrote: > 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);
Should probably be in an eval {} (looks like it used to be given the extra spaces?)? Otherwise the renewal task will show up as having failed just because the old certificate wasn't revoked? > + }; > + > + 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