Refactor Acme plugin into AcmeChallenge objects, dns01

This commit is contained in:
Mathias Petermann 2020-10-21 17:01:12 +02:00
parent b91cebf245
commit 812e1dee92
4 changed files with 118 additions and 115 deletions

View File

@ -3,6 +3,7 @@ from flask_script import Manager
import sys import sys
from lemur.constants import SUCCESS_METRIC_STATUS 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.dns_providers.service import get_all_dns_providers, set_domains
from lemur.extensions import metrics, sentry from lemur.extensions import metrics, sentry
from lemur.plugins.base import plugins from lemur.plugins.base import plugins
@ -19,7 +20,7 @@ def get_all_zones():
""" """
print("[+] Starting dns provider zone lookup and configuration.") print("[+] Starting dns provider zone lookup and configuration.")
dns_providers = get_all_dns_providers() 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}" function = f"{__name__}.{sys._getframe().f_code.co_name}"
log_data = { log_data = {
@ -29,7 +30,7 @@ def get_all_zones():
for dns_provider in dns_providers: for dns_provider in dns_providers:
try: try:
zones = acme_plugin.get_all_zones(dns_provider) zones = acme_dns_handler.get_all_zones(dns_provider)
set_domains(dns_provider, zones) set_domains(dns_provider, zones)
except Exception as e: except Exception as e:
print("[+] Error with DNS Provider {}: {}".format(dns_provider.name, e)) print("[+] Error with DNS Provider {}: {}".format(dns_provider.name, e))

View File

@ -229,6 +229,12 @@ class AcmeDnsHandler(AcmeHandler):
current_app.logger.error(f"Unable to fetch DNS Providers: {e}") current_app.logger.error(f"Unable to fetch DNS Providers: {e}")
self.all_dns_providers = [] 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): def get_dns_challenges(self, host, authorizations):
"""Get dns challenges for provided domain""" """Get dns challenges for provided domain"""
@ -393,27 +399,8 @@ class AcmeDnsHandler(AcmeHandler):
def finalize_authorizations(self, acme_client, authorizations): def finalize_authorizations(self, acme_client, authorizations):
for authz_record in authorizations: for authz_record in authorizations:
self.complete_dns_challenge(acme_client, authz_record) self.complete_dns_challenge(acme_client, authz_record)
for authz_record in authorizations:
dns_challenges = authz_record.dns_challenge self.cleanup_dns_challenges(acme_client, authorizations)
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),
)
return authorizations return authorizations

View File

@ -14,11 +14,15 @@ import OpenSSL
from acme import challenges from acme import challenges
from flask import current_app 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.plugins.base import plugins
from lemur.destinations import service as destination_service from lemur.destinations import service as destination_service
from lemur.destinations.models import Destination 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): class AcmeChallengeMissmatchError(LemurException):
@ -53,10 +57,11 @@ class AcmeChallenge(object):
""" """
raise NotImplementedError 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 Ideally the challenge should be cleaned up, after the validation is done
:param challenge: Needed to identify the challenge to be removed :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 :param validation_target: Needed to remove the validation
""" """
raise NotImplementedError raise NotImplementedError
@ -73,9 +78,9 @@ class AcmeHttpChallenge(AcmeChallenge):
:param issuer_options: :param issuer_options:
:return: :raise Exception: :return: :raise Exception:
""" """
acme = AcmeHandler() self.acme = AcmeHandler()
authority = issuer_options.get("authority") 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) orderr = acme_client.new_order(csr)
@ -149,15 +154,101 @@ class AcmeHttpChallenge(AcmeChallenge):
return response return response
def cleanup(self, challenge, validation_target): def cleanup(self, challenge, acme_client, validation_target):
pass pass
class AcmeDnsChallenge(AcmeChallenge): class AcmeDnsChallenge(AcmeChallenge):
challengeType = challenges.DNS01 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): def deploy(self, challenge, acme_client, validation_target):
pass pass
def cleanup(self, challenge, validation_target): def cleanup(self, authorizations, acme_client, validation_target):
pass """
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)

View File

@ -11,7 +11,6 @@
.. moduleauthor:: Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com> .. moduleauthor:: Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com>
.. moduleauthor:: Curtis Castrapel <ccastrapel@netflix.com> .. moduleauthor:: Curtis Castrapel <ccastrapel@netflix.com>
""" """
import json
import OpenSSL.crypto import OpenSSL.crypto
import josepy as jose import josepy as jose
@ -23,14 +22,13 @@ from flask import current_app
from lemur.authorizations import service as authorization_service from lemur.authorizations import service as authorization_service
from lemur.dns_providers import service as dns_provider_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.extensions import metrics, sentry
from lemur.plugins import lemur_acme as acme from lemur.plugins import lemur_acme as acme
from lemur.plugins.bases import IssuerPlugin 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.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): class ACMEIssuerPlugin(IssuerPlugin):
@ -84,28 +82,6 @@ class ACMEIssuerPlugin(IssuerPlugin):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ACMEIssuerPlugin, self).__init__(*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): def get_ordered_certificate(self, pending_cert):
self.acme = AcmeDnsHandler() self.acme = AcmeDnsHandler()
acme_client, registration = self.acme.setup_acme_client(pending_cert.authority) 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): def get_ordered_certificates(self, pending_certs):
self.acme = AcmeDnsHandler() self.acme = AcmeDnsHandler()
self.acme_dns_challenge = AcmeDnsChallenge()
pending = [] pending = []
certs = [] certs = []
for pending_cert in pending_certs: for pending_cert in pending_certs:
@ -250,76 +227,23 @@ class ACMEIssuerPlugin(IssuerPlugin):
} }
) )
# Ensure DNS records get deleted # Ensure DNS records get deleted
self.acme.cleanup_dns_challenges( self.acme_dns_challenge.cleanup(
entry["acme_client"], entry["authorizations"] entry["authorizations"], entry["acme_client"]
) )
return certs return certs
def create_certificate(self, csr, issuer_options): def create_certificate(self, csr, issuer_options):
""" """
Creates an ACME certificate. Creates an ACME certificate using the DNS-01 challenge.
:param csr: :param csr:
:param issuer_options: :param issuer_options:
:return: :raise Exception: :return: :raise Exception:
""" """
self.acme = AcmeDnsHandler() acme_dns_challenge = AcmeDnsChallenge()
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: return acme_dns_challenge.create_certificate(csr, issuer_options)
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
@staticmethod @staticmethod
def create_authority(options): def create_authority(options):