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. |
exclude_paths |
list[str] |
See below | Paths excluded from all security checks. |
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. |
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. |
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 |
list[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.trusted_proxy_depth: Must be >= 1.
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 |
list[str] \| None |
None |
Allowed IPs/CIDRs. None disables (allow all). |
blacklist |
list[str] |
[] |
Blocked IPs/CIDRs. |
whitelist_countries |
list[str] |
[] |
Allowed countries. Non-empty = only listed pass (unknown blocked). Overrides blocked_countries. |
blocked_countries |
list[str] |
[] |
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. |
auto_ban_duration |
int |
3600 |
Ban duration in seconds. |
Validators:
whitelistandblacklist: Each entry validated as a valid IP or CIDR range viaipaddress.ip_address()/ip_network().
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 |
dict[str, ThreatBanConfig] |
{} |
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 |
list[BehaviorRuleConfig] |
[] |
Behavior rules merged into every route. |
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,
),
],
)
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-*, and all sec-ch-ua* headers. enabled_detection_categories narrows the regex scan to a subset of the 18 known categories; custom user patterns always run regardless.
| Field | Type | Default | Description |
|---|---|---|---|
excluded_detection_headers |
set[str] |
set() |
Header names skipped by detection. 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 |
set[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 |
Deprecated. IPInfo API token. |
ipinfo_db_path |
Path \| None |
data/ipinfo/country_asn.mmdb |
Deprecated. Path to IPInfo database. |
Model validator: If blocked_countries or whitelist_countries are set, geo_ip_handler must be provided (or ipinfo_token for backward compatibility). 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)}. |
Cloud Provider Blocking¶
| Field | Type | Default | Description |
|---|---|---|---|
block_cloud_providers |
set[str] \| None |
None |
Providers to block. A bare name ("AWS", "GCP", "Azure") blocks the whole provider; a region carve-out ("GCP:!us-central1") blocks the provider except that region. Region scoping is supported for GCP and AWS. |
cloud_ip_refresh_interval |
int |
3600 |
Seconds between IP range refreshes (60-86400). |
Validator: each entry is kept only if the part before an optional :!region suffix is one of {"AWS", "GCP", "Azure"}; an entry that fails this check is dropped, not the whole set.
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. |
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_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. |
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. |
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. |
log_format |
"text" \| "json" |
"text" |
Log output format. |
custom_log_file |
str \| None |
None |
Path to a custom log file. |
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. |
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.
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. |
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. Raises ValueError on invalid entries. |
validate_proxy_depth |
trusted_proxy_depth |
Must be >= 1. Raises ValueError otherwise. |
validate_cloud_providers |
block_cloud_providers |
Silently filters invalid providers: an entry is kept only if the part before an optional :!region suffix is "AWS", "GCP", or "Azure". |
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. |
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. |
Silent filtering
validate_cloud_providers silently drops unrecognized provider names. {"AWS", "InvalidProvider"} becomes {"AWS"} without raising an error.