v3.10.0: build only the checks your config can actually trigger (17 down to 4, and a 1.6ms import) #61
rennf93
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
That is the whole upgrade. Your config keeps working, your decorators keep working, and you do not need to change a line.
The short version
A default
SecurityConfig()used to build 17 security checks and run every one of them on every request. It now builds 4.import guard_corewent from roughly 963ms to roughly 1.6ms.Where the 13 checks went
The checks were always there. Most of them just could not do anything.
UserAgentCheckblocks requests whose user agent matches a pattern you configured. Ifblocked_user_agentsis empty and no route decorator sets one, it has nothing to match against. It still ran on every request: it got constructed, called, read two attributes off the request, found nothing to do, and returned. Multiply that by thirteen checks and every request you serve.Now each check answers one question when the pipeline is built: can this configuration ever make me fire? If the answer is no, the check is never constructed.
That question is a new classmethod:
The base implementation returns
True. A check that does not override it always runs.Why this cannot quietly stop protecting you
This is the part I spent the most time on, because getting it wrong means silently serving traffic you thought was being checked.
Dropping a check is a speed optimization. It is never a security decision. Four things enforce that:
IpSecurityCheckis never dropped, ever. It fronts a ban lookup whose store other processes can write to, so no configuration can prove it unreachable.enable_dynamic_rules=Truekeeps every check the rules engine can switch on, whatever the rest of your config says.There is also a test that runs the same request through the old 17 check pipeline and the new one and fails if they ever disagree.
Changing config at runtime still works
If you mutate config after startup, the pipeline notices and rebuilds itself before the next request.
Whole assignments, in place list and set and dict edits, route decorator changes, and newly registered routes all count. The check costs one integer comparison per request when nothing has changed.
Where the import time went
import guard_coreused to pull in the entire handler tree, and through it aiohttp, requests, redis, maxminddb, and if you had guard-agent installed, cryptography.Now it pulls in none of them. If you never block cloud providers, you never load the HTTP client that fetches cloud IP ranges.
Three new extras go with it:
They are additive in 3.x, so nothing breaks if you ignore them. Configure a feature whose extra is missing and
SecurityConfigtells you which one to install instead of failing with anImportErrorsomewhere in the middle of a request.Fixes you might actually hit
DynamicRuleManager.stop()never stopped anything. Flask and Django deployments leaked a still polling background thread on every shutdown or worker recycle. Fixed.guard_core.syncwas handing out the asyncSecurityDecoratorandBehaviorTracker. Callinginitialize_redis()on them returned a coroutine nobody awaited, so Redis backed behaviour tracking silently never ran. This affected Flask and Django adapters only if they imported from the top levelguard_core.sync, which the shipping ones do not.security_headers_appliedandcsp_violationevents could never fire, because nothing ever wired an agent handler into the security headers handler. They fire now. Silence them withmuted_event_typesif you do not want them.block_cloud_providersused to be reported as an allowlist failure, which sent you looking in the wrong place.If you write your own checks
applies_tois the extension point. Add it to your check and it participates in elimination:Skip it and your check always runs, same as before.
One rule if you write one: the predicate must be at least as wide as what
check()can actually do. Ifcheck()can block under some config,applies_tomust returnTruefor that config. When in doubt, returnTrue.Worth checking before you upgrade
If you mutate
SecurityConfigat runtime and do not use dynamic rules, you were relying on every check reading live config on every request. That still works, but it now works because the pipeline rebuilds rather than because everything always ran. If you do anything unusual there, upgrade in staging first.Full changelog: https://github.com/rennf93/guard-core/blob/master/CHANGELOG.md
All reactions