X-Git-Url: https://git.proxmox.com/?p=proxmox-acme.git;a=blobdiff_plain;f=src%2FPVE%2FACME%2FDNSChallenge.pm;h=300cdc97d1a8cf07a58b30a5f4ddc0f76af26842;hp=f62333b2410052e30954ac0f9e32b34b1bdbeee8;hb=21e6ed300747091929b194d8d9395e6b2e098433;hpb=169250014f2639f2696828b53c1f778f206db6a8 diff --git a/src/PVE/ACME/DNSChallenge.pm b/src/PVE/ACME/DNSChallenge.pm index f62333b..300cdc9 100644 --- a/src/PVE/ACME/DNSChallenge.pm +++ b/src/PVE/ACME/DNSChallenge.pm @@ -6,196 +6,425 @@ use warnings; use Digest::SHA qw(sha256); use PVE::Tools; +use PVE::ACME; + use base qw(PVE::ACME::Challenge); my $ACME_PATH = '/usr/share/proxmox-acme/proxmox-acme'; sub supported_challenge_types { - return { 'dns-01' => 1 }; + return ["dns-01"]; } sub type { return 'dns'; } -my $api_name_list = [ - 'acmedns', - 'acmeproxy', - 'active24', - 'ad', - 'ali', - 'autodns', - 'aws', - 'azure', - 'cf', - 'clouddns', - 'cloudns', - 'cn', - 'conoha', - 'constellix', - 'cx', - 'cyon', - 'da', - 'ddnss', - 'desec', - 'dgon', - 'dnsimple', - 'do', - 'doapi', - 'domeneshop', - 'dp', - 'dpi', - 'dreamhost', - 'duckdns', - 'durabledns', - 'dyn', - 'dynu', - 'dynv6', - 'easydns', - 'euserv', - 'exoscale', - 'freedns', - 'gandi_livedns', - 'gcloud', - 'gd', - 'gdnsdk', - 'he', - 'hexonet', - 'hostingde', - 'infoblox', - 'internetbs', - 'inwx', - 'ispconfig', - 'jd', - 'kas', - 'kinghost', - 'knot', - 'leaseweb', - 'lexicon', - 'linode', - 'linode_v4', - 'loopia', - 'lua', - 'maradns', - 'me', - 'miab', - 'misaka', - 'myapi', - 'mydevil', - 'mydnsjp', - 'namecheap', - 'namecom', - 'namesilo', - 'nederhost', - 'neodigit', - 'netcup', - 'nic', - 'nsd', - 'nsone', - 'nsupdate', - 'nw', - 'one', - 'online', - 'openprovider', - 'opnsense', - 'ovh', - 'pdns', - 'pleskxml', - 'pointhq', - 'rackspace', - 'rcode0', - 'regru', - 'schlundtech', - 'selectel', - 'servercow', - 'tele3', - 'ultra', - 'unoeuro', - 'variomedia', - 'vscale', - 'vultr', - 'yandex', - 'zilore', - 'zone', - 'zonomi', -]; +# describe the data schema of the supported plugins, e.g.: +# 'dnsprovider' => { +# name => 'Full name of Plugin', +# fields => { +# 'FOO_API_KEY' => { +# description => "The API key", +# default => "none", +# optional => 1, +# type => 'string', +# }, +# # ... +# }, +# }, +my $plugins = { + '1984hosting' => {}, + 'acmedns' => { + name => 'acme-dns', + fields => { + 'ACMEDNS_UPDATE_URL' => { + description => 'The API update endpoint', + type => 'string', + }, + 'ACMEDNS_USERNAME' => { + description => 'The acme-dns user', + type => 'string', + }, + 'ACMEDNS_PASSWORD' => { + description => 'The acme-dns password', + type => 'string', + }, + 'ACMEDNS_SUBDOMAIN' => { + description => 'The subdomain you got from acme-dns registration', + type => 'string', + }, + }, + }, + 'acmeproxy' => {}, + 'active24' => { + name => 'Active24', + fields => { + 'ACTIVE24_Token' => { + description => "The API key", + type => 'string', + }, + }, + }, + 'ad' => { + name => 'Alwaysdata', + fields => { + 'AD_API_KEY' => { + description => "The API key", + type => 'string', + }, + }, + }, + 'ali' => { + name => 'Alibaba Cloud DNS', + fields => { + 'Ali_API' => { + description => 'The API endpoint', + default => "https://alidns.aliyuncs.com/", + type => 'string', + optional => 1, + }, + 'Ali_Key' => { + description => 'The API Key', + type => 'string', + }, + 'Ali_Secret' => { + description => 'The API Secret', + type => 'string', + }, + }, + }, + 'anx' => {}, + 'arvan' => {}, + 'autodns' => {}, + 'aws' => { + name => 'Amazon Route53 (AWS)', + fields => { + 'AWS_ACCESS_KEY_ID' => { + name => 'ACCESS_KEY_ID', + description => 'The AWS access-key ID', + type => 'string', + }, + 'AWS_SECRET_ACCESS_KEY' => { + name => 'SECRET_ACCESS_KEY', + description => 'The AWS access-key secret', + type => 'string', + }, + }, + }, + 'azure' => {}, + 'cf' => { + name => 'Cloudflare Managed DNS', + description => 'Either provide global account key and email, or CF API token and Account ID.', + fields => { + 'CF_Key' => { + description => 'The Cloudflare Global API Key', + type => 'string', + }, + 'CF_Email' => { + description => 'The Cloudflare Account EMail-Address', + type => 'string', + }, + 'CF_Token' => { + description => 'The new Cloudflare API Token', + type => 'string', + }, + 'CF_Account_ID' => { + description => 'The new Cloudflare API Account ID', + type => 'string', + }, + 'CF_Zone_ID' => { + description => 'For Zone restricted API Token', + type => 'string', + }, + }, + }, + 'clouddns' => {}, + 'cloudns' => {}, + 'cn' => {}, + 'conoha' => {}, + 'constellix' => {}, + 'cx' => {}, + 'cyon' => {}, + 'da' => {}, + 'ddnss' => {}, + 'desec' => {}, + 'df' => {}, + 'dgon' => { + name => 'DigitalOcean DNS', + fields => { + 'DO_API_KEY' => { + description => 'The DigitalOcean API Key', + type => 'string', + }, + }, + }, + 'dnsimple' => {}, + 'do' => {}, + 'doapi' => {}, + 'domeneshop' => {}, + 'dp' => {}, + 'dpi' => {}, + 'dreamhost' => {}, + 'duckdns' => {}, + 'durabledns' => {}, + 'dyn' => {}, + 'dynu' => {}, + 'dynv6' => {}, + 'easydns' => {}, + 'edgedns' => {}, + 'euserv' => {}, + 'exoscale' => {}, + 'freedns' => {}, + 'gandi_livedns' => {}, + 'gcloud' => {}, + 'gd' => { + name => 'GoDaddy', + fields => { + 'GD_Key' => { + description => 'The GoDaddy API Key', + type => 'string', + }, + 'GD_Secret' => { + description => 'The GoDaddy API Secret', + type => 'string', + }, + }, + }, + 'gdnsdk' => {}, + 'he' => {}, + 'hetzner' => {}, + 'hexonet' => {}, + 'hostingde' => {}, + 'huaweicloud' => {}, + 'infoblox' => {}, + 'infomaniak' => {}, + 'internetbs' => {}, + 'inwx' => { + name => 'INWX', + fields => { + 'INWX_User' => { + description => 'The INWX username', + type => 'string', + }, + 'INWX_Password' => { + description => 'The INWX password', + type => 'string', + }, + }, + }, + 'ionos' => {}, + 'ispconfig' => {}, + 'jd' => {}, + 'joker' => {}, + 'kappernet' => { + name => 'kapper.net', + fields => { + 'KAPPERNETDNS_Key' => { + description => 'Your kapper.net API key', + type => 'string', + }, + 'KAPPERNETDNS_Secret' => { + description => 'Your kapper.net API secret', + type => 'string', + }, + }, + }, + 'kas' => {}, + 'kinghost' => {}, + 'knot' => {}, + 'leaseweb' => {}, + 'lexicon' => {}, + 'linode' => {}, + 'linode_v4' => {}, + 'loopia' => {}, + 'lua' => {}, + 'maradns' => {}, + 'me' => {}, + 'miab' => {}, + 'misaka' => {}, + 'myapi' => {}, + 'mydevil' => {}, + 'mydnsjp' => {}, + 'namecheap' => {}, + 'namecom' => {}, + 'namesilo' => {}, + 'nederhost' => {}, + 'neodigit' => {}, + 'netcup' => {}, + 'netlify' => {}, + 'nic' => {}, + 'njalla' => {}, + 'nm' => {}, + 'nsd' => {}, + 'nsone' => {}, + 'nsupdate' => {}, + 'nw' => {}, + 'one' => {}, + 'online' => {}, + 'openprovider' => {}, + 'openstack' => {}, + 'opnsense' => {}, + 'ovh' => { + name => 'OVH', + fields => { + 'OVH_END_POINT' => { + description => "The OVH endpoint", + default => "ovh-eu", + optional => 1, + type => 'string', + }, + 'OVH_AK' => { + description => "The application key.", + type => 'string', + }, + 'OVH_AS' => { + description => "The application secret.", + type => 'string', + }, + 'OVH_CK' => { + description => "The consumer key.", + optional => 1, + type => 'string', + }, + }, + }, + 'pdns' => { + name => 'PowerDNS server', + fields => { + 'PDNS_Url' => { + description => "The PowerDNS API endpoint.", + type => 'string', + }, + 'PDNS_ServerId'=> { + type => 'string', + }, + 'PDNS_Token'=> { + type => 'string', + }, + 'PDNS_Ttl'=> { + type => 'integer', + }, + }, + }, + 'pleskxml' => {}, + 'pointhq' => {}, + 'rackcorp' => {}, + 'rackspace' => {}, + 'rcode0' => {}, + 'regru' => {}, + 'scaleway' => {}, + 'schlundtech' => {}, + 'selectel' => {}, + 'servercow' => {}, + 'simply' => {}, + 'tele3' => {}, + 'transip' => {}, + 'ultra' => {}, + 'unoeuro' => {}, + 'variomedia' => {}, + 'vscale' => {}, + 'vultr' => {}, + 'world4you' => {}, + 'yandex' => {}, + 'zilore' => {}, + 'zone' => {}, + 'zonomi' => {}, +}; + +sub get_supported_plugins { + return $plugins; +} sub properties { return { api => { description => "API plugin name", type => 'string', - enum => $api_name_list, + enum => [sort keys %$plugins], }, data => { type => 'string', - description => 'DNS plugin data.', + description => 'DNS plugin data. (base64 encoded)', }, + 'validation-delay' => { + type => 'integer', + description => 'Extra delay in seconds to wait before requesting validation.' + .' Allows to cope with a long TTL of DNS records.', + # low default, but our bet is that the acme-challenge domain isn't + # cached at all, so it hopefully shouldn't run into TTL issues + default => 30, + optional => 1, + minimum => 0, + maximum => 2 * 24 * 60 * 60, + } }; } sub options { return { api => {}, - data => {}, + data => { optional => 1 }, nodes => { optional => 1 }, disable => { optional => 1 }, + 'validation-delay' => { optional => 1 }, }; } -my $outfunc = sub { - my $line = shift; - print "$line\n"; -}; +my $proxmox_acme_command = sub { + my ($self, $acme, $auth, $data, $action) = @_; -sub extract_challenge { - my ($self, $challenge) = @_; + die "No plugin data for DNSChallenge\n" if !defined($data->{plugin}); - return PVE::ACME::Challenge->extract_challenge($challenge, 'dns-01'); -} - -sub get_subplugins { - return $api_name_list; -} + my $alias = $data->{alias}; + my $domain = $auth->{identifier}->{value}; -# The order of the parameters passed to proxmox-acme is important -# proxmox-acme setup $plugin [$domain|$alias] $txtvalue $plugin_conf_string -sub setup { - my ($self, $data) = @_; + my $challenge = $self->extract_challenge($auth->{challenges}); + my $key_auth = $acme->key_authorization($challenge->{token}); - die "No plugin data for DNSChallenge\n" if !defined($data->{plugin}); - my $domain = $data->{plugin}->{alias} ? $data->{plugin}->{alias} : $data->{domain}; - my $txtvalue = PVE::ACME::encode(sha256($data->{key_authorization})); + my $txtvalue = PVE::ACME::encode(sha256($key_auth)); my $dnsplugin = $data->{plugin}->{api}; my $plugin_conf_string = $data->{plugin}->{data}; # for security reasons, we execute the command as nobody # we can't verify that the code of the DNSPlugins are harmless. - my $cmd = ["setpriv", "--reuid", "nobody", "--regid", "nogroup", "--clear-groups", "--"]; - push @$cmd, "/usr/bin/bash", $ACME_PATH, "setup", $dnsplugin, $domain; - push @$cmd, $txtvalue, $plugin_conf_string; + my $cmd = ["setpriv", "--reuid", "nobody", "--regid", "nogroup", "--clear-groups", "--reset-env", "--"]; + + # The order of the parameters passed to proxmox-acme is important + # proxmox-acme $plugin <$domain|$alias> $txtvalue [$plugin_conf_string] + push @$cmd, "/bin/bash", $ACME_PATH, $action, $dnsplugin; + if ($alias) { + push @$cmd, $alias; + } else { + push @$cmd, $domain; + } + my $input = "$txtvalue\n"; + $input .= "$plugin_conf_string\n" if $plugin_conf_string; + + PVE::Tools::run_command($cmd, input => $input); + + $data->{url} = $challenge->{url}; + + return $domain; +}; - PVE::Tools::run_command($cmd, outfunc => $outfunc); +sub setup { + my ($self, $acme, $auth, $data) = @_; + + my $domain = $proxmox_acme_command->($self, $acme, $auth, $data, 'setup'); print "Add TXT record: _acme-challenge.$domain\n"; + + my $delay = $data->{plugin}->{'validation-delay'} // 30; + if ($delay > 0) { + print "Sleeping $delay seconds to wait for TXT record propagation\n"; + sleep($delay); # don't care for EINTR + } } -# The order of the parameters passed to proxmox-acme is important -# proxmox-acme teardown $plugin [$domain|$alias] $txtvalue $plugin_conf_string sub teardown { - my ($self, $data) = @_; + my ($self, $acme, $auth, $data) = @_; - die "No plugin data for DNSChallenge\n" if !defined($data->{plugin}); - my $domain = $data->{plugin}->{alias} ? $data->{plugin}->{alias} : $data->{domain}; - my $txtvalue = PVE::ACME::encode(sha256($data->{key_authorization})); - my $dnsplugin = $data->{plugin}->{api}; - my $plugin_conf_string = $data->{plugin}->{data}; - - # for security reasons, we execute the command as nobody - # we can't verify that the code of the DNSPlugins are harmless. - my $cmd = ["setpriv", "--reuid", "nobody", "--regid", "nogroup", "--clear-groups", "--"]; - push @$cmd, "/usr/bin/bash", "$ACME_PATH", "teardown", $dnsplugin, $domain ; - push @$cmd, $txtvalue, $plugin_conf_string; - PVE::Tools::run_command($cmd, outfunc => $outfunc); + my $domain = $proxmox_acme_command->($self, $acme, $auth, $data, 'teardown'); print "Remove TXT record: _acme-challenge.$domain\n"; }