Geo + cloud-provider blocking
Blocking by country and by cloud provider, and the IP-enrichment pipeline behind it.
Country blocking and cloud-provider blocking get reached for together, but they answer different questions and have different limits.
Two tools, two jobs
Country blocking - compliance, not security. Use for sanctions enforcement (OFAC, EU, UK lists), export-control regimes, data residency requirements that forbid serving certain regions. Don't use as a defense against attackers; any sophisticated actor uses a VPN.
Cloud-provider blocking - a real security tool. Use for locking down admin paths, blocking known bot/scanner infrastructure that mostly runs on AWS / GCP / Azure, reducing low-effort scraping. Doesn't help against determined attackers, who move off public-cloud IP space (residential proxies, compromised endpoints) once cloud is blocked.
How an IP becomes a country or a provider
| Geo pipeline | Cloud pipeline | |
|---|---|---|
| Output | Country (ISO code), region, city, ASN | Provider name (AWS / GCP / Azure) |
| Source | IPInfo SaaS (per-request lookup) | Public range feeds (cached locally) |
| Caching | Per request via API | In-memory CIDR ranges, refreshed periodically |
| Failure mode | Geo data missing on events | Lookup falls back to "no match" |
blocked_countries vs whitelist_countries
Each takes ISO country codes, but at the global SecurityConfig level they are NOT symmetric modes.
Deny-list (blocked_countries): block requests from any listed country. Everywhere else is allowed. This is the only mode that works standalone at the global level.
blocked_countries=["CN", "RU", "KP", "IR"]Use when you serve most of the world and a few specific regions are off-limits for compliance reasons.
whitelist_countries is an exemption list, not an allow-list. The global country check only runs when blocked_countries is non-empty. A global whitelist_countries on its own is never evaluated. It does not create deny-by-default. Even with both set, a country in neither list resolves to "not affected" and is allowed. At the global level, whitelisting means "exempt these countries from the block list" - nothing more.
Logging the verdicts - log_country_check_level
log_country_check_levelUnion[Literal[INFO, DEBUG, WARNING, ERROR, CRITICAL], NoneType]Log level for per-request country verdicts that are not blocks (whitelisted / not-affected). Set to None to silence them. Blocked-country hits log at log_suspicious_level instead; no-rules and no-geolocation cases always log at DEBUG.
'INFO'Every request that reaches the country check produces a verdict, and each band logs differently. Blocked hits always log at WARNING - not tunable. No-rules-configured and no-geolocation-available cases always log at DEBUG - also not tunable. This field controls only the middle band, the non-block verdicts (whitelisted / not affected), which default to INFO. Set it to None to silence them entirely when per-request geo lines drown your logs.
block_cloud_providers - three values, case-sensitive
A Python set. Three valid values. Anything else is silently ignored.
"AWS"- Amazon Web Services (largest range set: EC2, Lambda, CloudFront, NAT gateways)"GCP"- Google Cloud Platform (Compute Engine, Cloud Run, Cloud Functions egress)"Azure"- Microsoft Azure (VMs, Functions, App Service egress)
{"AWS"} is honored. {"aws"}, {"Aws"}, {"amazon"} are silently ignored - no error, no log, no blocks.
Typical use: lock down /admin paths from cloud-originated traffic. Most legitimate admin users connect from corporate IPs, not EC2.
Refresh
Provider IP ranges change. AWS adds and removes blocks every few days. The local cache refreshes, but not every request.
Startup - lazy. Ranges aren't loaded eagerly. The first request that needs cloud-provider evaluation triggers the initial fetch. That first request can have noticeable extra latency.
Steady state - timer. After initial load, a background timer refreshes on the cadence set by cloud_ip_refresh_interval. Bounds: 60s minimum, 86400s (24h) maximum. Default in the example app: 3600s (1 hour).
ipinfo_token is deprecated
The geo pipeline still works through this field for backward compatibility, but the source marks it deprecated in favor of a pluggable geo_ip_handler.
For existing code: keeps working today. If you already set ipinfo_token in app.py, the geo lookup still happens through IPInfo. Country resolution, region, city, ASN - all unchanged at runtime. No urgent migration; you won't break anything by leaving it as-is.
For new code: don't reach for it. The deprecation flag in the source is the signal. This surface will change. Building new integrations against ipinfo_token means future migration work. Use geo_ip_handler - pluggable means you choose the geo backend.
Wiring - everything off by default
SecurityConfig(
ipinfo_token=ipinfo_token or None,
blocked_countries=[],
whitelist_countries=[],
block_cloud_providers=set(),
cloud_ip_refresh_interval=3600,
)All five fields are present; all five are at their disabled values. Geo + cloud blocking is opt-in. Flip them on when you have a specific need.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| AWS traffic isn't being blocked | Case mismatch - block_cloud_providers expects exact strings: "AWS", "GCP", "Azure". {"aws"} is silently ignored. | Confirm the set contains literal {"AWS"} (uppercase). Log SecurityConfig at boot as a sanity check. |
| Geo data missing on events | ipinfo_token is None - no API key, no lookup. Or IPInfo quota exhausted. | Check the env var. Check IPInfo dashboard for quota. If migrating: plan geo_ip_handler config. |
| Slow first request after deploy | Lazy cloud-range fetch - the very first request to need cloud evaluation pays the fetch cost. | Warm the cache via a startup probe that exercises cloud-range evaluation. |
Set whitelist_countries globally but nothing is blocked | The global country check only runs when blocked_countries is non-empty - a global whitelist alone is never evaluated. | Use the route decorator's whitelist_countries for deny-by-default, or configure blocked_countries. |
| Geo log lines flooding at INFO | log_country_check_level defaults to INFO for non-block verdicts on every checked request. | Set log_country_check_level=None (or a lower level). Blocks still log at WARNING. |