From 85d99ded730d8155c2778b0cd64191d4108fe5dd Mon Sep 17 00:00:00 2001 From: Jasmine Schladen Date: Wed, 2 Dec 2020 09:20:09 -0800 Subject: [PATCH 1/6] Add email notifications for CA cert expiry --- docs/administration.rst | 9 + docs/developer/plugins/index.rst | 5 +- docs/production/index.rst | 20 +- lemur/common/celery.py | 36 ++++ lemur/notifications/cli.py | 29 ++- lemur/notifications/messaging.py | 79 ++++++++ .../templates/authority_expiration.html | 172 ++++++++++++++++++ lemur/tests/test_messaging.py | 45 +++++ 8 files changed, 390 insertions(+), 5 deletions(-) create mode 100644 lemur/plugins/lemur_email/templates/authority_expiration.html diff --git a/docs/administration.rst b/docs/administration.rst index 3ef484be..94b15829 100644 --- a/docs/administration.rst +++ b/docs/administration.rst @@ -348,6 +348,15 @@ Lemur supports sending certificate expiration notifications through SES and SMTP LEMUR_SECURITY_TEAM_EMAIL_INTERVALS = [15, 2] +.. data:: LEMUR_AUTHORITY_CERT_EXPIRATION_EMAIL_INTERVALS + :noindex: + + Notification interval set for CA certificate expiration notifications. If unspecified, the value [365, 180] is used (roughly one year and 6 months). + + :: + + LEMUR_AUTHORITY_CERT_EXPIRATION_EMAIL_INTERVALS = [365, 180] + Celery Options --------------- diff --git a/docs/developer/plugins/index.rst b/docs/developer/plugins/index.rst index c2a8c48a..ad45692e 100644 --- a/docs/developer/plugins/index.rst +++ b/docs/developer/plugins/index.rst @@ -215,12 +215,13 @@ Notification ------------ Lemur includes the ability to create Email notifications by **default**. These notifications -currently come in the form of expiration and rotation notices. Lemur periodically checks certificate expiration dates and +currently come in the form of expiration and rotation notices for all certificates, expiration notices for CA certificates, +and ACME certificate creation failure notices. Lemur periodically checks certificate expiration dates and determines if a given certificate is eligible for notification. There are currently only two parameters used to determine if a certificate is eligible; validity expiration (date the certificate is no longer valid) and the number of days the current date (UTC) is from that expiration date. -Expiration notifications can also be configured for Slack or AWS SNS. Rotation notifications are not configurable. +Certificate expiration notifications can also be configured for Slack or AWS SNS. Other notifications are not configurable. Notifications sent to a certificate owner and security team (`LEMUR_SECURITY_TEAM_EMAIL`) can currently only be sent via email. There are currently two objects that are available for notification plugins. The first is `NotificationPlugin`, which is the base object for diff --git a/docs/production/index.rst b/docs/production/index.rst index c6f561ca..d6e925a4 100644 --- a/docs/production/index.rst +++ b/docs/production/index.rst @@ -325,7 +325,7 @@ celery tasks or cron jobs that run these commands. There are currently three commands that could/should be run on a periodic basis: -- `notify` +- `notify expirations` and `notify authority_expirations` - `check_revoked` - `sync` @@ -334,13 +334,15 @@ If you are using LetsEncrypt, you must also run the following: - `fetch_all_pending_acme_certs` - `remove_old_acme_certs` -How often you run these commands is largely up to the user. `notify` and `check_revoked` are typically run at least once a day. +How often you run these commands is largely up to the user. `notify` should be run once a day (more often will result in +duplicate notifications). `check_revoked` is typically run at least once a day. `sync` is typically run every 15 minutes. `fetch_all_pending_acme_certs` should be ran frequently (Every minute is fine). `remove_old_acme_certs` can be ran more rarely, such as once every week. Example cron entries:: 0 22 * * * lemuruser export LEMUR_CONF=/Users/me/.lemur/lemur.conf.py; /www/lemur/bin/lemur notify expirations + 0 22 * * * lemuruser export LEMUR_CONF=/Users/me/.lemur/lemur.conf.py; /www/lemur/bin/lemur notify authority_expirations */15 * * * * lemuruser export LEMUR_CONF=/Users/me/.lemur/lemur.conf.py; /www/lemur/bin/lemur source sync -s all 0 22 * * * lemuruser export LEMUR_CONF=/Users/me/.lemur/lemur.conf.py; /www/lemur/bin/lemur certificate check_revoked @@ -382,6 +384,20 @@ Example Celery configuration (To be placed in your configuration file):: 'expires': 180 }, 'schedule': crontab(hour="*"), + }, + 'notify_expirations': { + 'task': 'lemur.common.celery.notify_expirations', + 'options': { + 'expires': 180 + }, + 'schedule': crontab(hour=22, minute=0), + }, + 'notify_authority_expirations': { + 'task': 'lemur.common.celery.notify_authority_expirations', + 'options': { + 'expires': 180 + }, + 'schedule': crontab(hour=22, minute=0), } } diff --git a/lemur/common/celery.py b/lemur/common/celery.py index f9d58bd9..f428927e 100644 --- a/lemur/common/celery.py +++ b/lemur/common/celery.py @@ -820,6 +820,42 @@ def notify_expirations(): return log_data +@celery.task(soft_time_limit=3600) +def notify_authority_expirations(): + """ + This celery task notifies about expiring certificate authority certs + :return: + """ + function = f"{__name__}.{sys._getframe().f_code.co_name}" + task_id = None + if celery.current_task: + task_id = celery.current_task.request.id + + log_data = { + "function": function, + "message": "notify for certificate authority cert expiration", + "task_id": task_id, + } + + if task_id and is_task_active(function, task_id, None): + log_data["message"] = "Skipping task: Task is already active" + current_app.logger.debug(log_data) + return + + current_app.logger.debug(log_data) + try: + cli_notification.authority_expirations() + except SoftTimeLimitExceeded: + log_data["message"] = "Notify expiring CA Time limit exceeded." + current_app.logger.error(log_data) + sentry.captureException() + metrics.send("celery.timeout", "counter", 1, metric_tags={"function": function}) + return + + metrics.send(f"{function}.success", "counter", 1) + return log_data + + @celery.task(soft_time_limit=3600) def enable_autorotate_for_certs_attached_to_endpoint(): """ diff --git a/lemur/notifications/cli.py b/lemur/notifications/cli.py index a2848117..7012e9c8 100644 --- a/lemur/notifications/cli.py +++ b/lemur/notifications/cli.py @@ -10,6 +10,7 @@ from flask_script import Manager from lemur.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS from lemur.extensions import sentry, metrics from lemur.notifications.messaging import send_expiration_notifications +from lemur.notifications.messaging import send_authority_expiration_notifications manager = Manager(usage="Handles notification related tasks.") @@ -24,7 +25,7 @@ manager = Manager(usage="Handles notification related tasks.") ) def expirations(exclude): """ - Runs Lemur's notification engine, that looks for expired certificates and sends + Runs Lemur's notification engine, that looks for expiring certificates and sends notifications out to those that have subscribed to them. Every certificate receives notifications by default. When expiration notifications are handled outside of Lemur @@ -50,3 +51,29 @@ def expirations(exclude): metrics.send( "expiration_notification_job", "counter", 1, metric_tags={"status": status} ) + + +def authority_expirations(): + """ + Runs Lemur's notification engine, that looks for expiring certificate authority certificates and sends + notifications out to the security team and owner. + + :return: + """ + status = FAILURE_METRIC_STATUS + try: + print("Starting to notify subscribers about expiring certificate authority certificates!") + success, failed = send_authority_expiration_notifications() + print( + "Finished notifying subscribers about expiring certificate authority certificates! " + "Sent: {success} Failed: {failed}".format( + success=success, failed=failed + ) + ) + status = SUCCESS_METRIC_STATUS + except Exception as e: + sentry.captureException() + + metrics.send( + "authority_expiration_notification_job", "counter", 1, metric_tags={"status": status} + ) diff --git a/lemur/notifications/messaging.py b/lemur/notifications/messaging.py index 2658e1a0..9b299a24 100644 --- a/lemur/notifications/messaging.py +++ b/lemur/notifications/messaging.py @@ -62,6 +62,34 @@ def get_certificates(exclude=None): return certs +def get_expiring_authority_certificates(): + """ + Finds all certificate authority certificates that are eligible for expiration notifications. + :return: + """ + now = arrow.utcnow() + authority_expiration_intervals = current_app.config.get("LEMUR_AUTHORITY_CERT_EXPIRATION_EMAIL_INTERVALS", + [365, 180]) + max_not_after = now + timedelta(days=max(authority_expiration_intervals) + 1) + + q = ( + database.db.session.query(Certificate) + .filter(Certificate.not_after < max_not_after) + .filter(Certificate.notify == true()) + .filter(Certificate.expired == false()) + .filter(Certificate.revoked == false()) + .filter(Certificate.root_authority_id.isnot(None)) + .filter(Certificate.authority_id.is_(None)) + ) + + certs = [] + for c in windowed_query(q, Certificate.id, 10000): + days_remaining = (c.not_after - now).days + if days_remaining in authority_expiration_intervals: + certs.append(c) + return certs + + def get_eligible_certificates(exclude=None): """ Finds all certificates that are eligible for certificate expiration notification. @@ -90,6 +118,25 @@ def get_eligible_certificates(exclude=None): return certificates +def get_eligible_authority_certificates(): + """ + Finds all certificate authority certificates that are eligible for certificate expiration notification. + Returns the set of all eligible CA certificates, grouped by owner and interval, with a list of applicable certs. + :return: + """ + certificates = defaultdict(dict) + all_certs = get_expiring_authority_certificates() + now = arrow.utcnow() + + # group by owner + for owner, owner_certs in groupby(all_certs, lambda x: x.owner): + # group by expiration interval + for interval, interval_certs in groupby(owner_certs, lambda x: (x.not_after - now).days): + certificates[owner][interval] = list(interval_certs) # list(c for c in interval_certs) + + return certificates + + def send_plugin_notification(event_type, data, recipients, notification): """ Executes the plugin and handles failure. @@ -176,6 +223,38 @@ def send_expiration_notifications(exclude): return success, failure +def send_authority_expiration_notifications(): + """ + This function will check for upcoming certificate authority certificate expiration, + and send out notification emails at configured intervals. + """ + success = failure = 0 + + # security team gets all + security_email = current_app.config.get("LEMUR_SECURITY_TEAM_EMAIL") + + for owner, owner_cert_groups in get_eligible_authority_certificates().items(): + for interval, certificates in owner_cert_groups.items(): + notification_data = [] + + for certificate in certificates: + cert_data = certificate_notification_output_schema.dump( + certificate + ).data + notification_data.append(cert_data) + + email_recipients = security_email + [owner] + if send_default_notification( + "authority_expiration", notification_data, email_recipients, + notification_options=[{'name': 'interval', 'value': interval}] + ): + success = len(email_recipients) + else: + failure = len(email_recipients) + + return success, failure + + def send_default_notification(notification_type, data, targets, notification_options=None): """ Sends a report to the specified target via the default notification plugin. Applicable for any notification_type. diff --git a/lemur/plugins/lemur_email/templates/authority_expiration.html b/lemur/plugins/lemur_email/templates/authority_expiration.html new file mode 100644 index 00000000..2c077bf5 --- /dev/null +++ b/lemur/plugins/lemur_email/templates/authority_expiration.html @@ -0,0 +1,172 @@ + + + + + + + + Lemur + + +
+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ Lemur +
+
+ + + + + + + + + + + + + + +
+ Your CA certificate(s) are expiring in {{ message.options | interval }} days! +
+
+ + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ Hi, +
+
This is a Lemur CA certificate expiration notice. The follow CA certificates are expiring soon; + please take manual action to renew them if necessary. You may also disable notifications via the + Notify toggle in Lemur if they are no longer in use. + + + {% for certificate in message.certificates %} + + + + + + {% if not loop.last %} + + + + {% endif %} + {% endfor %} + +
+ {{ certificate.name }} +
+ + {{ certificate.endpoints | length }} Endpoints +
{{ certificate.owner }} +
{{ certificate.validityEnd | time }} + Details +
+
+
+ Your action is required if the above CA certificates are still needed. +
+
Best,
Lemur +
+ + + + + + +
*All expiration times are in UTC
+
+
+
+ + + + + + + + + +
You received this mandatory email announcement to update you about + important changes to your TLS certificate. +
+
© 2020 Lemur
+
+
+
+
diff --git a/lemur/tests/test_messaging.py b/lemur/tests/test_messaging.py index 13b6c9b3..9a9a5ad3 100644 --- a/lemur/tests/test_messaging.py +++ b/lemur/tests/test_messaging.py @@ -5,6 +5,7 @@ import boto3 import pytest from freezegun import freeze_time from moto import mock_ses +from lemur.tests.factories import AuthorityFactory, CertificateFactory @mock_ses @@ -125,3 +126,47 @@ def test_send_pending_failure_notification(notification_plugin, async_issuer_plu verify_sender_email() assert send_pending_failure_notification(pending_certificate) + + +def test_get_authority_certificates(): + from lemur.notifications.messaging import get_expiring_authority_certificates + + certificate_1 = create_cert_that_expires_in_days(180) + certificate_2 = create_cert_that_expires_in_days(365) + create_cert_that_expires_in_days(364) + create_cert_that_expires_in_days(366) + create_cert_that_expires_in_days(179) + create_cert_that_expires_in_days(181) + create_cert_that_expires_in_days(1) + + assert set(get_expiring_authority_certificates()) == {certificate_1, certificate_2} + + +@mock_ses +def test_send_authority_expiration_notifications(): + from lemur.notifications.messaging import send_authority_expiration_notifications + verify_sender_email() + + create_cert_that_expires_in_days(180) + create_cert_that_expires_in_days(180) # two on the same day results in a single email + create_cert_that_expires_in_days(365) + create_cert_that_expires_in_days(364) + create_cert_that_expires_in_days(366) + create_cert_that_expires_in_days(179) + create_cert_that_expires_in_days(181) + create_cert_that_expires_in_days(1) + + assert send_authority_expiration_notifications() == (2, 0) + + +def create_cert_that_expires_in_days(days): + now = arrow.utcnow() + not_after = now + timedelta(days=days, hours=1) # a bit more than specified since we'll check in the future + + authority = AuthorityFactory() + certificate = CertificateFactory() + certificate.not_after = not_after + certificate.notify = True + certificate.root_authority_id = authority.id + certificate.authority_id = None + return certificate From 4b93c81adda18fda63927f94b3bdb80f0032178b Mon Sep 17 00:00:00 2001 From: Jasmine Schladen Date: Wed, 2 Dec 2020 11:46:18 -0800 Subject: [PATCH 2/6] Fix typo --- lemur/plugins/lemur_email/templates/authority_expiration.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lemur/plugins/lemur_email/templates/authority_expiration.html b/lemur/plugins/lemur_email/templates/authority_expiration.html index 2c077bf5..e4ae8199 100644 --- a/lemur/plugins/lemur_email/templates/authority_expiration.html +++ b/lemur/plugins/lemur_email/templates/authority_expiration.html @@ -75,7 +75,7 @@ -
This is a Lemur CA certificate expiration notice. The follow CA certificates are expiring soon; +
This is a Lemur CA certificate expiration notice. The following CA certificates are expiring soon; please take manual action to renew them if necessary. You may also disable notifications via the Notify toggle in Lemur if they are no longer in use. Date: Thu, 3 Dec 2020 11:30:34 -0800 Subject: [PATCH 3/6] PR feedback --- docs/administration.rst | 101 ++++++++++++++++-- docs/production/index.rst | 2 +- lemur/notifications/cli.py | 8 +- lemur/notifications/messaging.py | 2 +- .../templates/authority_expiration.html | 4 +- lemur/tests/test_messaging.py | 32 +++--- 6 files changed, 113 insertions(+), 36 deletions(-) diff --git a/docs/administration.rst b/docs/administration.rst index 94b15829..ae393ac0 100644 --- a/docs/administration.rst +++ b/docs/administration.rst @@ -262,22 +262,101 @@ and are used when Lemur creates the CSR for your certificates. LEMUR_DEFAULT_AUTHORITY = "verisign" +.. _NotificationOptions: + Notification Options -------------------- -Lemur currently has very basic support for notifications. Currently only expiration notifications are supported. Actual notification -is handled by the notification plugins that you have configured. Lemur ships with the 'Email' notification that allows expiration emails -to be sent to subscribers. +Lemur supports a small variety of notification types through a set of notification plugins. +By default, Lemur configures a standard set of email notifications for all certificates. -Templates for expiration emails are located under `lemur/plugins/lemur_email/templates` and can be modified for your needs. -Notifications are sent to the certificate creator, owner and security team as specified by the `LEMUR_SECURITY_TEAM_EMAIL` configuration parameter. +**Plugin-capable notifications** -Certificates marked as inactive will **not** be notified of upcoming expiration. This enables a user to essentially -silence the expiration. If a certificate is active and is expiring the above will be notified according to the `LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS` or -30, 15, 2 days before expiration if no intervals are set. +These notifications can be configured to use all available notification plugins. -Lemur supports sending certificate expiration notifications through SES and SMTP. +Supported types: +* Certificate expiration + +**Email-only notifications** + +These notifications can only be sent via email and cannot use other notification plugins. + +Supported types: + +* CA certificate expiration +* Pending ACME certificate failure +* Certificate rotation (currently disabled in code) + +**Default notifications** + +When a certificate is created, the following email notifications are created for it if they do not exist. +If these notifications already exist, they will be associated with the new certificate. + +* ``DEFAULT__X_DAY``, where X is the set of values specified in ``LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS`` and defaults to 30, 15, and 2 if not specified. The owner's username will replace ````. +* ``DEFAULT_SECURITY_X_DAY``, where X is the set of values specified in ``LEMUR_SECURITY_TEAM_EMAIL_INTERVALS`` and defaults to ``LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS`` if not specified (which also defaults to 30, 15, and 2 if not specified). + +These notifications can be disabled if desired. They can also be unassociated with a specific certificate. + +**Disabling notifications** + +Notifications can be disabled either for an individual certificate (which disables all notifications for that certificate) +or for an individual notification object (which disables that notification for all associated certificates). +At present, disabling a notification object will only disable certificate expiration notifications, and not other types, +since other notification types don't use notification objects. + +**Certificate expiration** + +Certificate expiration notifications are sent when the scheduled task to send certificate expiration notifications runs +(see :ref:`PeriodicTasks`). Specific patterns of certificate names may be excluded using ``--exclude`` (when using +cron; you may specify this multiple times for multiple patterns) or via the config option ``EXCLUDE_CN_FROM_NOTIFICATION`` +(when using celery; this is a list configuration option, meaning you specify multiple values, such as +``['exclude', 'also exclude']``). The specified exclude pattern will match if found anywhere in the certificate name. + +When the periodic task runs, Lemur checks for certificates meeting the following conditions: + +* Certificate has notifications enabled +* Certificate is not expired +* Certificate is not revoked +* Certificate name does not match the `exclude` parameter +* Certificate has at least one associated notification object +* That notification is active +* That notification's configured interval and unit match the certificate's remaining lifespan + +All eligible certificates are then grouped by owner and applicable notification. For each notification and certificate group, +Lemur will send the expiration notification using whichever plugin was configured for that notification object. +In addition, Lemur will send an email to the certificate owner and security team (as specified by the +``LEMUR_SECURITY_TEAM_EMAIL`` configuration parameter). + +**CA certificate expiration** + +Certificate authority certificate expiration notifications are sent when the scheduled task to send authority certificate +expiration notifications runs (see :ref:`PeriodicTasks`). Notifications are sent via the intervals configured in the +configuration parameter ``LEMUR_AUTHORITY_CERT_EXPIRATION_EMAIL_INTERVALS``, with a default of 365 and 180 days. + +When the periodic task runs, Lemur checks for certificates meeting the following conditions: + +* Certificate has notifications enabled +* Certificate is not expired +* Certificate is not revoked +* Certificate is associated with a CA +* Certificate's remaining lifespan matches one of the configured intervals + +All eligible certificates are then grouped by owner and expiration interval. For each interval and certificate group, +Lemur will send the CA certificate expiration notification via email to the certificate owner and security team +(as specified by the ``LEMUR_SECURITY_TEAM_EMAIL`` configuration parameter). + +**Pending ACME certificate failure** + +Whenever a pending ACME certificate fails to be issued, Lemur will send a notification via email to the certificate owner +and security team (as specified by the ``LEMUR_SECURITY_TEAM_EMAIL`` configuration parameter). This email is not sent if +the pending certificate had notifications disabled. + +**Email notifications** + +Templates for emails are located under `lemur/plugins/lemur_email/templates` and can be modified for your needs. + +The following configuration options are supported: .. data:: LEMUR_EMAIL_SENDER :noindex: @@ -318,7 +397,7 @@ Lemur supports sending certificate expiration notifications through SES and SMTP :: - LEMUR_EMAIL = 'lemur.example.com' + LEMUR_EMAIL = 'lemur@example.com' .. data:: LEMUR_SECURITY_TEAM_EMAIL @@ -333,7 +412,7 @@ Lemur supports sending certificate expiration notifications through SES and SMTP .. data:: LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS :noindex: - Lemur notification intervals + Lemur notification intervals. If unspecified, the value [30, 15, 2] is used. :: diff --git a/docs/production/index.rst b/docs/production/index.rst index 93bad406..106f6b99 100644 --- a/docs/production/index.rst +++ b/docs/production/index.rst @@ -325,7 +325,7 @@ celery tasks or cron jobs that run these commands. There are currently three commands that could/should be run on a periodic basis: -- `notify expirations` and `notify authority_expirations` +- `notify expirations` and `notify authority_expirations` (see :ref:`NotificationOptions` for configuration info) - `check_revoked` - `sync` diff --git a/lemur/notifications/cli.py b/lemur/notifications/cli.py index 7012e9c8..3c29693b 100644 --- a/lemur/notifications/cli.py +++ b/lemur/notifications/cli.py @@ -40,9 +40,7 @@ def expirations(exclude): print("Starting to notify subscribers about expiring certificates!") success, failed = send_expiration_notifications(exclude) print( - "Finished notifying subscribers about expiring certificates! Sent: {success} Failed: {failed}".format( - success=success, failed=failed - ) + f"Finished notifying subscribers about expiring certificates! Sent: {success} Failed: {failed}" ) status = SUCCESS_METRIC_STATUS except Exception as e: @@ -66,9 +64,7 @@ def authority_expirations(): success, failed = send_authority_expiration_notifications() print( "Finished notifying subscribers about expiring certificate authority certificates! " - "Sent: {success} Failed: {failed}".format( - success=success, failed=failed - ) + f"Sent: {success} Failed: {failed}" ) status = SUCCESS_METRIC_STATUS except Exception as e: diff --git a/lemur/notifications/messaging.py b/lemur/notifications/messaging.py index 9b299a24..5aa6b3ee 100644 --- a/lemur/notifications/messaging.py +++ b/lemur/notifications/messaging.py @@ -132,7 +132,7 @@ def get_eligible_authority_certificates(): for owner, owner_certs in groupby(all_certs, lambda x: x.owner): # group by expiration interval for interval, interval_certs in groupby(owner_certs, lambda x: (x.not_after - now).days): - certificates[owner][interval] = list(interval_certs) # list(c for c in interval_certs) + certificates[owner][interval] = list(interval_certs) return certificates diff --git a/lemur/plugins/lemur_email/templates/authority_expiration.html b/lemur/plugins/lemur_email/templates/authority_expiration.html index e4ae8199..984a7483 100644 --- a/lemur/plugins/lemur_email/templates/authority_expiration.html +++ b/lemur/plugins/lemur_email/templates/authority_expiration.html @@ -76,7 +76,9 @@

This is a Lemur CA certificate expiration notice. The following CA certificates are expiring soon; - please take manual action to renew them if necessary. You may also disable notifications via the + please take manual action to renew them if necessary. Note that rotating a root CA requires + advanced planing and the respective trustStores need to be updated. A sub-CA, on the other hand, + does not require any changes to the trustStore. You may also disable notifications via the Notify toggle in Lemur if they are no longer in use. diff --git a/lemur/tests/test_messaging.py b/lemur/tests/test_messaging.py index 9a9a5ad3..0845d468 100644 --- a/lemur/tests/test_messaging.py +++ b/lemur/tests/test_messaging.py @@ -131,13 +131,13 @@ def test_send_pending_failure_notification(notification_plugin, async_issuer_plu def test_get_authority_certificates(): from lemur.notifications.messaging import get_expiring_authority_certificates - certificate_1 = create_cert_that_expires_in_days(180) - certificate_2 = create_cert_that_expires_in_days(365) - create_cert_that_expires_in_days(364) - create_cert_that_expires_in_days(366) - create_cert_that_expires_in_days(179) - create_cert_that_expires_in_days(181) - create_cert_that_expires_in_days(1) + certificate_1 = create_ca_cert_that_expires_in_days(180) + certificate_2 = create_ca_cert_that_expires_in_days(365) + create_ca_cert_that_expires_in_days(364) + create_ca_cert_that_expires_in_days(366) + create_ca_cert_that_expires_in_days(179) + create_ca_cert_that_expires_in_days(181) + create_ca_cert_that_expires_in_days(1) assert set(get_expiring_authority_certificates()) == {certificate_1, certificate_2} @@ -147,19 +147,19 @@ def test_send_authority_expiration_notifications(): from lemur.notifications.messaging import send_authority_expiration_notifications verify_sender_email() - create_cert_that_expires_in_days(180) - create_cert_that_expires_in_days(180) # two on the same day results in a single email - create_cert_that_expires_in_days(365) - create_cert_that_expires_in_days(364) - create_cert_that_expires_in_days(366) - create_cert_that_expires_in_days(179) - create_cert_that_expires_in_days(181) - create_cert_that_expires_in_days(1) + create_ca_cert_that_expires_in_days(180) + create_ca_cert_that_expires_in_days(180) # two on the same day results in a single email + create_ca_cert_that_expires_in_days(365) + create_ca_cert_that_expires_in_days(364) + create_ca_cert_that_expires_in_days(366) + create_ca_cert_that_expires_in_days(179) + create_ca_cert_that_expires_in_days(181) + create_ca_cert_that_expires_in_days(1) assert send_authority_expiration_notifications() == (2, 0) -def create_cert_that_expires_in_days(days): +def create_ca_cert_that_expires_in_days(days): now = arrow.utcnow() not_after = now + timedelta(days=days, hours=1) # a bit more than specified since we'll check in the future From 42957cffc7e08f2830a844e3006946f7e9b413f6 Mon Sep 17 00:00:00 2001 From: Jasmine Schladen Date: Thu, 3 Dec 2020 16:10:36 -0800 Subject: [PATCH 4/6] PR feedback: add config option to enable rotation emails, add cert count and type to email --- docs/administration.rst | 8 +++++++- lemur/certificates/service.py | 10 ++++++++++ lemur/common/celery.py | 5 +++-- lemur/notifications/messaging.py | 5 ++++- .../lemur_email/templates/authority_expiration.html | 7 ++++++- 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/docs/administration.rst b/docs/administration.rst index ae393ac0..1415e598 100644 --- a/docs/administration.rst +++ b/docs/administration.rst @@ -286,7 +286,7 @@ Supported types: * CA certificate expiration * Pending ACME certificate failure -* Certificate rotation (currently disabled in code) +* Certificate rotation **Default notifications** @@ -352,6 +352,12 @@ Whenever a pending ACME certificate fails to be issued, Lemur will send a notifi and security team (as specified by the ``LEMUR_SECURITY_TEAM_EMAIL`` configuration parameter). This email is not sent if the pending certificate had notifications disabled. +**Certificate rotation** + +Whenever a cert is rotated, Lemur will send a notification via email to the certificate owner. This notification is +disabled by default; to enable it, you must set the option ``--notify`` (when using cron) or the configuration parameter +``ENABLE_ROTATION_NOTIFICATION`` (when using celery). + **Email notifications** Templates for emails are located under `lemur/plugins/lemur_email/templates` and can be modified for your needs. diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index 3d3e2ca0..f205da3f 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -864,3 +864,13 @@ def cleanup_after_revoke(certificate): database.update(certificate) return error_message + + +def get_issued_cert_count_for_authority(authority): + """ + Returns the count of certs issued by the specified authority. + + :return: + """ + query = database.session_query(Certificate.id).filter(Authority.id == authority.id) + return database.get_count(query) diff --git a/lemur/common/celery.py b/lemur/common/celery.py index f428927e..9dc4bd0a 100644 --- a/lemur/common/celery.py +++ b/lemur/common/celery.py @@ -656,11 +656,12 @@ def certificate_rotate(**kwargs): current_app.logger.debug(log_data) try: + notify = current_app.config.get("ENABLE_ROTATION_NOTIFICATION", None) if region: log_data["region"] = region - cli_certificate.rotate_region(None, None, None, None, True, region) + cli_certificate.rotate_region(None, None, None, notify, True, region) else: - cli_certificate.rotate(None, None, None, None, True) + cli_certificate.rotate(None, None, None, notify, True) except SoftTimeLimitExceeded: log_data["message"] = "Certificate rotate: Time limit exceeded." current_app.logger.error(log_data) diff --git a/lemur/notifications/messaging.py b/lemur/notifications/messaging.py index 5aa6b3ee..75d829b1 100644 --- a/lemur/notifications/messaging.py +++ b/lemur/notifications/messaging.py @@ -19,9 +19,10 @@ from sqlalchemy import and_ from sqlalchemy.sql.expression import false, true from lemur import database +from lemur.certificates import service as certificates_service from lemur.certificates.models import Certificate from lemur.certificates.schemas import certificate_notification_output_schema -from lemur.common.utils import windowed_query +from lemur.common.utils import windowed_query, is_selfsigned from lemur.constants import FAILURE_METRIC_STATUS, SUCCESS_METRIC_STATUS from lemur.extensions import metrics, sentry from lemur.pending_certificates.schemas import pending_certificate_output_schema @@ -241,6 +242,8 @@ def send_authority_expiration_notifications(): cert_data = certificate_notification_output_schema.dump( certificate ).data + cert_data['self_signed'] = is_selfsigned(certificate.parsed_cert) + cert_data['issued_cert_count'] = certificates_service.get_issued_cert_count_for_authority(certificate.root_authority) notification_data.append(cert_data) email_recipients = security_email + [owner] diff --git a/lemur/plugins/lemur_email/templates/authority_expiration.html b/lemur/plugins/lemur_email/templates/authority_expiration.html index 984a7483..7c343417 100644 --- a/lemur/plugins/lemur_email/templates/authority_expiration.html +++ b/lemur/plugins/lemur_email/templates/authority_expiration.html @@ -91,7 +91,12 @@ {{ certificate.name }}
- {{ certificate.endpoints | length }} Endpoints + {% if certificate.self_signed %} + Root + {% else %} + Subordinate + {% endif %} CA +
{{ certificate.issued_cert_count }} issued certificates
{{ certificate.owner }}
{{ certificate.validityEnd | time }} Details From 576c69c8e571df37f0f4459e540fb7eb945720fb Mon Sep 17 00:00:00 2001 From: Jasmine Schladen Date: Thu, 3 Dec 2020 17:56:39 -0800 Subject: [PATCH 5/6] Fix DB query for cert count for authority --- lemur/certificates/service.py | 3 +-- lemur/tests/test_certificates.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index f205da3f..b22090b6 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -872,5 +872,4 @@ def get_issued_cert_count_for_authority(authority): :return: """ - query = database.session_query(Certificate.id).filter(Authority.id == authority.id) - return database.get_count(query) + return database.db.session.query(Certificate).filter(Certificate.authority_id == authority.id).count() diff --git a/lemur/tests/test_certificates.py b/lemur/tests/test_certificates.py index a0a03e65..7c96c491 100644 --- a/lemur/tests/test_certificates.py +++ b/lemur/tests/test_certificates.py @@ -1377,3 +1377,15 @@ def test_boolean_filter(client): headers=VALID_ADMIN_HEADER_TOKEN, ) assert resp.status_code == 200 + + +def test_issued_cert_count_for_authority(authority): + from lemur.tests.factories import CertificateFactory + from lemur.certificates.service import get_issued_cert_count_for_authority + + # create a few certs issued by the authority + CertificateFactory(authority=authority, name="test_issued_cert_count_for_authority1") + CertificateFactory(authority=authority, name="test_issued_cert_count_for_authority2") + CertificateFactory(authority=authority, name="test_issued_cert_count_for_authority3") + + assert get_issued_cert_count_for_authority(authority) == 3 From 29aeb9b298c8eac6696acfe7a8f82b7274cc0d8a Mon Sep 17 00:00:00 2001 From: Jasmine Schladen Date: Thu, 3 Dec 2020 17:59:13 -0800 Subject: [PATCH 6/6] Subordinate -> Intermediate wording --- lemur/plugins/lemur_email/templates/authority_expiration.html | 2 +- lemur/tests/test_certificates.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lemur/plugins/lemur_email/templates/authority_expiration.html b/lemur/plugins/lemur_email/templates/authority_expiration.html index 7c343417..30cfb3fd 100644 --- a/lemur/plugins/lemur_email/templates/authority_expiration.html +++ b/lemur/plugins/lemur_email/templates/authority_expiration.html @@ -94,7 +94,7 @@ {% if certificate.self_signed %} Root {% else %} - Subordinate + Intermediate {% endif %} CA
{{ certificate.issued_cert_count }} issued certificates
{{ certificate.owner }} diff --git a/lemur/tests/test_certificates.py b/lemur/tests/test_certificates.py index 7c96c491..c33743d0 100644 --- a/lemur/tests/test_certificates.py +++ b/lemur/tests/test_certificates.py @@ -1383,6 +1383,8 @@ def test_issued_cert_count_for_authority(authority): from lemur.tests.factories import CertificateFactory from lemur.certificates.service import get_issued_cert_count_for_authority + assert get_issued_cert_count_for_authority(authority) == 0 + # create a few certs issued by the authority CertificateFactory(authority=authority, name="test_issued_cert_count_for_authority1") CertificateFactory(authority=authority, name="test_issued_cert_count_for_authority2")