I put Cloudflare and fail2ban in front of my FastAPI server. Here's what still got through.

Renzo Franceschini9 min read
securityfastapicloudflarewafthreat-reports

TL;DR: Edge protection dropped my attack volume by 94%. Then I watched 748 Layer 7 attacks hit in a single hour anyway. This is the data, and it's a useful lesson about what application security actually protects against.


The setup

I maintain Guard Core, an open-source security platform for API applications. As part of building its cloud dashboard, I connected a live FastAPI server to a telemetry pipeline and let it sit on the public internet. No hiding behind obscurity. Public DNS, published endpoints, the works.

Every week I get a threat report summarizing what hit the server - which paths got probed, which IPs kept showing up, what categories of attack were attempted.

This post is about three consecutive weeks of those reports, and what they taught me about the difference between edge protection and application-layer security.


Week 1: naked on the internet

Dates: March 23-29, 2026 Configuration: Public FastAPI server. fastapi-guard middleware adapter active. No Cloudflare. No fail2ban. No WAF.

The numbers:

MetricValue
Total attacks blocked745
Unique attackers74
Peak dayWed Mar 25 - 472 attacks
Peak hour16:00-17:00 UTC - 154 attacks

Attack category breakdown:

  • Suspicious Pattern: 375 (50.3%)
  • Forbidden: 369 (49.5%)
  • Rate Limit: 1 (0.1%)

Top attacked paths:

/.env          18 hits   2.4%
/robots.txt    17 hits   2.3%
/info.php      10 hits   1.3%
/v1/.env        9 hits   1.2%
/parameters     9 hits   1.2%
/v2/.env        9 hits   1.2%
/.git/config    8 hits   1.1%
/.env.example   7 hits   0.9%
/.env.save      6 hits   0.8%
/dns-query      6 hits   0.8%

Top user agents:

curl/8.7.1              207 hits
Mozilla/5.0 (Windows NT 10.0...)  52 hits
Mozilla/5.0 (Windows NT 10.0...)  18 hits
Go-http-client/2.0                17 hits
Mozilla/5.0 (Windows NT 10.0...)   8 hits

A few things jump out:

curl/8.7.1 was the single busiest client by a wide margin. Legitimate users don't hit your API with curl 207 times in a week. This is automated scanning.

The .env obsession is real. Look at that list: /.env, /v1/.env, /v2/.env, /.env.example, /.env.save - five different variations on the same file. Bots know that half-competent deployments accidentally leave dotfiles web-accessible, and they know .env files typically contain database passwords, API keys, and cloud credentials. One exposed .env is a full compromise.

The attack spread is bursty. Wed Mar 25 saw 472 attacks, 63% of the week's total, in a single day. One scanner campaign, scanning a slice of the IPv4 space, landing on my server by chance.

If you're running a FastAPI app right now with no edge protection, your numbers probably look similar.


Week 2: Cloudflare and fail2ban go up

Dates: March 30-April 5, 2026 Configuration: Same server, now behind Cloudflare (free tier, Under Attack Mode off, default WAF rules) and with fail2ban running at the OS level. fastapi-guard middleware still active.

The numbers:

MetricValueChange
Total attacks blocked48▼ 93.6%
Unique attackers19▼ 74%
Peak dayWed Apr 1 - 24 attacks
Peak hour17:00-18:00 UTC - 19 attacks

Attack category breakdown:

  • Suspicious Pattern: 42 (87.5%)
  • Forbidden: 4 (8.3%)
  • Rate Limit: 2 (4.2%)

Top attacked paths:

/robots.txt      13 hits  27.1%
/.env             6 hits  12.5%
/.git/config      4 hits   8.3%
/auth/jwt/login   2 hits   4.2%
/parameters       2 hits   4.2%

The headline number is the 93.6% drop. Cloudflare's free tier plus fail2ban, configured with defaults, absorbed roughly fifteen-sixteenths of the scanning traffic I'd been seeing the week before.

This is the story most "why you need a WAF" posts stop at. Big drop, shiny graph, implicit conclusion: edge protection solved the problem. Done.

But look closer at the composition shift. In week 1, Forbidden attacks (mostly blunt probes for paths that shouldn't exist, blocked by simple deny rules) were 49.5% of the total. In week 2, Forbidden fell to 8.3% and Suspicious Pattern rose to 87.5% of what remained.

In other words: Cloudflare and fail2ban are excellent at catching the loud, obvious scanning traffic. The stuff that makes it through looks more like targeted application-layer probing. The attackers who bother to rotate user agents, space out their requests, and look like legitimate browsers.

This is the point where a reasonable person concludes "edge protection is doing its job, I'm safe now" and stops investigating.

Then week 3 happened.


Week 3: the story gets weird

Dates: April 6-12, 2026 Configuration: Unchanged from week 2. Cloudflare free tier, fail2ban, fastapi-guard.

The numbers:

MetricValue
Total attacks blocked748
Peak daySun Apr 12 - 748 attacks
Peak hour20:00-21:00 UTC - 748 attacks

Yes, you read that right. Every single attack that week happened in a one-hour window on Sunday.

Attack category breakdown:

  • Forbidden: 379 (50.7%)
  • Suspicious Pattern: 221 (29.5%)
  • Rate Limit: 148 (19.8%)

Top attacked paths:

/api/v1/health    36 hits  4.8%
/api/v1/*         20 hits  2.7%
/api/*            20 hits  2.7%
/.env             11 hits  1.5%
/parameters       10 hits  1.3%
/robots.txt        9 hits  1.2%
/.git/config       7 hits  0.9%
/.env.example      5 hits  0.7%
/login             4 hits  0.5%
/v2/*              4 hits  0.5%

A few things here are genuinely interesting:

The 19.8% Rate Limit share. In week 1 this was 0.1% (literally 1 request). In week 2 it was 4.2%. In week 3 it's 148 rate-limited requests in an hour. Somebody was hammering.

The target paths changed shape. Week 1 and week 2 were dominated by dotfile probes (.env, .git/config). Week 3's top paths are /api/v1/health, /api/v1/*, /api/*. They're probing for the API surface itself. This isn't generic scanning; it's reconnaissance against a specific service.

This is the exact profile of traffic Cloudflare is least likely to stop. It's not volumetric (under 1000 requests/hour, well below WAF rate limits). It's not signature-matching (no obvious SQL injection strings). It's a methodical enumeration of an API surface - something an edge WAF sees as "requests with reasonable user agents hitting real endpoints." That's why fastapi-guard caught it and Cloudflare let it through.

Note on the "0 unique attackers" anomaly: My initial read of this report flagged 0 unique attackers alongside 748 attacks, which was surprising enough to stop and investigate. The answer turned out to be boring and honest: the dashboard computes unique attackers as COUNT(DISTINCT ip_hash) where ip_hash is SHA-256 of the client IP, and the query excludes rows where ip_hash IS NULL. For this specific burst, every single event arrived at the telemetry pipeline with an empty ip_address field, so every row had a NULL ip_hash, and the distinct-count collapsed to 0. The upstream cause is the agent behind Cloudflare not reliably extracting the real client IP from CF-Connecting-IP / X-Forwarded-For during that window; the wildcard paths (/api/v1/*, /api/*) in the top-paths list are consistent with route-pattern logging firing on requests where the concrete path and client IP both failed to materialize. It's a real gap, the reports are only as good as the fields their source populates, and I'm investigating the agent's IP-extraction path now. I'm calling it out here because publishing the raw number without explaining it would be dishonest, and because it's the kind of edge case any serious technical reader is going to spot.


What this actually teaches us

Three consecutive weeks. Same server. Three radically different pictures.

Week 1 taught me what unfiltered internet traffic looks like against a public API. Loud, broad, dotfile-obsessed. The kind of thing a WAF stops trivially.

Week 2 taught me that most "security" products are really noise filters. Cloudflare and fail2ban cut 94% of the traffic, and the traffic they cut was almost entirely the low-effort stuff. Worth doing. Not sufficient.

Week 3 taught me that application-layer security catches the things edge protection is built to miss. 748 attacks hitting /api/v1/* and /api/* through a Cloudflare-protected server, because those look like ordinary API traffic to a WAF. They're not ordinary. They're enumeration.

The common posture is "I'm behind Cloudflare, I'm fine." The data says: you're behind Cloudflare, you're filtering the loud traffic, and you're blind to everything else.


What Layer 7 detection actually does differently

A WAF operates at the network edge. It sees packets, TLS handshakes, request headers, and whatever signatures its vendor has shipped. It's pattern-matching against generic attack classes - SQL injection keywords, XSS payloads, common CVE fingerprints.

A Layer 7 middleware runs inside your application. It sees:

  • The full request body after TLS termination
  • Your actual route patterns (so /api/v1/users/{id} is distinguishable from /api/v1/users/1337 probing)
  • Your application's business logic context
  • Per-endpoint rate limits (not just per-IP)
  • Cross-request behavioral patterns against your specific endpoints

The detection categories in the reports above, Suspicious Pattern, Forbidden, Rate Limit, aren't signature matches. They're behavioral classifications made with knowledge of what the application is. That's why they catch enumeration against /api/v1/* that a generic WAF sees as benign.

This isn't "WAFs are bad." WAFs are great at what they do. It's "WAFs and application middleware are different layers that catch different things, and you need both."


How to see this data for your own API

Guard Core is MIT licensed. The fastapi-guard adapter works as a drop-in middleware for FastAPI. If you want the data in your inbox every week without running the reporting pipeline yourself, the cloud dashboard sends a weekly threat report like the ones in this post. Free tier is 10K events/month - enough for most small-to-moderate APIs.

from guard import SecurityConfig, SecurityMiddleware

config = SecurityConfig(
    rate_limit=100,
    auto_ban_threshold=5,
    enable_penetration_detection=True,
)

app.add_middleware(SecurityMiddleware, config=config)

That's the minimum. You'll start seeing events immediately.

The three weekly reports above were generated by the same system. If you connect your own API, the reports you get will look structurally identical - same categories, same top-paths format, same heatmap. The content, of course, will be whatever's actually hitting you.

Which, based on what I've seen, is almost certainly more than you think.


- Renzo Franceschini (@rennf93)

If you want to discuss this, I'm reachable via GitHub issues or the Guard Core discussions. Happy to go deeper on any of the data, the detection pipeline, or the cloud dashboard architecture.