fix(rotation): use configurable default policy for certs with NULL rotation_policy_id - #355
fix(rotation): use configurable default policy for certs with NULL rotation_policy_id#355evan-datadog wants to merge 19 commits into
Conversation
…tation_policy_id Certs with rotation_policy_id = NULL cross-joined against all rotation_policies rows, matching the maximum days value (70) rather than the intended default. Fix: correlated subquery + COALESCE in the SQL expression so NULL certs use LEMUR_DEFAULT_ROTATION_POLICY_DAYS (default 60). Same guard in the Python instance method. Fixes CLOUDR-2089. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Workspace: local
…ays() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Workspace: local
60 was duplicated in both _default_rotation_days() and lemur.conf.py. Comment out the conf.py line — the code fallback is authoritative. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Workspace: local
…t rotation days Remove the hardcoded 60 fallback from _default_rotation_days(); the value is now set exclusively in lemur.conf.py (LEMUR_DEFAULT_ROTATION_POLICY_DAYS). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Workspace: local
This comment has been minimized.
This comment has been minimized.
Code Review FindingsFindings from a dual-engine review (Claude 1.
|
|
Hey, I got Claude to review this PR and cherry-picked some of the findings that I think are worth addressing. Thanks! |
|
Hey thanks so much fixing them now |
…tests, plugin guard - _default_rotation_days(): .get(..., 60) prevents KeyError when key absent from config - .scalar_subquery() → .as_scalar() for SQLAlchemy 1.3.24 compatibility - Add LEMUR_DEFAULT_ROTATION_POLICY_DAYS = 60 to default.conf.py and tests/conf.py - Add unit tests: config override, missing-key fallback, instance-level rotation window - Add class-level SQL test (requires CI postgres) - is_attached_to_endpoint: guard against plugins missing get_endpoint_certificate_names - Add .worktrees/ to .gitignore Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Workspace: local
a655118 to
ab985c8
Compare
…t.conf.py Canonical default (30) now lives in lemur/default.conf.py and tests/conf.py rather than as a magic fallback in Python code. Updates stale docstring reference to LEMUR_DEFAULT_ROTATION_POLICY_DAYS in models.py. Adds comment in manage.py cross-referencing the dual role of this key. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Workspace: local
The hasattr guard introduced a fail-open path: plugins without get_endpoint_certificate_names (GCP, Azure) would return False, bypassing the revocation safety check. The original AttributeError is already fail-closed. Also restores the removed docstring. Workspace: local
_parse_plugin_description and send_source_destination_pairing_metrics were deleted during earlier is_attached_to_endpoint edits. Workspace: local
…otation_days If the config value is None, non-integer, or non-positive, fall back to 30 and log a warning rather than letting timedelta crash (Python path) or COALESCE(subquery, NULL) silently exclude all NULL-policy certs (SQL path). Workspace: local
There was a problem hiding this comment.
Pull request overview
Fixes certificate rotation-window evaluation for certificates without an assigned rotation policy.
Changes:
- Uses correlated SQL policy lookup with configurable fallback.
- Adds NULL-policy tests and configuration documentation.
- Ignores local worktree directories.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
.gitignore |
Ignores .worktrees/. |
local/src/lemur.conf.py |
Adds local configuration example. |
lemur/certificates/models.py |
Corrects Python and SQL rotation-window logic. |
lemur/default.conf.py |
Defines the default rotation interval. |
lemur/manage.py |
Documents configuration reuse. |
lemur/tests/conf.py |
Sets the test rotation interval. |
lemur/tests/test_certificates.py |
Adds NULL-policy regression tests. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| # --------------------------------------------------------------------------- | ||
| # in_rotation_window — NULL rotation_policy tests | ||
| # --------------------------------------------------------------------------- |
| from lemur.certificates.models import Certificate | ||
| from lemur.tests.factories import CertificateFactory | ||
| import arrow | ||
|
|
||
| inside = CertificateFactory() | ||
| inside.rotation_policy = None | ||
| inside.not_after = arrow.utcnow().shift(days=30).datetime | ||
|
|
||
| outside = CertificateFactory() | ||
| outside.rotation_policy = None | ||
| outside.not_after = arrow.utcnow().shift(days=90).datetime | ||
|
|
||
| session.flush() | ||
|
|
||
| results = Certificate.query.filter(Certificate.in_rotation_window).all() | ||
| result_ids = {c.id for c in results} | ||
|
|
||
| assert inside.id in result_ids | ||
| assert outside.id not in result_ids |
60 days is the industry convention for 1-year certs (per CLOUDR-2089). 30 was too aggressive a step-down from the previous effective 70-day window. Workspace: local
- Change default rotation fallback from 30 to 60 days everywhere
- Update fallback assertion in test_default_rotation_days_fallback_when_key_absent
- Strengthen SQL regression test: seed a wide (90-day) policy and an
explicit-short (30-day) policy cert so the old cartesian-join bug
would be caught; remove stale service.py claim from PR description
Workspace: local
Workspace: local
|
I ran https://github.com/DataDog/claude-marketplace/tree/main/dual-agents-review on this PR and got the output below. Issue 1 seems legit, and straightforward to address. Issue 2 is worth addressing too. I'm not against shortening the revocation window from 60 days to 30 days, but I'm worried that lumping that together with the current change might make it more difficult to revert the shortening should an issue arise, so I recommend keeping the rotation time uniformly at 60 days for now. Many of the other findings are worth addressing not because they are significant but because they are easy to fix. As for finding 16, the change to .gitignore is probably incidental, and might make sense to revert. Dual Code Review ReportBranch: SummaryBoth engines converged strongly on one critical regression: the new The second theme, surfaced by nearly every Claude reviewer, is a 30 vs. 60 day default divergence: Beyond those two, the core SQL fix (correlated subquery + Critical IssuesIssue #1:
|
…nify 30/60 default - is_attached_to_endpoint: return True (fail-closed) + error log + capture_exception when source plugin lacks get_endpoint_certificate_names(), so a possibly still-deployed cert is never silently revoked (was fail-open returning False) - manage.py InitializeApp: seed default RotationPolicy via _default_rotation_days() instead of hardcoded 30 fallback, so seed and NULL-policy fallback can't diverge - default.conf.py/local conf: correct comment (lemur init), 60 example - tests: fail-closed assertion, fix contradictory docstring Workspace: local
|
Addressed the dual-review findings (comment 5257447900) and validated in the sandbox:
Sandbox validation (validate-70d-rota): deployed this branch to Keeping rotation at 60 days per your recommendation (no window shortening in this change). |
…T_ROTATION_INTERVAL The seed (lemur init / InitializeApp) previously skipped when a 'default' RotationPolicy row already existed, so deployments seeded by the historical migration (30d) or with a custom value (e.g. sandbox 35d) kept diverging from the 60d NULL-policy fallback. Extract sync_default_rotation_policy() into policies/service.py: creates the policy if missing, otherwise updates days in place to _default_rotation_days() (config-driven, default 60). Add unit tests. Workspace: local
|
Follow-up to Issue #2: the previous commit only unified the default for new seeds — an existing Now |
… 60 days lemur init is NOT invoked by conductor deploys (containers run lemur start / celery worker / celery beat only), so the InitializeApp sync alone would not reach existing deployments. Add a data migration (chained to head a1b2c3d4e5f6) that updates the named 'default' rotation policy to 60 days (the LEMUR_DEFAULT_ROTATION_INTERVAL default), applied via the documented 'lemur db upgrade' step. Downgrade reverts to 30. Workspace: local
|
Important correction to the previous comment: So the Net: fresh seeds get 60 via |
…2c3d4e5f6) The temporary_break_glass_grants migration (a1b2c3d4e5f6) is on a separate branch, not master. Master head is 44d67c1988a2 (matches existing deployments, e.g. sandbox). Re-chain so the migration applies cleanly via lemur db upgrade. Workspace: local
… (review swarm finding C) endpoint_service.get_by_name() can return None; previously endpoint.source would raise AttributeError. Log error + capture_exception and return False (nothing to be attached to) instead of crashing. Add unit test for the endpoint-not-found case. Workspace: local
|
Ran a review swarm (6 lenses × 3 cheap models + aggregator) over the full diff. Verdict: LGTM with minor changes. Applied the one legitimate finding:
Findings assessed as not actionable:
Branch now: |
|
https://github.com/DataDog/k8s-resources/pull/171269 not strictly necessary difference betwwen being where we want things to be configured. |
… this PR The is_attached_to_endpoint fail-closed + endpoint-None guard is unrelated to the rotation-window fix and belongs in its own PR. Restore service.py to master and remove the is_attached_to_endpoint tests; they will be re-applied in a separate change. Workspace: local
| default instead of cross-joining against all rotation_policies rows. | ||
| :return: | ||
| """ | ||
| policy_days = ( |
There was a problem hiding this comment.
mind simplifying this to just a if statement for the existence of a rotation_policy_id or not
Summary
Root cause (IR-57191 / CLOUDR-2089):
in_rotation_windowused a bareRotationPolicy.daysreference that produced a cartesian join against all rotation_policies rows. For certs withrotation_policy_id = NULL, this silently matched the maximumdaysvalue across all policy rows (70) rather than raising or using a configured default.Fixes:
COALESCEin the SQL expression path, so each cert resolves its own policy'sdays, withNULLfalling back toLEMUR_DEFAULT_ROTATION_INTERVALNoneguard to the Python instance path for the same fallback_default_rotation_days()helper: readsLEMUR_DEFAULT_ROTATION_INTERVAL(default 60), validates it is a positive int, and logs a warning + falls back to 60 if invalidLEMUR_DEFAULT_ROTATION_INTERVAL = 60indefault.conf.pyandtests/conf.py— single source of truth for both the DB seed and the NULL-policy fallbackmanage.pyseeds the"default"RotationPolicy row via_default_rotation_days()instead of a hardcoded 30 fallback, so the seeded policy and the runtime NULL-policy fallback can never diverge."default"policies (follow-up to Issue Create ci-dd.yml #2):lemur initnow callssync_default_rotation_policy()(new inpolicies/service.py) — creates the policy if missing, otherwise updates an existing row'sdaysin place to the configured default. Deployments seeded by the historical migration (30d) or with a custom value (e.g. sandbox 35d) are brought in sync with the 60d NULL-policy fallback on the nextlemur init.lemur initis not run by conductor deploys (containers runlemur start/celery worker/celery beatonly), so the sync alone wouldn't reach existing DBs. Added migrationc3d4e5f6a7b8that updates the named"default"RotationPolicy to 60 days, applied via the documentedlemur db upgradestep (downgrade reverts to 30). Chained from the master head44d67c1988a2(thea1b2c3d4e5f6break-glass migration is on a separate branch, not master) so it applies cleanly to existing deployments.max(days)= 70 for all certs). After this fix, certs with an explicit shorter policy no longer match the 70-day window — they use only their own policy'sdays. Expect a step-down in reissue/expiry-check volume after merge; this is correct behavior, not a regression.Changes
lemur/certificates/models.pyin_rotation_windowhybrid property: correlated subquery + COALESCE (SQL path);None-policy guard (instance path); new_default_rotation_days()helper readingLEMUR_DEFAULT_ROTATION_INTERVAL(validated, default 60);.as_scalar()for SQLAlchemy 1.3.24 compatlemur/policies/service.pysync_default_rotation_policy(): creates the"default"policy if missing, or updates an existing row'sdaysto_default_rotation_days()— closes the seed-vs-fallback divergence for existing deploymentslemur/manage.pyInitializeAppcallssync_default_rotation_policy()(was a hardcoded 30 fallback + skip-if-exists) — existing"default"policies are synced to the configured default onlemur initlemur/migrations/versions/c3d4e5f6a7b8_*.py"default"RotationPolicy to 60 days (theLEMUR_DEFAULT_ROTATION_INTERVALdefault) — reaches existing deployments vialemur db upgrade, sincelemur initisn't run on deploy; chained from master head44d67c1988a2; downgrade reverts to 30lemur/default.conf.pyLEMUR_DEFAULT_ROTATION_INTERVAL = 60; correct comment (lemur init, notcreate_config)lemur/tests/conf.pyLEMUR_DEFAULT_ROTATION_INTERVAL = 60lemur/tests/test_certificates.pyrotation_policy=None; SQL-level test with wide-policy fixture to prove no cross-join; fixed contradictory docstring.gitignore.worktrees/local/src/lemur.conf.pyLEMUR_DEFAULT_ROTATION_INTERVAL(60)Sandbox validation (validate-70d-rota, 2026-08-11)
Deployed this branch (
v130078016-de08cd9f) to the Lemur sandbox and validated against real + synthetic certs:_default_rotation_days()= 60 post-deploy<=), 90d → OUT — all PASSget_rotation_candidates()end-to-end + instance/SQL path parity — PASS"default"policy 35d vs 60d fallback — now unified) — both addressed in this revisionlemur db upgraderan44d67c1988a2 -> c3d4e5f6a7b8, updating the"default"policy from 35 → 60 days; custom policies untouched (saml=65); alembic version advanced toc3d4e5f6a7b8mutable-latest-prod, all test certs cleaned upTest plan
_default_rotation_days()usesLEMUR_DEFAULT_ROTATION_INTERVALvalue when set_default_rotation_days()returns 60 (no KeyError) when key absentin_rotation_windowreturnsTruefor NULL-policy cert expiring in 30 daysin_rotation_windowreturns falsy for NULL-policy cert expiring in 90 daysin_rotation_windowincludes a NULL-policy cert inside the default window, excludes one outside, and excludes an explicit-short-policy cert that the old cross-join would have pulled in (wide-policy fixture)sync_default_rotation_policy()creates the"default"policy at 60 when missing; updates an existing 30d row to 60; no-ops when already 60c3d4e5f6a7b8applied vialemur db upgrade—"default"policy 35 → 60, custom policies untouched, alembic version advancedEXPLAINonCertificate.in_rotation_windowshows correlated subquery, no cross-join (manual verification)Fixes CLOUDR-2089
Workspace:
local🤖 Generated with Claude Code