SecurityConfig Reference¶
SecurityConfig is a Pydantic BaseModel that controls all guard-core behavior. Adapter developers should expose relevant fields to their users while keeping internal fields (agent, dynamic rules) as implementation details.
Core Settings¶
| Field | Type | Default | Description |
|---|---|---|---|
passive_mode |
bool |
False |
Log-only mode. Logs and emits events but never blocks. |
fail_secure |
bool |
True |
Block the request with 500 when any security check raises an unexpected exception. False logs and falls through (fail-open); opt-in only for staging diagnostics. |
exclude_paths |
list[str] |
See below | Paths that skip detection and behavioral analysis; ban enforcement and rate limiting still apply. See Ban Configuration. |
custom_error_responses |
dict[int, str] |
{} |
Override error messages for specific HTTP status codes. |
enforce_https |
bool |
False |
Redirect HTTP requests to HTTPS globally. |
custom_request_check |
Callable \| None |
None |
Global async function for custom request validation. |
auth_verifier |
Any |
None |
Global default verifier for require_auth/api_key_auth routes with no per-route verifier=: verifier(request, credential) -> Principal \| None. Sync or async in async deployments; sync-only under WSGI. |
custom_response_modifier |
Callable \| None |
None |
Global async function to modify responses. |
route_resolution_strict |
bool |
False |
Block with 500 when the adapter reports it could not resolve the route, instead of running the pipeline with no per-route config. Also turns requests to paths the app does not serve into 500s rather than 404s. See Reporting a Failed Match. |
on_error |
Callable[[str, BaseException, dict], None] \| None |
None |
Best-effort callback invoked when a middleware/agent step fails, receiving (stage, exception, context). stage is one of agent_init, geoip, transport_send, encryption. Also forwarded to AgentConfig.on_error when the agent is enabled; see Agent / Telemetry. |
on_block |
Callable[[GuardRequest, dict[str, Any]], None] \| None |
None |
Fired exactly once per block decision with (request, payload); in passive mode fires at flag time with status_code=None. Payload keys: check_name, reason, trigger_info, passive_mode, client_ip, path, method, status_code. Not fired for custom_request/custom_validators (application-authored), the HTTPS redirect, or a Redis-unavailable response (use on_error). Sync or async under ASGI; sync-only under WSGI (async raises TypeError). A raising hook is logged, never propagated. |
Default exclude_paths: ["/docs", "/redoc", "/openapi.json", "/openapi.yaml", "/favicon.ico", "/static"]
Adapter Exposure
All core settings should be exposed to end users. passive_mode is particularly useful for deployment rollouts.
Proxy Configuration¶
| Field | Type | Default | Description |
|---|---|---|---|
trusted_proxies |
tuple[str, ...] |
() |
Trusted proxy IPs or CIDR ranges for X-Forwarded-For. |
trusted_proxy_depth |
int |
1 |
Number of proxies in the X-Forwarded-For chain. |
trust_x_forwarded_proto |
bool |
False |
Trust X-Forwarded-Proto header for HTTPS detection. |
Validators:
trusted_proxies: Each entry is validated as a valid IP address or CIDR range, or the literal string"unix", which marks a peer-less connection (norequest.client_host, e.g. a Unix domain socket) as a trusted hop soX-Forwarded-Forstill resolves the real client.whitelistandblacklistdo not accept"unix".trusted_proxies: if any entry is a/0network (0.0.0.0/0or::/0), construction logs aWARNINGnaming the risk (every peer becomes trusted to setX-Forwarded-For); the literal"unix"token is exempt from this check.trusted_proxy_depth: Must be >= 1.
trusted_proxy_depth is the number of hops you vouch for, not a maximum: the connecting peer must itself be listed in trusted_proxies. A chain shorter than the configured depth, or a depth-selected entry that is itself a trusted proxy, now logs a one-time WARNING. A declared depth that over-counts the real hops -- an entry to the right of the depth-selected one is not itself a listed trusted proxy -- also logs a one-time WARNING and, unlike the first two, corrects the resolved identity: it walks the chain right to left and returns the first entry that is not a listed trusted proxy, instead of the plain (and, in that case, client-rotatable) depth-indexed entry. See Unsatisfiable and Over-Counted Depth.
Your app server must not pre-resolve the client itself
Leaving trusted_proxies unset only means "X-Forwarded-For is never trusted" if your ASGI/WSGI server isn't already applying that header before guard-core runs. uvicorn's default proxy_headers=True (and equivalent settings in Gunicorn/Hypercorn) does exactly that. See Deployment Prerequisite for the fix.
IP Management¶
| Field | Type | Default | Description |
|---|---|---|---|
whitelist |
tuple[str, ...] \| None |
None |
Allowed IPs/CIDRs. None disables (allow all). |
blacklist |
tuple[str, ...] |
() |
Blocked IPs/CIDRs. |
whitelist_countries |
frozenset[str] |
frozenset() |
Allowed countries. Non-empty = only listed pass (unknown blocked). Overrides blocked_countries. |
blocked_countries |
frozenset[str] |
frozenset() |
Country codes always blocked. |
blocked_user_agents |
list[str] |
[] |
Regex patterns for blocked user agents. |
enable_ip_banning |
bool |
True |
Enable automatic IP banning. |
auto_ban_threshold |
int |
10 |
Suspicious requests before auto-ban (>= 1). |
auto_ban_duration |
int |
3600 |
Ban duration in seconds (>= 1). |
Validators:
whitelistandblacklist: Each entry validated as a valid IP or CIDR range viaipaddress.ip_address()/ip_network().whitelist: if any entry is a/0network (0.0.0.0/0or::/0), construction logs aWARNINGnaming the risk (every address becomes whitelisted, soblacklist,blocked_countriesand IP bans cannot block anyone). Precedence is unchanged: a/0whitelist still allows every address, this is a signal only.
Whitelist Semantics
whitelist=None means "no whitelist" (all IPs pass). whitelist=[] means "empty whitelist" (no IPs pass). Adapter developers should document this distinction.
Per-Category Bans¶
threat_ban_config lets each detection category carry its own threshold and duration. The pipeline tracks per-category counts in suspicious_request_counts, then walks the matched categories of the current detection. The first category whose own count reaches its threshold triggers a category-tagged ban; if no per-category entry matches, the flat auto_ban_threshold / auto_ban_duration fallback fires off the total count instead.
| Field | Type | Default | Description |
|---|---|---|---|
threat_ban_config |
MappingProxyType[str, ThreatBanConfig] |
mappingproxy({}) |
Per-category ban policy. Validator rejects unknown keys. |
When to use:
- You want SQL injection detections to be a single-strike ban for a week, but XSS detections to be a 3-strike ban for an hour.
- You want to keep the existing flat threshold for any category you do not name.
- You want the audit log reasons to disambiguate which category triggered the ban (
"penetration_attempt:sqli"vs the flat"penetration_attempt").
from guard_core.models import SecurityConfig, ThreatBanConfig
config = SecurityConfig(
auto_ban_threshold=10,
auto_ban_duration=3600,
threat_ban_config={
"sqli": ThreatBanConfig(threshold=1, duration=604800),
"xss": ThreatBanConfig(threshold=3, duration=86400),
},
)
See Ban Configuration for the ThreatBanConfig model and the full fall-through rule.
Global Behavior Rules¶
global_behavior_rules applies behavior rules to every route without requiring decorators. The merged rules are run alongside any decorator-specified rules. The most common use is service-wide 404-noise correlation, but the same shape supports usage, frequency, and return_pattern rules.
| Field | Type | Default | Description |
|---|---|---|---|
global_behavior_rules |
tuple[BehaviorRuleConfig, ...] |
() |
Behavior rules merged into every route. Immutable: reassign the whole field to change it, .append() raises AttributeError. |
behavior_scan_response_body |
bool |
False |
Read response bodies to evaluate return_pattern rules whose pattern is not status: (json:, regex:, or a bare substring). Off by default: no response body is ever read for pattern matching, and constructing a return_pattern rule with a non-status: pattern while this is False raises ValueError instead of silently accepting a rule that can never match. status: patterns match on status_code alone and are unaffected. |
behavior_max_response_body_inspect_bytes |
int |
262144 |
Maximum bytes read from the start of a response body and held for return_pattern inspection when behavior_scan_response_body is True. Bounds what guard-core retains, not what the application produces; a streaming response stays streaming to the client. See Protocols - BoundedResponseBodyReader. |
body_read_timeout |
float |
3.0 |
Seconds to wait for an adapter's read_body_prefix/body call before giving up. Bounds the request-body detection read and the response-body behaviour-rule read against a stalled or misbehaving adapter/stream; on timeout the body is treated as unavailable, the same fail-closed outcome already used when the adapter raises. The async guard_core tree bounds the wait via asyncio.wait_for. The sync tree (guard_core.sync) cannot cancel a blocking call from the outside, so each read attempt runs on its own daemon thread and this value bounds how long the caller joins that thread instead; see sync_body_read_max_concurrent for the concurrent-thread budget. |
sync_body_read_max_concurrent |
int |
64 |
Maximum daemon threads the sync (guard_core.sync) tree may have blocked at once inside an adapter's read_body_prefix/body call. Once that many threads are already blocked on a stalled read, further attempts queue for the same body_read_timeout budget and then give up, logging the exhaustion, instead of spawning without limit. |
When to use:
- You want a global "ban after 20 404s in 5 minutes" rule that does not require touching every route.
- You want detection-correlated thresholds,
correlate_with_detection=Truehalves the threshold (floor 1) when the IP has any positivesuspicious_request_countsentry, so probing that already triggered a regex hit gets banned faster. - You want a service-wide frequency or usage cap for any caller, regardless of which route they hit.
from guard_core.models import BehaviorRuleConfig, SecurityConfig
config = SecurityConfig(
global_behavior_rules=[
BehaviorRuleConfig(
rule_type="return_pattern",
threshold=20,
window=300,
pattern="status:404",
action="ban",
ban_duration=3600,
correlate_with_detection=True,
),
],
)
A json:, regex:, or bare-substring pattern additionally requires behavior_scan_response_body=True and an adapter that implements BoundedResponseBodyReader; without both, construction rejects the rule outright rather than accepting one that can never match:
config = SecurityConfig(
behavior_scan_response_body=True,
behavior_max_response_body_inspect_bytes=65536,
global_behavior_rules=[
BehaviorRuleConfig(
rule_type="return_pattern",
threshold=5,
window=300,
pattern="json:error.code==AUTH_FAIL",
action="ban",
),
],
)
See Behavior Rules for the full field reference.
Detection Exclusions¶
These fields opt request components out of penetration detection. The header set is merged with a hardcoded default that already excludes host, user-agent, accept, accept-encoding, connection, origin, referer, all sec-fetch-*, all sec-ch-ua*, and the proxy identity headers forwarded, x-forwarded-for, x-forwarded-host, x-forwarded-proto, x-real-ip, x-client-ip, x-cluster-client-ip, cf-connecting-ip, true-client-ip, fly-client-ip, and x-envoy-external-address (their values are addresses or hostnames set by infrastructure, not the client, so a private address like 192.168.65.1 never trips the SSRF pattern). An excluded header still gets the always-on command-injection check, so X-Real-IP: $(id) is still detected; client IP attribution is unaffected, since it never used pattern detection. enabled_detection_categories narrows the regex scan to a subset of the 18 known categories; custom user patterns always run regardless. An empty enabled_detection_categories while enable_penetration_detection is True logs a WARNING at construction: detection would run on every request but never match anything.
| Field | Type | Default | Description |
|---|---|---|---|
excluded_detection_headers |
set[str] |
set() |
Headers exempted only from the categories known to false-positive on their typical values: identity and proxy headers such as X-Forwarded-For, X-Real-IP, Host, Origin and Via skip ssrf only, and every other category (sqli, xss, cmd_injection, template, Log4Shell) still scans them. A name outside the built-in identity set inherits the same rule when its value looks like an address chain, otherwise it gets no exclusion at all. Merged with the hardcoded default list. |
excluded_detection_params |
set[str] |
set() |
Query parameter names skipped by detection. |
excluded_detection_body_fields |
set[str] |
set() |
Top-level JSON body keys skipped by detection. |
enabled_detection_categories |
frozenset[str] |
full ALL_DETECTION_CATEGORIES |
Categories scanned for. Validator rejects unknown labels. |
When to use:
- A first-party endpoint accepts JSON containing literals (Markdown source, code blobs, URL-shaped query params) that look like attacks but are not.
- A regression in one category's regex is producing false positives faster than you can write a fix, disable the category temporarily.
- A privacy-sensitive header value should not be scanned at all.
- You want different routes to have different opt-outs, pair this with
@security.detection_exclusion(...)on the route.
from guard_core.models import SecurityConfig
config = SecurityConfig(
excluded_detection_params={"q", "search", "filter"},
excluded_detection_body_fields={"description", "markdown"},
enabled_detection_categories={"sqli", "xss", "cmd_injection", "ssrf"},
)
IP Lifecycle Controls¶
These fields tune cold-start and horizontal-scale behaviour for the geo-IP and cloud-IP subsystems. They are inert by default, only adjust if you have a specific cold-start or scale-out problem.
| Field | Type | Default | Description |
|---|---|---|---|
lazy_init |
bool |
True |
When True (default), run the IPInfo MMDB download and cloud-IP provider fetches as a background task during startup instead of awaiting them inline, so app boot never blocks on multi-second network calls. Cloud and geo layers are inert until the task completes. Set to False to await them inline for synchronous-init guarantees. |
geo_ip_db_max_age |
int |
86400 |
Maximum age in seconds for the IPInfo MMDB before re-download. Range 3600 - 604800. |
cloud_ip_store |
CloudIpStoreProtocol \| None |
None |
Pluggable cloud-IP backend. None uses the in-memory default; auto-upgraded to Redis when Redis is enabled. |
When to use:
lazy_init=Trueto keep startup non-blocking when IPInfo MMDB or cloud-IP provider fetches are slow. The background warmup runs concurrently with normal request handling; cloud-provider blocking and geo checks become active once the background task finishes. Rate limiting, IP banning, pattern detection, and other layers remain fully active throughout the warmup window.lazy_initonly takes effect when Redis is enabled and the adapter callsinitialize_redis_handlers()from its own startup hook (for example fastapi-guard's lifespan integration), see Provider Status below for the accessor that lets a Kubernetes/ALB warmup probe (or any health endpoint) tell when that window has closed.geo_ip_db_max_ageto tighten or loosen the IPInfo refresh cadence, match it to your IPInfo plan's update frequency.cloud_ip_storeto point multiple horizontally-scaled instances at a single pre-populated Redis namespace, skipping per-instance cloud-IP cold starts.
from guard_core.handlers.cloud_ip_stores import RedisCloudIpStore
from guard_core.handlers.redis_handler import RedisManager
from guard_core.models import SecurityConfig
config = SecurityConfig(
lazy_init=True,
geo_ip_db_max_age=43200,
)
shared_store = RedisCloudIpStore(RedisManager(config))
config_with_shared_store = SecurityConfig(cloud_ip_store=shared_store)
Provider Status¶
cloud_handler.get_status() (the module-level singleton) and your IPInfoManager instance's get_status() report per-provider readiness, the last successful refresh timestamp, and a cheap entry count. HandlerInitializer is adapter-internal, its get_initialization_status() combines both into one payload, and adapters expose that combined payload as their status surface (fastapi-guard: SecurityMiddleware.get_initialization_status(), or add_status_route(app) → GET /_guard/status).
Cloud-only status, callable anywhere:
from guard_core.handlers.cloud_handler import cloud_handler
cloud_status = cloud_handler.get_status()
# {
# "AWS": {"ready": True, "last_refreshed": datetime(...), "entries": 3421},
# "GCP": {"ready": False, "last_refreshed": None, "entries": 0},
# ...
# }
Geo-IP status, call get_status() on the IPInfoManager instance you passed in as geo_ip_handler (there is no module singleton: the manager is token-gated, so it is instantiated per app, not at import time):
geo_status = ip_info_manager.get_status()
# {"ready": True, "last_refreshed": datetime(...), "entries": 494}
Combined cloud + geo-IP payload, for a warmup probe or health endpoint, read it through your adapter rather than reconstructing HandlerInitializer yourself (fastapi-guard):
from guard.status import add_status_route
add_status_route(app, path="/_guard/status") # GET /_guard/status -> combined payload
# or, in-process: security_middleware.get_initialization_status()
# {
# "cloud_providers": { ...as above... },
# "geo_ip": { ...as above... },
# }
geo_ip is None when no geo_ip_handler is configured. A custom geo_ip_handler that does not implement get_status() still reports ready (from the required is_initialized property) with last_refreshed/entries as placeholders. This is synchronous, dependency-free, and cheap enough to poll from a warmup probe or health endpoint, it is exactly what to wire up for the "cannot tolerate any inert window" case above.
See Cloud IP Store for the protocol contract and the Redis namespace migration note.
Geolocation¶
| Field | Type | Default | Description |
|---|---|---|---|
geo_ip_handler |
GeoIPHandler \| None |
None |
Custom geolocation handler implementing the protocol. |
ipinfo_token |
str \| None |
None |
IPInfo API token for IP geolocation. |
ipinfo_db_path |
Path \| None |
data/ipinfo/country_asn.mmdb |
Path to the local IPInfo MMDB database file. |
Model validator: If blocked_countries or whitelist_countries are set, geo_ip_handler must be provided (or ipinfo_token, from which one is constructed automatically). Raises ValueError otherwise.
Rate Limiting¶
| Field | Type | Default | Description |
|---|---|---|---|
enable_rate_limiting |
bool |
True |
Master switch for rate limiting. |
rate_limit |
int |
10 |
Maximum requests per window (global). |
rate_limit_window |
int |
60 |
Window duration in seconds (global). |
endpoint_rate_limits |
dict[str, tuple[int, int]] |
{} |
Per-endpoint overrides {path: (limit, window)}. |
enable_rate_limit_auto_ban |
bool |
False |
Feed rate-limit violations into the same auto-ban engine penetration detection uses. Requires enable_ip_banning to actually ban; off by default, so this is zero behavior change unless enabled. |
enable_rate_limit_auto_ban reuses the Per-Category Bans machinery: each active-mode (non-passive) rate-limit violation increments the rate_limit category of suspicious_request_counts and runs the same threshold logic, threat_ban_config["rate_limit"] first if present, otherwise the flat auto_ban_threshold / auto_ban_duration policy, banning with reason="rate_limit_exceeded" (distinct from the "penetration_attempt" reason the detection path uses). See Rate-limit auto-ban for the full behavior. Like the rest of the auto-ban state, suspicious_request_counts is in-memory and per-process, so multi-replica deployments do not share rate-limit auto-ban counts across replicas.
Cloud Provider Blocking¶
| Field | Type | Default | Description |
|---|---|---|---|
block_cloud_providers |
frozenset[str] \| None |
None |
Providers to block: "AWS", "GCP", "Azure", "DigitalOcean", "Linode", "Vultr". A bare name blocks the whole provider; a region carve-out ("GCP:!us-central1") blocks the provider except that region. Region metadata exists for AWS and GCP only, so a carve-out on the other four exempts nothing and the whole provider stays blocked. An unrecognized provider name raises ValueError. |
cloud_ip_refresh_interval |
int |
3600 |
Seconds between IP range refreshes (60-86400). |
Validator: each entry is valid when the part before an optional :!region suffix is one of the six provider names; any invalid entry raises ValueError naming it (nothing is silently dropped).
Security Headers¶
| Field | Type | Default | Description |
|---|---|---|---|
security_headers |
dict[str, Any] \| None |
See below | Security headers configuration dict. |
Default structure:
{
"enabled": True,
"hsts": {"max_age": 31536000, "include_subdomains": True, "preload": False},
"csp": None,
"frame_options": "SAMEORIGIN",
"content_type_options": "nosniff",
"xss_protection": "1; mode=block",
"referrer_policy": "strict-origin-when-cross-origin",
"permissions_policy": "geolocation=(), microphone=(), camera=()",
"custom": None,
}
CORS¶
| Field | Type | Default | Description |
|---|---|---|---|
enable_cors |
bool |
False |
Enable CORS header injection. |
cors_allow_origins |
list[str] |
["*"] |
Allowed origins. |
cors_allow_methods |
list[str] |
["GET", "POST", ...] |
Allowed HTTP methods. |
cors_allow_headers |
list[str] |
["*"] |
Allowed request headers. |
cors_allow_credentials |
bool |
False |
Allow credentials in CORS requests. |
cors_expose_headers |
list[str] |
[] |
Headers exposed in CORS responses. |
cors_max_age |
int |
600 |
Preflight cache duration in seconds. |
Redis¶
| Field | Type | Default | Description |
|---|---|---|---|
enable_redis |
bool |
True |
Master switch for Redis. |
redis_url |
str \| None |
"redis://localhost:6379" |
Redis connection URL. |
redis_prefix |
str |
"guard_core:" |
Key prefix for namespace isolation. |
redis_socket_connect_timeout |
float \| None |
2.0 |
Seconds to wait establishing a TCP connection. Must be positive; None disables (blocks indefinitely on a partitioned Redis). |
redis_socket_timeout |
float \| None |
2.0 |
Seconds to wait on a read/write before raising. Must be positive; None means no timeout. |
redis_health_check_interval |
int |
30 |
Seconds between pooled-connection health checks. 0 disables. |
redis_max_connections |
int \| None |
None |
Cap on the connection pool size. None uses redis-py's default. |
redis_retries |
int |
1 |
Retries (with exponential backoff) on transient connection/timeout errors. 0 disables. |
redis_fail_open |
bool |
False |
On Redis outage, fail_secure governs by default. Set True to skip the failing check and let the request through, treating Redis outages as an availability concern distinct from other check failures. Covers Redis unavailability at startup, a GuardRedisError raised from any security check mid-request, and the rate limiter's Redis path: True falls back to an in-memory window with a one-time process warning (with several workers the effective limit becomes workers times rate_limit), False raises so fail_secure decides the outcome. |
Detection Engine¶
| Field | Type | Default | Range | Description |
|---|---|---|---|---|
enable_penetration_detection |
bool |
True |
N/A | Master switch for threat detection. |
detection_compiler_timeout |
float |
2.0 |
0.1 - 10.0 | Timeout for pattern compilation/matching (s). |
detection_max_content_length |
int |
10000 |
1000 - 100000 | Maximum content length for detection. |
detection_preserve_attack_patterns |
bool |
True |
N/A | Preserve attack patterns during truncation. |
detection_semantic_threshold |
float |
0.7 |
0.0 - 1.0 | Threshold for semantic attack detection. |
detection_anomaly_threshold |
float |
3.0 |
1.0 - 10.0 | Std deviations slower than average to flag an anomaly (never faster). |
detection_anomaly_emission_cooldown |
float |
60.0 |
1.0 - 3600.0 | Minimum seconds between anomaly events for the same pattern. Raise to reduce noise on low-traffic apps. |
detection_min_samples_for_anomaly |
int |
30 |
10 - 1000 | Minimum samples recorded for a pattern before statistical-anomaly detection engages. Raise to reduce false fires on low-traffic apps. |
detection_slow_pattern_threshold |
float |
0.1 |
0.01 - 1.0 | Seconds to consider a pattern slow. |
detection_monitor_history_size |
int |
1000 |
100 - 10000 | Recent metrics to keep in history. |
detection_max_tracked_patterns |
int |
1000 |
100 - 5000 | Maximum patterns to track for performance. |
detection_max_body_inspect_bytes |
int |
262144 |
1024 - 10485760 | Body size cap read/scanned for detection; distinct from detection_max_content_length and max_request_size. A body whose declared Content-Length exceeds this cap is read through the adapter's bounded reader (read_body_prefix) and only the first this-many bytes are scanned, if the adapter implements one; without a bounded reader the body is not read at all, the same skip as before this cap existed. Either way a one-time warning names the cap, the client, and which of the two happened. A signature that straddles the cap boundary is still caught: a small overlap (the longest compiled pattern length, up to 256 bytes) is read past the cap on every path, including a declared oversized Content-Length. |
detection_max_scan_values |
int |
512 |
2 - 100000 | Maximum values (query params, headers, JSON keys/values, form/multipart fields) scanned per request; remaining values are skipped and a one-time warning logs the client IP once reached. Each named value costs two scan units (name, then value), so the minimum is 2. |
detection_max_scan_chars |
int |
65536 |
1024 - 262144 | Maximum total characters, across every value handed to the pattern engine per request (counted at the same point as detection_max_scan_values), before remaining values are skipped and a one-time warning logs the client IP. A value already in progress when the budget is checked is always scanned in full; only values that would start after the budget is spent are skipped, so a single large value is never silently dropped (GHSA-3hfx-8m47-5f9h residual). |
detection_max_json_depth |
int |
32 |
1 - 1000 | Maximum nesting depth of a JSON request body walked structurally. A dict or list reached at this depth is not descended into: it is serialized back to text and scanned as one value instead, bounded by detection_max_content_length, so content hidden below the cap is still scanned as text rather than structurally. A one-time warning logs the client IP once reached (GHSA-f6cf-jjhc-qp85). |
detection_threat_score_threshold |
float |
1.0 |
0.0 - 10.0 | Anomaly/threat score required to flag a request. |
detection_scan_body |
bool |
True |
N/A | Scan the request body during detection; False restricts detection to path/query/headers. |
The detection engine follows one SecurityConfig per process: detect_penetration_attempt(request, config) configures the shared detection singleton from config the first time it is called, and reconfigures it whenever called with a different SecurityConfig object (compared by identity, so calling it repeatedly with the same object is a no-op). Alternating between two SecurityConfig objects on every other request reconfigures on every call, discarding the previously compiled pattern cache each time. The swap itself is a single attribute assignment: the new state is built completely before it replaces the old one, so a concurrent request always sees a fully-built state, never a half-constructed one. There is no lock around the swap.
Logging¶
| Field | Type | Default | Description |
|---|---|---|---|
log_suspicious_level |
"INFO" \| "DEBUG" \| "WARNING" \| "ERROR" \| "CRITICAL" \| None |
"WARNING" |
Log level for suspicious requests. None disables. |
log_request_level |
Same as above | None |
Log level for all requests. None disables. |
log_country_check_level |
Same as above (default "INFO") |
"INFO" |
Log level for non-block country verdicts (whitelisted / not-affected). None disables. Blocked-country hits log at log_suspicious_level instead (default WARNING); no-rules / no-geolocation always log at DEBUG. |
muted_check_logs |
frozenset[str] |
frozenset() |
Security check names to mute from pipeline logging entirely. Validator rejects unknown check names. |
log_sensitive_headers |
frozenset[str] |
frozenset() |
Header names redacted from guard log lines, matched case-insensitively. Merged with the hardcoded default set (authorization, proxy-authorization, cookie, x-api-key). Also redacts the value in the detection engine's per-header Potential attack detected line. |
log_sensitive_params |
frozenset[str] |
frozenset() |
Query-parameter names whose values are redacted from guard log lines, matched case-insensitively. Merged with the hardcoded default set (access_token, refresh_token, api_key, apikey, token, password, secret, client_secret, signature). Redacts the URL segment of log_activity lines and the value in the detection engine's per-parameter Potential attack detected line. |
log_sensitive_body_fields |
frozenset[str] |
frozenset() |
JSON key, form-field, and multipart text-part names whose values are redacted, matched case-insensitively. Merged with the same hardcoded default set as log_sensitive_params. Redacts only the value in the detection engine's per-field Potential attack detected line. |
log_format |
"text" \| "json" |
"text" |
Log output format. |
custom_log_file |
str \| None |
None |
Path to a custom log file. |
setup_custom_logging() (called by every adapter at middleware init with log_format and custom_log_file) always attaches its own console handler to the guard_core logger, but that handler's console output yields to the host's root handlers whenever they exist, whichever was configured first, so the event propagates to the host's own root handlers exactly once instead of printing twice. The custom_log_file handler is unaffected and is attached in every case. See Logging Configuration for detail.
Agent / Telemetry¶
Internal Configuration
Agent fields are typically not exposed to end users. They are used for Guard Agent SaaS integration.
| Field | Type | Default | Description |
|---|---|---|---|
enable_agent |
bool |
False |
Enable Guard Agent telemetry. |
agent_api_key |
str \| None |
None |
API key for the SaaS platform. |
agent_strict |
bool |
False |
Raise at middleware init instead of degrading to agent-off when an enabled agent cannot be initialized. |
agent_endpoint |
str |
"https://api.guard-core.com" |
Agent endpoint URL. |
agent_project_id |
str \| None |
None |
Project identifier. |
agent_buffer_size |
int |
100 |
Events to buffer before flush. |
agent_flush_interval |
int |
30 |
Seconds between automatic flushes. |
agent_enable_events |
bool |
True |
Send security events. |
agent_enable_metrics |
bool |
True |
Send performance metrics. |
agent_timeout |
int |
30 |
HTTP request timeout in seconds. |
agent_retry_attempts |
int |
3 |
Retry attempts for failed requests. |
agent_project_encryption_key |
str \| None |
None |
Per-project AES-256-GCM key that switches the agent to the encrypted events endpoint. Required for API keys with encryption enforced server-side. |
agent_guard_version |
str \| None |
None |
Framework wrapper version (e.g. fastapi-guard's __version__) reported alongside agent telemetry. |
agent_status_interval |
int |
300 |
Seconds between agent status reports to the SaaS. Must be between 60 and 86400. |
agent_high_watermark_ratio |
float \| None |
None |
Buffer occupancy ratio that triggers an early flush. None defers to the agent's own default (0.8). |
agent_max_concurrent_flushes |
int \| None |
None |
Maximum concurrent early-flush operations. None defers to the agent's own default (1). |
agent_buffer_overflow_policy |
Literal["drop", "block", "raise"] \| None |
None |
Behavior when the agent's in-memory buffer is full. None defers to the agent's own default ("drop"). |
agent_backoff_factor |
float \| None |
None |
Backoff factor for agent HTTP retries. None defers to the agent's own default. |
agent_sensitive_headers |
list[str] \| None |
None |
Header names excluded from telemetry payloads. None defers to the agent's own default. |
agent_max_payload_size |
int \| None |
None |
Maximum payload size in bytes included in events. None defers to the agent's own default. |
agent_compression_enabled |
bool \| None |
None |
Gzip-compress outgoing batch bodies above agent_compression_threshold. None defers to the agent's own default. |
agent_compression_threshold |
int \| None |
None |
Minimum body size in bytes before gzip compression applies. None defers to the agent's own default. |
agent_install_id |
str \| None |
None |
Override the agent install ID. None auto-generates one. |
agent_payload_signing_secret |
str \| None |
None |
HMAC-SHA256 secret used to sign the X-Payload-Signature header. |
enable_enrichment |
bool |
False |
Populate guard.* metadata on every event and metric with project identity, deterministic threat score, matched dynamic rule, and per-IP behavioral correlation keys. Requires enable_agent=True. |
muted_event_types |
frozenset[str] |
frozenset() |
Event types to mute from telemetry dispatch. Validator rejects unknown values. |
muted_metric_types |
frozenset[str] |
frozenset() |
Metric types to mute from telemetry dispatch. Validator rejects unknown values. |
enable_otel |
bool |
False |
Enable OpenTelemetry span/metric export. Requires the otel extra. |
otel_service_name |
str |
"guard-core" |
Service name reported on the OpenTelemetry resource. |
otel_exporter_endpoint |
str \| None |
None |
OTLP HTTP endpoint for OpenTelemetry export. |
otel_resource_attributes |
dict[str, str] |
{} |
Additional OpenTelemetry resource attributes (e.g. deployment.environment, service.version). |
enable_logfire |
bool |
False |
Enable Logfire span/metric export. Requires the logfire extra. |
logfire_service_name |
str |
"guard-core" |
Service name reported to Logfire. |
Validator: agent_api_key is required when enable_agent is True. agent_buffer_overflow_policy rejects any value other than "drop", "block", or "raise" at construction time. enable_enrichment=True without enable_agent=True raises ValueError.
SecurityConfig.on_error (documented under Core Settings hooks) is also forwarded to AgentConfig.on_error when the agent is enabled, so the same callback receives agent-side transport_send and encryption failures in addition to guard-core's own agent_init and geoip failures.
All eleven fields above with a None default follow the same rule: to_agent_config() omits a field from the AgentConfig call entirely when it is None, so an unset field is controlled by AgentConfig's own default rather than by a value duplicated into guard-core.
Dynamic Rules¶
| Field | Type | Default | Description |
|---|---|---|---|
enable_dynamic_rules |
bool |
False |
Enable dynamic rule updates from SaaS platform. |
dynamic_rule_interval |
int |
300 |
Seconds between rule update checks. |
dynamic_rules_cache_path |
Path \| None |
None |
Optional local JSON file persisting the last-known dynamic rules snapshot so a restart during a SaaS outage restores the last applied rules instead of base config. Redis holds the primary snapshot whenever a redis_handler is present; the file is an additional opt-in fallback that is only written and read when this path is set. Neither store expires: the Redis key and the file persist until you remove them, even after you disable dynamic rules. |
emergency_mode |
bool |
False |
Emergency lockdown mode (set by dynamic rules). |
emergency_whitelist |
list[str] |
[] |
Emergency whitelist IPs (set by dynamic rules). |
Validator: enable_agent must be True when enable_dynamic_rules is True.
Validators¶
SecurityConfig includes Pydantic validators that run on instantiation:
| Validator | Fields | Behavior |
|---|---|---|
validate_ip_lists |
whitelist, blacklist |
Validates IP addresses and CIDR ranges. Raises ValueError on invalid entries. |
validate_trusted_proxies |
trusted_proxies |
Validates proxy IPs and CIDR ranges, plus the literal "unix" token. Raises ValueError on invalid entries. |
warn_trusted_proxies_prefix_zero |
model-level | Logs a WARNING when trusted_proxies contains a /0 network. Does not raise; construction still succeeds. |
warn_whitelist_prefix_zero |
model-level | Logs a WARNING when whitelist contains a /0 network. Does not raise; construction still succeeds, and precedence is unchanged. |
warn_empty_enabled_detection_categories |
model-level | Logs a WARNING when enabled_detection_categories is empty while enable_penetration_detection is True. Does not raise; construction still succeeds. |
validate_proxy_depth |
trusted_proxy_depth |
Must be >= 1. Raises ValueError otherwise. |
validate_cloud_providers |
block_cloud_providers |
Requires the part before an optional :!region suffix to be "AWS", "GCP", or "Azure". Raises ValueError naming any entry that fails this check. |
validate_geo_ip_handler_exists |
model-level | Requires geo_ip_handler when blocked_countries or whitelist_countries is set. Falls back to IPInfoManager if ipinfo_token is provided. Also re-run from __setattr__/model_copy when blocked_countries, whitelist_countries, geo_ip_handler, or ipinfo_token is reassigned after construction. As of 4.0.0, __setattr__/model_copy reassignment revalidation also covers every collection-typed field (exclude_paths, global_behavior_rules, security_headers, the CORS and OTel attribute fields, and the detection-exclusion sets among them), not only the geo-state fields. |
validate_agent_config |
model-level | Requires agent_api_key when enable_agent is True. Requires enable_agent when enable_dynamic_rules is True. |
validate_optional_extras_installed |
model-level | Requires the redis extra when enable_redis is True, the cloud extra (aiohttp or requests) when cloud blocking is enabled (block_cloud_providers or enable_dynamic_rules), and the geo extra (maxminddb) when country rules are configured with no custom geo_ip_handler. Raises ValueError naming the missing extra's install command, checked via importlib.util.find_spec (never a bare import). See Installation. |
warn_unknown_fields |
model-level, mode="before" |
Compares the constructor keyword arguments against model_fields (and any field's alias) and logs a guard_core.models warning naming each unknown key, since SecurityConfig still allows unknown keys through (extra="ignore") rather than raising. Construction still succeeds and the unknown key is still dropped; only a log line is added, so a typo'd field name is no longer a silent no-op. extra="forbid" is the intended behavior at a future major release. |
Unknown provider names raise
validate_cloud_providers rejects a block_cloud_providers entry whose provider name (the part before an optional :!region suffix) is not "AWS", "GCP", or "Azure". {"AWS", "InvalidProvider"} raises ValueError naming InvalidProvider instead of silently blocking only "AWS".