From 2a6dda07ebb57814a994a9f6428edb2c5afff410 Mon Sep 17 00:00:00 2001 From: Curtis Castrapel Date: Fri, 27 Jul 2018 14:15:14 -0700 Subject: [PATCH 1/3] Show and send error for pending certs --- lemur/notifications/messaging.py | 39 +++++ lemur/pending_certificates/cli.py | 29 ++++ lemur/plugins/lemur_acme/plugin.py | 3 +- .../plugins/lemur_email/templates/failed.html | 161 ++++++++++++++++++ .../pending_certificates/view/view.tpl.html | 6 + 5 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 lemur/plugins/lemur_email/templates/failed.html diff --git a/lemur/notifications/messaging.py b/lemur/notifications/messaging.py index 4600ac61..cd4ff0f1 100644 --- a/lemur/notifications/messaging.py +++ b/lemur/notifications/messaging.py @@ -24,6 +24,7 @@ from lemur.common.utils import windowed_query from lemur.certificates.schemas import certificate_notification_output_schema from lemur.certificates.models import Certificate +from lemur.pending_certificates.schemas import pending_certificate_output_schema from lemur.plugins import plugins from lemur.plugins.utils import get_plugin_option @@ -172,6 +173,44 @@ def send_rotation_notification(certificate, notification_plugin=None): return True +def send_pending_failure_notification(pending_cert, notify_owner=True, notify_security=True, notification_plugin=None): + """ + Sends a report to certificate owners when their pending certificate failed to be created. + + :param pending_cert: + :param notification_plugin: + :return: + """ + status = FAILURE_METRIC_STATUS + + if not notification_plugin: + notification_plugin = plugins.get( + current_app.config.get('LEMUR_DEFAULT_NOTIFICATION_PLUGIN', 'email-notification') + ) + + data = pending_certificate_output_schema.dump(pending_cert).data + data["security_email"] = current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL') + + if notify_owner: + try: + notification_plugin.send('failed', data, [data['owner']], pending_cert) + status = SUCCESS_METRIC_STATUS + except Exception as e: + sentry.captureException() + + if notify_security: + try: + notification_plugin.send('failed', data, data["security_email"], pending_cert) + status = SUCCESS_METRIC_STATUS + except Exception as e: + sentry.captureException() + + metrics.send('notification', 'counter', 1, metric_tags={'status': status, 'event_type': 'rotation'}) + + if status == SUCCESS_METRIC_STATUS: + return True + + def needs_notification(certificate): """ Determine if notifications for a given certificate should diff --git a/lemur/pending_certificates/cli.py b/lemur/pending_certificates/cli.py index 6e12c53b..fd7591b1 100644 --- a/lemur/pending_certificates/cli.py +++ b/lemur/pending_certificates/cli.py @@ -4,9 +4,15 @@ .. moduleauthor:: James Chuong .. moduleauthor:: Curtis Castrapel """ + +import copy +import sys + +from flask import current_app from flask_script import Manager from lemur.authorities.service import get as get_authority +from lemur.notifications.messaging import send_pending_failure_notification from lemur.pending_certificates import service as pending_certificate_service from lemur.plugins.base import plugins from lemur.users import service as user_service @@ -56,6 +62,10 @@ def fetch_all_acme(): for acme-issued certificates because it will configure all of the DNS challenges prior to resolving any certificates. """ + + log_data = { + "function": "{}.{}".format(__name__, sys._getframe().f_code.co_name) + } pending_certs = pending_certificate_service.get_pending_certs('all') user = user_service.get_by_username('lemur') new = 0 @@ -88,7 +98,26 @@ def fetch_all_acme(): new += 1 else: pending_certificate_service.increment_attempt(pending_cert) + pending_certificate_service.update( + cert.get("pending_cert").id, + status=str(cert.get("last_error"))[0:128] + ) failed += 1 + if pending_cert.number_attempts > 0: + error_log = copy.deepcopy(log_data) + error_log["message"] = "Deleting pending certificate" + error_log["pending_cert_id"] = pending_cert.id + error_log["last_error"] = cert.get("last_error") + error_log["cn"] = pending_cert.cn + current_app.logger.error(error_log) + if 1 == 0: + send_pending_failure_notification(pending_cert, notify_owner=pending_cert.notify) + pending_certificate_service.delete_by_id(pending_cert.id) + log_data["message"] = "Complete" + log_data["new"] = new + log_data["failed"] = failed + log_data["wrong_issuer"] = wrong_issuer + current_app.logger.debug(log_data) print( "[+] Certificates: New: {new} Failed: {failed} Not using ACME: {wrong_issuer}".format( new=new, diff --git a/lemur/plugins/lemur_acme/plugin.py b/lemur/plugins/lemur_acme/plugin.py index f472a965..0d3e9c2a 100644 --- a/lemur/plugins/lemur_acme/plugin.py +++ b/lemur/plugins/lemur_acme/plugin.py @@ -300,11 +300,12 @@ class ACMEIssuerPlugin(IssuerPlugin): "order": order, "dns_provider_options": dns_provider_options, }) - except (ClientError, ValueError, Exception): + except (ClientError, ValueError, Exception) as e: current_app.logger.error("Unable to resolve pending cert: {}".format(pending_cert), exc_info=True) certs.append({ "cert": False, "pending_cert": pending_cert, + "last_error": e, }) for entry in pending: diff --git a/lemur/plugins/lemur_email/templates/failed.html b/lemur/plugins/lemur_email/templates/failed.html new file mode 100644 index 00000000..63e37fb5 --- /dev/null +++ b/lemur/plugins/lemur_email/templates/failed.html @@ -0,0 +1,161 @@ + + + + + + + + Lemur + + +
+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ Lemur +
+
+ + + + + + + + + + + + + + +
+ Your certificate request has failed! +
+
+ + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ Hi, +
This is a Lemur certificate failure notice. We were unable to create or rotate your certificate. Please retry your request. The reason for the failure is listed below. +
+ + + + + + + + +
+ {{ message.certificates.name }} +
+ +
{{ message.certificates.owner }} +
{{ message.certificates.status }} +
+
+
+ If you are having any trouble, please reach out to {{ ", ".join(message.certificates.security_email) }}. +
+
Best,
Lemur +
+ + + + + + +
*All times are in UTC
+
+
+
+ + + + + + + + + +
You received this mandatory email announcement to update you about + important changes to your TLS certificate. +
+
© 2016 Lemur
+
+
+
+
diff --git a/lemur/static/app/angular/pending_certificates/view/view.tpl.html b/lemur/static/app/angular/pending_certificates/view/view.tpl.html index 8aaf9f47..d480cc2d 100644 --- a/lemur/static/app/angular/pending_certificates/view/view.tpl.html +++ b/lemur/static/app/angular/pending_certificates/view/view.tpl.html @@ -80,6 +80,12 @@ {{ pendingCertificate.numberAttempts }} +
  • + Latest Status + + {{ pendingCertificate.status }} + +
  • Date Created From e16c1de00171c112d131da5c92663692bf5758a0 Mon Sep 17 00:00:00 2001 From: Curtis Castrapel Date: Fri, 27 Jul 2018 14:17:50 -0700 Subject: [PATCH 2/3] Error logging --- lemur/pending_certificates/cli.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lemur/pending_certificates/cli.py b/lemur/pending_certificates/cli.py index fd7591b1..647c5407 100644 --- a/lemur/pending_certificates/cli.py +++ b/lemur/pending_certificates/cli.py @@ -103,16 +103,17 @@ def fetch_all_acme(): status=str(cert.get("last_error"))[0:128] ) failed += 1 - if pending_cert.number_attempts > 0: - error_log = copy.deepcopy(log_data) + error_log = copy.deepcopy(log_data) + error_log["message"] = "Pending certificate creation failure" + error_log["pending_cert_id"] = pending_cert.id + error_log["last_error"] = cert.get("last_error") + error_log["cn"] = pending_cert.cn + + if pending_cert.number_attempts > 4: error_log["message"] = "Deleting pending certificate" - error_log["pending_cert_id"] = pending_cert.id - error_log["last_error"] = cert.get("last_error") - error_log["cn"] = pending_cert.cn - current_app.logger.error(error_log) - if 1 == 0: - send_pending_failure_notification(pending_cert, notify_owner=pending_cert.notify) - pending_certificate_service.delete_by_id(pending_cert.id) + send_pending_failure_notification(pending_cert, notify_owner=pending_cert.notify) + pending_certificate_service.delete_by_id(pending_cert.id) + current_app.logger.error(error_log) log_data["message"] = "Complete" log_data["new"] = new log_data["failed"] = failed From 2bb00bc666c2c237b018b8bfe47cc7bc4a3b0982 Mon Sep 17 00:00:00 2001 From: Curtis Castrapel Date: Fri, 27 Jul 2018 14:20:22 -0700 Subject: [PATCH 3/3] requirements --- requirements-dev.txt | 4 ++-- requirements-docs.txt | 8 ++++---- requirements-tests.txt | 6 +++--- requirements.txt | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 0a7369d9..2abd7a43 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,7 +10,7 @@ certifi==2018.4.16 # via requests cfgv==1.1.0 # via pre-commit chardet==3.0.4 # via requests flake8==3.5.0 -identify==1.1.3 # via pre-commit +identify==1.1.4 # via pre-commit idna==2.7 # via requests invoke==1.1.0 mccabe==0.6.1 # via flake8 @@ -24,7 +24,7 @@ requests-toolbelt==0.8.0 # via twine requests==2.19.1 # via requests-toolbelt, twine six==1.11.0 # via cfgv, pre-commit toml==0.9.4 # via pre-commit -tqdm==4.23.4 # via twine +tqdm==4.24.0 # via twine twine==1.11.0 urllib3==1.23 # via requests virtualenv==16.0.0 # via pre-commit diff --git a/requirements-docs.txt b/requirements-docs.txt index 5d779a84..2a688d6f 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -15,8 +15,8 @@ asyncpool==1.0 babel==2.6.0 # via sphinx bcrypt==3.1.4 blinker==1.4 -boto3==1.7.61 -botocore==1.10.61 +boto3==1.7.62 +botocore==1.10.62 certifi==2018.4.16 cffi==1.11.5 chardet==3.0.4 @@ -52,7 +52,7 @@ markupsafe==1.0 marshmallow-sqlalchemy==0.14.0 marshmallow==2.15.3 mock==2.0.0 -ndg-httpsclient==0.5.0 +ndg-httpsclient==0.5.1 packaging==17.1 # via sphinx paramiko==2.4.1 pbr==4.1.1 @@ -78,7 +78,7 @@ retrying==1.3.3 s3transfer==0.1.13 six==1.11.0 snowballstemmer==1.2.1 # via sphinx -sphinx-rtd-theme==0.4.0 +sphinx-rtd-theme==0.4.1 sphinx==1.7.6 sphinxcontrib-httpdomain==1.7.0 sphinxcontrib-websupport==1.1.0 # via sphinx diff --git a/requirements-tests.txt b/requirements-tests.txt index 22361c6f..c3405365 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -8,9 +8,9 @@ asn1crypto==0.24.0 # via cryptography atomicwrites==1.1.5 # via pytest attrs==18.1.0 # via pytest aws-xray-sdk==0.95 # via moto -boto3==1.7.62 # via moto +boto3==1.7.65 # via moto boto==2.49.0 # via moto -botocore==1.10.62 # via boto3, moto, s3transfer +botocore==1.10.65 # via boto3, moto, s3transfer certifi==2018.4.16 # via requests cffi==1.11.5 # via cryptography chardet==3.0.4 # via requests @@ -36,7 +36,7 @@ mock==2.0.0 # via moto more-itertools==4.2.0 # via pytest moto==1.3.3 nose==1.3.7 -pbr==4.1.1 # via mock +pbr==4.2.0 # via mock pluggy==0.6.0 # via pytest py==1.5.4 # via pytest pyaml==17.12.1 # via moto diff --git a/requirements.txt b/requirements.txt index 6af5cd92..0474e651 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,8 +13,8 @@ asn1crypto==0.24.0 # via cryptography asyncpool==1.0 bcrypt==3.1.4 # via flask-bcrypt, paramiko blinker==1.4 # via flask-mail, flask-principal, raven -boto3==1.7.62 -botocore==1.10.62 # via boto3, s3transfer +boto3==1.7.65 +botocore==1.10.65 # via boto3, s3transfer certifi==2018.4.16 cffi==1.11.5 # via bcrypt, cryptography, pynacl chardet==3.0.4 # via requests @@ -51,11 +51,11 @@ marshmallow==2.15.3 mock==2.0.0 # via acme ndg-httpsclient==0.5.1 paramiko==2.4.1 -pbr==4.1.1 # via mock +pbr==4.2.0 # via mock pem==18.1.0 psycopg2==2.7.5 pyasn1-modules==0.2.2 # via python-ldap -pyasn1==0.4.3 # via ndg-httpsclient, paramiko, pyasn1-modules, python-ldap +pyasn1==0.4.4 # via ndg-httpsclient, paramiko, pyasn1-modules, python-ldap pycparser==2.18 # via cffi pyjwt==1.6.4 pynacl==1.2.1 # via paramiko