Use Cloudflare as DNS provider for LE certs (#945)

* Use Cloudflare as DNS provider for LE certs

* Better handle dns_provider plugins
This commit is contained in:
Harm Weites 2018-02-22 17:17:28 +01:00 committed by kevgliss
parent 2578970f7d
commit 5d18838868
5 changed files with 103 additions and 16 deletions

View File

@ -1,3 +1,4 @@
CloudFlare==1.7.5
Flask==0.12
Flask-RESTful==0.3.6
Flask-SQLAlchemy==2.1

View File

@ -0,0 +1,76 @@
import time
import CloudFlare
from flask import current_app
def cf_api_call():
cf_key = current_app.config.get('ACME_CLOUDFLARE_KEY', '')
cf_email = current_app.config.get('ACME_CLOUDFLARE_EMAIL', '')
return CloudFlare.CloudFlare(email=cf_email, token=cf_key)
def find_zone_id(host):
elements = host.split('.')
cf = cf_api_call()
n = 1
while n < 5:
n = n + 1
domain = '.'.join(elements[-n:])
current_app.logger.debug("Trying to get ID for zone {0}".format(domain))
try:
zone = cf.zones.get(params={'name': domain, 'per_page': 1})
except Exception as e:
current_app.logger.error("Cloudflare API error: %s" % e)
pass
if len(zone) == 1:
break
if len(zone) == 0:
current_app.logger.error('No zone found')
return
else:
return zone[0]['id']
def wait_for_dns_change(change_id, account_number=None):
cf = cf_api_call()
zone_id, record_id = change_id
while True:
r = cf.zones.get(zone_id, record_id)
current_app.logger.debug("Record status: %s" % r['status'])
if r['status'] == 'active':
break
time.sleep(1)
return
def create_txt_record(host, value, account_number):
cf = cf_api_call()
zone_id = find_zone_id(host)
if not zone_id:
return
txt_record = {'name': host, 'type': 'TXT', 'content': value}
current_app.logger.debug("Creating TXT record {0} with value {1}".format(host, value))
try:
r = cf.zones.dns_records.post(zone_id, data=txt_record)
except Exception as e:
current_app.logger.error('/zones.dns_records.post %s: %s' % (txt_record['name'], e))
return zone_id, r['id']
def delete_txt_record(change_id, account_number, host, value):
cf = cf_api_call()
zone_id, record_id = change_id
current_app.logger.debug("Removing record with id {0}".format(record_id))
try:
cf.zones.dns_records.delete(zone_id, record_id)
except Exception as e:
current_app.logger.error('/zones.dns_records.post: %s' % e)

View File

@ -25,8 +25,6 @@ from lemur.common.utils import validate_conf
from lemur.plugins.bases import IssuerPlugin
from lemur.plugins import lemur_acme as acme
from .route53 import delete_txt_record, create_txt_record, wait_for_r53_change
def find_dns_challenge(authz):
for combo in authz.body.resolved_combinations:
@ -45,12 +43,13 @@ class AuthorizationRecord(object):
self.change_id = change_id
def start_dns_challenge(acme_client, account_number, host):
def start_dns_challenge(acme_client, account_number, host, dns_provider):
current_app.logger.debug("Starting DNS challenge for {0}".format(host))
authz = acme_client.request_domain_challenges(host)
[dns_challenge] = find_dns_challenge(authz)
change_id = create_txt_record(
change_id = dns_provider.create_txt_record(
dns_challenge.validation_domain_name(host),
dns_challenge.validation(acme_client.key),
account_number
@ -64,8 +63,8 @@ def start_dns_challenge(acme_client, account_number, host):
)
def complete_dns_challenge(acme_client, account_number, authz_record):
wait_for_r53_change(authz_record.change_id, account_number=account_number)
def complete_dns_challenge(acme_client, account_number, authz_record, dns_provider):
dns_provider.wait_for_dns_change(authz_record.change_id, account_number=account_number)
response = authz_record.dns_challenge.response(acme_client.key)
@ -96,12 +95,12 @@ def request_certificate(acme_client, authorizations, csr):
OpenSSL.crypto.FILETYPE_PEM, cert_response.body
).decode('utf-8')
# https://github.com/alex/letsencrypt-aws/commit/853ea7f93f141fe18d9ef12aee6b3388f98b4830
pem_certificate_chain = b"\n".join(
OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
pem_certificate_chain = "\n".join(
OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert.decode("utf-8"))
for cert in acme_client.fetch_chain(cert_response)
).decode('utf-8')
current_app.logger.debug("{0} {1}".format(type(pem_certificate). type(pem_certificate_chain)))
return pem_certificate, pem_certificate_chain
@ -113,12 +112,15 @@ def setup_acme_client():
key = jose.JWKRSA(key=generate_private_key('RSA2048'))
current_app.logger.debug("Connecting with directory at {0}".format(directory_url))
client = Client(directory_url, key)
registration = client.register(
messages.NewRegistration.from_data(email=email)
)
current_app.logger.debug("Connected: {0}".format(registration.uri))
client.agree_to_tos(registration)
return client, registration
@ -129,26 +131,30 @@ def get_domains(options):
:param options:
:return:
"""
current_app.logger.debug("Fetching domains")
domains = [options['common_name']]
if options.get('extensions'):
for name in options['extensions']['sub_alt_names']['names']:
domains.append(name.value)
domains.append(name)
current_app.logger.debug("Got these domains: {0}".format(domains))
return domains
def get_authorizations(acme_client, account_number, domains):
def get_authorizations(acme_client, account_number, domains, dns_provider):
authorizations = []
try:
for domain in domains:
authz_record = start_dns_challenge(acme_client, account_number, domain)
authz_record = start_dns_challenge(acme_client, account_number, domain, dns_provider)
authorizations.append(authz_record)
for authz_record in authorizations:
complete_dns_challenge(acme_client, account_number, authz_record)
complete_dns_challenge(acme_client, account_number, authz_record, dns_provider)
finally:
for authz_record in authorizations:
dns_challenge = authz_record.dns_challenge
delete_txt_record(
dns_provider.delete_txt_record(
authz_record.change_id,
account_number,
dns_challenge.validation_domain_name(authz_record.host),
@ -177,6 +183,9 @@ class ACMEIssuerPlugin(IssuerPlugin):
]
validate_conf(current_app, required_vars)
self.dns_provider_name = current_app.config.get('ACME_DNS_PROVIDER', 'route53')
current_app.logger.debug("Using DNS provider: {0}".format(self.dns_provider_name))
self.dns_provider = __import__(self.dns_provider_name, globals(), locals(), [], 1)
super(ACMEIssuerPlugin, self).__init__(*args, **kwargs)
def create_certificate(self, csr, issuer_options):
@ -191,7 +200,7 @@ class ACMEIssuerPlugin(IssuerPlugin):
acme_client, registration = setup_acme_client()
account_number = current_app.config.get('ACME_AWS_ACCOUNT_NUMBER')
domains = get_domains(issuer_options)
authorizations = get_authorizations(acme_client, account_number, domains)
authorizations = get_authorizations(acme_client, account_number, domains, self.dns_provider)
pem_certificate, pem_certificate_chain = request_certificate(acme_client, authorizations, csr)
# TODO add external ID (if possible)
return pem_certificate, pem_certificate_chain, None

View File

@ -3,7 +3,7 @@ from lemur.plugins.lemur_aws.sts import sts_client
@sts_client('route53')
def wait_for_r53_change(change_id, client=None):
def wait_for_dns_change(change_id, client=None):
_, change_id = change_id
while True:

View File

@ -34,6 +34,7 @@ with open(os.path.join(ROOT, 'lemur', '__about__.py')) as f:
install_requires = [
'CloudFlare==1.7.5',
'Flask==0.12',
'Flask-RESTful==0.3.6',
'Flask-SQLAlchemy==2.1',