fix(rotation): default NULL-policy certs to the 'default' rotation policy - #362
Conversation
…licy 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
…e 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
…-candidate query 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
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
… 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
Workspace: local
| from lemur.policies.models import RotationPolicy | ||
|
|
||
|
|
||
| def get_rotation_policy_from_config(): |
There was a problem hiding this comment.
i dont think this function is worthwhile, let's just keep this service.py file simple. I think we should just read the value directly in the caller
There was a problem hiding this comment.
Read what? the config value? we would have to update everywhere that RotationPolicy.days is referenced. Makes more sense; it is after all just a number and doesnt really need ot be a whole table in the dabase but it is currently and thats a much larger work of art.
There was a problem hiding this comment.
im confused as to why were not calling policy.get in the other file? why do we have two getters in the policy file now?
There was a problem hiding this comment.
why do we call this every time we reissue a cert?
There was a problem hiding this comment.
its not two but three; but I get your point (heh). Would you be ok if I just renamed it to update_default_rotation_policy ?
If thats the case do you want me to also put it in the manage.py instead of the whole
_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", 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))
policy_service.create(days=days, name=_DEFAULT_ROTATION_INTERVAL)
There was a problem hiding this comment.
isn't the default policy supposed to be set once at policy creation? why is it set at cert issuance?
There was a problem hiding this comment.
I am assuming that we are going to need to update this as cert lifespan goes down (a rota policy of 60 days makes zero sense when the certs only last for a max of 49 days, we would be reissuing our entire fleet every day). I want a sane way to tune this policy via a config change instead of sshing into the pod and hand writing to the database.
My plan is to have update_default_rotation_policy get called everytime we calculate what certs need to be rotated so if we decide to rollout a config change that sets the policy to 45 days or 30 it will get rolled out to every cert that uses the default policy.
That way we just have a single policy that the vast majority of certs use; if there is some strange subset that need a different policy they can have that and be on their own.
There was a problem hiding this comment.
yes but the default policy is a config value, why do we need to query it at cert issuance? why not query it and set it at lemur boot?
…date 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
…nstall_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
…thod 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
CLOUDR-2089 Workspace: local
This comment has been minimized.
This comment has been minimized.
Remove unnecessary blank lines before the closing parenthesis.
…g 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
…stence 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
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
|
With the new changes it has been repushed to sandbox and is fully working. |
Summary
Certs created without an explicit
rotation_policy(the import/discovery and ACME/pending-cert paths) end up withrotation_policy_id = NULL. That's the root cause behind the ~559 NULL-policy certs in prod (and 231 in sandbox) — and because of the old cross-join bug they were effectively being rotated on a 70-day window.This is the simple fix (vs. the over-engineered #355): instead of touching the rotation-window SQL, it makes the
Certificateconstructor fall back to the nameddefaultRotationPolicy so no newly created cert can ever be NULL-policy.The
defaultrow is kept in sync withLEMUR_DEFAULT_ROTATION_INTERVAL— and the sync fires right before the rotation-candidate query (get_all_pending_reissue), so a config change takes effect on the next rotation pass regardless of whether any cert is created.Change
lemur/certificates/models.py_get_default_rotation_policy()— pure getter (fetch, create-if-missing with config days, no config-sync).Certificate.__init__useskwargs.get("rotation_policy") or _get_default_rotation_policy().lemur/policies/service.pysync_default_rotation_policy()— creates or updates thedefaultrow'sdaystoLEMUR_DEFAULT_ROTATION_INTERVAL.lemur/certificates/service.pyget_all_pending_reissue()callssync_default_rotation_policy()at the top, so the row is in sync before the in-rotation-window query runs.lemur/default.conf.pyLEMUR_DEFAULT_ROTATION_INTERVAL = 60(canonical config).lemur/tests/conf.pyLEMUR_DEFAULT_ROTATION_INTERVAL = 60.lemur/manage.py60— both the seed and the NULL-policy fallback read the same single flag (LEMUR_DEFAULT_ROTATION_INTERVAL).Scripts:
experimental/users/evan.mcelheny/local/scripts/lemur/lemur-rota.shcopiesrotation_policy.pyinto a lemur pod and runs it against the pod's Postgres. Safe to re-run (all actions are idempotent). Tested in sandbox.Run in order:
Sandbox validation
Audit (pre-change)
defaultFixes CLOUDR-2089