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==0.12
|
||||||
Flask-RESTful==0.3.6
|
Flask-RESTful==0.3.6
|
||||||
Flask-SQLAlchemy==2.1
|
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.bases import IssuerPlugin
|
||||||
from lemur.plugins import lemur_acme as acme
|
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):
|
def find_dns_challenge(authz):
|
||||||
for combo in authz.body.resolved_combinations:
|
for combo in authz.body.resolved_combinations:
|
||||||
|
@ -45,12 +43,13 @@ class AuthorizationRecord(object):
|
||||||
self.change_id = change_id
|
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)
|
authz = acme_client.request_domain_challenges(host)
|
||||||
|
|
||||||
[dns_challenge] = find_dns_challenge(authz)
|
[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_domain_name(host),
|
||||||
dns_challenge.validation(acme_client.key),
|
dns_challenge.validation(acme_client.key),
|
||||||
account_number
|
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):
|
def complete_dns_challenge(acme_client, account_number, authz_record, dns_provider):
|
||||||
wait_for_r53_change(authz_record.change_id, account_number=account_number)
|
dns_provider.wait_for_dns_change(authz_record.change_id, account_number=account_number)
|
||||||
|
|
||||||
response = authz_record.dns_challenge.response(acme_client.key)
|
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
|
OpenSSL.crypto.FILETYPE_PEM, cert_response.body
|
||||||
).decode('utf-8')
|
).decode('utf-8')
|
||||||
|
|
||||||
# https://github.com/alex/letsencrypt-aws/commit/853ea7f93f141fe18d9ef12aee6b3388f98b4830
|
pem_certificate_chain = "\n".join(
|
||||||
pem_certificate_chain = b"\n".join(
|
OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert.decode("utf-8"))
|
||||||
OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
|
|
||||||
for cert in acme_client.fetch_chain(cert_response)
|
for cert in acme_client.fetch_chain(cert_response)
|
||||||
).decode('utf-8')
|
).decode('utf-8')
|
||||||
|
|
||||||
|
current_app.logger.debug("{0} {1}".format(type(pem_certificate). type(pem_certificate_chain)))
|
||||||
return pem_certificate, 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'))
|
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)
|
client = Client(directory_url, key)
|
||||||
|
|
||||||
registration = client.register(
|
registration = client.register(
|
||||||
messages.NewRegistration.from_data(email=email)
|
messages.NewRegistration.from_data(email=email)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
current_app.logger.debug("Connected: {0}".format(registration.uri))
|
||||||
|
|
||||||
client.agree_to_tos(registration)
|
client.agree_to_tos(registration)
|
||||||
return client, registration
|
return client, registration
|
||||||
|
|
||||||
|
@ -129,26 +131,30 @@ def get_domains(options):
|
||||||
:param options:
|
:param options:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
current_app.logger.debug("Fetching domains")
|
||||||
|
|
||||||
domains = [options['common_name']]
|
domains = [options['common_name']]
|
||||||
if options.get('extensions'):
|
if options.get('extensions'):
|
||||||
for name in options['extensions']['sub_alt_names']['names']:
|
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
|
return domains
|
||||||
|
|
||||||
|
|
||||||
def get_authorizations(acme_client, account_number, domains):
|
def get_authorizations(acme_client, account_number, domains, dns_provider):
|
||||||
authorizations = []
|
authorizations = []
|
||||||
try:
|
try:
|
||||||
for domain in domains:
|
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)
|
authorizations.append(authz_record)
|
||||||
|
|
||||||
for authz_record in authorizations:
|
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:
|
finally:
|
||||||
for authz_record in authorizations:
|
for authz_record in authorizations:
|
||||||
dns_challenge = authz_record.dns_challenge
|
dns_challenge = authz_record.dns_challenge
|
||||||
delete_txt_record(
|
dns_provider.delete_txt_record(
|
||||||
authz_record.change_id,
|
authz_record.change_id,
|
||||||
account_number,
|
account_number,
|
||||||
dns_challenge.validation_domain_name(authz_record.host),
|
dns_challenge.validation_domain_name(authz_record.host),
|
||||||
|
@ -177,6 +183,9 @@ class ACMEIssuerPlugin(IssuerPlugin):
|
||||||
]
|
]
|
||||||
|
|
||||||
validate_conf(current_app, required_vars)
|
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)
|
super(ACMEIssuerPlugin, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def create_certificate(self, csr, issuer_options):
|
def create_certificate(self, csr, issuer_options):
|
||||||
|
@ -191,7 +200,7 @@ class ACMEIssuerPlugin(IssuerPlugin):
|
||||||
acme_client, registration = setup_acme_client()
|
acme_client, registration = setup_acme_client()
|
||||||
account_number = current_app.config.get('ACME_AWS_ACCOUNT_NUMBER')
|
account_number = current_app.config.get('ACME_AWS_ACCOUNT_NUMBER')
|
||||||
domains = get_domains(issuer_options)
|
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)
|
pem_certificate, pem_certificate_chain = request_certificate(acme_client, authorizations, csr)
|
||||||
# TODO add external ID (if possible)
|
# TODO add external ID (if possible)
|
||||||
return pem_certificate, pem_certificate_chain, None
|
return pem_certificate, pem_certificate_chain, None
|
||||||
|
|
|
@ -3,7 +3,7 @@ from lemur.plugins.lemur_aws.sts import sts_client
|
||||||
|
|
||||||
|
|
||||||
@sts_client('route53')
|
@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
|
_, change_id = change_id
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
Loading…
Reference in New Issue