Traefik plugin for Crowdsec - WAF and IP protection
  • Go 72.7%
  • HTML 12.8%
  • Shell 12.4%
  • Makefile 2.1%
Find a file
Manuel Sabban 710e88808b
add the bot detection feature to the crowdsec traefik bouncer (#343)
* add the bot detection feature to the crowdsec traefik bouncer

* fix the return value

* add http status normalization

* fix appsec query test return values

* add boundaries for appsec response

* 🎨 satisfy golangci-lint on the appsec response path

gofmt on bouncer_test.go (merge artifact), a doc comment revive requires
on the exported AppSecResponse, and nolint directives for the linters the
new appsecQuery signature trips: tagliatelle on the snake_case json tags
that are Appsec's wire format, and nilnil/gocognit/gocyclo/funlen on
appsecQuery itself, matching how New is already handled.

No feature code changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* 🐛 keep serving the configured ban page for appsec bans

An appsec "ban" with a structured body was served straight from appsec,
so an operator with banFilePath configured silently lost their ban page
as soon as Crowdsec 1.8 started returning structured JSON — and got a
blank 403 when appsec sent no user_body_content at all. Route the ban
action back through handleBanServeHTTP, which keeps the template, the
content type, the remediation header value and the trace templating.

Only actions that must carry their own content, challenge among them,
are relayed from appsec. Those now fall back to banTemplateContentType
when appsec sends no Content-Type, instead of letting Go sniff it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* 🍱 reduce test and logic in appsec query

* 🍱 use same appsec response limit as defined for user

* 🍱 fix lint

* 🐛 fix tests

* 🍱 revert to 1Mo appsec response body

* 📝 examples: add a bot detection example

The PR relays the whole AppSec remediation rather than collapsing it into a
403, which is what makes the bot challenge possible: a ban is a status code,
a challenge is a status code plus a body, cookies and headers. Nothing
demonstrated that, so this adds an example alongside the existing ones.

Verified end to end against CrowdSec 1.8.1: the challenge page is served, the
browser solves the proof of work, the submission is accepted and the request
reaches the backend.

Three things break this setup silently, and all three cost time here, so the
README names them:

- The router must also match /crowdsec-internal. The challenge page loads its
  fingerprint script and its proof-of-work worker from there by absolute path,
  so a router scoped to the application prefix alone returns 404 and the
  challenge can never be solved, with no error logged anywhere.
- The acquisition must load the config that calls SendChallenge(), which is
  appsec-bot-challenge-scoring; the -balanced variant only carries the
  rejection threshold. Loading the latter alone processes requests and
  challenges nothing. The wildcard form from the CrowdSec enable guide is used
  instead of a hand-written list, since it also picks up the exclusion configs
  the collection ships.
- Referencing an appsec-config that is not installed is fatal rather than
  degraded. CrowdSec refuses to start and, because crowdsecAppsecFailureBlock
  defaults to true, every request then returns a 403 that reads like a WAF
  decision. appsec-default needs appsec-virtual-patching and
  appsec-generic-rules, so both are in COLLECTIONS.

The routing requirement is in the main README too, since it applies to anyone
enabling bot detection and not just to this example.

The example runs the plugin from the working tree, as relaying the remediation
is not in a release yet; the catalog lines are present and commented out.

* 🍱 reduce complexity and set the remediationHeader to 'challenge' instead of decision.Action

* 📝 examples: add a bot detection example

The PR relays the whole AppSec remediation rather than collapsing it into a
403, which is what makes the bot challenge possible: a ban is a status code,
a challenge is a status code plus a body, cookies and headers. Nothing
demonstrated that, so this adds an example alongside the existing ones.

Verified end to end against CrowdSec 1.8.1: the challenge page is served, the
browser solves the proof of work, the submission is accepted and the request
reaches the backend.

Two things break this setup silently, and both cost time here, so the README
names them:

- The router must also match /crowdsec-internal. The challenge page loads its
  fingerprint script and its proof-of-work worker from there by absolute path,
  so a router scoped to the application prefix alone returns 404 and the
  challenge can never be solved, with no error logged anywhere.
- The acquisition must load the config that calls SendChallenge(), which is
  appsec-bot-challenge-scoring; the -balanced variant only carries the
  rejection threshold. Loading the latter alone processes requests and
  challenges nothing. The wildcard form from the CrowdSec enable guide is used
  instead of a hand-written list, since it also picks up the exclusion configs
  the collection ships.

The routing requirement is in the main README too, since it applies to anyone
enabling bot detection and not just to this example, along with the supported
versions stated the same way as the existing Appsec line.

* ✅ tests: drop the challenge Content-Type fallback test

c8d7751 removed the fallback that set Content-Type from banTemplateContentType
when the appsec response carried none, but left the test asserting it, so the
suite has been red since.

Removing the test rather than restoring the fallback, per maxlerebourg.

* 🍱 harmonize appsecServer vs appsec in test

* 🍱 reduce loc and go closer to Lua implementation

* 💄 fix log statusCode 401 -> 403

* 🍱 fix lint

* 🍱 add guard on appsec response

* 🍱 fix lint

---------

Co-authored-by: mhx <mathieu@hanotaux.fr>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
2026-09-12 14:58:48 +02:00
.assets
.github
examples add the bot detection feature to the crowdsec traefik bouncer (#343) 2026-09-12 14:58:48 +02:00
pkg
tests
vendor
.gitignore
.golangci.yml
.traefik.yml
acquis.yaml
ban.html
bouncer.go add the bot detection feature to the crowdsec traefik bouncer (#343) 2026-09-12 14:58:48 +02:00
bouncer_logging_test.go
bouncer_test.go add the bot detection feature to the crowdsec traefik bouncer (#343) 2026-09-12 14:58:48 +02:00
captcha.html
docker-compose.local.yml
docker-compose.yml
go.mod
go.sum
LICENSE
Makefile add the bot detection feature to the crowdsec traefik bouncer (#343) 2026-09-12 14:58:48 +02:00
README.md add the bot detection feature to the crowdsec traefik bouncer (#343) 2026-09-12 14:58:48 +02:00
renovate.json
version.go

GitHub GitHub go.mod Go version GitHub tag (latest SemVer) Build Status Go Report Card

Crowdsec Bouncer Traefik plugin

New! This plugin now supports AppSec feature including virtual patching and capabilities support for your legacy ModSecurity rules.

This plugin aims to implement a Crowdsec Bouncer in a Traefik plugin.

CrowdSec is an open-source and collaborative IPS (Intrusion Prevention System) and a security suite. We leverage local behavior analysis and crowd power to build the largest CTI network in the world.

The purpose is to enable Traefik to authorize or block requests from IPs based on their reputation and behavior.

The Crowdsec utility will provide the community blocklist which contains highly reported and validated IPs banned from the Crowdsec network.

When used with Crowdsec it will leverage the local API which will analyze Traefik logs and take decisions on the requests made by users/bots. Malicious actors will be banned based on patterns used against your website.

Appsec feature is supported from plugin version 1.2.0 and Crowdsec 1.6.0.

Appsec bot detection is supported from plugin version 1.8.0 and Crowdsec 1.8.0.

The AppSec Component offers:

  • Low-effort virtual patching capabilities.
  • Support for your legacy ModSecurity rules.
  • Combining classic WAF benefits with advanced CrowdSec features for otherwise difficult advanced behavior detection.

More information on appsec in the Crowdsec Documentation.

Remediation offered by Crowdsec and supported by the plugin can be either ban or captcha.
For the ban remediation the user will be blocked in Traefik (HTTP 403).
For the captcha remediation, the user will be redirected to a page to complete a captcha challenge.

On successfull completion, he will be cleaned for a specified period of time before a new resolution challenge is expected if Crowdsec still has a decision to verify the user behavior. See the example captcha for more informations and configuration intructions.
The following captcha providers are supported now:

There are 5 operating modes (CrowdsecMode) for this plugin:

Mode Description
none If the client IP is on ban list, it will get a http code 403 response. Otherwise, request will continue as usual. All request call the Crowdsec LAPI
live If the client IP is on ban list, it will get a http code 403 response. Otherwise, request will continue as usual. The bouncer can leverage use of a local cache in order to reduce the number of requests made to the Crowdsec LAPI. It will keep in cache the status for each IP that makes queries.
stream Stream Streaming mode allows you to keep in the local cache only the Banned IPs, every requests that does not hit the cache is authorized. Every minute, the cache is updated with news from the Crowdsec LAPI.
alone Standalone mode, similar to the streaming mode but the blacklisted IPs are fetched on the CAPI. Every 2 hours, the cache is updated with news from the Crowdsec CAPI. It does not include any locally banned IP, but can work without a crowdsec service.
appsec Disable Crowdsec IP checking but apply Crowdsec Appsec checking. This mode is intended to be used when Crowdsec IP checking is applied at the Firewall Level.

The streaming mode is recommended for performance, decisions are updated every 60 sec by default and that's the only communication between Traefik and Crowdsec. Every request that happens hits the cache for quick decisions.

The cache can be local to Traefik in memory or using a separate Redis instance.

Below are Mermaid diagrams detailling how each mode work:

Mode none workflow

A Ban decision exists in CrowdsecLAPI

sequenceDiagram
    participant User
    participant TraefikPlugin
    User->>TraefikPlugin: Can I access that webpage
    create participant CrowdsecLAPI
    TraefikPlugin-->>CrowdsecLAPI: Does the User IP has a Crowdsec Decision ?
    destroy CrowdsecLAPI
    CrowdsecLAPI-->>TraefikPlugin: Yes a ban Decision
    TraefikPlugin->>User: No, HTTP 403

No decision in CrowdsecLAPI

sequenceDiagram
    participant User
    participant TraefikPlugin
    User->>TraefikPlugin: Can I access that webpage
    create participant CrowdsecLAPI
    TraefikPlugin-->>CrowdsecLAPI: Does the User IP has a crowdsec decision ?
    destroy CrowdsecLAPI
    CrowdsecLAPI-->>TraefikPlugin: Nothing, all good!
    destroy TraefikPlugin
    TraefikPlugin->>Webserver: Forwarding this HTTP Request from User
    Webserver->>User: HTTP Response
Mode live workflow

A Ban decision exists in CrowdsecLAPI but not in cache

sequenceDiagram
    participant User
    participant TraefikPlugin
    User->>TraefikPlugin: Can I access that webpage
    create participant PluginCache
    TraefikPlugin-->>PluginCache: Does the User IP has a crowdsec decision ?
    PluginCache-->>TraefikPlugin: Nothing, all good!
    create participant CrowdsecLAPI
    TraefikPlugin-->>CrowdsecLAPI: Does the User IP has a crowdsec decision ?
    destroy CrowdsecLAPI
    CrowdsecLAPI-->>TraefikPlugin: Yes a ban Decision
    TraefikPlugin-->>PluginCache: Store the information for this IP for DefaultDecisionSeconds
    destroy PluginCache
    PluginCache-->>TraefikPlugin: Done
    TraefikPlugin->>User: No, HTTP 403

No decision in cache

sequenceDiagram
    participant User
    participant TraefikPlugin
    User->>TraefikPlugin: Can I access that webpage
    create participant PluginCache
    TraefikPlugin-->>PluginCache: Does the User IP has a crowdsec decision ?
    PluginCache-->>TraefikPlugin: Nothing, all good!
    create participant CrowdsecLAPI
    TraefikPlugin-->>CrowdsecLAPI: Does the User IP has a crowdsec decision ?
    destroy CrowdsecLAPI
    CrowdsecLAPI-->>TraefikPlugin: Nothing, all good!
    TraefikPlugin-->>PluginCache: Store the information for this IP for DefaultDecisionSeconds
    destroy PluginCache
    PluginCache-->>TraefikPlugin: Done
    TraefikPlugin->>Webserver: Forwarding this HTTP Request from User
    Webserver->>User: HTTP Response
Mode stream workflow

Cache Synchronization every UpdateIntervalSeconds

sequenceDiagram
    participant TraefikPlugin
    participant CrowdsecLAPI
    TraefikPlugin->>CrowdsecLAPI: What are the current decisions
    destroy CrowdsecLAPI
    CrowdsecLAPI->>TraefikPlugin: Here is the list
    create participant PluginCache
    TraefikPlugin-->>PluginCache: Store this list
    destroy PluginCache
    PluginCache-->>TraefikPlugin: Done

A Ban decision exists in cache

sequenceDiagram
    participant User
    participant TraefikPlugin
    User->>TraefikPlugin: Can I access that webpage
    create participant PluginCache
    TraefikPlugin-->>PluginCache: Does the User IP has a crowdsec decision ?
    destroy PluginCache
    PluginCache-->>TraefikPlugin: Yes a ban decision
    destroy TraefikPlugin
    TraefikPlugin->>User: No, HTTP 403

No decision in cache

sequenceDiagram
    participant User
    participant TraefikPlugin
    User->>TraefikPlugin: Can I access that webpage
    create participant PluginCache
    TraefikPlugin-->>PluginCache: Does the User IP has a crowdsec decision ?
    destroy PluginCache
    PluginCache-->>TraefikPlugin: Nothing, all good!
    destroy TraefikPlugin
    TraefikPlugin->>Webserver: Forwarding this HTTP Request from User
    Webserver->>User: HTTP Response
Mode alone Workflow

Cache Synchronization every 2 hours to the Crowdsec Central API

sequenceDiagram
    participant TraefikPlugin
    participant CrowdsecCAPI
    TraefikPlugin->>CrowdsecCAPI: What are the current decisions from CAPI
    destroy CrowdsecCAPI
    CrowdsecCAPI->>TraefikPlugin: Here is the list
    create participant PluginCache
    TraefikPlugin-->>PluginCache: Store this list
    destroy PluginCache
    PluginCache-->>TraefikPlugin: Done

A Ban decision exists in cache

sequenceDiagram
    participant User
    participant TraefikPlugin
    User->>TraefikPlugin: Can I access that webpage
    create participant PluginCache
    TraefikPlugin-->>PluginCache: Does the User IP has a crowdsec decision ?
    destroy PluginCache
    PluginCache-->>TraefikPlugin: Yes a ban decision
    destroy TraefikPlugin
    TraefikPlugin->>User: No, HTTP 403

No decision in cache

sequenceDiagram
    participant User
    participant TraefikPlugin
    User->>TraefikPlugin: Can I access that webpage
    create participant PluginCache
    TraefikPlugin-->>PluginCache: Does the User IP has a crowdsec decision ?
    destroy PluginCache
    PluginCache-->>TraefikPlugin: Nothing, all good!
    destroy TraefikPlugin
    TraefikPlugin->>Webserver: Forwarding this HTTP Request from User
    Webserver->>User: HTTP Response
Mode appsec workflow

The request is detected as malicious

sequenceDiagram
    participant User
    participant TraefikPlugin
    User->>TraefikPlugin: Can I access that webpage
    create participant CrowdsecAppSec
    TraefikPlugin-->>CrowdsecAppSec: Is this request malicious ?
    destroy CrowdsecAppSec
    CrowdsecAppSec-->>TraefikPlugin: Yes I think so
    destroy TraefikPlugin
    TraefikPlugin->>User: No, HTTP 403

The request is not detected as malicious

sequenceDiagram
    participant User
    participant TraefikPlugin
    User->>TraefikPlugin: Can I access that webpage
    create participant CrowdsecAppSec
    TraefikPlugin-->>CrowdsecAppSec: Is this request malicious ?
    destroy CrowdsecAppSec
    CrowdsecAppSec-->>TraefikPlugin: No I don't think so
    destroy TraefikPlugin
    TraefikPlugin->>Webserver: Forwarding this HTTP Request from User
    Webserver->>User: HTTP Response
Captcha decision workflow
sequenceDiagram
    participant User
    participant TraefikPlugin
    User->>TraefikPlugin: Can I access that webpage
    create participant PluginCache
    TraefikPlugin-->>PluginCache: Does the User IP has a Crowdsec Decision ?
    PluginCache-->>TraefikPlugin: Yes a Catpcha Decision
    TraefikPlugin->>User: Please complete this captcha
    User->>TraefikPlugin: Fine, done!
    create participant ProviderCaptcha
    TraefikPlugin-->>ProviderCaptcha: Is the validation OK ?
    destroy ProviderCaptcha
    ProviderCaptcha-->>TraefikPlugin: Yes
    TraefikPlugin-->>PluginCache: Set the User IP Clean for captchaGracePeriodSeconds
    destroy PluginCache
    PluginCache-->>TraefikPlugin: Done
    destroy TraefikPlugin
    TraefikPlugin->>Webserver: Forwarding this HTTP Request from User
    Webserver->>User: HTTP Response

Usage

To get started, use the docker-compose.yml file.

You can run it with:

make run

Note

Important

Some of the behaviours and configuration parameters are shared globally across all crowdsec middlewares even if you declare different middlewares with different settings.

Cache is shared by all services: This means if an IP is banned, all services which are protected by an instance of the plugin will deny requests from that IP

If you define different caches for different middlewares, only the first one to be instantiated will be bound to the crowdsec stream.

Overall, this middleware is designed in such a way that only one instance of the plugin is possible. You can have multiple crowdsec middlewares in the same cluster, the key parameters must be aligned (MetricsUpdateIntervalSeconds, CrowdsecMode, CrowdsecAppsecEnabled, etc.)

Warning

Appsec maximum body limit is defaulted to 10MB > Be careful when you upgrade to >1.4.x

Variables

  • Enabled
    • bool
    • default: false
    • Enable the plugin
  • LogLevel
    • string
    • default: INFO, expected values are: DEBUG, INFO, WARN, ERROR
    • Log are written to stdout / stderr or file if LogFilePath is provided
  • LogFormat
    • string
    • default: common, expected values are: common, json
    • Log format: common for traditional text logs, json for structured JSON logs
  • LogFilePath
    • string
    • default: ""
    • File Path to write logs, must be writable by Traefik, Log rotation may require a restart of traefik
  • MetricsUpdateIntervalSeconds
    • int64
    • default: 600
    • Interval in seconds between metrics updates to Crowdsec
    • If set to zero or less, metrics collection is disabled
  • CrowdsecMode
    • string
    • default: live, expected values are: none, live, stream, alone, appsec
  • CrowdsecAppsecEnabled
    • bool
    • default: false
    • Enable Crowdsec Appsec Server (WAF).
  • CrowdsecAppsecHost
    • string
    • default: "crowdsec:7422"
    • Crowdsec Appsec Server available on which host and port.
  • CrowdsecAppsecTlsInsecureVerify
    • bool
    • default: false
    • Disable verification of certificate presented by Appsec
  • CrowdsecAppsecTlsCertificateAuthority
    • string
    • default: ""
    • PEM-encoded Certificate Authority used to verify Appsec's server certificate. When empty (and crowdsecAppsecTlsInsecureVerify is false), the host's system trust store is used.
  • CrowdsecAppsecScheme
    • string
    • default: value of CrowdsecLapiScheme, expected values are: http, https
  • CrowdsecAppsecPath
    • string
    • default: "/"
    • Crowdsec Appsec Server available on this path. Will be appended to CrowdsecAppsecHost. Need to finish with "/".
  • CrowdsecAppsecFailureBlock
    • bool
    • default: true
    • Block request when Crowdsec Appsec Server have a status 500.
  • CrowdsecAppsecUnreachableBlock
    • bool
    • default: true
    • Block request when Crowdsec Appsec Server is unreachable.
  • CrowdsecAppsecBodyLimit
    • int64
    • default: 10485760 (= 10MB)
    • Transmit only the first number of bytes to Crowdsec Appsec Server.
  • CrowdsecAppsecUnreadableBodyBlock
    • bool
    • default: true
    • Behaviour when the request body cannot be buffered for inspection (HTTP/2 or HTTP/3 request without a Content-Length, typically a bidirectional gRPC stream). When false the request is forwarded to the Appsec Server with headers only (the body is left to stream through untouched). When true the request is blocked outright. Mirrors the reference bouncers' APPSEC_DROP_UNREADABLE_BODY option.
  • CrowdsecAppsecKey
    • string
    • default: value of CrowdsecLapiKey
    • Crowdsec AppSec key for the bouncer.
  • CrowdsecLapiScheme
    • string
    • default: http, expected values are: http, https
  • CrowdsecLapiHost
    • string
    • default: "crowdsec:8080"
    • Crowdsec LAPI available on which host and port.
  • CrowdsecLapiPath
    • string
    • default: "/"
    • Crowdsec LAPI Server available on this path. Will be appended to CrowdsecLapiHost. Need to finish with "/".
  • CrowdsecLapiKey
    • string
    • default: ""
    • Crowdsec LAPI key for the bouncer.
  • CrowdsecLapiTlsInsecureVerify
    • bool
    • default: false
    • Disable verification of certificate presented by Crowdsec LAPI
  • CrowdsecLapiTlsCertificateAuthority
    • string
    • default: ""
    • PEM-encoded Certificate Authority used to verify the LAPI's server certificate. When empty (and crowdsecLapiTlsInsecureVerify is false), the host's system trust store is used.
  • CrowdsecLapiTlsCertificateBouncer
    • string
    • default: ""
    • PEM-encoded client Certificate of the Bouncer
  • CrowdsecLapiTlsCertificateBouncerKey
    • string
    • default: ""
    • PEM-encoded client private key of the Bouncer
  • ClientTrustedIPs
    • string
    • default: []
    • List of client IPs to trust, they will bypass any check from the bouncer or cache (useful for LAN or VPN IP)
  • RemediationHeadersCustomName
    • string
    • default: ""
    • Name of the header you want in response when request are handled by plugin (possible value of the header ban, challenge, captcha or solved-captcha)
  • ForwardedHeadersCustomName
    • string
    • default: "X-Forwarded-For"
    • Name of the header where the real IP of the client should be retrieved
  • ForwardedHeadersTrustedIPs
    • []string
    • default: []
    • List of IPs of trusted Proxies that are in front of traefik (ex: Cloudflare)
  • RedisCacheEnabled
    • bool
    • default: false
    • enable Redis cache instead of in-memory cache
  • RedisCacheHost
    • string
    • default: "redis:6379"
    • hostname and port for the Redis write host (primary)
  • RedisCacheReadHosts
    • []string
    • default: []
    • List of Redis replica hostnames (host:port) to use for read operations. Reads are distributed round-robin across replicas. Falls back to RedisCacheHost when empty.
    • Note: when set, reads are not retried against RedisCacheHost (the primary) if the replicas are unreachable. With RedisCacheUnreachableBlock at its default (true), a replica outage will therefore block/delay requests even though the primary is healthy.
  • RedisCachePassword
    • string
    • default: ""
    • Password for the Redis service
  • RedisCacheDatabase
    • string
    • default: ""
    • Database selection for the Redis service
  • RedisCacheUnreachableBlock
    • bool
    • default: true
    • Block request when Redis is unreachable (if Redis is unreachable, 1-second delay is added to each request)
  • HTTPTimeoutSeconds
    • int64
    • default: 10
    • Default timeout in seconds for contacting Crowdsec LAPI
  • UpdateIntervalSeconds
    • int64
    • default: 60
    • Used only in stream mode, the interval between requests to fetch blacklisted IPs from LAPI
  • UpdateMaxFailure
    • int64
    • default: 0
    • In stream and alone mode, the maximum number of time we can not reach Crowdsec before blocking traffic (set -1 to never block)
    • In live and none mode, only -1 has an effect: a request is let through instead of blocked when Crowdsec cannot be reached. Any other value blocks on the first failure, there is no failure counter on that path.
  • StreamStartupBlock
    • bool
    • default: true
    • Used only in stream and alone mode, controls whether the initial stream update runs synchronously or asynchronously during plugin initialization
    • When true, plugin initialization waits for Crowdsec to be ready before serving traffic.
    • Warning: When false, all requests bypass remediation until the first stream sync completes — banned IPs will be allowed through during this window. Only disable when startup availability is more important than blocking at startup.
  • DefaultDecisionSeconds
    • int64
    • default: 60
    • Used only in live mode, maximum decision duration
  • RemediationStatusCode
    • int
    • default: 403
    • HTTP status code for banned user (not captcha)
  • CrowdsecCapiMachineId
    • string
    • Used only in alone mode, login for Crowdsec CAPI
  • CrowdsecCapiPassword
    • string
    • Used only in alone mode, password for Crowdsec CAPI
  • CrowdsecCapiScenarios
    • []string
    • Used only in alone mode, scenarios for Crowdsec CAPI
  • CaptchaProvider
    • string
    • Provider to validate the captcha, expected values are: hcaptcha, recaptcha, turnstile or custom
  • CaptchaCustomJsURL
    • string
    • If CaptchaProvider is custom, URL used to load the challenge in the HTML (in case of hcaptcha: https://hcaptcha.com/1/api.js)
  • CaptchaCustomValidateURL
    • string
    • If CaptchaProvider is custom, URL used to validate the challenge (in case of hcaptcha: https://api.hcaptcha.com/siteverify)
  • CaptchaCustomKey
    • string
    • If CaptchaProvider is custom, used to set class name of the div used by captcha provider (in case of hcaptcha: h-captcha)
  • CaptchaCustomResponse
    • string
    • If CaptchaProvider is custom, used to set the field in the POST body from the captcha.html to Traefik (in case of hcaptcha: h-captcha-response)
  • CaptchaSiteKey
    • string
    • Site key for the captcha provider
  • CaptchaSecretKey
    • string
    • Site secret key for the captcha provider
  • CaptchaGracePeriodSeconds
    • int64
    • default: 1800 (= 30 minutes)
    • Period after validation of a captcha before a new validation is required if Crowdsec decision is still valid
  • CaptchaFilePath
    • string
    • default: /captcha.html
    • Path where the captcha template is stored. The Content-Type header is automatically inferred from the file extension.
  • BanFilePath
    • string
    • default: ""
    • Path where the ban file is stored (default empty ""=disabled). The Content-Type header is automatically inferred from the file extension.
  • TraceHeadersCustomName
    • string
    • default: ""
    • Request Header name whose value to inject in ban HTML response (default empty ""=disabled)

Configuration

For each plugin, the Traefik static configuration must define the module name (as is usual for Go packages).

The following declaration (given here in YAML) defines a plugin:

Note that you don't need to copy all thoses settings but only the ones you want to use.
See the examples for advanced usage.

# Static configuration

experimental:
  plugins:
    bouncer:
      moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
      version: vX.Y.Z # To update
# Simplified dynamic configuration

http:
  routers:
    my-router:
      rule: host(`whoami.localhost`)
      service: service-foo
      entryPoints:
        - web
      middlewares:
        - crowdsec

  services:
    service-foo:
      loadBalancer:
        servers:
          - url: http://127.0.0.1:5000

  middlewares:
    crowdsec:
      plugin:
        bouncer:
          enabled: true
          logLevel: DEBUG
          crowdsecMode: live
          crowdsecLapiKey: privateKey-foo
          crowdsecLapiHost: crowdsec:8080
# Full dynamic configuration

http:
  routers:
    my-router:
      rule: host(`whoami.localhost`)
      service: service-foo
      entryPoints:
        - web
      middlewares:
        - crowdsec

  services:
    service-foo:
      loadBalancer:
        servers:
          - url: http://127.0.0.1:5000

  middlewares:
    crowdsec:
      plugin:
        bouncer:
          enabled: false
          logLevel: DEBUG
          logFormat: common
          LogFilePath: ""
          updateIntervalSeconds: 60
          updateMaxFailure: 0
          streamStartupBlock: true
          defaultDecisionSeconds: 60
          remediationStatusCode: 403
          httpTimeoutSeconds: 10
          crowdsecMode: live
          crowdsecAppsecEnabled: false
          crowdsecAppsecScheme: ""
          crowdsecAppsecHost: crowdsec:7422
          crowdsecAppsecPath: "/"
          crowdsecAppsecFailureBlock: true
          crowdsecAppsecUnreachableBlock: true
          crowdsecAppsecBodyLimit: 10485760
          crowdsecAppsecUnreadableBodyBlock: false
          crowdsecLapiKey: privateKey-foo
          crowdsecLapiScheme: http
          crowdsecLapiHost: crowdsec:8080
          crowdsecLapiPath: "/"
          crowdsecLapiTLSInsecureVerify: false
          crowdsecCapiMachineId: login
          crowdsecCapiPassword: password
          crowdsecCapiScenarios:
            - crowdsecurity/http-path-traversal-probing
            - crowdsecurity/http-xss-probing
            - crowdsecurity/http-generic-bf
          forwardedHeadersTrustedIPs:
            - 10.0.10.23/32
            - 10.0.20.0/24
          clientTrustedIPs:
            - 192.168.1.0/24
          forwardedHeadersCustomName: X-Custom-Header
          remediationHeadersCustomName: cs-remediation
          redisCacheEnabled: false
          redisCacheHost: "redis-primary:6379"
          redisCacheReadHosts:
            - "redis-replica-1:6379"
            - "redis-replica-2:6379"
          redisCachePassword: password
          redisCacheDatabase: "5"
          redisCacheUnreachableBlock: true
          crowdsecLapiTLSCertificateAuthority: |-
            -----BEGIN CERTIFICATE-----
            MIIEBzCCAu+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
            ...
            Q0veeNzBQXg1f/JxfeA39IDIX1kiCf71tGlT
            -----END CERTIFICATE-----
          crowdsecLapiTLSCertificateBouncer: |-
            -----BEGIN CERTIFICATE-----
            MIIEHjCCAwagAwIBAgIUOBTs1eqkaAUcPplztUr2xRapvNAwDQYJKoZIhvcNAQEL
            ...
            RaXAnYYUVRblS1jmePemh388hFxbmrpG2pITx8B5FMULqHoj11o2Rl0gSV6tHIHz
            N2U=
            -----END CERTIFICATE-----
          crowdsecLapiTLSCertificateBouncerKey: |-
            -----BEGIN RSA PRIVATE KEY-----
            MIIEogIBAAKCAQEAtYQnbJqifH+ZymePylDxGGLIuxzcAUU4/ajNj+qRAdI/Ux3d
            ...
            ic5cDRo6/VD3CS3MYzyBcibaGaV34nr0G/pI+KEqkYChzk/PZRA=
            -----END RSA PRIVATE KEY-----
          captchaProvider: hcaptcha
          captchaSiteKey: FIXME
          captchaSecretKey: FIXME
          captchaGracePeriodSeconds: 1800
          captchaHTMLFilePath: /captcha.html
          banHTMLFilePath: /ban.html
          traceHeadersCustomName: X-Request-ID
          metricsUpdateIntervalSeconds: 600

Fill variable with value of file

CrowdsecLapiTlsCertificateBouncerKey, CrowdsecLapiTlsCertificateBouncer, CrowdsecLapiTlsCertificateAuthority, CrowdsecAppsecTlsCertificateAuthority, CrowdsecCapiMachineId, CrowdsecCapiPassword, CrowdsecLapiKey, CrowdsecAppsecKey, CaptchaSiteKey, CaptchaSecretKey and RedisCachePassword can be provided with the content as raw or through a file path that Traefik can read.
The file variable will be used as preference if both content and file are provided for the same variable.

Format is:

  • Content: VariableName: XXX
  • File : VariableNameFile: /path

Authenticate with LAPI

You can authenticate to the LAPI either with LAPIKEY or by using client certificates.
Please see below for more details on each option.

Generate LAPI KEY

You can generate a crowdsec API key for the LAPI.
You can follow the documentation here: docs.crowdsec.net/docs/user_guides/lapi_mgmt

docker compose -f docker-compose-local.yml up -d crowdsec
docker exec crowdsec cscli bouncers add crowdsecBouncer

This LAPI key must be set where is noted FIXME-LAPI-KEY in the docker-compose.yml

..
whoami:
  labels:
    - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY"
    - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapischeme=http"
    - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapihost=crowdsec:8080"
..
crowdsec:
  environment:
    BOUNCER_KEY_TRAEFIK: FIXME-LAPI-KEY

Note:

Crowdsec does not require a specific format for la LAPI-key, you may use something like FIXME-LAPI-KEY but that is not recommanded for obvious reasons

You can then run all the containers:

docker compose up -d

Use certificates to authenticate with CrowdSec

You can follow the example in examples/tls-auth to view how to authenticate with client certificates with the LAPI.
In that case, communications with the LAPI must go through HTTPS.

A script is available to generate certificates in examples/tls-auth/gencerts.sh and must be in the same directory as the inputs for the PKI creation.

Use HTTPS to communicate with the LAPI

Set crowdsecLapiScheme to https. The plugin then validates Crowdsec's server certificate. Three options:

  • Publicly trusted certificate (e.g. Let's Encrypt behind a reverse proxy): leave crowdsecLapiTLSCertificateAuthority empty and crowdsecLapiTLSInsecureVerify false. The plugin falls back to the host's system trust store (the traefik image ships ca-certificates).
  • Private/self-signed CA: set crowdsecLapiTLSCertificateAuthority (or …File) to the PEM-encoded CA that signed Crowdsec's server cert.
  • Skip verification entirely (not recommended for production): set crowdsecLapiTLSInsecureVerify to true.

Crowdsec must be listening in HTTPS for this to work. Please see the tls-auth example or the official documentation: docs.crowdsec.net/docs/local_api/tls_auth/

Use HTTPS to communicate with the Appsec

Set crowdsecAppsecScheme to https. Same three options as for the LAPI, prefixed crowdsecAppsec… instead of crowdsecLapi…: empty CA + secure verify falls back to the system trust store, a custom CA pins to your private PKI, and crowdsecAppsecTLSInsecureVerify=true skips verification altogether.

Currently AppSec does not support mTLS authentication for the AppSec Component.

AppSec bot detection: route /crowdsec-internal

When AppSec bot detection is enabled, the challenge page it returns loads its fingerprint script from /crowdsec-internal/challenge/fpscanner.js, an absolute path. Any router protected by this middleware therefore has to match that prefix as well, otherwise the script 404s, the proof-of-work never runs, and the client is stuck on the challenge page with no error anywhere:

  - "traefik.http.routers.my-router.rule=PathPrefix(`/my-app`) || PathPrefix(`/crowdsec-internal`)"

The backend service never sees these requests: the plugin forwards them to the AppSec component and returns its response directly, so the prefix only needs to reach a router carrying the middleware.

CrowdSec documents the same requirement, that the bouncer must forward /crowdsec-internal/challenge/* unchanged: see enabling bot detection and the challenge protocol.

See examples/bot-detection/README.md.

Manually add an IP to the blocklist (for testing purposes)

docker compose up -d crowdsec
docker exec crowdsec cscli decisions add --ip 10.0.0.10 -d 10m # this will be effective 10min
docker exec crowdsec cscli decisions remove --ip 10.0.0.10
docker exec crowdsec cscli decisions add --ip 10.0.0.10 -d 10m -t captcha # this will return a captcha challenge
docker exec crowdsec cscli decisions remove --ip 10.0.0.10 -t captcha

Examples

1. Behind another proxy service (ex: clouflare) examples/behind-proxy/README.md

2. With Redis as an external shared cache examples/redis-cache/README.md

3. Using Trusted IP (ex: LAN OR VPN) that won't get filtered by crowdsec examples/trusted-ips/README.md

4. Using Crowdsec and Traefik installed as binary in a single VM examples/binary-vm/README.md

5. Using https communication and tls authentication with Crowdsec examples/tls-auth/README.md

6. Using Crowdsec and Traefik in Kubernetes examples/kubernetes/README.md

7. Using Traefik in standalone mode without Crowdsec examples/standalone-mode/README.md

8. Using Traefik with AppSec feature enabled examples/appsec-enabled/README.md

9. Using Traefik with Captcha remediation feature enabled examples/captcha/README.md

10. Using Traefik with Custom Ban HTML Page examples/custom-ban-page/README.md

11. Using Traefik with Custom Captcha Whiketkeeperexamples/custom-captcha/README.md

12. Using Traefik with AppSec bot detection enabled examples/bot-detection/README.md

Local Mode

Traefik also offers a developer mode that can be used for temporary testing of plugins not hosted on GitHub. To use a plugin in local mode, the Traefik static configuration must define the module name (as is usual for Go packages) and a path to a Go workspace, which can be the local GOPATH or any directory.

The plugins must be placed in the ./plugins-local directory, which should be in the working directory of the process running the Traefik binary. The source code of the plugin should be organized as follows:

./plugins-local/
    └── src
        └── github.com
            └── maxlerebourg
                └── crowdsec-bouncer-traefik-plugin
                    ├── bouncer.go
                    ├── bouncer_test.go
                    ├── go.mod
                    ├── LICENSE
                    ├── Makefile
                    ├── readme.md
                    └── vendor/*

For local development, a docker-compose.local.yml is provided which reproduces the directory layout needed by Traefik.
This works once you have generated and filled your LAPI-KEY (crowdsecLapiKey), if not read above for informations.

docker compose -f docker-compose.local.yml up -d

Equivalent to

make run_local

About

mathieuHa and I have been using Traefik since 2020 at Primadviz. We come from a web development and security engineer background and wanted to add the power of a very promising technology (Crowdsec) to the edge router we love.

We initially ran into this project: github.com/fbonalair/traefik-crowdsec-bouncer It was using traefik and forward auth middleware to verify every request.
They had to go through a webserver which then contacts another webservice (the crowdsec LAPI) to make a decision based on the source IP.
We initially proposed some improvements by implementing a streaming mode and a local cache.
With the Traefik hackathon we decided to implement our solution directly as a Traefik plugin which could be found by everyone on plugins.traefik.io and be more performant.