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:

2 Likes

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

2.999.202506091451 adds support for negative baseline scoring to DOSDeadlinePenalty.

# Negate any static requests...
DOSDeadlinePenalty 0 -1
# ... that completes within 50 ms
DOSDeadlinePenalty 0.05 0
# Add a point for each response that takes longer than 1.5s
DOSDeadlinePenalty 1.5 1

It’s also possible to clear the curve in /etc/httpd/conf.d/shield.conf by setting “off”:

# Clear everything
DOSDeadlinePenalty off
DOSDeadlinePenalty 1 5
DOSDeadlinePenalty 5 15

With adjustments, the curve now reads: requests < 1000 ms score 0, 1000 ms <= request < 5000 ms scores 5, and request > 5000 ms scores 15.

If you’d like to check requests - for curve fitting - setup a custom log profile in httpd-custom.conf:

LogFormat "%{ms}T %h %V %{SITE_ID}e %t \"%r\" %>s %b" monitor

Then to log for each site of interest, add to /etc/httpd/conf/siteXX/timing-log:

CustomLog /var/log/httpd/site-timings monitor

Run htrebuild to recompile Apache configuration.

I’m finding it’s a little aggressive with the default values, mostly just for Wordpress and Xenforo users. And really just on the Admin side when some plugins will make a ton of page loads.
Just set these overrides and am hoping for the best.

Seems to be the page getting hit the most, page 1 60 for /wp-admin/admin-ajax.php

    DOSPageCount        60
    DOSPageInterval     45
    DOSPageDeadline     3 10

Ok, I had it all backwards, for those that also got confused like me:

  • DOSPageCount: The number of requests for an individual page that triggers blacklisting. This is set to 2, which is low (and aggressive) – increase this value to reduce false-positives.
  • DOSSiteCount: The total number of requests for the same site by the same IP address. By default, this is set to 50. You can increase to 100 to reduce false-positives.
  • DOSPageInterval: Number of seconds for DOSPageCount. By default, this is set to 1 second. That means that if you don’t change it, requesting 2 pages in 1 second will temporarily blacklist an IP address.
  • DOSSiteInterval: Similar to DOSPageInterval, this option specifies the number of seconds that DOSSiteCount monitors. By default, this is set to 1 second. That means that if a single IP address requests 50 resources on the same website in a single second, it will be temporarily blacklisted.

Unfortunately, Wordpress Admin makes a boat load of requests to /wp-admin/admin-ajax.php
Even with DosPageInterval at 30 and DosPageCount at 180 it triggers.

    <Files "admin-ajax.php">
        DOSEnabled off
    </Files>

Is the only thing that seems to work without affecting the effectiveness for everything else.
Since admin-ajax.php requires authentication, this should be ok for now.

Values are sourced from /etc/httpd/conf.d/shield.conf. It won’t overwrite on RPM update subject to change. These are the present defaults:

LoadModule shield_module   modules/mod_shield.so

<IfModule shield_module>
    # Optionally enable granular logging
    #LogLevel shield:trace8

    # Thresholds
    # Site: all resources on a hostname
    DOSSiteCount        500
    DOSSiteInterval     120
    # Page: specific same-page resource
    DOSPageInterval 120
    DOSPageCount 180

    # Maximum response time for a page. Exceeding it scores a higher value
    DOSPageDeadline 0 -1
    DOSPageDeadline 0.1 0
    DOSPageDeadline 0.5 5
    DOSPageDeadline 1 20
    DOSPageDeadline 2.5 35
    DOSPageDeadline 5 45

    # Status responses for various requests on a site + penalties
    DOSSiteStatusPenalty 302 25
    DOSSiteStatusPenalty 301 25
    DOSSiteStatusPenalty 404 50

    # Successive blocking duration. All blocks are retained for 24 hours
    DOSBlockingPeriod 5 60 120 300 3600 7200 86400

    # Accepts CIDR format
    DOSWhiteList        127.0.0.1

    # Status code when blocked
    DOSHTTPStatus       429

    # Strip query string when normalizing request
    DOSCanonicalize     on

    # BEGIN WP BRUTE-FORCE BLOCK

    # This is a sample WP bean counter for wp-login.php/xmlrpc.php
    # apnscp provides a scope, apache.shield-wordpress, to toggle:
    #
    # cpcmd scope:set apache.shield-wordpress true

    # Block wp-login brute-force attempts
    <Files "wp-login.php">
        <If "%{REQUEST_METHOD} != 'POST'">
            DOSEnabled off
        </If>
        DOSPageCount 3
        DOSPageInterval 2
    </Files>

    # Block xmlrpc.php brute-force attempts
    <Files "xmlrpc.php">
        <If "%{REQUEST_METHOD} != 'POST'">
              DOSEnabled off
        </If>
        DOSPageCount 5
        DOSPageInterval 60
    </Files>

    # END WP BRUTE-FORCE BLOCK

</IfModule>

If admin-ajax.php is completing in under 100 ms with the current curve, then the request isn’t counted toward DOSPageCount. You can verify this with a timing log mentioned above.

evasive.conf in /etc/httpd/conf.d gets renamed to evasive.conf.rpmsave when mod_shield RPM replaces it, so it should no longer be applicable. Is this server deploying mod_shield from RPM or the git repo?

The server has the package installed from the testing repo and my overrides are in httpd-custom to avoid losing changes on update.

I’m also not referring to evasive.conf but found documentation on the config options which is what I referenced above.

Most people will take the easy approach and drop limits super low to prevent false positives. I’m still having issues with wordpress sites triggering page with 1 180 in the tmp file. Will try enabling timings and see what happens but it’s hard to put this on customers to “try now” 20x without them just saying Fix It!!!

Enable the status handler to view active settings + blocks. RPM always includes shield.conf, so those values - as narrow and impractical as they are - wouldn’t be in play here.

Here are my current curve settings:

# Clear curve inherited from conf.d/shield.conf
DOSPageDeadline off

DOSPageDeadline 0 -1
DOSPageDeadline 0.2 0
DOSPageDeadline 1 1
DOSPageDeadline 2 5
DOSPageDeadline 3 20
DOSPageDeadline 5 30

These adjustments cleanup penalizing large polling durations in Discourse (>30s). Likewise anything under 200ms is ignored, anything over 1s is penalized at least 1 additional point.

Here’s a screengrab of a vanilla WordPress install. AJAX frequency is well above 666 ms/req. Each pingback is ~20ms, so this request would be negated with the curve provided above.

I just keep getting this:

page 1 180 domain.com 1749569639
/wp-json/presto-player/v1/settings

The same was true for admin-ajax.php which I clearly whitelisted.

shield settings:

DOSSiteCount	500
DOSSiteInterval	120
DOSSiteStatusPenalty	301 25
DOSSiteStatusPenalty	302 25
DOSSiteStatusPenalty	404 50
DOSPageCount	180
DOSPageInterval	30
DOSPageDeadline	0.00 -1
DOSPageDeadline	0.10 0
DOSPageDeadline	0.50 5
DOSPageDeadline	1.00 20
DOSPageDeadline	2.50 35
DOSPageDeadline	5.00 45
DOSBlockingPeriod	5 60 120 300 3600 7200 86400
DOSHTTPStatus	429

Those are default except the page interval I dropped to 30.
Are you proposing the DOSPageDeadline changes may be all I need to curb these page 1 180 blocks?

Yes, no, and perhaps. It depends what the request characteristics look like. Is admin-ajax.php getting called every 500 ms with a 500 ms round-trip time? If so, 30 seconds / (500 ms/request / 1000 ms/second) * 5 points/request = 300 points in a 30 second window.

Use DevTools in Chrome to evaluate how frequently admin-ajax.php is being called (screencap above). Based upon that behavior, this will help you dial in the curve. Every request in is 1 point. You can negate points by setting a negative score in DOSPageDeadline. Also adding “LogLevel shield:trace8” will add extra logging to error_log to help discover how scores are calculated.

:warning::warning::warning:
DOSPageDeadline off must be present to clear the curve established in /etc/httpd/conf.d/shield.conf otherwise you are adding new thresholds or overwriting existing thresholds.

Packages off apnscp-testing do not update automatically. dnf --disablerepo='*' --enablerepo='apnscp-testing' update mod_shield will update with the latest release. I’ve detuned blocking scores for longer durations; 20 may be too extreme depending upon page load time. I’d look at the “Time” column under DevTools > Network in Chrome to get an idea of how expensive these page requests are and that they are firing sequentially.

A lot of this stuff isn’t really making sense, there’s a ton of options, calculations and everything else.
It makes sense to you because you’ve been working on this, I’ll see if I can enable the Timings and enable extra logging, most of the triggers have been by customers actually creating posts or doing things in their admin backend. I don’t have a good site to test with personally. I can at least login as my customers and see if I get the same issues or not.

I’m going to assume that this feature will not be widely adopted by the general ApisCP user base. It’s neat in theory but with 200+ accounts on a server with 1-20 domains per account, and each domain having different software, plugins and general usage, this is going to be hard to have good sane options as defautls. I’m likely going to just drop the numbers low enough to prevent false positives which will end up nullifying the effectiveness of all these improvements to mod_evasive as mod_shield.

Maybe on my own website for lithiumhosting.com would something like this be effective, but as a global policy for a server full of customer controlled websites, it’s a hard sell.

I want it to work, but I’ve spent more time on this so far than it will likely save me in the future and pissed off two customers with constant support tickets to get unblocked when they are just trying to use their site. Yes, this is testing, but I can’t test without using production sites.

Ok, trying to do your changes. Added the custom monitor log and the DOSPageDeadline changes.

Reloading httpd: AH00526: Syntax error on line 35 of /etc/httpd/conf/httpd-custom.conf:
DOSPageDeadline requires at least two arguments, Apply penalty score by page response deadline
Bailing on bad config```
Line 35: `DOSPageDeadline off`

Nevermind, forgot to update to version 2.999-202506100032 first.

I ended up using this in my httpd-custom

      LogFormat "%{ms}T %h %V %{SITE_ID}e %t \"%r\" %>s %b" monitor
      <IfModule shield_module>
          <Location /shield>
              SetHandler shield-handler
              Require ip XX.XX.XX.XX
          </Location>
          DOSHashTableSize    6151
          DOSCache shmcb:none(1048576)
          DOSPageInterval     30
          DOSPageDeadline off
          DOSPageDeadline 0 -1
          DOSPageDeadline 0.2 0
          DOSPageDeadline 1 1
          DOSPageDeadline 2 5
          DOSPageDeadline 3 20
          DOSPageDeadline 5 30
          <Files "admin-ajax.php">
              DOSPageCount 360
              DOSPageInterval 15
          </Files>
      </IfModule>

can this also have support for X forward for, i have apiscp behind a reverse proxy and it would be nice to use this without everyone appearing as my reverse proxies ip

3683457
site 1 50 domain.com 1749645253
/wp-admin/admin-ajax.php

I cannot figure out why it’s being triggered at 50 when the shield.conf has

DOSPageCount    180
DOSPageInterval    30

And httpd-custom has:

    DOSHashTableSize    6151
    DOSCache shmcb:none(1048576)
    DOSPageInterval     30
    DOSPageDeadline off
    DOSPageDeadline 0 -1
    DOSPageDeadline 0.2 0
    DOSPageDeadline 1 1
    DOSPageDeadline 2 5
    DOSPageDeadline 3 20
    DOSPageDeadline 5 30
    <Files "admin-ajax.php">
        DOSPageCount 360
        DOSPageInterval 15
    </Files>

Use remoteip to update the client IP, which is how it works out of the box with Cloudflare’s network.

In /etc/httpd/conf/httpd-custom.conf add:

LoadModule remoteip_module modules/mod_remoteip.so
# Header passed by downstream
RemoteIPHeader X-Forwarded-For
# Trusted downstream proxy
RemoteIPTrustedProxy 192.168.0.4

It’s hitting “DOSSiteCount”, which isn’t defined in the context of <Files …>. Try adding DOSSiteCount 360 within <Files …>, if that works I’ll address per-dir merge logic later this evening.