From 5f1e4f713c470c4812c29391e582c98160df76a8 Mon Sep 17 00:00:00 2001 From: "evan.mcelheny" Date: Wed, 12 Aug 2026 13:15:49 -0400 Subject: [PATCH 01/16] fix(rotation): default NULL-policy certs to the 'default' rotation policy Certs created without an explicit rotation_policy (import/discovery/ACME paths) end up with NULL rotation_policy_id. Make the Certificate constructor fall back to the named 'default' RotationPolicy, keeping that row in sync with LEMUR_DEFAULT_ROTATION_INTERVAL: creates it if missing, or updates its days when the config changes. CLOUDR-2089 Workspace: local --- lemur/certificates/models.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py index add8cfc49..c0dc7ff6f 100644 --- a/lemur/certificates/models.py +++ b/lemur/certificates/models.py @@ -74,6 +74,26 @@ def get_sequence(name): return root, seq +def _get_default_rotation_policy(): + """ + Returns the named "default" RotationPolicy, keeping it in sync with the + LEMUR_DEFAULT_ROTATION_INTERVAL config: creates it if missing, or updates + its days to the configured value if it differs. + + Used as the fallback for certs created without an explicit rotation policy, + so every newly created cert gets the default policy instead of a NULL one — + and changing the config takes effect on the next cert creation. + """ + days = current_app.config.get("LEMUR_DEFAULT_ROTATION_INTERVAL", 60) + policy = RotationPolicy.query.filter_by(name="default").first() + if policy is None: + policy = RotationPolicy(name="default", days=days) + db.session.add(policy) + elif policy.days != days: + policy.days = days + return policy + + def get_or_increase_name(name, serial): certificates = Certificate.query.filter(Certificate.name == name).all() @@ -229,7 +249,7 @@ def __init__(self, **kwargs): self.roles = list(set(kwargs.get("roles", []))) self.replaces = kwargs.get("replaces", []) self.rotation = kwargs.get("rotation") - self.rotation_policy = kwargs.get("rotation_policy") + self.rotation_policy = kwargs.get("rotation_policy") or _get_default_rotation_policy() self.key_type = kwargs.get("key_type") self.signing_algorithm = defaults.signing_algorithm(cert) self.bits = defaults.bitstrength(cert) From 580195cd28aa97cf9ea533458d485dab134141eb Mon Sep 17 00:00:00 2001 From: "evan.mcelheny" Date: Wed, 12 Aug 2026 13:29:52 -0400 Subject: [PATCH 02/16] config: set LEMUR_DEFAULT_ROTATION_INTERVAL=60 and unify on the single flag Add LEMUR_DEFAULT_ROTATION_INTERVAL=60 to default.conf.py and tests/conf.py, and align manage.py's fallback default to 60 so the init-time policy seeding and the Certificate NULL-policy fallback both read the same value. CLOUDR-2089 Workspace: local --- lemur/default.conf.py | 5 +++++ lemur/manage.py | 2 +- lemur/tests/conf.py | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lemur/default.conf.py b/lemur/default.conf.py index bd67bf7a3..368ed4bd2 100644 --- a/lemur/default.conf.py +++ b/lemur/default.conf.py @@ -12,6 +12,11 @@ CORS = False DEBUG = False +# Default rotation policy: days before expiry a cert is eligible for rotation. +# Certs created without an explicit rotation_policy fall back to the named +# "default" policy, which is kept in sync with this value. +LEMUR_DEFAULT_ROTATION_INTERVAL = 60 + # Logging LOG_LEVEL = "DEBUG" diff --git a/lemur/manage.py b/lemur/manage.py index bc49fa101..71e56b0a5 100755 --- a/lemur/manage.py +++ b/lemur/manage.py @@ -292,7 +292,7 @@ def run(self, password): "[-] Default rotation interval policy already created, skipping...!\n" ) else: - days = current_app.config.get("LEMUR_DEFAULT_ROTATION_INTERVAL", 30) + days = current_app.config.get("LEMUR_DEFAULT_ROTATION_INTERVAL", 60) sys.stdout.write( "[+] Creating default certificate rotation policy of {days} days before issuance.\n".format( days=days diff --git a/lemur/tests/conf.py b/lemur/tests/conf.py index 26f2f3937..31064106c 100644 --- a/lemur/tests/conf.py +++ b/lemur/tests/conf.py @@ -76,6 +76,7 @@ def get_random_secret(length): LEMUR_DEFAULT_LOCATION = "Los Gatos" LEMUR_DEFAULT_ORGANIZATION = "Example, Inc." LEMUR_DEFAULT_ORGANIZATIONAL_UNIT = "Example" +LEMUR_DEFAULT_ROTATION_INTERVAL = 60 LEMUR_ALLOW_WEEKEND_EXPIRATION = False From 3e26ff63b5133aedbe59b382ef7211ab158da9a4 Mon Sep 17 00:00:00 2001 From: "evan.mcelheny" Date: Wed, 12 Aug 2026 13:41:31 -0400 Subject: [PATCH 03/16] refactor(rotation): split getter from sync; fire sync before rotation-candidate query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the NULL-policy fallback into a pure getter (_get_default_rotation_policy, create-if-missing, no config-sync) and add a separate sync_default_rotation_policy() that creates/updates the 'default' row to LEMUR_DEFAULT_ROTATION_INTERVAL. Call sync_default_rotation_policy() at the top of get_all_pending_reissue() so the row is always in sync with config before it is used — a config change takes effect on the next rotation pass regardless of cert creation. CLOUDR-2089 Workspace: local --- lemur/certificates/models.py | 15 ++++++--------- lemur/certificates/service.py | 3 +++ lemur/policies/service.py | 22 ++++++++++++++++++++++ 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py index c0dc7ff6f..637298fb2 100644 --- a/lemur/certificates/models.py +++ b/lemur/certificates/models.py @@ -76,21 +76,18 @@ def get_sequence(name): def _get_default_rotation_policy(): """ - Returns the named "default" RotationPolicy, keeping it in sync with the - LEMUR_DEFAULT_ROTATION_INTERVAL config: creates it if missing, or updates - its days to the configured value if it differs. + Returns the named "default" RotationPolicy, creating it (with days from + LEMUR_DEFAULT_ROTATION_INTERVAL) if it does not exist yet. - Used as the fallback for certs created without an explicit rotation policy, - so every newly created cert gets the default policy instead of a NULL one — - and changing the config takes effect on the next cert creation. + Pure getter — it does NOT sync days to the config. Use + ``policy_service.sync_default_rotation_policy()`` (called before the + rotation-candidate query) for that. """ - days = current_app.config.get("LEMUR_DEFAULT_ROTATION_INTERVAL", 60) policy = RotationPolicy.query.filter_by(name="default").first() if policy is None: + days = current_app.config.get("LEMUR_DEFAULT_ROTATION_INTERVAL", 60) policy = RotationPolicy(name="default", days=days) db.session.add(policy) - elif policy.days != days: - policy.days = days return policy diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index b05f489a0..9d9fee150 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -44,6 +44,7 @@ from lemur.notifications.messaging import send_revocation_notification from lemur.notifications.models import Notification from lemur.pending_certificates.models import PendingCertificate +from lemur.policies import service as policy_service from lemur.plugins.base import plugins from lemur.plugins.utils import get_plugin_option from lemur.roles import service as role_service @@ -276,6 +277,8 @@ def get_all_pending_reissue(): :return: """ + # Keep the "default" rotation policy in sync with config before we use it. + policy_service.sync_default_rotation_policy() return ( Certificate.query.filter(Certificate.rotation == true()) .filter(not_(Certificate.replaced.any())) diff --git a/lemur/policies/service.py b/lemur/policies/service.py index aa7339295..84b197214 100644 --- a/lemur/policies/service.py +++ b/lemur/policies/service.py @@ -6,10 +6,32 @@ .. moduleauthor:: Kevin Glisson """ +from flask import current_app + from lemur import database from lemur.policies.models import RotationPolicy +def sync_default_rotation_policy(): + """ + Create or update the named "default" RotationPolicy so its ``days`` matches + the LEMUR_DEFAULT_ROTATION_INTERVAL config. + + Called at the start of the rotation-candidate query + (``certificates.service.get_all_pending_reissue``) so the row is always in + sync with config before it is used — a config change takes effect on the + next rotation pass, regardless of whether any cert is created. + """ + days = current_app.config.get("LEMUR_DEFAULT_ROTATION_INTERVAL", 60) + policies = get_by_name("default") + if not policies: + return create(days=days, name="default") + policy = policies[0] + if policy.days != days: + update(policy.id, days=days) + return policy + + def get(policy_id): """ Retrieves policy by its ID. From 00cf889990c3f11834799141a4883a6eede02b4c Mon Sep 17 00:00:00 2001 From: "evan.mcelheny" Date: Wed, 12 Aug 2026 13:56:09 -0400 Subject: [PATCH 04/16] refactor(rotation): consolidate into get_rotation_policy_from_config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove _get_default_rotation_policy helper — Certificate.__init__ now inlines RotationPolicy.query.filter_by(name='default').first() directly. Single function get_rotation_policy_from_config() in policies/service.py handles create-or-sync to LEMUR_DEFAULT_ROTATION_INTERVAL, called from get_all_pending_reissue() before the rotation-candidate query. CLOUDR-2089 Workspace: local --- lemur/certificates/models.py | 19 +------------------ lemur/certificates/service.py | 3 +-- lemur/policies/service.py | 14 ++++++-------- 3 files changed, 8 insertions(+), 28 deletions(-) diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py index 637298fb2..c191329ba 100644 --- a/lemur/certificates/models.py +++ b/lemur/certificates/models.py @@ -74,23 +74,6 @@ def get_sequence(name): return root, seq -def _get_default_rotation_policy(): - """ - Returns the named "default" RotationPolicy, creating it (with days from - LEMUR_DEFAULT_ROTATION_INTERVAL) if it does not exist yet. - - Pure getter — it does NOT sync days to the config. Use - ``policy_service.sync_default_rotation_policy()`` (called before the - rotation-candidate query) for that. - """ - policy = RotationPolicy.query.filter_by(name="default").first() - if policy is None: - days = current_app.config.get("LEMUR_DEFAULT_ROTATION_INTERVAL", 60) - policy = RotationPolicy(name="default", days=days) - db.session.add(policy) - return policy - - def get_or_increase_name(name, serial): certificates = Certificate.query.filter(Certificate.name == name).all() @@ -246,7 +229,7 @@ def __init__(self, **kwargs): self.roles = list(set(kwargs.get("roles", []))) self.replaces = kwargs.get("replaces", []) self.rotation = kwargs.get("rotation") - self.rotation_policy = kwargs.get("rotation_policy") or _get_default_rotation_policy() + self.rotation_policy = kwargs.get("rotation_policy") or RotationPolicy.query.filter_by(name="default").first() self.key_type = kwargs.get("key_type") self.signing_algorithm = defaults.signing_algorithm(cert) self.bits = defaults.bitstrength(cert) diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index 9d9fee150..084fb1d55 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -277,8 +277,7 @@ def get_all_pending_reissue(): :return: """ - # Keep the "default" rotation policy in sync with config before we use it. - policy_service.sync_default_rotation_policy() + policy_service.get_rotation_policy_from_config() return ( Certificate.query.filter(Certificate.rotation == true()) .filter(not_(Certificate.replaced.any())) diff --git a/lemur/policies/service.py b/lemur/policies/service.py index 84b197214..0fa8ce91a 100644 --- a/lemur/policies/service.py +++ b/lemur/policies/service.py @@ -12,15 +12,13 @@ from lemur.policies.models import RotationPolicy -def sync_default_rotation_policy(): +def get_rotation_policy_from_config(): """ - Create or update the named "default" RotationPolicy so its ``days`` matches - the LEMUR_DEFAULT_ROTATION_INTERVAL config. - - Called at the start of the rotation-candidate query - (``certificates.service.get_all_pending_reissue``) so the row is always in - sync with config before it is used — a config change takes effect on the - next rotation pass, regardless of whether any cert is created. + Return the named "default" RotationPolicy, keeping it in sync with + LEMUR_DEFAULT_ROTATION_INTERVAL: creates it if missing, updates its days + if the config has changed. Single source of truth used both as the + NULL-policy fallback in Certificate.__init__ and as the pre-query sync + in get_all_pending_reissue. """ days = current_app.config.get("LEMUR_DEFAULT_ROTATION_INTERVAL", 60) policies = get_by_name("default") From 474bc22a0a367332d1875d17c58321b6217ffa17 Mon Sep 17 00:00:00 2001 From: "evan.mcelheny" Date: Wed, 12 Aug 2026 14:09:59 -0400 Subject: [PATCH 05/16] config: move LEMUR_DEFAULT_ROTATION_INTERVAL to lemur.conf.py via env var Remove it from default.conf.py (never loaded in prod). Add it to local/src/lemur.conf.py alongside the other LEMUR_DEFAULT_* params, reading from os.environ with a default of 60. CLOUDR-2089 Workspace: local --- lemur/default.conf.py | 4 ---- local/src/lemur.conf.py | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lemur/default.conf.py b/lemur/default.conf.py index 368ed4bd2..a27c18d2b 100644 --- a/lemur/default.conf.py +++ b/lemur/default.conf.py @@ -12,10 +12,6 @@ CORS = False DEBUG = False -# Default rotation policy: days before expiry a cert is eligible for rotation. -# Certs created without an explicit rotation_policy fall back to the named -# "default" policy, which is kept in sync with this value. -LEMUR_DEFAULT_ROTATION_INTERVAL = 60 # Logging diff --git a/local/src/lemur.conf.py b/local/src/lemur.conf.py index 048789769..f79bd55ba 100644 --- a/local/src/lemur.conf.py +++ b/local/src/lemur.conf.py @@ -220,6 +220,9 @@ def get_random_secret(length): LEMUR_DEFAULT_ORGANIZATIONAL_UNIT = str( os.environ.get("LEMUR_DEFAULT_ORGANIZATIONAL_UNIT", "") ) +LEMUR_DEFAULT_ROTATION_INTERVAL = int( + os.environ.get("LEMUR_DEFAULT_ROTATION_INTERVAL", 60) +) LEMUR_DEFAULT_AUTHORITY = str(os.environ.get("LEMUR_DEFAULT_AUTHORITY", "ExampleCa")) From cf33bd3b902e10b8cfd50c95f6d30c31dc5a49a0 Mon Sep 17 00:00:00 2001 From: "evan.mcelheny" Date: Wed, 12 Aug 2026 14:12:58 -0400 Subject: [PATCH 06/16] revert: restore lemur/default.conf.py to master Workspace: local --- lemur/default.conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lemur/default.conf.py b/lemur/default.conf.py index a27c18d2b..bd67bf7a3 100644 --- a/lemur/default.conf.py +++ b/lemur/default.conf.py @@ -12,7 +12,6 @@ CORS = False DEBUG = False - # Logging LOG_LEVEL = "DEBUG" From 1e77c21bfd91462a76ef63f1931be80a0e715f04 Mon Sep 17 00:00:00 2001 From: evan-datadog Date: Wed, 12 Aug 2026 14:21:41 -0400 Subject: [PATCH 07/16] Update service.py --- lemur/policies/service.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lemur/policies/service.py b/lemur/policies/service.py index 0fa8ce91a..575e065bd 100644 --- a/lemur/policies/service.py +++ b/lemur/policies/service.py @@ -16,8 +16,9 @@ def get_rotation_policy_from_config(): """ Return the named "default" RotationPolicy, keeping it in sync with LEMUR_DEFAULT_ROTATION_INTERVAL: creates it if missing, updates its days - if the config has changed. Single source of truth used both as the - NULL-policy fallback in Certificate.__init__ and as the pre-query sync + if the config has changed. This policy is the NULL-policy fallback in + Certificate.__init__. + The default rotation policy is refreshed as a part of the pre-query sync in get_all_pending_reissue. """ days = current_app.config.get("LEMUR_DEFAULT_ROTATION_INTERVAL", 60) From 9f45a3aba8de5baa16d5c25ae10bb27f7eae3f45 Mon Sep 17 00:00:00 2001 From: "evan.mcelheny" Date: Wed, 12 Aug 2026 15:16:44 -0400 Subject: [PATCH 08/16] refactor(rotation): rename to update_default_rotation_policy; consolidate manage.py Rename get_rotation_policy_from_config -> update_default_rotation_policy to accurately reflect that it upserts (create-or-sync) rather than reads. Replace the manual 17-line get-or-create block in manage.py InitializeApp with a single call to policy_service.update_default_rotation_policy(), which also handles the config-sync case that the old code silently skipped. CLOUDR-2089 Workspace: local --- lemur/certificates/service.py | 2 +- lemur/manage.py | 19 +------------------ lemur/policies/service.py | 2 +- 3 files changed, 3 insertions(+), 20 deletions(-) diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index 084fb1d55..9601cb5e0 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -277,7 +277,7 @@ def get_all_pending_reissue(): :return: """ - policy_service.get_rotation_policy_from_config() + policy_service.update_default_rotation_policy() return ( Certificate.query.filter(Certificate.rotation == true()) .filter(not_(Certificate.replaced.any())) diff --git a/lemur/manage.py b/lemur/manage.py index 71e56b0a5..2691c5a19 100755 --- a/lemur/manage.py +++ b/lemur/manage.py @@ -282,24 +282,7 @@ def run(self, password): "DEFAULT_SECURITY", recipients=recipients ) - _DEFAULT_ROTATION_INTERVAL = "default" - default_rotation_interval = policy_service.get_by_name( - _DEFAULT_ROTATION_INTERVAL - ) - - if default_rotation_interval: - sys.stdout.write( - "[-] Default rotation interval policy already created, skipping...!\n" - ) - else: - days = current_app.config.get("LEMUR_DEFAULT_ROTATION_INTERVAL", 60) - sys.stdout.write( - "[+] Creating default certificate rotation policy of {days} days before issuance.\n".format( - days=days - ) - ) - policy_service.create(days=days, name=_DEFAULT_ROTATION_INTERVAL) - + policy_service.update_default_rotation_policy() sys.stdout.write("[/] Done!\n") diff --git a/lemur/policies/service.py b/lemur/policies/service.py index 575e065bd..cb001b584 100644 --- a/lemur/policies/service.py +++ b/lemur/policies/service.py @@ -12,7 +12,7 @@ from lemur.policies.models import RotationPolicy -def get_rotation_policy_from_config(): +def update_default_rotation_policy(): """ Return the named "default" RotationPolicy, keeping it in sync with LEMUR_DEFAULT_ROTATION_INTERVAL: creates it if missing, updates its days From ba71df65927791893e08b14012e81d3d66833a62 Mon Sep 17 00:00:00 2001 From: "evan.mcelheny" Date: Wed, 12 Aug 2026 15:31:01 -0400 Subject: [PATCH 09/16] refactor(rotation): call update_default_rotation_policy at boot via install_plugins Move the sync out of get_all_pending_reissue() into the existing app.app_context() block in factory.install_plugins(), which runs on every process boot (web/celery/beat) via create_app(). This guarantees the default policy is in sync with config at startup rather than only when a rotation candidate query runs. CLOUDR-2089 Workspace: local --- lemur/certificates/service.py | 2 -- lemur/factory.py | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index 9601cb5e0..b05f489a0 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -44,7 +44,6 @@ from lemur.notifications.messaging import send_revocation_notification from lemur.notifications.models import Notification from lemur.pending_certificates.models import PendingCertificate -from lemur.policies import service as policy_service from lemur.plugins.base import plugins from lemur.plugins.utils import get_plugin_option from lemur.roles import service as role_service @@ -277,7 +276,6 @@ def get_all_pending_reissue(): :return: """ - policy_service.update_default_rotation_policy() return ( Certificate.query.filter(Certificate.rotation == true()) .filter(not_(Certificate.replaced.any())) diff --git a/lemur/factory.py b/lemur/factory.py index bdef4c653..3c0bafa7a 100644 --- a/lemur/factory.py +++ b/lemur/factory.py @@ -300,3 +300,6 @@ def install_plugins(app): "Domain authorization warmup failed, this is a best effort call:\n%s\n" % (traceback.format_exc()) ) + + from lemur.policies import service as policy_service + policy_service.update_default_rotation_policy() From 0dd2a90cf6ddcb0c84a90d03b09e2d28d619dd23 Mon Sep 17 00:00:00 2001 From: "evan.mcelheny" Date: Wed, 12 Aug 2026 15:35:44 -0400 Subject: [PATCH 10/16] refactor(rotation): give configure_default_rotation_policy its own method in factory Rather than piggy-backing off install_plugins, introduce a dedicated configure_default_rotation_policy(app) function that follows the same pattern as the other configure_* calls in create_app(). CLOUDR-2089 Workspace: local --- lemur/factory.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lemur/factory.py b/lemur/factory.py index 3c0bafa7a..089e75e23 100644 --- a/lemur/factory.py +++ b/lemur/factory.py @@ -68,6 +68,7 @@ def create_app(app_name=None, blueprints=None, config=None): configure_logging(app) configure_database(app) install_plugins(app) + configure_default_rotation_policy(app) @app.teardown_appcontext def teardown(exception=None): @@ -77,6 +78,19 @@ def teardown(exception=None): return app +def configure_default_rotation_policy(app): + """ + Ensures the named "default" RotationPolicy exists and its days are in sync + with LEMUR_DEFAULT_ROTATION_INTERVAL. Called once at boot via create_app() + so every process (web, celery worker, celery beat) starts with the policy + matching the configured value. + """ + from lemur.policies import service as policy_service + + with app.app_context(): + policy_service.update_default_rotation_policy() + + def from_file(file_path, silent=False): """ Updates the values in the config from a Python file. This function @@ -301,5 +315,4 @@ def install_plugins(app): % (traceback.format_exc()) ) - from lemur.policies import service as policy_service - policy_service.update_default_rotation_policy() + From 6b3e3450f77c0dfa6cfa34359b9678f6fc170e4e Mon Sep 17 00:00:00 2001 From: "evan.mcelheny" Date: Wed, 12 Aug 2026 15:39:57 -0400 Subject: [PATCH 11/16] log create vs update in update_default_rotation_policy CLOUDR-2089 Workspace: local --- lemur/policies/service.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lemur/policies/service.py b/lemur/policies/service.py index cb001b584..6aa657bfc 100644 --- a/lemur/policies/service.py +++ b/lemur/policies/service.py @@ -24,9 +24,15 @@ def update_default_rotation_policy(): days = current_app.config.get("LEMUR_DEFAULT_ROTATION_INTERVAL", 60) policies = get_by_name("default") if not policies: + current_app.logger.info( + "[+] Creating default rotation policy: days=%d", days + ) return create(days=days, name="default") policy = policies[0] if policy.days != days: + current_app.logger.info( + "[~] Updating default rotation policy: days %d -> %d", policy.days, days + ) update(policy.id, days=days) return policy From 73fddb2f1f31a31de8d5b1927ab8ef8fef376bd3 Mon Sep 17 00:00:00 2001 From: evan-datadog Date: Wed, 12 Aug 2026 15:49:21 -0400 Subject: [PATCH 12/16] Clean up formatting in factory.py Remove unnecessary blank lines before the closing parenthesis. --- lemur/factory.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lemur/factory.py b/lemur/factory.py index 089e75e23..2578da248 100644 --- a/lemur/factory.py +++ b/lemur/factory.py @@ -314,5 +314,3 @@ def install_plugins(app): "Domain authorization warmup failed, this is a best effort call:\n%s\n" % (traceback.format_exc()) ) - - From 6831cf8364d07fff205de1476caa4e3058bece51 Mon Sep 17 00:00:00 2001 From: "evan.mcelheny" Date: Wed, 12 Aug 2026 16:16:17 -0400 Subject: [PATCH 13/16] fix(rotation): guard configure_default_rotation_policy against missing table create_app() is called at pytest collection time before the in-memory SQLite DB has any tables. Skip the rotation policy sync when the rotation_policies table doesn't exist yet (tests, lemur db upgrade on a fresh DB). CLOUDR-2089 Workspace: local --- lemur/factory.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lemur/factory.py b/lemur/factory.py index 2578da248..2a25fad33 100644 --- a/lemur/factory.py +++ b/lemur/factory.py @@ -86,9 +86,13 @@ def configure_default_rotation_policy(app): matching the configured value. """ from lemur.policies import service as policy_service + from sqlalchemy import inspect with app.app_context(): - policy_service.update_default_rotation_policy() + # Skip if the table doesn't exist yet (e.g. during tests before + # migrations have run, or during `lemur db upgrade` on a fresh DB). + if inspect(db.engine).has_table("rotation_policies"): + policy_service.update_default_rotation_policy() def from_file(file_path, silent=False): From 898b6363661ee5576c87e8d3f2ff49fc0b2519ae Mon Sep 17 00:00:00 2001 From: "evan.mcelheny" Date: Wed, 12 Aug 2026 16:21:41 -0400 Subject: [PATCH 14/16] fix(rotation): catch OperationalError instead of inspecting table existence inspect(db.engine).has_table() uses a separate connection which can return stale results against SQLite in-memory test DBs. Catch OperationalError directly so the sync is skipped cleanly whenever the rotation_policies table doesn't exist yet. CLOUDR-2089 Workspace: local --- lemur/factory.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lemur/factory.py b/lemur/factory.py index 2a25fad33..115f80884 100644 --- a/lemur/factory.py +++ b/lemur/factory.py @@ -86,13 +86,18 @@ def configure_default_rotation_policy(app): matching the configured value. """ from lemur.policies import service as policy_service - from sqlalchemy import inspect + from sqlalchemy.exc import OperationalError with app.app_context(): - # Skip if the table doesn't exist yet (e.g. during tests before - # migrations have run, or during `lemur db upgrade` on a fresh DB). - if inspect(db.engine).has_table("rotation_policies"): + try: policy_service.update_default_rotation_policy() + except OperationalError: + # rotation_policies table doesn't exist yet (fresh DB / migrations + # not yet run). Safe to skip — the policy will be synced on the + # next boot after migrations complete. + app.logger.debug( + "Skipping default rotation policy sync: table not ready" + ) def from_file(file_path, silent=False): From 2ca9244b50ba310d8c4a70e90c7ba508bcbc5834 Mon Sep 17 00:00:00 2001 From: "evan.mcelheny" Date: Wed, 12 Aug 2026 16:30:45 -0400 Subject: [PATCH 15/16] fix(rotation): catch ProgrammingError; guard Certificate.__init__ query CI uses PostgreSQL where 'no such table' raises ProgrammingError, not OperationalError. Catch both in configure_default_rotation_policy. Also guard the RotationPolicy fallback query in Certificate.__init__ so test environments where create_all() hasn't run yet don't blow up during fixture setup. CLOUDR-2089 Workspace: local --- lemur/certificates/models.py | 5 ++++- lemur/factory.py | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py index c191329ba..1265c1ef8 100644 --- a/lemur/certificates/models.py +++ b/lemur/certificates/models.py @@ -229,7 +229,10 @@ def __init__(self, **kwargs): self.roles = list(set(kwargs.get("roles", []))) self.replaces = kwargs.get("replaces", []) self.rotation = kwargs.get("rotation") - self.rotation_policy = kwargs.get("rotation_policy") or RotationPolicy.query.filter_by(name="default").first() + try: + self.rotation_policy = kwargs.get("rotation_policy") or RotationPolicy.query.filter_by(name="default").first() + except Exception: + self.rotation_policy = kwargs.get("rotation_policy") self.key_type = kwargs.get("key_type") self.signing_algorithm = defaults.signing_algorithm(cert) self.bits = defaults.bitstrength(cert) diff --git a/lemur/factory.py b/lemur/factory.py index 115f80884..d7d3689a2 100644 --- a/lemur/factory.py +++ b/lemur/factory.py @@ -86,15 +86,15 @@ def configure_default_rotation_policy(app): matching the configured value. """ from lemur.policies import service as policy_service - from sqlalchemy.exc import OperationalError + from sqlalchemy.exc import OperationalError, ProgrammingError with app.app_context(): try: policy_service.update_default_rotation_policy() - except OperationalError: + except (OperationalError, ProgrammingError): # rotation_policies table doesn't exist yet (fresh DB / migrations - # not yet run). Safe to skip — the policy will be synced on the - # next boot after migrations complete. + # not yet run, or test DB before create_all). Safe to skip — the + # policy will be synced on the next boot after migrations complete. app.logger.debug( "Skipping default rotation policy sync: table not ready" ) From 6d33127e6ef0540ee8b170499409fb0331675943 Mon Sep 17 00:00:00 2001 From: evan-datadog Date: Thu, 13 Aug 2026 12:38:22 -0400 Subject: [PATCH 16/16] Update models.py --- lemur/certificates/models.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py index 1265c1ef8..c191329ba 100644 --- a/lemur/certificates/models.py +++ b/lemur/certificates/models.py @@ -229,10 +229,7 @@ def __init__(self, **kwargs): self.roles = list(set(kwargs.get("roles", []))) self.replaces = kwargs.get("replaces", []) self.rotation = kwargs.get("rotation") - try: - self.rotation_policy = kwargs.get("rotation_policy") or RotationPolicy.query.filter_by(name="default").first() - except Exception: - self.rotation_policy = kwargs.get("rotation_policy") + self.rotation_policy = kwargs.get("rotation_policy") or RotationPolicy.query.filter_by(name="default").first() self.key_type = kwargs.get("key_type") self.signing_algorithm = defaults.signing_algorithm(cert) self.bits = defaults.bitstrength(cert)