all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Wolfgang Bumiller <w.bumiller@proxmox.com>
To: pmg-devel@lists.proxmox.com
Subject: [pmg-devel] [PATCH api 6/8] add certificates api endpoint
Date: Tue,  9 Mar 2021 15:13:50 +0100	[thread overview]
Message-ID: <20210309141401.19237-7-w.bumiller@proxmox.com> (raw)
In-Reply-To: <20210309141401.19237-1-w.bumiller@proxmox.com>

This adds /nodes/{nodename}/certificates endpoint
containing:

  /custom/{type} - update smtp or api certificates manually
  /acme/{type} - update via acme

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
 src/Makefile                 |   1 +
 src/PMG/API2/Certificates.pm | 690 +++++++++++++++++++++++++++++++++++
 src/PMG/API2/Nodes.pm        |   7 +
 3 files changed, 698 insertions(+)
 create mode 100644 src/PMG/API2/Certificates.pm

diff --git a/src/Makefile b/src/Makefile
index ebc6bd8..e0629b2 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -155,6 +155,7 @@ LIBSOURCES =				\
 	PMG/API2/When.pm		\
 	PMG/API2/What.pm		\
 	PMG/API2/Action.pm		\
+	PMG/API2/Certificates.pm	\
 	PMG/API2/ACME.pm		\
 	PMG/API2/ACMEPlugin.pm		\
 	PMG/API2.pm			\
diff --git a/src/PMG/API2/Certificates.pm b/src/PMG/API2/Certificates.pm
new file mode 100644
index 0000000..d196af6
--- /dev/null
+++ b/src/PMG/API2/Certificates.pm
@@ -0,0 +1,690 @@
+package PMG::API2::Certificates;
+
+use strict;
+use warnings;
+
+use PVE::Certificate;
+use PVE::Exception qw(raise raise_param_exc);
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::Tools qw(extract_param file_get_contents file_set_contents);
+
+use PMG::CertHelpers;
+use PMG::NodeConfig;
+use PMG::RS::CSR;
+
+use PMG::API2::ACMEPlugin;
+
+use base qw(PVE::RESTHandler);
+
+my $acme_account_dir = PMG::CertHelpers::acme_account_dir();
+
+sub first_typed_pem_entry : prototype($$) {
+    my ($label, $data) = @_;
+
+    if ($data =~ /^(-----BEGIN (?<label>\Q$label\E)-----\n.*?\n-----END \g{label}-----)$/ms) {
+	chomp(my $content = $1);
+	return $content;
+    }
+    return undef;
+}
+
+sub pem_private_key : prototype($) {
+    my ($data) = @_;
+    return first_typed_pem_entry('PRIVATE KEY', $data);
+}
+
+sub pem_certificate : prototype($) {
+    my ($data) = @_;
+    return first_typed_pem_entry('CERTIFICATE', $data);
+}
+
+my sub restart_after_cert_update : prototype($) {
+    my ($type) = @_;
+
+    if ($type eq 'api') {
+	print "Restarting pmgproxy\n";
+	PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pmgproxy']);
+    }
+};
+
+my sub update_cert : prototype($$$$) {
+    my ($type, $cert_path, $certificate, $force) = @_;
+    my $code = sub {
+	print "Setting custom certificate file $cert_path\n";
+	PMG::CertHelpers::set_cert_file($certificate, $cert_path, $force);
+
+	restart_after_cert_update($type);
+    };
+    PMG::CertHelpers::cert_lock(10, $code);
+};
+
+__PACKAGE__->register_method ({
+    name => 'index',
+    path => '',
+    method => 'GET',
+    permissions => { user => 'all' },
+    description => "Node 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 => 'acme' },
+	    { name => 'custom' },
+	    { name => 'info' },
+	    { name => 'config' },
+	];
+    },
+});
+
+__PACKAGE__->register_method ({
+    name => 'info',
+    path => 'info',
+    method => 'GET',
+    permissions => { user => 'all' },
+    proxyto => 'node',
+    protected => 1,
+    description => "Get information about the node's certificates.",
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    node => get_standard_option('pve-node'),
+	},
+    },
+    returns => {
+	type => 'array',
+	items => get_standard_option('pve-certificate-info'),
+    },
+    code => sub {
+	my ($param) = @_;
+
+	my $res = [];
+	for my $path (&PMG::CertHelpers::API_CERT, &PMG::CertHelpers::SMTP_CERT) {
+	    eval {
+		my $info = PVE::Certificate::get_certificate_info($path);
+		push @$res, $info if $info;
+	    };
+	}
+	return $res;
+    },
+});
+
+__PACKAGE__->register_method ({
+    name => 'custom_cert_index',
+    path => 'custom',
+    method => 'GET',
+    permissions => { user => 'all' },
+    description => "Certificate index.",
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    node => get_standard_option('pve-node'),
+	},
+    },
+    returns => {
+	type => 'array',
+	items => {
+	    type => "object",
+	    properties => {},
+	},
+	links => [ { rel => 'child', href => "{type}" } ],
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return [
+	    { type => 'api' },
+	    { type => 'smtp' },
+	];
+    },
+});
+
+__PACKAGE__->register_method ({
+    name => 'upload_custom_cert',
+    path => 'custom/{type}',
+    method => 'POST',
+    permissions => { check => [ 'admin' ] },
+    description => 'Upload or update custom certificate chain and key.',
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    node => get_standard_option('pve-node'),
+	    certificates => {
+		type => 'string',
+		format => 'pem-certificate-chain',
+		description => 'PEM encoded certificate (chain).',
+	    },
+	    key => {
+		type => 'string',
+		description => 'PEM encoded private key.',
+		format => 'pem-string',
+		optional => 0,
+	    },
+	    type => get_standard_option('pmg-certificate-type'),
+	    force => {
+		type => 'boolean',
+		description => 'Overwrite existing custom or ACME certificate files.',
+		optional => 1,
+		default => 0,
+	    },
+	    restart => {
+		type => 'boolean',
+		description => 'Restart services.',
+		optional => 1,
+		default => 0,
+	    },
+	},
+    },
+    returns => get_standard_option('pve-certificate-info'),
+    code => sub {
+	my ($param) = @_;
+
+	my $type = extract_param($param, 'type'); # also used to know which service to restart
+	my $cert_path = PMG::CertHelpers::cert_path($type);
+
+	my $certs = extract_param($param, 'certificates');
+	$certs = PVE::Certificate::strip_leading_text($certs);
+
+	my $key = extract_param($param, 'key');
+	if ($key) {
+	    $key = PVE::Certificate::strip_leading_text($key);
+	    $certs = "$key\n$certs";
+	} else {
+	    my $private_key = pem_private_key($certs);
+	    if (!defined($private_key)) {
+		my $old = file_get_contents($cert_path);
+		$private_key = pem_private_key($old);
+		if (!defined($private_key)) {
+		    raise_param_exc({
+			'key' => "Attempted to upload custom certificate without (existing) key."
+		    })
+		}
+
+		# copy the old certificate's key:
+		$certs = "$key\n$certs";
+	    }
+	}
+
+	my $info;
+
+	my $code = sub {
+	    print "Setting custom certificate file $cert_path\n";
+	    $info = PMG::CertHelpers::set_cert_file($certs, $cert_path, $param->{force});
+
+	    if ($type eq 'api' && $param->{restart}) {
+		print "Restarting pmgproxy\n";
+		PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pmgproxy']);
+	    }
+	};
+
+	PMG::CertHelpers::cert_lock(10, $code);
+	die "$@\n" if $@;
+
+	if ($type eq 'smtp') {
+	    $code = sub {
+		my $cfg = PMG::Config->new();
+
+		print "Rewriting postfix config\n";
+		$cfg->set('mail', 'tls', 1);
+		$cfg->rewrite_config_postfix();
+
+		if ($param->{restart}) {
+		    print "Reloading postfix\n";
+		    PMG::Utils::service_cmd('postfix', 'reload');
+		}
+	    };
+	    PMG::Config::lock_config($code, "failed to reload postfix");
+	}
+
+	return $info;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'remove_custom_cert',
+    path => 'custom/{type}',
+    method => 'DELETE',
+    permissions => { check => [ 'admin' ] },
+    description => 'DELETE custom certificate chain and key.',
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    node => get_standard_option('pve-node'),
+	    type => get_standard_option('pmg-certificate-type'),
+	    restart => {
+		type => 'boolean',
+		description => 'Restart pmgproxy.',
+		optional => 1,
+		default => 0,
+	    },
+	},
+    },
+    returns => {
+	type => 'null',
+    },
+    code => sub {
+	my ($param) = @_;
+
+	my $type = extract_param($param, 'type');
+	my $cert_path = PMG::CertHelpers::cert_path($type);
+
+	my $code = sub {
+	    print "Deleting custom certificate files\n";
+	    unlink $cert_path;
+
+	    if ($param->{restart}) {
+		restart_after_cert_update($type);
+	    }
+	};
+
+	PMG::CertHelpers::cert_lock(10, $code);
+	die "$@\n" if $@;
+
+	return undef;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'acme_cert_index',
+    path => 'acme',
+    method => 'GET',
+    permissions => { user => 'all' },
+    description => "ACME Certificate index.",
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    node => get_standard_option('pve-node'),
+	},
+    },
+    returns => {
+	type => 'array',
+	items => {
+	    type => "object",
+	    properties => {},
+	},
+	links => [ { rel => 'child', href => "{type}" } ],
+    },
+    code => sub {
+	my ($param) = @_;
+
+	return [
+	    { type => 'api' },
+	    { type => 'smtp' },
+	];
+    },
+});
+
+my $order_certificate = sub {
+    my ($acme, $acme_node_config) = @_;
+
+    my $plugins = PMG::API2::ACMEPlugin::load_config();
+
+    print "Placing ACME order\n";
+    my ($order_url, $order) = $acme->new_order([ keys %{$acme_node_config->{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);
+
+	# force lower case, like get_acme_conf does
+	my $domain = lc($auth->{identifier}->{value});
+	if ($auth->{status} eq 'valid') {
+	    print "$domain is already validated!\n";
+	} else {
+	    print "The validation for $domain is pending!\n";
+
+	    my $domain_config = $acme_node_config->{domains}->{$domain};
+	    die "no config for domain '$domain'\n" if !$domain_config;
+
+	    my $plugin_id = $domain_config->{plugin};
+
+	    my $plugin_cfg = $plugins->{ids}->{$plugin_id};
+	    die "plugin '$plugin_id' for domain '$domain' not found!\n"
+		if !$plugin_cfg;
+
+	    my $data = {
+		plugin => $plugin_cfg,
+		alias => $domain_config->{alias},
+	    };
+
+	    my $plugin = PVE::ACME::Challenge->lookup($plugin_cfg->{type});
+	    $plugin->setup($acme, $auth, $data);
+
+	    print "Triggering validation\n";
+	    eval {
+		die "no validation URL returned by plugin '$plugin_id' for domain '$domain'\n"
+		    if !defined($data->{url});
+
+		$acme->request_challenge_validation($data->{url});
+		print "Sleeping for 5 seconds\n";
+		sleep 5;
+		while (1) {
+		    $auth = $acme->get_authorization($auth_url);
+		    if ($auth->{status} eq 'pending') {
+			print "Status is still 'pending', trying again in 10 seconds\n";
+			sleep 10;
+			next;
+		    } elsif ($auth->{status} eq 'valid') {
+			print "Status is 'valid', domain '$domain' OK!\n";
+			last;
+		    }
+		    die "validating challenge '$auth_url' failed - status: $auth->{status}\n";
+		}
+	    };
+	    my $err = $@;
+	    eval { $plugin->teardown($acme, $auth, $data) };
+	    warn "$@\n" if $@;
+	    die $err if $err;
+	}
+    }
+    print "\nAll domains validated!\n";
+    print "\nCreating CSR\n";
+    # Currently we only support dns entries, so extract those from the order:
+    my $san = [
+	map {
+	    $_->{value}
+	} grep {
+	    $_->{type} eq 'dns'
+	} $order->{identifiers}->@*
+    ];
+    die "DNS identifiers are required to generate a CSR.\n" if !scalar @$san;
+    my ($csr_der, $key) = PMG::RS::CSR::generate_csr($san, {});
+
+    my $finalize_error_cnt = 0;
+    print "Checking order status\n";
+    while (1) {
+	$order = $acme->get_order($order_url);
+	if ($order->{status} eq 'pending') {
+	    print "still pending, trying to finalize order\n";
+	    # FIXME
+	    # to be compatible with and without the order ready state we try to
+	    # finalize even at the 'pending' state and give up after 5
+	    # unsuccessful tries this can be removed when the letsencrypt api
+	    # definitely has implemented the 'ready' state
+	    eval {
+		$acme->finalize_order($order->{finalize}, $csr_der);
+	    };
+	    if (my $err = $@) {
+		die $err if $finalize_error_cnt >= 5;
+
+		$finalize_error_cnt++;
+		warn $err;
+	    }
+	    sleep 5;
+	    next;
+	} elsif ($order->{status} eq 'ready') {
+	    print "Order is ready, finalizing order\n";
+	    $acme->finalize_order($order->{finalize}, $csr_der);
+	    sleep 5;
+	    next;
+	} elsif ($order->{status} eq 'processing') {
+	    print "still processing, 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->{certificate});
+
+    return ($cert, $key);
+};
+
+# Filter domains and raise an error if the list becomes empty.
+my $filter_domains = sub {
+    my ($acme_config, $type) = @_;
+
+    my $domains = $acme_config->{domains};
+    foreach my $domain (keys %$domains) {
+	my $entry = $domains->{$domain};
+	if (!(grep { $_ eq $type } PVE::Tools::split_list($entry->{usage}))) {
+	    delete $domains->{$domain};
+	}
+    }
+
+    if (!%$domains) {
+	raise("No domains configured for type '$type'\n", 400);
+    }
+};
+
+__PACKAGE__->register_method ({
+    name => 'new_acme_cert',
+    path => 'acme/{type}',
+    method => 'POST',
+    permissions => { check => [ 'admin' ] },
+    description => 'Order a new certificate from ACME-compatible CA.',
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    node => get_standard_option('pve-node'),
+	    type => get_standard_option('pmg-certificate-type'),
+	    force => {
+		type => 'boolean',
+		description => 'Overwrite existing custom certificate.',
+		optional => 1,
+		default => 0,
+	    },
+	},
+    },
+    returns => {
+	type => 'string',
+    },
+    code => sub {
+	my ($param) = @_;
+
+	my $type = extract_param($param, 'type'); # also used to know which service to restart
+	my $cert_path = PMG::CertHelpers::cert_path($type);
+	raise_param_exc({'force' => "Custom certificate exists but 'force' is not set."})
+	    if !$param->{force} && -e $cert_path;
+
+	my $node_config = PMG::NodeConfig::load_config();
+	my $acme_config = PMG::NodeConfig::get_acme_conf($node_config);
+	raise("ACME domain list in configuration is missing!", 400)
+	    if !$acme_config || !$acme_config->{domains}->%*;
+
+	$filter_domains->($acme_config, $type);
+
+	my $rpcenv = PMG::RESTEnvironment->get();
+	my $authuser = $rpcenv->get_user();
+
+	my $realcmd = sub {
+	    STDOUT->autoflush(1);
+	    my $account = $acme_config->{account};
+	    my $account_file = "${acme_account_dir}/${account}";
+	    die "ACME account config file '$account' does not exist.\n"
+		if ! -e $account_file;
+
+	    print "Loading ACME account details\n";
+	    my $acme = PMG::RS::Acme->load($account_file);
+
+	    my ($cert, $key) = $order_certificate->($acme, $acme_config);
+	    my $certificate = "$key\n$cert";
+
+	    update_cert($type, $cert_path, $certificate, $param->{force});
+
+	    if ($type eq 'smtp') {
+		my $code = sub {
+		    my $cfg = PMG::Config->new();
+
+		    print "Rewriting postfix config\n";
+		    $cfg->set('mail', 'tls', 1);
+		    if ($cfg->rewrite_config_postfix()) {
+			print "Reloading postfix\n";
+			PMG::Utils::service_cmd('postfix', 'reload');
+		    }
+		};
+		PMG::Config::lock_config($code, "failed to reload postfix");
+	    }
+
+	    die "$@\n" if $@;
+	};
+
+	return $rpcenv->fork_worker("acmenewcert", undef, $authuser, $realcmd);
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'renew_acme_cert',
+    path => 'acme/{type}',
+    method => 'PUT',
+    permissions => { check => [ 'admin' ] },
+    description => "Renew existing certificate from CA.",
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    node => get_standard_option('pve-node'),
+	    type => get_standard_option('pmg-certificate-type'),
+	    force => {
+		type => 'boolean',
+		description => 'Force renewal even if expiry is more than 30 days away.',
+		optional => 1,
+		default => 0,
+	    },
+	},
+    },
+    returns => {
+	type => 'string',
+    },
+    code => sub {
+	my ($param) = @_;
+
+	my $type = extract_param($param, 'type'); # also used to know which service to restart
+	my $cert_path = PMG::CertHelpers::cert_path($type);
+
+	raise("No current (custom) certificate found, please order a new certificate!\n")
+	    if ! -e $cert_path;
+
+	my $expires_soon = PVE::Certificate::check_expiry($cert_path, time() + 30*24*60*60);
+	raise_param_exc({'force' => "Certificate does not expire within the next 30 days, and 'force' is not set."})
+	    if !$expires_soon && !$param->{force};
+
+	my $node_config = PMG::NodeConfig::load_config();
+	my $acme_config = PMG::NodeConfig::get_acme_conf($node_config);
+	raise("ACME domain list in configuration is missing!", 400)
+	    if !$acme_config || !$acme_config->{domains}->%*;
+
+	$filter_domains->($acme_config, $type);
+
+	my $rpcenv = PMG::RESTEnvironment->get();
+	my $authuser = $rpcenv->get_user();
+
+	my $old_cert = PVE::Tools::file_get_contents($cert_path);
+
+	my $realcmd = sub {
+	    STDOUT->autoflush(1);
+	    my $account = $acme_config->{account};
+	    my $account_file = "${acme_account_dir}/${account}";
+	    die "ACME account config file '$account' does not exist.\n"
+		if ! -e $account_file;
+
+	    print "Loading ACME account details\n";
+	    my $acme = PMG::RS::Acme->load($account_file);
+
+	    my ($cert, $key) = $order_certificate->($acme, $acme_config);
+	    my $certificate = "$key\n$cert";
+
+	    update_cert($type, $cert_path, $certificate, 1);
+
+	    if (defined($old_cert)) {
+		print "Revoking old certificate\n";
+		eval { $acme->revoke_certificate($old_cert, undef) };
+		warn "Revoke request to CA failed: $@" if $@;
+	    }
+	};
+
+	return $rpcenv->fork_worker("acmerenew", undef, $authuser, $realcmd);
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'revoke_acme_cert',
+    path => 'acme/{type}',
+    method => 'DELETE',
+    permissions => { check => [ 'admin' ] },
+    description => "Revoke existing certificate from CA.",
+    protected => 1,
+    proxyto => 'node',
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    node => get_standard_option('pve-node'),
+	    type => get_standard_option('pmg-certificate-type'),
+	},
+    },
+    returns => {
+	type => 'string',
+    },
+    code => sub {
+	my ($param) = @_;
+
+	my $type = extract_param($param, 'type'); # also used to know which service to restart
+	my $cert_path = PMG::CertHelpers::cert_path($type);
+
+	my $node_config = PMG::NodeConfig::load_config();
+	my $acme_config = PMG::NodeConfig::get_acme_conf($node_config);
+	raise("ACME domain list in configuration is missing!", 400)
+	    if !$acme_config || !$acme_config->{domains}->%*;
+
+	$filter_domains->($acme_config, $type);
+
+	my $rpcenv = PMG::RESTEnvironment->get();
+	my $authuser = $rpcenv->get_user();
+
+	my $cert = PVE::Tools::file_get_contents($cert_path);
+	$cert = pem_certificate($cert)
+	    or die "no certificate section found in '$cert_path'\n";
+
+	my $realcmd = sub {
+	    STDOUT->autoflush(1);
+	    my $account = $acme_config->{account};
+	    my $account_file = "${acme_account_dir}/${account}";
+	    die "ACME account config file '$account' does not exist.\n"
+		if ! -e $account_file;
+
+	    print "Loading ACME account details\n";
+	    my $acme = PMG::RS::Acme->load($account_file);
+
+	    print "Revoking old certificate\n";
+	    eval { $acme->revoke_certificate($cert, undef) };
+	    if (my $err = $@) {
+		# is there a better check?
+		die "Revoke request to CA failed: $err" if $err !~ /"Certificate is expired"/;
+	    }
+
+	    my $code = sub {
+		print "Deleting certificate files\n";
+		unlink $cert_path;
+
+		# FIXME: Regenerate self-signed `api` certificate.
+		restart_after_cert_update($type);
+	    };
+
+	    PMG::CertHelpers::cert_lock(10, $code);
+	    die "$@\n" if $@;
+	};
+
+	return $rpcenv->fork_worker("acmerevoke", undef, $authuser, $realcmd);
+    }});
+
+1;
diff --git a/src/PMG/API2/Nodes.pm b/src/PMG/API2/Nodes.pm
index c0f5963..b6f0cd5 100644
--- a/src/PMG/API2/Nodes.pm
+++ b/src/PMG/API2/Nodes.pm
@@ -27,6 +27,7 @@ use PMG::API2::Postfix;
 use PMG::API2::MailTracker;
 use PMG::API2::Backup;
 use PMG::API2::PBS::Job;
+use PMG::API2::Certificates;
 
 use base qw(PVE::RESTHandler);
 
@@ -85,6 +86,11 @@ __PACKAGE__->register_method ({
     path => 'pbs',
 });
 
+__PACKAGE__->register_method ({
+    subclass => "PMG::API2::Certificates",
+    path => 'certificates',
+});
+
 __PACKAGE__->register_method ({
     name => 'index',
     path => '',
@@ -126,6 +132,7 @@ __PACKAGE__->register_method ({
 	    { name => 'subscription' },
 	    { name => 'termproxy' },
 	    { name => 'rrddata' },
+	    { name => 'certificates' },
 	];
 
 	return $result;
-- 
2.20.1





  parent reply	other threads:[~2021-03-09 14:14 UTC|newest]

Thread overview: 32+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-03-09 14:13 [pmg-devel] [RFC api/gui/wtk/acme 0/many] Certificates & ACME Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH api 1/8] depend on libpmg-rs-perl and proxmox-acme Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH api 2/8] add PMG::CertHelpers module Wolfgang Bumiller
2021-03-11 10:05   ` Dominik Csapak
2021-03-12 13:55     ` Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH api 3/8] add PMG::NodeConfig module Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH api 4/8] cluster: sync acme/ and acme-plugins.conf Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH api 5/8] api: add ACME and ACMEPlugin module Wolfgang Bumiller
2021-03-11 10:41   ` Dominik Csapak
2021-03-12 14:10     ` Wolfgang Bumiller
2021-03-09 14:13 ` Wolfgang Bumiller [this message]
2021-03-11 11:06   ` [pmg-devel] [PATCH api 6/8] add certificates api endpoint Dominik Csapak
2021-03-12 14:51     ` Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH api 7/8] add node-config api entry points Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH api 8/8] add acme and cert subcommands to pmgconfig Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH gui] add certificates and acme view Wolfgang Bumiller
2021-03-11 12:35   ` Dominik Csapak
2021-03-09 14:13 ` [pmg-devel] [PATCH acme] add missing 'use PVE::Acme' statement Wolfgang Bumiller
2021-03-12 15:00   ` [pmg-devel] applied: " Thomas Lamprecht
2021-03-09 14:13 ` [pmg-devel] [PATCH widget-toolkit 1/7] Utils: add ACME related utilities Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH widget-toolkit 2/7] add ACME related data models Wolfgang Bumiller
2021-03-11 12:41   ` Dominik Csapak
2021-03-09 14:13 ` [pmg-devel] [PATCH widget-toolkit 3/7] add ACME forms: Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH widget-toolkit 4/7] add certificate panel Wolfgang Bumiller
2021-03-09 14:13 ` [pmg-devel] [PATCH widget-toolkit 5/7] add ACME account panel Wolfgang Bumiller
2021-03-11 13:51   ` Dominik Csapak
2021-03-11 15:14     ` Thomas Lamprecht
2021-03-11 15:16       ` Dominik Csapak
2021-03-11 15:27         ` Thomas Lamprecht
2021-03-09 14:14 ` [pmg-devel] [PATCH widget-toolkit 6/7] add ACME plugin editing Wolfgang Bumiller
2021-03-09 14:14 ` [pmg-devel] [PATCH widget-toolkit 7/7] add ACME domain editing Wolfgang Bumiller
2021-03-10 12:27 ` [pmg-devel] [RFC api/gui/wtk/acme 0/many] Certificates & ACME Dominik Csapak

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20210309141401.19237-7-w.bumiller@proxmox.com \
    --to=w.bumiller@proxmox.com \
    --cc=pmg-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal