Skip to main content
Apache logo
Apache

Apache module with live and stream remediation, AppSec (WAF) forwarding, bot-detection challenge and usage metrics.

  • ●Live | Stream
  • ✓WAF / AppSec
  • ×Captcha
  • ✓Bot Detection
  • ✓Remediation Metrics
  • ×mTLS
  • ×Prometheus
  • GitHub·Hub

Protect Apache with IP Blocking, WAF & Bot Detection

v1.0 packages available as github artifacts

mod_crowdsec 1.0 is a full rewrite of 0.1. Its packages are attached to the GitHub release.

1.0 configuration is backward compatible with 0.1; Upgrade and adapt your existing configuration to take advantage of the new features.

Enable the WAF and Bot Detection for optimal protection

After installing the module, enable the AppSec (WAF) Component to get virtual patching and defense against known CVEs, SQL injection, XSS and other application-layer attacks.

Follow the dedicated AppSec QuickStart for Apache after completing initial setup.

How does it work?​

The component is an Apache module. It runs in-process in every Apache child, and does up to two things on the request path:

  1. Decision lookup — the client address is checked against the CrowdSec decisions, and the request is allowed, blocked or redirected.
  2. Request inspection (optional) — the request is forwarded to the AppSec Component for WAF inspection, and its verdict decides the request. This is also what serves the bot-detection challenge.

Decisions are obtained in one of two modes:

  • Stream mode (recommended) — the decision set is polled in the background into a shared cache, so no request ever waits on the local API.
  • Live mode — the local API is queried per request, with a short-lived cache per address.

Supported features:

  • Ban remediation, returning a status code or redirecting to a page of your choice
  • Bot-detection challenge, served through AppSec
  • IPv4 and IPv6, single addresses and ranges
  • WAF/AppSec over TCP or a unix socket
  • Usage metrics reported to the local API, so the component shows up in cscli metrics show bouncers and in the Console
  • A status handler for the module's own counters

There is no captcha support: an AppSec captcha remediation is resolved through CrowdsecRemediationFallback, which blocks by default. See Known limits.

At the back, the module uses libcurl for its API calls and mod_socache for the shared decision cache.

Requirements​

  • Apache 2.4.16 or later
  • mod_socache_shmcb, for the shared decision cache (mandatory in stream mode)
  • libcurl 7.40 or later
  • A CrowdSec local API, and the AppSec Component if you want WAF inspection or bot detection

Installation​

info

Packages are built for Debian (bullseye, bookworm, trixie), Ubuntu (jammy, noble) and Enterprise Linux 8, 9 and 10, on amd64/x86_64 and arm64/aarch64. On any other platform, build from source.

Download the .deb matching your distribution and architecture from the latest release, then:

SH
sudo apt install ./crowdsec-apache2-bouncer_*_amd64.deb

apt accepts a local path where apt-get does not, and it pulls in what the package needs: apache2, libcurl and gettext-base. dpkg -i works only once those are already installed — without gettext-base the package cannot write the API key into its configuration.

If CrowdSec is installed on this host, the package registers a bouncer with it, writes the key into /etc/crowdsec/bouncers/crowdsec-apache2-bouncer.conf, enables the module and reloads Apache. With no cscli on the host it says so and leaves an <API_KEY> placeholder, and it does not retry on a later upgrade — see Register the bouncer by hand.

Register the bouncer by hand​

Only needed if CrowdSec is not installed on this host, if the key could not be generated, or if you installed from source. On the local API server:

SH
sudo cscli bouncers add apache

Then put the key in /etc/crowdsec/bouncers/crowdsec-apache2-bouncer.conf:

APACHE
CrowdsecURL    http://127.0.0.1:8080
CrowdsecAPIKey <the key you just generated>

Upgrading from 0.1​

The upgrade is backward compatible. Stream mode, AppSec, bot detection and metrics are opt-in through the new directives below.

Configuration​

Configuration is plain Apache configuration. The package ships a commented default at /etc/crowdsec/bouncers/crowdsec-apache2-bouncer.conf, which is where the API key is written, and which Apache reaches through a snippet of its own: conf.d/crowdsec.conf on Enterprise Linux, mods-available/crowdsec.conf on Debian and Ubuntu.

Most directives are server-wide. The ones that decide whether a request is checked are also valid in <Directory> and <Location>, so you can protect part of a site and not the rest. See the configuration reference for each directive's scope.

Stream mode​

Recommended: no request ever waits on the local API.

/etc/crowdsec/bouncers/crowdsec-apache2-bouncer.conf
APACHE/etc/crowdsec/bouncers/crowdsec-apache2-bouncer.conf
CrowdsecURL    http://127.0.0.1:8080
CrowdsecAPIKey <your key>

CrowdsecMode stream
CrowdsecUpdateFrequency 10s
CrowdsecCache shmcb:crowdsec(16777216)
CrowdsecFallback allow

The shipped configuration already sets Crowdsec On at server level, so that is all it takes to have it apply everywhere.

Size the cache for your decision count

CrowdsecCache is mandatory in stream mode and must be large enough to hold every decision you receive. Budget about 128 bytes per decision. The shipped shmcb:crowdsec(16777216) is the recommended floor and covers around 100,000 decisions; 1,000,000 wants 128 MB. Undersized, decisions are silently evicted and those clients stop being blocked — the error log reports it after each pull, and so does the status handler.

Live mode​

No shared cache required, at the cost of an API round trip for each client not already cached.

APACHE
CrowdsecURL    http://127.0.0.1:8080
CrowdsecAPIKey <your key>
CrowdsecMode live

A CrowdsecCache is still worth setting in live mode: without it, every Apache child queries the API separately for the same client.

AppSec (WAF)​

Both lines are already in the shipped configuration, commented out. Uncomment them, at server level:

APACHE
CrowdsecAppsecURL http://127.0.0.1:7422
CrowdsecAppsec On

Crowdsec On is there already too.

Prefer that to a <Location /> block. See Protect only part of a site if you need only partial activation.

CrowdsecAppsecPath must match the path of the AppSec datasource.

For the engine side of this — rules, acquisition, verification — follow the AppSec QuickStart for Apache.

Bot detection​

Prerequisites

Bot detection is AppSec with the challenge configs loaded on the engine, so there is nothing to add on the Apache side beyond the CrowdsecAppsec you already have. Install the collection and point your AppSec datasource at its configs by following Enable bot detection, which also covers the thresholds.

The engine loads only the appsec-configs named in the datasource, so installing the collection on its own changes nothing and no request is ever challenged. Once both are done, this is the confirmation:

SH
sudo grep "WAF challenge runtime initialized" /var/log/crowdsec.log

Without that line the engine is inspecting requests but has no challenge to serve.

The challenge serves its own assets and submissions under CrowdsecAppsecInternalPath (/crowdsec-internal/challenge/ by default). Everything under that prefix is reserved for the engine: it is always sent to AppSec — including in scopes where Crowdsec is Off, since a challenged visitor cannot complete the challenge without it — and never reaches your application. A path under the prefix that the engine does not answer gets a 404.

Behind a reverse proxy or CDN​

The module uses the client address Apache reports, so mod_remoteip has to be configured for it to police the real client:

APACHE
RemoteIPHeader        X-Forwarded-For
RemoteIPInternalProxy 10.0.0.0/8
danger

With RemoteIPHeader set and no trusted-proxy list, any client can choose the address being policed, which is enough to evade a ban. Either set RemoteIPInternalProxy/RemoteIPTrustedProxy to your actual proxies, or set CrowdsecTrustUseragentIP Off to pin the module to the real TCP peer.

The module cannot check this for you — mod_remoteip keeps its trusted-proxy list to itself — so nothing warns you about it.

To see which address is being policed, log both. %a is the address the module acts on, and %{c}a is the real TCP peer — %h and %a both follow the rewrite, so only the {c} form shows you the peer.

APACHE
LogFormat "%a (peer %{c}a) %t \"%r\" %>s %{CROWDSEC_STATUS}e" crowdsec

Observability​

Status handler​

The module exposes its own counters through a handler. Keep it private:

APACHE
<Location /crowdsec-status>
SetHandler crowdsec-status
Crowdsec Off
CrowdsecAppsec Off
Require ip 127.0.0.1
</Location>
CrowdsecAppsec Off matters as much as Crowdsec Off

Left on with bot detection enabled, the status endpoint is served the challenge interstitial instead of its own counters.

Apache merges <Location> sections in the order it reads them, so a <Location /> that turns things on later in the configuration overrides this block and puts the endpoint back behind the WAF. Enabling Crowdsec and CrowdsecAppsec at server level instead — as the shipped configuration does — keeps that from happening whatever order your files are read in.

It reports mode, url, cache, the usage-metrics counters, and in stream mode primed, pulls, stored, retained, stored_age_s and last_pull_age_ms.

stored is what the last full pull wrote and stored_age_s is how long ago that was; incremental pulls leave both alone. retained estimates how much of that is still in the cache, extrapolated from a sample of keys re-read after every pull, so treat it as an indicator rather than a count. retained below stored means the cache is evicting decisions and needs to be larger. For what the cache holds right now, read the active_decisions counters in the metrics block instead: they move as decisions arrive and are withdrawn.

Log variables​

The module sets these in subprocess_env, for use in LogFormat:

VariableValues
CROWDSEC_STATUSallow, ban, challenge, or why it could not decide
CROWDSEC_SOURCEmemo, cache, live, unprimed
CROWDSEC_APPSEC_STATUSthe verdict action, body-bypass, parse-fail, http-<code>, or a transport error
APACHE
LogFormat "%h %l %u %t \"%r\" %>s %b %{CROWDSEC_STATUS}e %{CROWDSEC_SOURCE}e %{CROWDSEC_APPSEC_STATUS}e" crowdsec
CustomLog /var/log/httpd/crowdsec_access_log crowdsec

LogFormat only names the format; the CustomLog is what writes it. The log directory is /var/log/apache2/ on Debian and Ubuntu.

An ErrorDocument on the blocked status code hides these

They belong to the request that was decided, and an ErrorDocument makes Apache log the internal redirect instead, where they are unset — so the line reads - for all three. Enterprise Linux ships exactly that: ErrorDocument 403 /.noindex.html in conf.d/welcome.conf. See Return a custom page or status code.

For the stream poller's own messages:

APACHE
LogLevel info crowdsec_module:info

Usage metrics​

Request and decision counts are reported to the local API every CrowdsecMetricsInterval, which is what makes the component appear in cscli metrics show bouncers and in the Console. It is on by default wherever CrowdsecURL is set.

Three counters are reported:

CounterUnitMeaning
processedrequestRequests the module checked, one per request.
droppedrequestRequests it refused, attributed to the origin of the decision behind each one.
active_decisionsIPDecisions currently held in the shared cache. Stream mode only — live mode keeps no local decision store.

Every counter is broken down by ip_type (ipv4 or ipv6), and dropped and active_decisions also by origin: crowdsec, CAPI, cscli, appsec, console, lists:<name> and so on. Usage metrics explains what each origin means.

Checking from the local API​

The package registers the component under a generated name, so take it from the list first:

SH
sudo cscli bouncers list

Type is crowdsec-apache2-bouncer and Version is the module's own, both read from the User-Agent it sends — which makes this the quickest way to confirm which build a host is actually running. Then, for one component:

SH
sudo cscli bouncers inspect cs-apache2-bouncer-1761134702

On top of the list's columns this reports the OS, which the module reads from /etc/os-release, along with the auth type and the last pull. Then the counters themselves:

SH
sudo cscli metrics show bouncers

Add --no-unit for exact numbers instead of ones abbreviated with k and M suffixes, and -o json to script against them.

The first figures take a while to appear

cscli shows what the local API has accepted, so nothing appears until the first push lands — up to CrowdsecMetricsInterval, which is 15 minutes by default. The status handler shows the window as it accumulates, so it is the faster way to confirm the counters are moving at all. To shorten the wait while testing, lower the interval and put it back afterwards; anything under a minute is warned about in the error log.

Expect active_decisions to disagree with cscli decisions list: the local API deduplicates the stream it sends. Usage metrics explains why.

Checking the WAF and bot-detection side​

dropped{origin=appsec} is as much as the module can say about inspection: what the rules did is engine-side, and the engine is where to look for it.

SH
sudo cscli metrics show appsec
sudo cscli alerts list --kind waf
sudo cscli alerts list --kind bot-detection

Known limits​

CrowdsecAppsecBodyLimitAction bypass is a WAF bypass

Anyone can pad a request body past CrowdsecAppsecBodyLimit to skip inspection entirely. Keep the default drop unless you have a specific reason not to.

  • A gzipped request body is inspected compressed. The body is captured before Apache inflates it, so AppSec sees the compressed bytes and rules matching body content do not fire. Your application still receives correct plaintext. This is shared with the other remediation components.
  • No captcha. An AppSec captcha remediation is resolved through CrowdsecRemediationFallback, which defaults to ban.
  • Decisions from the stream are enforced as a ban. The decision stream carries no interstitial, so a challenge or captcha decision coming from the local API becomes a ban rather than being served.
  • active_decisions is reported in stream mode only. Live mode keeps no local decision store, so there is nothing to count. Same as the NGINX component.
  • processed counts one per request, where the HAProxy SPOA component counts one per connection when its TCP stage runs. Comparing the two components' request volumes directly will mislead.
  • A new decision is not instant, and the delay differs by mode. In stream mode the worst case is CrowdsecUpdateFrequency + CrowdsecLocalCacheTimeout, so 12s with the defaults. In live mode it is CrowdsecCacheTimeout + CrowdsecLocalCacheTimeout, so 62s with the defaults. CrowdsecLocalCacheTimeout is the knob for propagation speed and costs only extra shared-cache reads; lowering CrowdsecCacheTimeout instead makes every child query the API more often.

Configuration reference​

Scope: server means server and virtual host configuration only; server + directory means it is also valid in <Directory> and <Location>.

Durations take either spelling: Apache's (200ms, 5s, 2min, 3h) or the Go one the rest of CrowdSec uses (15m, 1h30m). A bare number is seconds.

Decision source​

DirectiveScopeDefaultDescription
CrowdsecURLserver—Base URL of the local API, e.g. http://localhost:8080.
CrowdsecAPIKeyserver—Component key, from cscli bouncers add.
CrowdsecModeserverlivelive or stream. stream requires CrowdsecCache. Servers sharing a cache must agree on the mode; a mismatch is refused at startup.
CrowdsecUpdateFrequencyserver10sHow often to poll the decision stream.
CrowdsecCacheserver—Shared decision cache. Set it on the main server; virtual hosts inherit it. Required by stream mode. The shipped configuration sets shmcb:crowdsec(16777216), which holds roughly 100,000 decisions — see the sizing warning.
CrowdsecCacheTimeoutserver60sHow long one address's verdict is held in the shared cache. Live mode only: it is what stops every child querying the API for the same client.
CrowdsecLocalCacheTimeoutserver2sHow long one address's verdict is held within a single Apache child, in front of the shared cache. Adds to the time a new decision takes to apply, so keep it small. Capped at CrowdsecCacheTimeout.
CrowdsecConnectTimeoutserver1sConnection timeout to the local API.
CrowdsecTimeoutserver5sTotal budget for one API request. A stream pull is allowed a multiple of this.

Enforcement​

DirectiveScopeDefaultDescription
Crowdsecserver + directoryOffCheck requests in this scope.
CrowdsecBlockedHTTPCodeserver + directory429Status returned to a blocked client: 429, 403 or 500. The shipped configuration sets 403.
CrowdsecLocationserver + directory—Redirect a blocked client here instead. A path or a full URL, and it supports expressions, e.g. /blocked?ip=%{REMOTE_ADDR}.
CrowdsecFallbackserver + directoryfailWhat to do when CrowdSec cannot be consulted, including before the stream has loaded: fail (500), block, or allow. The shipped configuration sets allow.
CrowdsecRemediationFallbackserver + directorybanWhat an AppSec remediation this component cannot serve (captcha, or one you defined) becomes: ban or allow. Does not apply to local API decisions, which are always enforced as a ban.
CrowdsecTrustUseragentIPserver + directoryOnTrust the client address reported by modules such as mod_remoteip. Off uses the connection's real peer.

AppSec​

DirectiveScopeDefaultDescription
CrowdsecAppsecURLserver—AppSec address: http://127.0.0.1:7422, or unix:/run/crowdsec/appsec.sock. Unset disables AppSec entirely.
CrowdsecAppsecPathserver/Path AppSec listens on, matching the datasource's path. A 404 from AppSec usually means this does not match.
CrowdsecAppsecserver + directoryOffSend requests in this scope to AppSec for inspection.
CrowdsecAppsecAlwaysSendserver + directoryOffInspect even when the client is already blocked.
CrowdsecAppsecFailureActionserver + directorypassthroughWhat to do when AppSec cannot be consulted: passthrough or deny. An availability choice, not a security one.
CrowdsecAppsecConnectTimeoutserver200msConnection timeout to AppSec.
CrowdsecAppsecTimeoutserver1sTotal budget for one AppSec request, excluding the time spent reading the client's body.
CrowdsecAppsecBodyLimitserver + directory1mHow much request body to forward for inspection. 0 forwards none, leaving the WAF blind to anything carried in a body. Buffered per request in flight, so the worst case is this times MaxRequestWorkers.
CrowdsecAppsecBodyLimitActionserver + directorydropWhat to do with a body over the limit: drop blocks the request, bypass lets it through uninspected.
CrowdsecAppsecInternalPathserver/crowdsec-internal/challenge/Prefix the bot-detection challenge serves its assets and submissions from. Always sent to AppSec, never to the origin. Must match the engine, which has this path compiled in.

Metrics​

DirectiveScopeDefaultDescription
CrowdsecMetricsserverOnReport usage metrics to CrowdsecURL's /v1/usage-metrics. Off disables the counters as well, not just the reporting.
CrowdsecMetricsIntervalserver15mHow often to report. Floored at 10s; anything under a minute is for testing and is warned about in the error log.

How-to guides​

Protect only part of a site​

Crowdsec defaults to Off, and any scope can override what it inherits:

APACHE
# Nothing is checked by default
Crowdsec Off

<Location /admin>
Crowdsec On
CrowdsecAppsec On
</Location>

<Location /healthz>
Crowdsec Off
</Location>

Return a custom page or status code​

Either send blocked clients elsewhere with CrowdsecLocation:

APACHE
CrowdsecLocation /sorry.html

Or keep them where they are and pick the status:

APACHE
CrowdsecBlockedHTTPCode 403
ErrorDocument 403 /sorry.html
On Enterprise Linux something already answers 403

conf.d/welcome.conf ships ErrorDocument 403 /.noindex.html, so a blocked client gets the distribution's test page — 1.4 MB of it — rather than anything of yours, and the log variables disappear from the access log line. Point ErrorDocument at your own page for whichever code you block with, or drop the directive from welcome.conf.

Verify the component is in use​

The Hub ships two test items, both triggered by one probe path: /crowdsec-test-NtktlJHV4TfBSK3wvlhiOBnl. Neither creates a decision, so it won't lock you out:

SH
curl -I https://<your-site>/crowdsec-test-NtktlJHV4TfBSK3wvlhiOBnl

The request is meant to go through: crowdsecurity/appsec-generic-test is an out-of-band rule, which reports without blocking. What matters is which alerts arrive:

SH
sudo cscli alerts list -s crowdsecurity/http-generic-test
sudo cscli alerts list -s crowdsecurity/appsec-generic-test
AlertWhat it tells you
crowdsecurity/http-generic-testThe log processor is reading Apache's logs. That is the detection side, and it says nothing about the bouncer.
crowdsecurity/appsec-generic-testThe module forwarded the request to AppSec and got an answer back. Needs CrowdsecAppsec On in the scope you probed, and the virtual patching collection installed.

Send the probe from outside the host. The engine whitelists private source addresses by default, and a whitelisted event raises no alert. Each scenario also has a blackhole window, 5 minutes for the HTTP one and 1 minute for the AppSec one, so a second probe inside it produces nothing.

Checking that it blocks​

Both test scenarios are declared remediation: false, so they prove the request reached the engine, not that the module enforces anything. Enforcing needs a decision — and a decision is not scoped to one component.

The module polices the address mod_remoteip reports, so where a trusted proxy is already configured — see Behind a reverse proxy or CDN — send the forwarded header from the trusted hop:

SH
sudo cscli decisions add --ip 192.0.2.1 --duration 10m
curl -I -H 'X-Forwarded-For: 192.0.2.1' http://127.0.0.1/
sudo cscli decisions delete --ip 192.0.2.1

Without mod_remoteip in the picture the header is ignored and the request is judged on the real peer, which is the whole point of CrowdsecTrustUseragentIP. To run the test on a host that has no proxy, trust loopback for it:

APACHE
RemoteIPHeader       X-Forwarded-For
RemoteIPTrustedProxy 127.0.0.1

Take them back out when you are done. On a host where anything already proxies to Apache over loopback, leaving them in lets a client pick the address being policed — the evasion the reverse proxy warning is about.

The request should be refused with CrowdsecBlockedHTTPCode, which the shipped configuration sets to 403.

Give the decision time to arrive before concluding anything. The shipped configuration is in live mode, where the worst case is CrowdsecCacheTimeout + CrowdsecLocalCacheTimeout — 62s with the defaults, which is why the ban above outlasts it. In stream mode it is CrowdsecUpdateFrequency + CrowdsecLocalCacheTimeout instead, so about 12s. An address you have never sent a request as has nothing cached, so in practice the first probe after decisions add is already refused.

That block should show up as a dropped{origin=cscli} in the status handler straight away, and in cscli metrics show bouncers once the first report lands — see Checking from the local API.

In stream mode you can also confirm the decision path without sending a request at all: ban 192.0.2.1 and watch stored and active_decisions climb in the status handler. That tells you the module is pulling the stream, which is the half that breaks silently. Live mode keeps no local store, so it shows nothing there.

CrowdSec Docs
We use cookies

This site uses cookies to help us improve your experience. You can accept or decline below.