Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added option perform-by-domain, to create wildcard SSL with base doma… #10

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ An example file is provided in `/usr/local/etc/letsencrypt/certbot-pdns.json`:
"base-url": "http://127.0.0.1:34022/api/v1",
"axfr-time": 5,
"http-auth": ["user", "secret_pass"],
"verify-cert": "False"
"verify-cert": "False",
"perform-by-domain": "False"
}
```

Expand All @@ -50,6 +51,7 @@ Configuration keys:
- api-key: Your PowerDNS API Key as specified in property `api-key` in file `/etc/powerdns/pdns.conf`
- base-url: The base URL for PowerDNS API. Require `api=yes` and `api-readonly=no` in file `/etc/powerdns/pdns.conf`
- axfr-time: The time in seconds to wait for AXFR in slaves. Can be set to 0 if there is only one authoritative server for the zone.
- perform-by-domain (optional): Allows to create wildcard SSL with base domain in the same certificate, e.g.: `-d 'example.org' -d '*.example.org'`

The following two keys are optional and added in case a (nginx) reverse proxy is used to secure access to the api:
- http-auth (optional): A list of two strings containing the Username and Password for a http-basic-authentication
Expand Down
3 changes: 2 additions & 1 deletion certbot-pdns.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"base-url": "http://127.0.0.1:34022/api/v1",
"axfr-time": 5,
"http-auth": ["user", "secret_pass"],
"verify-cert": "False"
"verify-cert": "False",
"perform-by-domain": "False"
}
33 changes: 31 additions & 2 deletions certbot_pdns/PdnsApiAuthenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@

logger = logging.getLogger(__name__)

RECORD_PREFIX = "_acme-challenge"


class PdnsApiAuthenticator:
api = None
zones = None
axfr_time = None
by_domain = False

def find_best_matching_zone(self, domain):
if domain is None or domain == "":
Expand Down Expand Up @@ -67,6 +70,12 @@ def prepare(self, conf_path):
self.api.set_verify_cert(config["verify-cert"])
if "http-auth" in config:
self.api.set_http_auth(config["http-auth"])
if "perform-by-domain" in config:
if config["perform-by-domain"] in ("True", "true", True):
self.by_domain = True
else:
self.by_domain = False

self.zones = self.api.list_zones()
# print(self.zones)
# raw_input('Press <ENTER> to continue')
Expand All @@ -82,12 +91,32 @@ def perform_single(self, achall, response, validation):

logger.debug("Found zone %s for domain %s" % (zone["name"], domain))

res = self.api.replace_record(zone["name"], "_acme-challenge." + domain + ".", "TXT", 1, "\"" + token.decode('utf-8') + "\"", False, False)
res = self.api.replace_record(zone["name"], RECORD_PREFIX + "." + domain + ".", content = "\"" + token.decode('utf-8') + "\"")
if res is not None:
raise errors.PluginError("Bad return from PDNS API when adding record: %s" % res)

return response

def perform_by_domain(self, domain_infos):
records = []
responses = []
for achall in domain_infos['achalls']:
records.append({'content': '"%s"' % achall['validation'].encode().decode('utf-8'),
'disabled': False,
'set-prt': False})
responses.append(achall['response'])

logger.debug("Found zone %s for domain %s"
% (domain_infos['zone']['name'], domain_infos['domain']))

res = self.api.replace_record(domain_infos['zone']['name'],
"%s.%s." % (RECORD_PREFIX, domain_infos['domain']),
records = records)
if res is not None:
raise errors.PluginError("Bad return from PDNS API when adding record: %s" % res)

return responses

def perform_notify(self, zone):
logger.info("Notifying zone %s..." % zone["name"])

Expand All @@ -106,7 +135,7 @@ def cleanup(self, achall):
zone = self.find_best_matching_zone(domain)
if zone is None:
return
res = self.api.delete_record(zone["name"], "_acme-challenge." + domain + ".", "TXT", 1, None, False, False)
res = self.api.delete_record(zone["name"], RECORD_PREFIX + "." + domain + ".")
if res is not None:
raise errors.PluginError("Bad return from PDNS API when deleting record: %s" % res)
self.update_soa(zone["name"])
Expand Down
35 changes: 28 additions & 7 deletions certbot_pdns/authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import zope.interface
from acme import challenges
from certbot import interfaces
from certbot import interfaces, errors
from certbot.plugins import common

from certbot_pdns.PdnsApiAuthenticator import PdnsApiAuthenticator
Expand Down Expand Up @@ -54,15 +54,36 @@ def prepare(self): # pylint: disable=missing-docstring
def perform(self, achalls): # pylint: disable=missing-docstring
responses = []
zones = []
domains = {}

for achall in achalls:
response, validation = achall.response_and_validation()
resp = self.backend.perform_single(achall, response, validation)
responses.append(resp)

domain = achall.domain
zone = self.backend.find_best_matching_zone(domain)
if zone not in zones:
zones.append(zone)

if not self.backend.by_domain:
resp = self.backend.perform_single(achall, response, validation)
responses.append(resp)

zone = self.backend.find_best_matching_zone(domain)
if zone not in zones:
zones.append(zone)
continue

if domain not in domains:
domains[domain] = {'domain': domain,
'achalls': []}
domains[domain]['zone'] = self.backend.find_best_matching_zone(domain)
if not domains[domain]['zone']:
raise errors.PluginError("Could not find zone for %s" % domain)

domains[domain]['achalls'].append({'response': response,
'validation': validation})

if self.backend.by_domain:
for domain_infos in domains.values():
responses.extend(self.backend.perform_by_domain(domain_infos))
if domain_infos['zone'] not in zones:
zones.append(domain_infos['zone'])

for zone in zones:
self.backend.perform_notify(zone)
Expand Down
88 changes: 46 additions & 42 deletions certbot_pdns/pdnsapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@

import requests

_HTTP_VALID_METHODS = ('DELETE',
'GET',
'PATCH',
'POST',
'PUT')


class PdnsApi:
api_key = None
Expand Down Expand Up @@ -37,26 +43,20 @@ def _query(self, uri, method, kwargs=None):
'Content-Type': 'application/json'
}

data = json.dumps(kwargs)

if method == "GET":
request = requests.get(self.base_url + uri, headers=headers,
auth=self.http_auth, verify=self.verify_cert)
elif method == "POST":
request = requests.post(self.base_url + uri, headers=headers, data=data,
auth=self.http_auth, verify=self.verify_cert)
elif method == "PUT":
request = requests.put(self.base_url + uri, headers=headers, data=data,
auth=self.http_auth, verify=self.verify_cert)
elif method == "PATCH":
request = requests.patch(self.base_url + uri, headers=headers, data=data,
auth=self.http_auth, verify=self.verify_cert)
elif method == "DELETE":
request = requests.delete(self.base_url + uri, headers=headers,
auth=self.http_auth, verify=self.verify_cert)
else:
data = None

if method not in _HTTP_VALID_METHODS:
raise ValueError("Invalid method '%s'" % method)

if method[0] == 'P':
data = json.dumps(kwargs)

request = getattr(requests, method.lower())(self.base_url + uri,
auth=self.http_auth,
verify=self.verify_cert,
headers=headers,
data=data)

return None if request.status_code == 204 else request.json()

def list_zones(self):
Expand All @@ -68,40 +68,44 @@ def get_zone(self, zone_name):
def update_zone(self, zone_name, data):
return self._query("/servers/localhost/zones/%s" % zone_name, "PUT", data)

def replace_record(self, zone_name, name, type, ttl, content, disabled, set_ptr):
return self._query("/servers/localhost/zones/%s" % zone_name, "PATCH", {"rrsets": [
{
"name": name,
"type": type,
"ttl": ttl,
"changetype": "REPLACE",
"records": [
{
"content": content,
def _patch_record(self, changetype, zone_name, name, type, ttl, content, disabled, set_ptr, records):
if not records:
records = [{"content": content,
"disabled": disabled,
"set-prt": set_ptr
}
]
}
]})
"set-prt": set_ptr}]

def delete_record(self, zone_name, name, type, ttl, content, disabled, set_ptr):
return self._query("/servers/localhost/zones/%s" % zone_name, "PATCH", {"rrsets": [
{
"name": name,
"type": type,
"ttl": ttl,
"changetype": "DELETE",
"records": [
{
"content": content,
"disabled": disabled,
"set-prt": set_ptr
}
]
"changetype": changetype,
"records": records
}
]})

def replace_record(self, zone_name, name, type = 'TXT', ttl = 1, content = None, disabled = False, set_ptr = False, records = None):
self._patch_record("REPLACE",
zone_name,
name,
type,
ttl,
content,
disabled,
set_ptr,
records)

def delete_record(self, zone_name, name, type = 'TXT', ttl = 1, content = None, disabled = False, set_ptr = False, records = None):
self._patch_record("DELETE",
zone_name,
name,
type,
ttl,
content,
disabled,
set_ptr,
records)

def notify_zone(self, zone_name):
return self._query("/servers/localhost/zones/%s/notify" % zone_name, "PUT")

Expand Down