From 812e1dee9253f2073ffe4c22cb0a2a8eecbc14d9 Mon Sep 17 00:00:00 2001 From: Mathias Petermann Date: Wed, 21 Oct 2020 17:01:12 +0200 Subject: [PATCH] Refactor Acme plugin into AcmeChallenge objects, dns01 --- lemur/dns_providers/cli.py | 5 +- lemur/plugins/lemur_acme/acme_handlers.py | 29 ++---- lemur/plugins/lemur_acme/challenge_types.py | 107 ++++++++++++++++++-- lemur/plugins/lemur_acme/plugin.py | 92 ++--------------- 4 files changed, 118 insertions(+), 115 deletions(-) diff --git a/lemur/dns_providers/cli.py b/lemur/dns_providers/cli.py index b14e0339..8f125cf8 100644 --- a/lemur/dns_providers/cli.py +++ b/lemur/dns_providers/cli.py @@ -3,6 +3,7 @@ from flask_script import Manager import sys from lemur.constants import SUCCESS_METRIC_STATUS +from lemur.plugins.lemur_acme.acme_handlers import AcmeDnsHandler from lemur.dns_providers.service import get_all_dns_providers, set_domains from lemur.extensions import metrics, sentry from lemur.plugins.base import plugins @@ -19,7 +20,7 @@ def get_all_zones(): """ print("[+] Starting dns provider zone lookup and configuration.") dns_providers = get_all_dns_providers() - acme_plugin = plugins.get("acme-issuer") + acme_dns_handler = AcmeDnsHandler() function = f"{__name__}.{sys._getframe().f_code.co_name}" log_data = { @@ -29,7 +30,7 @@ def get_all_zones(): for dns_provider in dns_providers: try: - zones = acme_plugin.get_all_zones(dns_provider) + zones = acme_dns_handler.get_all_zones(dns_provider) set_domains(dns_provider, zones) except Exception as e: print("[+] Error with DNS Provider {}: {}".format(dns_provider.name, e)) diff --git a/lemur/plugins/lemur_acme/acme_handlers.py b/lemur/plugins/lemur_acme/acme_handlers.py index ef1a8ece..e283e771 100644 --- a/lemur/plugins/lemur_acme/acme_handlers.py +++ b/lemur/plugins/lemur_acme/acme_handlers.py @@ -229,6 +229,12 @@ class AcmeDnsHandler(AcmeHandler): current_app.logger.error(f"Unable to fetch DNS Providers: {e}") self.all_dns_providers = [] + def get_all_zones(self, dns_provider): + dns_provider_options = json.loads(dns_provider.credentials) + account_number = dns_provider_options.get("account_id") + dns_provider_plugin = self.get_dns_provider(dns_provider.provider_type) + return dns_provider_plugin.get_zones(account_number=account_number) + def get_dns_challenges(self, host, authorizations): """Get dns challenges for provided domain""" @@ -393,27 +399,8 @@ class AcmeDnsHandler(AcmeHandler): def finalize_authorizations(self, acme_client, authorizations): for authz_record in authorizations: self.complete_dns_challenge(acme_client, authz_record) - for authz_record in authorizations: - dns_challenges = authz_record.dns_challenge - for dns_challenge in dns_challenges: - dns_providers = self.dns_providers_for_domain.get(authz_record.host) - for dns_provider in dns_providers: - # Grab account number (For Route53) - dns_provider_plugin = self.get_dns_provider( - dns_provider.provider_type - ) - dns_provider_options = json.loads(dns_provider.credentials) - account_number = dns_provider_options.get("account_id") - host_to_validate, _ = self.strip_wildcard(authz_record.host) - host_to_validate = self.maybe_add_extension( - host_to_validate, dns_provider_options - ) - dns_provider_plugin.delete_txt_record( - authz_record.change_id, - account_number, - dns_challenge.validation_domain_name(host_to_validate), - dns_challenge.validation(acme_client.client.net.key), - ) + + self.cleanup_dns_challenges(acme_client, authorizations) return authorizations diff --git a/lemur/plugins/lemur_acme/challenge_types.py b/lemur/plugins/lemur_acme/challenge_types.py index 17d750f3..92fb8903 100644 --- a/lemur/plugins/lemur_acme/challenge_types.py +++ b/lemur/plugins/lemur_acme/challenge_types.py @@ -14,11 +14,15 @@ import OpenSSL from acme import challenges from flask import current_app -from lemur.exceptions import LemurException +from lemur.dns_providers import service as dns_provider_service +from lemur.extensions import metrics, sentry + +from lemur.authorizations import service as authorization_service +from lemur.exceptions import LemurException, InvalidConfiguration from lemur.plugins.base import plugins from lemur.destinations import service as destination_service from lemur.destinations.models import Destination -from lemur.plugins.lemur_acme.acme_handlers import AcmeHandler +from lemur.plugins.lemur_acme.acme_handlers import AcmeHandler, AcmeDnsHandler class AcmeChallengeMissmatchError(LemurException): @@ -53,10 +57,11 @@ class AcmeChallenge(object): """ raise NotImplementedError - def cleanup(self, challenge, validation_target): + def cleanup(self, challenge, acme_client, validation_target): """ Ideally the challenge should be cleaned up, after the validation is done :param challenge: Needed to identify the challenge to be removed + :param acme_client: an already bootstrapped acme_client, to avoid passing all issuer_options and so on :param validation_target: Needed to remove the validation """ raise NotImplementedError @@ -73,9 +78,9 @@ class AcmeHttpChallenge(AcmeChallenge): :param issuer_options: :return: :raise Exception: """ - acme = AcmeHandler() + self.acme = AcmeHandler() authority = issuer_options.get("authority") - acme_client, registration = acme.setup_acme_client(authority) + acme_client, registration = self.acme.setup_acme_client(authority) orderr = acme_client.new_order(csr) @@ -149,15 +154,101 @@ class AcmeHttpChallenge(AcmeChallenge): return response - def cleanup(self, challenge, validation_target): + def cleanup(self, challenge, acme_client, validation_target): pass class AcmeDnsChallenge(AcmeChallenge): challengeType = challenges.DNS01 + def __init__(self): + self.dns_providers_for_domain = {} + try: + self.all_dns_providers = dns_provider_service.get_all_dns_providers() + except Exception as e: + metrics.send("AcmeHandler_init_error", "counter", 1) + sentry.captureException() + current_app.logger.error(f"Unable to fetch DNS Providers: {e}") + self.all_dns_providers = [] + + def create_certificate(self, csr, issuer_options): + """ + Creates an ACME certificate. + + :param csr: + :param issuer_options: + :return: :raise Exception: + """ + self.acme = AcmeDnsHandler() + authority = issuer_options.get("authority") + create_immediately = issuer_options.get("create_immediately", False) + acme_client, registration = self.acme.setup_acme_client(authority) + dns_provider = issuer_options.get("dns_provider", {}) + + if dns_provider: + dns_provider_options = dns_provider.options + credentials = json.loads(dns_provider.credentials) + current_app.logger.debug( + "Using DNS provider: {0}".format(dns_provider.provider_type) + ) + dns_provider_plugin = __import__( + dns_provider.provider_type, globals(), locals(), [], 1 + ) + account_number = credentials.get("account_id") + provider_type = dns_provider.provider_type + if provider_type == "route53" and not account_number: + error = "Route53 DNS Provider {} does not have an account number configured.".format( + dns_provider.name + ) + current_app.logger.error(error) + raise InvalidConfiguration(error) + else: + dns_provider = {} + dns_provider_options = None + account_number = None + provider_type = None + + domains = self.acme.get_domains(issuer_options) + if not create_immediately: + # Create pending authorizations that we'll need to do the creation + dns_authorization = authorization_service.create( + account_number, domains, provider_type + ) + # Return id of the DNS Authorization + return None, None, dns_authorization.id + + authorizations = self.acme.get_authorizations( + acme_client, + account_number, + domains, + dns_provider_plugin, + dns_provider_options, + ) + self.acme.finalize_authorizations( + acme_client, + account_number, + dns_provider_plugin, + authorizations, + dns_provider_options, + ) + pem_certificate, pem_certificate_chain = self.acme.request_certificate( + acme_client, authorizations, csr + ) + # TODO add external ID (if possible) + return pem_certificate, pem_certificate_chain, None + def deploy(self, challenge, acme_client, validation_target): pass - def cleanup(self, challenge, validation_target): - pass + def cleanup(self, authorizations, acme_client, validation_target): + """ + Best effort attempt to delete DNS challenges that may not have been deleted previously. This is usually called + on an exception + + :param authorizations: all the authorizations to be cleaned up + :param acme_client: an already bootstrapped acme_client, to avoid passing all issuer_options and so on + :param validation_target: Unused right now + :return: + """ + acme = AcmeDnsHandler() + acme.cleanup_dns_challenges(acme_client, authorizations) diff --git a/lemur/plugins/lemur_acme/plugin.py b/lemur/plugins/lemur_acme/plugin.py index f97bdb63..fe95a51f 100644 --- a/lemur/plugins/lemur_acme/plugin.py +++ b/lemur/plugins/lemur_acme/plugin.py @@ -11,7 +11,6 @@ .. moduleauthor:: Mikhail Khodorovskiy .. moduleauthor:: Curtis Castrapel """ -import json import OpenSSL.crypto import josepy as jose @@ -23,14 +22,13 @@ from flask import current_app from lemur.authorizations import service as authorization_service from lemur.dns_providers import service as dns_provider_service -from lemur.exceptions import InvalidConfiguration, UnknownProvider +from lemur.exceptions import InvalidConfiguration from lemur.extensions import metrics, sentry from lemur.plugins import lemur_acme as acme from lemur.plugins.bases import IssuerPlugin -from lemur.plugins.lemur_acme import cloudflare, dyn, route53, ultradns, powerdns from lemur.plugins.lemur_acme.acme_handlers import AcmeHandler, AcmeDnsHandler -from lemur.plugins.lemur_acme.challenge_types import AcmeHttpChallenge +from lemur.plugins.lemur_acme.challenge_types import AcmeHttpChallenge, AcmeDnsChallenge class ACMEIssuerPlugin(IssuerPlugin): @@ -84,28 +82,6 @@ class ACMEIssuerPlugin(IssuerPlugin): def __init__(self, *args, **kwargs): super(ACMEIssuerPlugin, self).__init__(*args, **kwargs) - def get_dns_provider(self, type): - self.acme = AcmeDnsHandler() - - provider_types = { - "cloudflare": cloudflare, - "dyn": dyn, - "route53": route53, - "ultradns": ultradns, - "powerdns": powerdns - } - provider = provider_types.get(type) - if not provider: - raise UnknownProvider("No such DNS provider: {}".format(type)) - return provider - - def get_all_zones(self, dns_provider): - self.acme = AcmeDnsHandler() - dns_provider_options = json.loads(dns_provider.credentials) - account_number = dns_provider_options.get("account_id") - dns_provider_plugin = self.get_dns_provider(dns_provider.provider_type) - return dns_provider_plugin.get_zones(account_number=account_number) - def get_ordered_certificate(self, pending_cert): self.acme = AcmeDnsHandler() acme_client, registration = self.acme.setup_acme_client(pending_cert.authority) @@ -154,6 +130,7 @@ class ACMEIssuerPlugin(IssuerPlugin): def get_ordered_certificates(self, pending_certs): self.acme = AcmeDnsHandler() + self.acme_dns_challenge = AcmeDnsChallenge() pending = [] certs = [] for pending_cert in pending_certs: @@ -250,76 +227,23 @@ class ACMEIssuerPlugin(IssuerPlugin): } ) # Ensure DNS records get deleted - self.acme.cleanup_dns_challenges( - entry["acme_client"], entry["authorizations"] + self.acme_dns_challenge.cleanup( + entry["authorizations"], entry["acme_client"] ) return certs def create_certificate(self, csr, issuer_options): """ - Creates an ACME certificate. + Creates an ACME certificate using the DNS-01 challenge. :param csr: :param issuer_options: :return: :raise Exception: """ - self.acme = AcmeDnsHandler() - authority = issuer_options.get("authority") - create_immediately = issuer_options.get("create_immediately", False) - acme_client, registration = self.acme.setup_acme_client(authority) - dns_provider = issuer_options.get("dns_provider", {}) + acme_dns_challenge = AcmeDnsChallenge() - if dns_provider: - dns_provider_options = dns_provider.options - credentials = json.loads(dns_provider.credentials) - current_app.logger.debug( - "Using DNS provider: {0}".format(dns_provider.provider_type) - ) - dns_provider_plugin = __import__( - dns_provider.provider_type, globals(), locals(), [], 1 - ) - account_number = credentials.get("account_id") - provider_type = dns_provider.provider_type - if provider_type == "route53" and not account_number: - error = "Route53 DNS Provider {} does not have an account number configured.".format( - dns_provider.name - ) - current_app.logger.error(error) - raise InvalidConfiguration(error) - else: - dns_provider = {} - dns_provider_options = None - account_number = None - provider_type = None + return acme_dns_challenge.create_certificate(csr, issuer_options) - domains = self.acme.get_domains(issuer_options) - if not create_immediately: - # Create pending authorizations that we'll need to do the creation - dns_authorization = authorization_service.create( - account_number, domains, provider_type - ) - # Return id of the DNS Authorization - return None, None, dns_authorization.id - - authorizations = self.acme.get_authorizations( - acme_client, - account_number, - domains, - dns_provider_plugin, - dns_provider_options, - ) - self.acme.finalize_authorizations( - acme_client, - account_number, - dns_provider_plugin, - authorizations, - dns_provider_options, - ) - pem_certificate, pem_certificate_chain = self.acme.request_certificate( - acme_client, authorizations, csr - ) - # TODO add external ID (if possible) - return pem_certificate, pem_certificate_chain, None @staticmethod def create_authority(options):