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:
parent
2578970f7d
commit
5d18838868
|
@ -1,3 +1,4 @@
|
|||
CloudFlare==1.7.5
|
||||
Flask==0.12
|
||||
Flask-RESTful==0.3.6
|
||||
Flask-SQLAlchemy==2.1
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue