mod_shield next-gen HTTP DoS protection. Early testing!

ApisCP has a new WAF, ladies and gentlemen!

mod_shield is the spiritual successor to mod_evasive that solves a couple key problems and adds several new enhancements to help combat unwanted traffic.

Evasive had a sharing problem - data in one httpd process wasn’t visible in another httpd process. An attack that closes its TCP connection may have subsequent TCP requests sent to a different process, which starts counting anew thereby bypassing existing counters. More httpd processes translates to greater likelihood of requests exceeding their block threshold (or avoiding it altogether!).

Shield implements a cyclic shared buffer - default 512 KB - for each tracking criteria (site, page, and blocks). Each record is 176 bytes allowing for 2,978 unique entries that covers approximately 25 unique requests/second over a 2 minute tracking window. Entries are evicted through TTL and LRU, more on that later.

Tech landscape has evolved significantly since Evasive was released in 2005. Cloudflare serving both as a CDN and firewall is common. Either scenario ensconces a malicious connection behind their network, which makes edge-router blocking impossible. Connections flow through - hopeful that Cloudflare steps in to block - and continue to flow after the initial blocking period subside.

Shield solves this by adding blocking generations, for each successive block a new TTL is set extending the block up to 1 day.

Here’s a sample request pattern from a site behind Cloudflare. Note that once blocked, traffic still flows through. Shield closes the connection immediately without further processing.

52.169.150.195 x.y - [08/Jun/2025:01:33:39 -0400] "GET /xx.php HTTP/1.1" 404 196 "-" "-"
52.169.150.195 x.y - [08/Jun/2025:01:33:39 -0400] "GET /flower.php HTTP/1.1" 404 196 "-" "-"
52.169.150.195 x.y - [08/Jun/2025:01:33:39 -0400] "GET /admin.php HTTP/1.1" 404 196 "-" "-"
52.169.150.195 x.y - [08/Jun/2025:01:33:39 -0400] "GET /alfa.php HTTP/1.1" 404 196 "-" "-"
52.169.150.195 x.y - [08/Jun/2025:01:33:39 -0400] "GET /autoload_classmap.php HTTP/1.1" 404 196 "-" "-"
52.169.150.195 x.y - [08/Jun/2025:01:33:39 -0400] "GET /xxx.php HTTP/1.1" 404 196 "-" "-"
52.169.150.195 x.y - [08/Jun/2025:01:33:39 -0400] "GET /NewFile.php HTTP/1.1" 404 196 "-" "-"
52.169.150.195 x.y - [08/Jun/2025:01:33:39 -0400] "GET /css.php HTTP/1.1" 404 196 "-" "-"
52.169.150.195 x.y - [08/Jun/2025:01:33:40 -0400] "GET /info.php HTTP/1.1" 404 196 "-" "-"
52.169.150.195 x.y - [08/Jun/2025:01:33:40 -0400] "GET /gecko.php HTTP/1.1" 404 196 "-" "-"
52.169.150.195 x.y - [08/Jun/2025:01:33:40 -0400] "GET /admin/controller/extension/extension/ultra.php HTTP/1.1" 429 227 "-" "-"
52.169.150.195 x.y - [08/Jun/2025:01:33:40 -0400] "GET /classwithtostring.php HTTP/1.1" 429 227 "-" "-"
52.169.150.195 x.y - [08/Jun/2025:01:33:40 -0400] "GET /moon.php HTTP/1.1" 429 227 "-" "-"
52.169.150.195 x.y - [08/Jun/2025:01:33:40 -0400] "GET /ioxi-o.php HTTP/1.1" 429 227 "-" "-"
52.169.150.195 x.y - [08/Jun/2025:01:33:40 -0400] "GET /chosen HTTP/1.1" 429 227 "-" "-"
52.169.150.195 x.y - [08/Jun/2025:01:33:40 -0400] "GET /cong.php HTTP/1.1" 429 227 "-" "-"
52.169.150.195 x.y - [08/Jun/2025:01:33:40 -0400] "GET /item.php HTTP/1.1" 429 227 "-" "-"
52.169.150.195 x.y - [08/Jun/2025:01:33:40 -0400] "GET /x.php HTTP/1.1" 429 227 "-" "-"
52.169.150.195 x.y - [08/Jun/2025:01:33:41 -0400] "GET /k.php HTTP/1.1" 429 227 "-" "-"
52.169.150.195 x.y - [08/Jun/2025:01:33:41 -0400] "GET /lv.php HTTP/1.1" 429 227 "-" "-"

What’s new

  • Shield monitors bi-directionally. In addition to ingress, the following egress parameters have been added:

    DOSSiteStatusPenalty (int status) (int score) increments (or decrements) score based upon status. May be used multiple times. Counts toward DOSSiteCount.

    Example: Adds 50 points if a 404 is returned DOSSiteStatusPenalty 404 50
    Example: Immediately trigger block if authentication fails DOSSiteStatusPenalty 401 10000

    DOSDeadlinePenalty (float time) (int score) increments (or decrements) score based upon request duration. Intended to thwart multiplexed attacks. May be used multiple times. Counts toward DOSPageCount.

    Example: Add 30 points if a page takes longer than 5 seconds to load DOSDeadlinePenalty 5 30
    Example: Add 1 point if a page takes longer than 500 ms to load DOSDeadlinePenalty 0.5 1

  • Blocking duration may grow for each successive block. This is useful if a connection is behind a downstream proxy, such as Cloudflare. Shield supports up to 16 blocking periods. When a block is active both RFC7231 Retry-After and working IETF draft RateLimit headers are sent to instruct the client to throttle requests.

    Example: First block is 5 seconds, then 30 seconds, 60 seconds, and finally 1 hour DOSBlockingPeriod 5 30 600

  • DOSWhitelist accepts CIDR notation as well as former * quad wildcards. May be used multiple times.

    Example: Whitelist IPv6 range DOSWhitelist 2a03:dead:beef::/24

  • DOSCache is discussed in Monitoring below.

  • Blocks in /tmp contain additional fields. Format is now: PID<NL>[site|page] GENERATION COUNT HOSTNAME ORIGIN-TIMESTAMP<NL>URI

Shield is intended to be backwards compatible with Evasive directives. From benchmarks, Shield is approximately 13% faster owing to its binary protocol. Memory-efficient storage permits extending monitoring windows well beyond previously advised 5 second thresholds. Likewise, since Apache’s cache provider is used internally, setting DOSCache to a provider other than shmcb (e.g. redis, memcache) is possible albeit concurrency issues may arise in high frequency environments.

Monitoring

A separate status handler has been added. It may be activated with the following directives in /etc/httpd/conf/httpd-custom.conf:

<Location /shield>
    SetHandler shield-handler
    # It's a good idea to limit traffic to your IP.
    # Accepts CIDR-style
    Require ip your.ip.address
</Location>

Of interest, adjust DOSCache if total (pre-expiry) entries reports a high eviction rate (>3%). It’s a standard shmcb directive, so to increase cache from 512 KB to 1 MB:

DOSCache shmcb:none(1048576)

Like to gawk at the junk you’re filtering? Use Cymru’s IP to ASN mapper:

(find /tmp -maxdepth 1 -type f -iname 'dos-*' | cut -d- -f2 | while read ip ; do
  echo "$ip: $(whois -h whois.cymru.com $ip | tail -n 1)"
done) | sort -t'|' -k3

Installing

dnf --enablerepo=apnscp-testing update mod_shield
sed -ie 's/evasive/shield/g' /etc/fail2ban/jail.conf
systemctl restart httpd fail2ban

Configuring

Defaults are provided in /etc/httpd/conf.d/shield.conf. Make changes to /etc/httpd/conf/httpd-custom.conf to prevent overwrite.

Enjoy :tada:

1 Like

Now, this is epic.

What kind of feedback is needed during testing for this to be promoted to GA and be production-ready? Anything you’re not-so-sure that mod_shield is going to handle properly we should keep an eye on?

I’ve been testing and it’s very promising. Blocking tons of bot traffic to forums and other abusive traffic.

1 Like

Quantifiable metrics, reduction in memory/CPU usage across the server. Reduction in TTFB from less resource competition. Anything that would make for a good success story for a white paper.

Any situation in which a server has a significant “pre-total (pre-expiry) entries scrolled out of cache” value (> 7%) and low index/cache usage (< 50%) or imbalanced index/cache ratio for any of the 3 cache categories (Blocks, Site tracking, Page tracking).

Defaults should be appropriate for most usage patterns (site browsing, webmail usage, API interaction). We’re working with a larger request window - so depending on how much of an abomination the site is - it’s possible to reach 500 requests in 2 minutes more readily. As an example, Foxhole HQ app preloaded every map tile each pageview, which was also a problem with the old mod_evasive module. Setting cache headers for these static files improved page reponsiveness and stopped triggering mod_evasive/mod_shield rules.

<FilesMatch "\.(gif|jpg|png|js|css|woff2|eot|svg)$">
    ExpiresActive On
    ExpiresDefault A864000
    Header unset Set-Cookie
    Header unset Last-Modified
</FilesMatch>
1 Like