add the bot detection feature to the crowdsec traefik bouncer - #1
Draft
sabban wants to merge 19 commits into
Draft
add the bot detection feature to the crowdsec traefik bouncer#1sabban wants to merge 19 commits into
sabban wants to merge 19 commits into
Conversation
sabban
marked this pull request as draft
June 23, 2026 14:48
maxlerebourg#325) * Add parameter to configure Ban Response Content-Type * Add testing for new BanResponseContentType parameter * Ensure there is a fallback to default Content-Type is user provided empty value * Set Content-Type even if banTemplate is nil * Add more edge cases for testing ban response Content-Type * Add CR/LF validation for BanResponseContentType * Add CaptchaResponseContentType to allow separate Content-Type configuration for captcha responses * Add testing for new CaptchaResponseContentType * Update README * Split nil and CR/LF response Content-Type value validation into separate function * Throw error instead of setting the default in case of empty parameter declaration * Update testing accordingly * ✨ remove HTML from var name, add tests and infer content type from filePath * 🍱 fix lint ? * 🍱 fix lint * 🍱 fix lint * 🍱 fix lint + naming * 🍱 fix lint * 🍱 fuck lint --------- Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
Bumps [actions/cache](https://github.com/actions/cache) from 5 to 6. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](actions/cache@v5...v6) --- updated-dependencies: - dependency-name: actions/cache dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Consider 502, 503 and 504 as unavaible for appsec Fixes maxlerebourg#337 * ✨ add test and the function isReverseProxyError --------- Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
…rebourg#332) * 🐛 fix appsec silently 403-ing gRPC streams with unreadable body A bidirectional gRPC stream is an HTTP/2 request with no Content-Length whose body never reaches EOF. Since maxlerebourg#321 removed the ContentLength guard, appsecQuery buffered it with io.ReadAll, which blocked until the request timed out and was turned into a 403 (issue maxlerebourg#323). The backend was never reached (OriginStatus:0). Mirror the reference lua-cs-bouncer behaviour: detect an unreadable body (ProtoMajor >= 2 && ContentLength < 0) and, instead of buffering it, forward the request to Appsec with headers only. Add a new CrowdsecAppsecDropUnreadableBody option (default false) that mirrors the reference APPSEC_DROP_UNREADABLE_BODY: when true, such requests are blocked outright instead of forwarded without their body. Readable HTTP/1.1 bodies are still buffered and inspected, so the bypass closed by maxlerebourg#321 stays closed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🚨 appsec: satisfy linters (gocritic ifElseChain, misspell) Rewrite the body-handling if/else chain in appsecQuery as a switch (gocritic) and use US spelling "behavior" (misspell). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🔇 appsec: drop redundant unreadable-body debug log Address review on maxlerebourg#332: the caller (handleNextServeHTTP) already logs the returned error with the request IP, so the inner Debug line duplicated it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🍱 increase gocyclo * 🍱 fix lint --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
…g#330) * ⬆️ bump example image versions to latest stable Align every docker-compose and Helm-values example on the same set: - Traefik v3.0.0 / v3.5.0 → v3.7.1 - Crowdsec v1.6.1-2 / v1.6.8 / latest → v1.7.8 - Plugin pin v1.3.0 / v1.4.5 / v1.5.0 → v1.6.0 No env / volume changes needed: every Crowdsec example already mounts /var/lib/crowdsec/data, so the v1.7 strict volume check is already satisfied (CROWDSEC_BYPASS_DB_VOLUME_CHECK is only required when running without persistence, as in the E2E suite). * 🤖 chore: adopt Renovate (weekly, self-hosted) to replace manual version bumps Automate the kind of bump this PR did by hand. Renovate covers what Dependabot could not reach here: - example docker-compose image tags (traefik / crowdsec) — native manager; - the plugin self-pin (`experimental.plugins.bouncer.version=` in compose args and `version:` in the Traefik Helm values) — customManager, github-tags; - the Crowdsec/Traefik image tags in the Helm values (no `repository` key, so matched by file) — customManager, docker; - the pinned Traefik binary in the e2e mock suite (`TRAEFIK_VERSION`) — customManager, github-releases; - go.mod + GitHub Actions — native managers (this is why we drop Dependabot: running both would open duplicate PRs). Runs weekly via a self-hosted workflow (Mondays 04:00 UTC, plus manual dispatch). Needs a RENOVATE_TOKEN secret (documented in the workflow). Renovate PRs trigger the existing e2e CI, so a Traefik/Crowdsec bump is validated to actually boot and bounce before merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🔒 ci: pin Go to 1.22 (yaegi ceiling) and cap Renovate's Go updates The plugin is interpreted by yaegi, bundled in Traefik. Even the latest Traefik (v3.7.1) ships yaegi v0.16.1, which only supports Go 1.22 — so the plugin's real ceiling is Go 1.22 on every current Traefik, regardless of the Go version Traefik itself is built with. - main.yml: build/test on Go 1.22 (was 1.23) so `go build`/`go test` reject newer stdlib early; yaegi v0.16.1 stays the required yaegi_test guard. - renovate.json: cap the go.mod `go`/`toolchain` directive at `<1.23` so Renovate keeps deps/actions/toolchain current but never pushes the plugin past what yaegi supports. Raise the cap when Traefik ships a newer yaegi. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🐛 ci(renovate): stop ignoring examples/ and tests/ so custom managers run config:recommended ships a default ignorePaths that excludes **/examples/** and **/tests/** (and **/vendor/**). That silently disabled 4 of the 5 custom managers and all example/helm coverage this PR adds — Renovate only saw the root docker-compose, go.mod and workflows. Override ignorePaths to keep only vendor/node_modules. Verified via `renovate --platform=local --dry-run=full`: package files detected go from 7 -> 32, and every custom manager now extracts its dep (plugin self-pin across 11 compose files, traefik/crowdsec helm tags, e2e TRAEFIK_VERSION pin). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ⏪ chore: drop one-time example version bumps, let Renovate own them The manual version catch-up across the example docker-compose files and the Helm values is now redundant: Renovate (this PR) covers all of them via its native docker-compose/docker managers plus the custom managers for the plugin self-pin and the e2e Traefik binary. Reverting these files to keep the PR a clean "adopt Renovate" change; Renovate will open the bump PRs itself. Note: examples/tls-auth uses crowdsecurity/crowdsec:latest again — Renovate does not pin rolling tags, so that one stays floating unless pinned separately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 📌 examples(tls-auth): pin crowdsec to v1.6.8 instead of :latest Rolling `latest` tags aren't reproducible and Renovate can't manage them. v1.6.8 is already the crowdsec tag used in the root docker-compose files and, unlike the `-2` build-suffixed tags, Renovate bumps it (→ v1.7.8). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 📌 examples: normalize crowdsec v1.6.1-2 -> v1.6.8 so Renovate can manage it Renovate's docker versioning won't advance a build-suffixed tag (v1.6.1-2) to a clean release (v1.7.8), so those 8 examples would have stayed frozen. v1.6.8 is already used in the root compose files and Renovate bumps it. Also updated the captcha README snippet to match its compose. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🍱 group all + no dashboard + branch prefix --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
…maxlerebourg#347) The 2026-07-03 runs surfaced two config problems: - The grouped "renovate/all" branch made Renovate fetch and hold the changelog of every upgrade instance separately (Traefik v3.0.0->v3.7.6 was fetched ~10x, once per compose file), which blew the default 4GB V8 heap: "FATAL ERROR: ... JavaScript heap out of memory" (exit 134), killing the run after the branch was pushed but before the PR was opened. Disable changelog fetching (release notes were truncated in the grouped PR body anyway) and raise the Node heap to 8GB as a safety net. - No gitAuthor was set, so commits were authored as Mend's renovate@whitesourcesoftware.com, which GitHub flags "Unverified" (Vigilant Mode) and Renovate warns about on every run. Use the token owner's noreply address instead. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Renovate Bot <22881669+maxlerebourg@users.noreply.github.com>
…#352) * Do not consider body unreadable when it's http.NoBody Fix maxlerebourg#351 * 🐛 appsec: only drop unreadable bodies for methods that carry one Gate the crowdsecAppsecUnreadableBodyBlock drop on the request method, mirroring the reference lua-cs-bouncer METHODS_WITH_BODY list (POST/PUT/PATCH/DELETE). Browser GETs over HTTP/3 never carry a Content-Length (quic-go always wraps the stream in a non-nil body with ContentLength -1), so they were all wrongly dropped; they are now forwarded to appsec headers-only, while gRPC streams (always POST) stay droppable. Fix maxlerebourg#351 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🍱 add test and remove useless comment * 🍱 add test for GET with unreadable body * 🍱 fix log * 🍱 fix test --------- Co-authored-by: mhx <mathieu@hanotaux.fr> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
* feat: Allow cache reading from replicas * 🍱 fix logic * ✨ add testing for redis with mock * 🍱 fix permission * 📝 test(e2e/redis): fix swapped IP→verdict comments The mock returns "f" (not banned) for 1.2.3.4 and "t" (banned) for 1.2.3.5, and the run.sh assertions match that. Both doc comments described the opposite mapping; correct them to match the code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ✅ test(e2e/redis): exercise read-from-replica path The redis scenario only set redisCacheHost, so it validated the writer but never the round-robin reader path this feature adds. Split the mock into two roles: the primary (--redis-addr) now answers every GET with a miss, while the replica (--redis-read-addr) serves the hardcoded verdicts. The scenario points redisCacheReadHosts at the replica (twice, to drive round-robin), so the banned-IP-blocked assertion only passes if the plugin actually reads decisions from the replica rather than the primary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 📝 docs: note replicas don't fall back to primary on outage When RedisCacheReadHosts is set, reads are not retried against the primary if the replicas are unreachable. Document that this, combined with the default RedisCacheUnreachableBlock=true, means a replica outage can block traffic while the primary is healthy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 🐛 fix(cache): avoid nil-pointer panic on empty redis read redisCache.get fell through to `switch err.Error()` when Get returned a nil error with an empty value, panicking on the nil error. simpleredis never returns that combination today (a miss yields RedisMiss), so it was unreachable in practice — but the read path is safer treating an empty, error-free read as a cache miss, which also guarantees err is non-nil before err.Error() is called. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 🍱 add test for rotation * 🐛 readd redis/run.sh * 🐛 fix redis/run.sh * 🐛 fix test label; remove log * 🍱 add tests for roundRobin * 🐛 fix test --------- Co-authored-by: maxlerebourg <maxlerebourg@gmail.com> Co-authored-by: mhx <mathieu@hanotaux.fr> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ✨ Renovate update version.go * 🍱 renovate every day
Co-authored-by: Renovate Bot <22881669+maxlerebourg@users.noreply.github.com>
Co-authored-by: Renovate Bot <22881669+maxlerebourg@users.noreply.github.com>
…version (maxlerebourg#365) * ✨ cicd: bump the version before tagging instead of after The version reported to the Crowdsec LAPI lives in version.go, so it must be correct in the very commit the tag points at. Every mechanism so far updated it *after* the tag existed, which cannot work: - release.yml ran on `release: published` and force-moved the tag. It also failed on all four of its runs and was removed in maxlerebourg#360. - The Renovate customManager on version.go uses the github-tags datasource, so it can only propose vX once vX is already tagged. The bump always lands after the tag. Result: v1.7.0 is tagged at a commit reading v1.6.0 (maxlerebourg#363), same shape as the earlier maxlerebourg#322. Replace both with a two-step flow that bumps first and tags last, so the released source always matches its tag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🍱 reduce loc + remove claude code comment * 🍱 remove useless spellcheck disable --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
add the bot detection feature to the crowdsec traefik bouncer
This is an early PR and depends on the crowdsec feature in the PR crowdsecurity/crowdsec#4268 This is not meant to be merged now, but to gather feedbacks.
The bouncer now parses structured AppSec JSON responses:
actionhttp_statususer_body_contentuser_cookiesuser_headersWhen AppSec returns an action other than
allow, the plugin writes the provided status, headers, cookies, and body back to the client. This lets CrowdSec serve the challenge HTML, set the__crowdsec_challengecookie, and expose the browser-side challenge flow through Traefik.Legacy behavior is preserved: an empty/non-structured AppSec
403still falls back to the existing ban handling.For challenge mode, the Traefik side does not need a new plugin option. It needs AppSec enabled, and the challenge protocol paths must be routed through the same CrowdSec middleware as the protected application route.
The challenge mode has to be enabled crowdsec side, though. Documentation is not published yet, but the WIP documentation is available at crowdsecurity/crowdsec-docs#1099 and crowdsecurity/crowdsec-docs#1100.
This PR is meant to gather feedback and not to be merged right now.
My test configuration docker file.