Compare commits

...
218 Commits
Author SHA1 Message Date
mathieuHaandmaxlerebourg 5c8a60118f 🐛 fix(user-agent) add version in ua to reduce warning logs from crowd… (#195)
* 🐛 fix(user-agent) add version in ua to reduce warning logs from crowdsec LAPI

* 🐛 fix(user-agent) add version in ua to reduce warning logs from crowdsec LAPI

* 🐛 fix(user-agent) remove whitespaces

* 🐛 fix(user-agent) Add Cap for Bouncer

---------

Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
2024-10-24 21:39:53 +02:00
mathieuHa 8fb0a016b6 Add Traefik Crowdsec Plugin Basic User Agent (#192)
*  Add Traefik Crowdsec Plugin Basic User Agent
2024-10-05 13:15:34 +02:00
maxlerebourg 45d5f38c4d add remediation header when plugin made decision (#189)
*  add remediation header when plugin made decision

* 🍱 add documentation
2024-09-25 19:30:27 +02:00
maxlerebourgandmax.lerebourg f1de1c924e 🔨 174 fix classname for Turnstile (#178)
Co-authored-by: max.lerebourg <max.lerebourg@monisnap.com>
2024-06-19 20:13:49 +02:00
mathieuHa 36e6043c32 📝 doc(cache) Specify that local cache is in memory and not in the filesystem (#177) 2024-06-09 16:20:55 +02:00
maxlerebourg 123cf15434 Add CrowdsecAppsecUnreachableBlock (#175)
*  Add CrowdsecAppsecUnreachableBlock

* 🍱 update readme

* 🍱 fix lint

* 🍱 fix lint
2024-06-09 10:59:45 +02:00
mathieuHa f89c5e25a9 ✏️ fix(validation) Replace typo on CaptchaProvider config validation (#176) 2024-06-09 10:47:17 +02:00
maxlerebourg 58946d9fa2 📄 Update README.md for lang doc (#170) 2024-05-22 11:24:11 +02:00
mathieuHaandMax Lerebourg 6187a722ca 167 feature update to go 122 (#168)
* ⬆️ Upgrade golang version

* 🚨 Optimize Lint for strings

* 🔒️ Add allow list of packages

* 🚨 Fix final lint

* 👷 Update ci

* 🍱 upgrade dependencies

* 🍱 fix comment

---------

Co-authored-by: Max Lerebourg <maxlerebourg@gmail.com>
2024-05-18 13:20:14 +02:00
maxlerebourgandmax.lerebourg 70ad0365f0 🐛 fix content-type header for ban and captcha page (#166)
* 🍱 fix content-type header for ban and captcha page

* 🍱 Add comment to warn future developer

---------

Co-authored-by: max.lerebourg <max.lerebourg@monisnap.com>
2024-05-16 18:40:28 +02:00
mathieuHa 8975216af8 📝 Update readme on loglevel written to stdout/stderr (#164) 2024-05-16 09:32:14 +02:00
mathieuHa e6b10b8ac4 ⬆️ 📝 Update exemples and version traefik 3, crowdsec 1.6.1-2, plugin … (#160)
* ⬆️ 📝 Update exemples and version traefik 3, crowdsec 1.6.1-2, plugin 1.3.0

* 📝 🐛 Fix race in exemple and doc for tls
2024-05-01 20:03:19 +02:00
mathieuHa 1f11531509 📝 Update doc for appsec with required collection and upgrade images (#158) 2024-05-01 19:50:00 +02:00
mathieuHaandmax.lerebourg ee97250acf Add grace period to reach LAPI without blocking further queries (#153)
*  Add grace period to reach LAPI without blocking further queries

* 🐛 Fix config validation for maxFailedStreamUpdate

* 🚨 Fix some lint issue

* 🚨 Bypass lint complexity on ServeHTTP

* 🍱 fix and improve

* 🚨 Fix lint

* 🚨 Fix lint

* 🐛 Fix logic for update max failure

* 📝 Update doc and docker compose local reset

* 🍱 fix log nightmare

* 🍱 fix

---------

Co-authored-by: max.lerebourg <max.lerebourg@monisnap.com>
2024-05-01 18:07:44 +02:00
mathieuHaandmaxlerebourg b6a0404efd Add user-agent header from crowdsec (#149)
*  Add user-agent header from crowdsec

* 🐛 : https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/issues/151

---------

Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
2024-04-16 13:26:46 +02:00
mathieuHa a0e701f985 📝 Update typo on doc for ban custom page (#147) 2024-04-05 09:30:49 +02:00
mathieuHaandmax.lerebourg b8dd883bb6 130 feature custom html error page integration on 403 like captcha but for ban (#145)
*  Add ban html template

* 📝 Add doc for custom ban page

* ⚰️ Remove old code due to merge

* ✏️ Fix merge remaining in html

*  render banTemplate from html/template to string

* 🚨 : fix lint

---------

Co-authored-by: max.lerebourg <max.lerebourg@monisnap.com>
2024-04-04 21:32:31 +02:00
cb4f95d5d0 📝 Add first diagrams (#144)
* 📝 Add first diagrams

* 📝 Add diagram for none mode

* 📝 Add diagrams for each mode

* 📝 Fix rendering

* 📝 Fix chart rendering

* 🍱 fix readme

* 🍱 fix readme

---------

Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
Co-authored-by: max.lerebourg <max.lerebourg@monisnap.com>
2024-04-04 07:26:30 +02:00
mathieuHaandMax Lerebourg 615e7ccf69 Add ban html template (#142)
*  Add ban html template

* 📝 Add doc for custom ban page

* 🍱 fix Mathieu work

* 🍱 fix lint

* 🍱 fix lint

* 🍱 fix lint

* 🍱 fix lint

---------

Co-authored-by: Max Lerebourg <maxlerebourg@gmail.com>
2024-04-03 17:58:18 +02:00
6059f23dc3 🐛 fix bug with recaptcha (#141) and add doc for recaptcha
* 🐛 fix bug with recaptcha

* 📝 Add documentation for recpatcha

---------

Co-authored-by: max.lerebourg <max.lerebourg@monisnap.com>
Co-authored-by: mhx <mathieu@hanotaux.fr>
2024-04-01 20:47:54 +02:00
497d1a2928 Implement captcha protection (#139)
*  Implement captcha protection

* 🍱 fix lint

* 🍱 fix lint

* 🍱 fix lint

* 📝 Update exemple doc

Signed-off-by: Mathieu Hanotaux <mathieu@hanotaux.fr>

* 📝 Add doc for the captcha and update some exemples

Signed-off-by: Mathieu Hanotaux <mathieu@hanotaux.fr>

* 📝 Update doc readme with some arguments

Signed-off-by: Mathieu Hanotaux <mathieu@hanotaux.fr>

* 📝 Update doc

Signed-off-by: Mathieu Hanotaux <mathieu@hanotaux.fr>

* 📝 generic documentation in readme on catpcha feature

Signed-off-by: Mathieu Hanotaux <mathieu@hanotaux.fr>

* 📝 Update exemple captcha

Signed-off-by: Mathieu Hanotaux <mathieu@hanotaux.fr>

* 📝 Fix rendering and typos

Signed-off-by: Mathieu Hanotaux <mathieu@hanotaux.fr>

* 🍱 fix readme

* 📝 update doc ongoing

Signed-off-by: Mathieu Hanotaux <mathieu@hanotaux.fr>

* 📝 Add doc on crowdsec config

Signed-off-by: Mathieu Hanotaux <mathieu@hanotaux.fr>

* 📝 Add sequence diagram for captcha exemple

Signed-off-by: Mathieu Hanotaux <mathieu@hanotaux.fr>

* Fix rendering and typos

Signed-off-by: Mathieu Hanotaux <mathieu@hanotaux.fr>

* 📝 add mermaid basics graphs

Signed-off-by: Mathieu Hanotaux <mathieu@hanotaux.fr>

* 📝 Update first diagram

Signed-off-by: Mathieu Hanotaux <mathieu@hanotaux.fr>

* 📝 Update first seq diagram

Signed-off-by: Mathieu Hanotaux <mathieu@hanotaux.fr>

* 🐛 Fix bug in diagram syntax

Signed-off-by: Mathieu Hanotaux <mathieu@hanotaux.fr>

* 📝 rework all diagrams

Signed-off-by: Mathieu Hanotaux <mathieu@hanotaux.fr>

* 📝 Update a bit diagrams

Signed-off-by: Mathieu Hanotaux <mathieu@hanotaux.fr>

* 🌐 Fix lang fr

* 🚸 change advice on uniq lapi confusing for users

*  Fix test du to rework on cache interface

* 🚨 Fix lint

---------

Signed-off-by: Mathieu Hanotaux <mathieu@hanotaux.fr>
Co-authored-by: max.lerebourg <max.lerebourg@monisnap.com>
Co-authored-by: Mathieu Hanotaux <mathieu@hanotaux.fr>
2024-04-01 11:41:28 +02:00
mathieuHa c3e0c2d4c3 🐛 Update documentation that help avoid a bug in traefik chart (#135)
* 🐛 Update documentation that help avoid a bug in traefik chart

* 🚨 Fix lint and remove comment
2024-02-19 20:47:53 +01:00
maxlerebourg 575d3a02e5 Add a logger instance to bouncer instance (#134)
*  Add a logger instance to bouncer instance

* 🍱 fix test

* 🍱 fix lint

* 🍱 fix lint

* 🍱 fix lint

* 🍱 fix lint

* 🍱 fix lint

* 🍱 fix test

* 🍱 fix test

* 🍱 fix lint + test

* 🍱 fix test

* 🍱 fix test

* 🍱 fix test

* 🍱 fix lint

* 🍱 fix lint
2024-02-11 11:52:47 +01:00
maxlerebourg 6c183d9231 add a new mode to enable only appsec checking (#128)
*  add a new mode to enable only appsec checking

* 🍱 fix comments
2024-02-06 19:42:28 +01:00
maxlerebourg bd71b58f19 🐛 fix coherence naming for forwardedHeadersCustomHeader became forwardedHeadersCustomName (#129) 2024-02-04 15:52:53 +01:00
maxlerebourgandMathieu Hanotaux b68c692ed1 add support for appsec in crowdsec (#123)
*  add support for appsec in crowdsec

* 🐛 lint

* 🐛 fix lint

* 🐛 fix lint

* 🐛 fix lint

* fix: comments

* 🐛 lint and doc

* 🐛 fix comment and lint

* 📝 Start documentation for appsec with exemple

* 📝 Fix readme typos and update example

* 🚨 Fix Lint

---------

Co-authored-by: Mathieu Hanotaux <mathieu@hanotaux.fr>
2024-01-24 14:11:34 +01:00
mathieuHa fc3da2fc2d 🐛 Add / in regexp for LAPI key (#117) 2023-09-24 13:56:19 +02:00
Rasmus 1a9bdc578f Exemples -> Examples (#116)
* Exemples -> Examples

* Exemples -> Examples

* Uppercase

* Uppercase v2

* Uppercase v3

* Uppercases

* Another one

* Add whoami because .gitignore is ignoring conf folder
2023-09-24 13:31:29 +02:00
mathieuHa f2aea695fc 🐛 Update regex to support = in lapikey update some exemples with 1.5.3 crowdsec (#115) 2023-09-19 20:57:29 +02:00
Joseph Kavanagh 2827fef273 🐛 env values need to be string (#112)
cannot unmarshal bool into Go struct field EnvVar.spec.template.spec.containers.env.value of type string
2023-09-05 13:26:50 +02:00
mathieuHaandMax Lerebourg 07c8fae927 🔧 Add conf variable: CrowdsecStreamTimeout (#110)
* 🔧 Add conf variable: CrowdsecStreamTimeout

* 🥅 catch error from sscan

*  fix ugly code

* 🐛 clean code

*  fix type + wrong type assertion

* 🚨 fix lint go simplify

* 📝 Update readme with new variable

* 🍱 fix variable naming

* 🍱 revert vendor file

* 🍱 fix lint

---------

Co-authored-by: Max Lerebourg <maxlerebourg@gmail.com>
2023-08-20 15:49:49 +02:00
mathieuHa 13c209be3f 📝 First version of doc update with single bouncer instance (#108) 2023-08-05 16:01:50 +02:00
maxlerebourg 93340ffa55 🍱 update dependencies redisclient to reduce memory consumption (#105)
* 🍱 update dependencies to avoid memory leak

* 🍱 update dep
2023-06-20 09:22:51 +02:00
maxlerebourg 0c2668d578 add redis database selection (#100)
*  add redis database selection

* 📝 update docs

* 📝 readme
2023-05-25 17:20:14 +02:00
maxlerebourg abae7ee028 📝 update version used (#97)
* 📝 update version used

* 📝 update doc

* 📝 documentation

* 📝 documentation

* 📝 versionning
2023-04-17 09:25:57 +02:00
maxlerebourg 1fcd4f4e2f remove down at start if crowdsec unavailable (#93)
*  remove down at start if crowdsec unavailable

* 🚨 fix lint
2023-03-12 20:49:00 +01:00
mathieuHa 39fcc38980 🐛Bump simple redis to 1.0.6 to fix bug hang with password, update doc on redis (#89) 2023-03-05 14:32:43 +01:00
Max Lerebourg dd322a966a 🍱 fix lint 2023-03-04 12:11:22 +01:00
Max Lerebourg f0bb140596 🍱 fix lint 2023-03-04 12:08:03 +01:00
Max Lerebourg 46e581eca2 🍱 fix readme to add redis pass 2023-03-04 12:05:09 +01:00
maxlerebourg 50690d1ac7 handle redis password (#87)
*  handle redis password

* 🍱 fix version
2023-03-04 11:51:54 +01:00
maxlerebourg b079073ff6 handle isHealthy in the main function and log error became… (#84)
*  handle isHealthy in the main function and log error became debug

* fix: lint

* fix: lint
2023-03-01 14:18:19 +01:00
maxlerebourg 976cbb7d1f 81 bug stream mode stops blocking (#82)
*  fix isHealthy issue at startup

* 🍱 not added in first commit ?

* 🍱 remove unused import

* 🍱 fix lint

* fix: lint
2023-01-30 14:03:10 +01:00
Max Lerebourg 80726df450 🐛 fix alone mode 2023-01-25 20:43:07 +01:00
maxlerebourg 4132445a79 cache decision in live mode for maximum defaultDecisionTimeout params (#79) 2023-01-18 21:03:09 +01:00
mathieuHaandmaxlerebourg 5b15d6fdc4 📝 Update Readme spelling and clarify (#76)
* 📝 Update Readme spelling and clarify

* 📝 Update Readme exemple

* 📝 Update Readme clarity

* 📝 Update Readme clarity

* 📝 Update Readme clarity

* 📝 Update Readme clarity

* 📝 Update Readme clarity

* 📝 Update Readme clarity

* 📝 Update Readme clarity

* 📝 Update Readme clarity

* 📝 Update Readme clarity

* Update README.md

* Update README.md

Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
2023-01-02 13:18:45 +01:00
maxlerebourgandMathieu HANOTAUX 1b310b2c15 Standalone mode come back (#74)
*  Standalone mode come back

* 🍱 fix lint

* 🐛 fix tests cache

* 🐛 fix tests

* 🐛 fix tests

* 🚨 fix lint

* 🚨 fix lint

* 🍱 add logging

* :rotating-light: fix lint

* 🍱 fix comments

* 🐛 fix tests

* 🚨 Fix lint

Co-authored-by: Mathieu HANOTAUX <mathieu@hanotaux.fr>
2023-01-02 11:53:57 +01:00
maxlerebourg 87a839f22d Update issue templates 2022-12-31 16:25:52 +01:00
maxlerebourg f14a4e3a68 add external simpleredis and add log for cache (#73)
*  add external simpleredis and add log for cache

* 🐛 fix comment
2022-12-31 16:18:08 +01:00
maxlerebourg 11b615b9bd 📃 Update issue templates 2022-12-31 11:31:49 +01:00
maxlerebourg 8f937d22a2 🐛 fix all incoherence (#71)
* 🐛 fix all incoherence

* 🐛 fix comment + lint
2022-12-31 11:15:36 +01:00
mathieuHa 041b0f67b7 📝 55 feature clean readme doc (#70)
* 📝 Clean documentation in Readme

* 📝 Update readme

* 📝 Place exemples in seperated README

* ⬆️  Update traefik to 2.9.6 and crowdsec to 1.4.3 in exemples

* 📝 Add exemple at the right place
2022-12-30 23:35:33 +01:00
mathieuHa 5458db93cf 📝 40 Add working exemple on kubernetes (#56)
* 📝 Add initial doc and file structure

* 🍱 Add struct for crowdsec

* 📝 Update doc install cs

* 🚧 Update exemple, add traefik settings

* 📝 Update doc for kubernetes

* 📝 update doc on kubernetes usage

* 📝 Add read logs from traefik

* 📝 Add read logs from traefik

* 📝 Update doc lisibility

* 📝 Update doc with new namespace

* 📝 Update doc with enable plugin in traefik

* 📝 Update doc on kube

* 📝 Finish exemple of Kubernetes

* 📝 Update after review
2022-12-30 22:20:45 +01:00
mathieuHa 29878e837c 🐛 65 confused about configuring tls (#69)
* 🐛 Fix bug reading LAPIKEY, update exemples

* 🚨 Fix lint
2022-12-30 11:24:14 +01:00
mathieuHa aff7c496c2 🐛Remove trailing and leading spaces between each IP or range (#68) 2022-12-29 10:30:19 +01:00
Mathieu HANOTAUX 1e82ecd1b9 🚑 Fix bug that prevent Treafik plugin to pull first logs 2022-12-11 21:48:19 +01:00
mathieuHa 4cca0bc2fb 55 feature clean readme doc (#59)
* 📝 Clean documentation in Readme

* 📝 Update readme
2022-12-05 20:52:10 +01:00
maxlerebourg fe3b3b05fd Merge pull request #58 from maxlerebourg/55-feature-clean-readme-doc
🍱 fix readme
2022-12-05 20:03:30 +01:00
Max Lerebourg 7466dbec6d 🍱 fix readme 2022-12-05 20:01:11 +01:00
maxlerebourg 9406b70219 Merge pull request #57 from maxlerebourg/46-feature-add-automated-tests-to-the-plugin
 Add test structure for bouncer pkg
2022-12-05 19:40:53 +01:00
Max Lerebourg a289c93d9b 💄 revert docker compose 2022-12-05 19:34:09 +01:00
Mathieu HANOTAUX b3a874f514 🚨 Fix Lint simplify 2022-12-05 19:27:56 +01:00
Max Lerebourg b4cb552ee0 💄 fix lint 2022-12-05 08:18:11 +01:00
Max Lerebourg b3d0203145 💄 fix lint 2022-12-05 08:17:33 +01:00
Max Lerebourg 8f5b7bb9af 💄 fix lint 2022-12-05 08:17:17 +01:00
Max Lerebourg 22e3febb45 💄 fix lint 2022-12-05 08:15:47 +01:00
Max Lerebourg 1f094d818a 💄 move getTlsConfig to configuration pkg 2022-12-05 08:10:56 +01:00
Max Lerebourg a17e082ba3 💄 fix lint 2022-12-04 21:56:03 +01:00
Max Lerebourg c311d8357d :lips-stick: fix lint 2022-12-04 21:53:33 +01:00
Max Lerebourg 7e9044c41d 🧪 fix test 2022-12-04 21:47:15 +01:00
Max Lerebourg 988f3ebeae add configuration pkg and tests 2022-12-04 21:40:14 +01:00
Max Lerebourg 91c31e3fa1 🍱 fix golgot attitude 2022-12-04 19:16:42 +01:00
Max Lerebourg aa1f00b055 Revert "add library until better solution"
This reverts commit c867453ab4.
2022-12-04 19:09:21 +01:00
Mathieu HANOTAUX c867453ab4 add library until better solution 2022-12-04 18:47:13 +01:00
Max Lerebourg f313b478e7 🍱 delete mhx brain failure 2022-12-04 17:04:58 +01:00
Max Lerebourg 2543127abb 🍱 fix lint 2022-12-04 16:58:49 +01:00
Max Lerebourg 5dd06567f4 🍱 test validate 2022-12-04 16:58:06 +01:00
Mathieu HANOTAUX 15baee5c07 Add tests for cache internal pkg 2022-12-04 16:30:27 +01:00
mathieuHa c29d8a20d3 📝 Add documentation exemple 4 for vm binary usage (#45)
* 📝 Add documentation exemple 4 for vm binary usage

* :hammer:Update binary version

* 🔨 update

* 🔨 update

* 🚧 Working Crowdsec with tls auth

* 🐛 Add changes to download the plugin from the service

* 🔧 Add config middle for https

* 🔧 Update config for the exemple

* 🔧 Add conf for certs, working example

* 📝 Add doc for binary vm Readme

* 📝 update documentation for exemple and make

* 🚨 Fix lint

* 🚨 Fix Lint End of File
2022-12-04 15:36:15 +01:00
Mathieu HANOTAUX 76e5c7497d Add test structure for bouncer pkg 2022-12-04 15:20:25 +01:00
maxlerebourg f69faaf66c Merge pull request #54 from mmetc/typos
fixed some typos/grammar/style
2022-12-02 08:02:50 +01:00
Marco Mariani 8d9bc33091 removed diff crumbs 2022-12-01 22:39:40 +01:00
Marco Mariani 02e0f532c7 fixed some typos/grammar 2022-12-01 15:03:25 +01:00
mathieuHaandMax Lerebourg a2fe60c621 51 feature support tls connections to crowdsec not signed by a public ca (#53)
*  Add support for insecure tls connections to LAPI

* 📝 Add documentation for the TLS insecure parameter

* 🚧 Add tls authority certificate and checks for params

* 📝 Add example for tls communication in readme and folder

* 📝 Update documentation and example for tls

* 🚨 Fix easy lint errors

* 🦺 logic to fetch certificates

* 🚨 Fix lint on readme

* ♻️ Refactor validate to fix lint and clean

* 🚧 Add doc, cert gen for crowdsec example

* 🚧 Progress on setting up Crowdsec with tls

* 🚧 Update certs validation for example

* ♻️ Add load variable from file or value and get client cert

* ♻️ Refactor getting variables

* 🚨 Fix lint, no new line on new files

* 🐛 Fix bug on condition check lapi key cert

* ♻️ Update after review

* ♻️ Update after review

* 🍱 fix mathieu code

* ♻️ Refactor logic of loading tls certificates

* 🍱 clean code

* 🍱 last fix

* 🍱 fix lint

* ♻️ Add documentation in readme, fix lint, remove unfinished tests

* 🐛 Fix conditions logics

* 🚨 Fix Lint

* ♻️ simplify code on getVariable

Co-authored-by: Max Lerebourg <maxlerebourg@gmail.com>
2022-11-30 17:54:49 +01:00
maxlerebourg b2ed600d5e Merge pull request #52 from maxlerebourg/mathieuHa-patch-1
Update issue templates
2022-11-27 20:52:22 +01:00
mathieuHa d80aabaa66 Update issue templates 2022-11-27 18:15:11 +01:00
mathieuHa 16d392ebeb 🐛 Revert condition of healthy stream in stream mode (#48)
* 🐛 Revert condition of healthy stream in stream mode

* 🔊 Update logs format for error case

* ♻️ Invest condition for is stream healthy
2022-11-27 11:52:41 +01:00
maxlerebourg 70558c781a Merge pull request #39 from maxlerebourg/38-fix-loging
🍱 fix loging
2022-11-20 12:47:59 +01:00
Max Lerebourg f99dbfc203 🍱 fix comment 2022-11-20 12:46:05 +01:00
Max Lerebourg b37f866ca9 🍱 fix lint :vomit: 2022-11-20 12:33:39 +01:00
Max Lerebourg 1dc50a8a8c 🍱 add more error context 2022-11-20 12:29:00 +01:00
Max Lerebourg 953e9f6bf4 🍱 fix lint 2022-11-20 12:12:20 +01:00
Max Lerebourg d1d64689af 🍱 fix logic + logs 2022-11-20 12:04:05 +01:00
Max Lerebourg 80d49e3969 🍱 fix loging 2022-11-19 22:23:12 +01:00
Max Lerebourg 72be1a68f5 🍱 fix loging 2022-11-19 22:18:47 +01:00
Max Lerebourg 15bca90b18 🍱 fix loging 2022-11-19 22:17:06 +01:00
mathieuHa 24c7801dac ⬆️ Bump to go 1.19 (#37)
* ⬆️ Bump to go 1.19

* ⬆️ Remove depreciated function and replace io/utils calls
2022-11-19 20:40:03 +01:00
mathieuHa be150e8aca 📝 Update README fix wrong version (#35)
Change version 1.10 to 1.1.0 on the change that deduplicated the cache
2022-11-19 19:37:23 +01:00
mathieuHaandMax Lerebourg 0a54f7b09f Update CI following latest plugindemo version (#34)
* Update CI following latest plugindemo version

* disable some linter

* Disable some linter

* Fix linter in redis, add error logger

* 🚨 Fix lint errors in private packages

* 🚨 Update lints in package and core code

* 🚨 exclude unfixable lint errors

* 🚨 update lint at package level and ignore necessary global variable

* 🍱 fix lint qnd merge

* 🚨 Fix Lint for string

* 🚨 Fix lint for bouncer

* 🚨 Fix error

* Fix weird linter error

Co-authored-by: Max Lerebourg <maxlerebourg@gmail.com>
2022-11-19 19:35:01 +01:00
maxlerebourg 2d0bea8eec Merge pull request #32 from maxlerebourg/28-bypassing-crowdsec-for-local-ips
28 bypassing crowdsec for local ips
2022-11-19 15:47:30 +01:00
MathieuHa bb714b5dfd Change debug to info log 2022-11-19 15:17:56 +01:00
MathieuHa bba5620187 Update all documentation and exemple 2022-11-19 13:46:15 +01:00
MathieuHa 73374ccefe Rework variables name, and logic to check IPs with strategy 2022-11-19 13:27:51 +01:00
MathieuHa 6b7f8655ac Remove Fixme In Doc 2022-11-19 12:28:50 +01:00
MathieuHa c9a3f1f8a8 Remove duration for remove csli crowdsec 2022-11-19 12:26:49 +01:00
MathieuHa 490e5e934f Merge remote-tracking branch 'origin' into 28-bypassing-crowdsec-for-local-ips 2022-11-19 12:22:56 +01:00
MathieuHa 5b0d4b533f Add logic return to bypass when IP is unfiltered, update documentation 2022-11-19 12:16:20 +01:00
Max Lerebourg 400de0d552 add redis timeout 2022-11-18 20:36:42 +01:00
MathieuHa 3742b6b540 Fix nil trusted checker 2022-11-17 09:34:16 +01:00
MathieuHa d2f0a9416d Add check to load param 2022-11-17 09:21:57 +01:00
MathieuHa c1f2131bc2 Add doc for trusted IP 2022-11-16 23:19:57 +01:00
MathieuHa 3f70e2b256 First implementation of the bypass feature for trusted IP 2022-11-16 23:17:41 +01:00
Max Lerebourg bf76ca9ef5 🍱 change logging 2022-11-15 19:50:22 +01:00
maxlerebourg 1baa5d7667 Merge pull request #31 from maxlerebourg/30-significant-slow-down-after-updating-to-v111-and-configuring-to-use-redis
 rewrite redis communication
2022-11-11 00:14:00 +01:00
Max Lerebourg 18f68de196 rewrite redis communication 2022-11-11 00:09:06 +01:00
maxlerebourg 4a5f1eca6a Merge pull request #24 from maxlerebourg/20-implement-shared-cache-with-an-external-redis-as-an-option
Create Environnement to work on redis cache
2022-11-06 13:02:58 +01:00
maxlerebourg cdda369fb8 Merge branch 'main' into 20-implement-shared-cache-with-an-external-redis-as-an-option 2022-11-06 13:02:40 +01:00
Max Lerebourg f0f28fecef 🍱 fix readme 2022-11-06 13:01:25 +01:00
Max Lerebourg 23620207f7 🍱 fix before version 2022-11-06 12:57:32 +01:00
maxlerebourg 4a674da9f9 Merge pull request #29 from maxlerebourg/max-impl-redis
Max impl redis
2022-11-06 12:50:19 +01:00
MathieuHa 0dfd18f18e Update documentation for cache 2022-11-05 21:55:29 +01:00
MathieuHa d8ee0a34eb Update documentation, exemples and code for redis support 2022-11-05 21:45:43 +01:00
maxlerebourg b50074dca4 Merge pull request #26 from maxlerebourg/max-impl-redis
Max impl redis
2022-11-05 20:01:51 +01:00
maxlerebourg 5044004ec2 Update cache.go 2022-10-26 10:26:19 +02:00
Max Lerebourg 41a46c0584 add delete 2022-10-24 21:56:38 +02:00
Max Lerebourg 0a186cf9a9 add redis ttl 2022-10-24 21:38:56 +02:00
Max Lerebourg ffcf4356fc redis included 2022-10-20 02:42:24 +02:00
Max Lerebourg f94e48aa03 not working... 2022-10-19 08:22:00 +02:00
Max Lerebourg a197194591 not working... 2022-10-19 08:21:01 +02:00
Max Lerebourg 44e329cd57 🍱 fix 2022-10-17 22:14:49 +02:00
maxlerebourg 81ffeabcec Merge pull request #25 from maxlerebourg/prepare-for-release-1.1.0
Update exemple traefik to version 2.9.1 and version of plugin 1.1.0 in exemples
2022-10-17 21:03:28 +02:00
MathieuHa e14d179612 Update exemple traefik to version 2.9.1 and version of plugin 1.1.0 2022-10-17 21:01:32 +02:00
MathieuHa 4058836678 Test library github.com/gomodule/redigo/redis 2022-10-17 20:58:27 +02:00
MathieuHa 87ed9e9c4e add bad library 2022-10-16 23:10:39 +02:00
MathieuHa 395c80dccf First version not working, unsafe library 2022-10-16 23:08:21 +02:00
MathieuHa 59268ee33d Merge remote-tracking branch 'origin' into 20-implement-shared-cache-with-an-external-redis-as-an-option 2022-10-16 22:07:01 +02:00
MathieuHa 781a83465e Create Environnement to work on redis cache 2022-10-16 20:47:29 +02:00
maxlerebourgandMathieuHa 8696501f61 add one instance of cache for all the service (#19)
*  add one instance of cache for all the service

* 🐛 fix the healthy issue

* 🐛 fix lint

* 🍱 upgrade codebase

* 🍱 fix lint

* 🐛 when there is master node shut down

* Set live mode as default, clean docker and recipes

* Restore live mode in readme as default

* 🍱 add channel management

* 🍱 order funcs

* 🍱 revert service in docker compose

*  logLevel added + cache put out the bouncer

* 🍱 fix

* 🍱 fix

* 🍱 fix

Co-authored-by: MathieuHa <mathieu@hanotaux.fr>
2022-10-16 20:14:41 +02:00
maxlerebourg f22fc2cd09 Merge pull request #22 from maxlerebourg/21-allow-the-real-ip-the-be-fetched-from-a-custom-header
 Add Custom header usage to find the user IP
2022-10-15 21:03:46 +02:00
MathieuHa 8eec1c5656 Add Custom header usage to find the user IP 2022-10-15 18:49:41 +02:00
552b30a9ef 16 read ip address from x forwarded for or x real ip instead of remoteaddr if it exists (#17)
* Handle X forwarded for in the plugin

* Update docker-compose.cloudflare.yml

Change to version v1.0.9

* 🍱 fix + isolate cache

* 🍱 missing files

* 🍱 lint

Co-authored-by: MathieuHa <mathieu@hanotaux.fr>
Co-authored-by: Max Lerebourg <maxlerebourg@gmail.com>
2022-10-15 14:00:32 +02:00
mathieuHa 6dde683a0a Add Missing line break for docker-compose 2022-10-15 09:59:14 +02:00
mathieuHa be0306eb49 Update README.md
Change number of modes
2022-10-15 09:57:22 +02:00
maxlerebourg b69bd77409 Merge pull request #15 from maxlerebourg/remove_alone
🍱 remove alone mode
2022-10-15 09:54:15 +02:00
Max Lerebourg 64a117d7b0 🍱 change default mode to live 2022-10-11 19:56:14 +02:00
Max Lerebourg d0ba71c0c8 🍱 clean code 2022-10-11 19:34:08 +02:00
Max Lerebourg 2e780b304d 🍱 remove alone mode 2022-10-11 19:28:57 +02:00
Max Lerebourg 4d7615dd19 🍱 fix waiter 2022-10-03 01:23:19 +02:00
MathieuHa 952fcd844f Clean docker-compose, update function name 2022-10-03 01:01:41 +02:00
MathieuHa 9cf4827768 Add Wait Stream feature 2022-10-03 00:37:41 +02:00
MathieuHa 6d8e811a03 Add metrics recipe to makefile 2022-10-03 00:21:34 +02:00
MathieuHa 18e4d42e8e Update readme, add makefile operations 2022-10-03 00:10:31 +02:00
MathieuHa 54183fbc97 Update Readme and docker-compose files with docs 2022-10-03 00:04:27 +02:00
Max Lerebourg c99a0ddc46 fix logger 2022-10-01 20:02:49 +02:00
Max Lerebourg f876458bb2 fix logger 2022-10-01 20:00:42 +02:00
Max Lerebourg 6c6a80efb4 fix 2022-10-01 19:44:06 +02:00
MathieuHa cc0261aa86 Add clean message when stream polling start 2022-10-01 19:42:22 +02:00
MathieuHa 465851b536 Fix name of docker volumes 2022-10-01 19:36:18 +02:00
maxlerebourg 9e92aeb449 Merge pull request #13 from maxlerebourg/fixMax
 fix loop
2022-10-01 19:33:09 +02:00
MathieuHa 37c7648854 Clean docker-compose and aquis.yaml 2022-10-01 19:28:09 +02:00
Max Lerebourg dd704d4274 fix 2022-10-01 19:23:20 +02:00
Max Lerebourg 1e4508f091 fix log 2022-10-01 19:18:12 +02:00
Max Lerebourg 89e8b5631e fix loop 2022-10-01 19:12:48 +02:00
Max Lerebourg 68adb31065 fix loop 2022-10-01 19:09:22 +02:00
mathieuHa 1ebde35693 Update README.md 2022-09-30 21:54:51 +02:00
mathieuHa 26913c5f7e Update README.md 2022-09-30 21:54:21 +02:00
mathieuHa ec0ffabc21 Update Readme
Show modes in a table
Fix gramar
2022-09-30 21:53:54 +02:00
Max Lerebourg 2f2cb60f74 🍱 fix 2022-09-30 01:38:00 +02:00
Max Lerebourg cb16130ca0 🍱 lint 2022-09-30 00:30:37 +02:00
Max Lerebourg d4fce42fd7 🍱 lint 2022-09-30 00:03:09 +02:00
Max Lerebourg 253caac5a4 🍱 lint 2022-09-29 22:19:35 +02:00
Max Lerebourg e122ce488c 🍱 lint 2022-09-29 22:18:08 +02:00
Max Lerebourg d9d92ed92d 🍱 lint 2022-09-29 22:08:52 +02:00
Max Lerebourg 3452c62860 add alone mode 2022-09-29 21:38:55 +02:00
Max Lerebourg b545eac5a8 fix lint 2022-09-29 14:01:33 +02:00
Max Lerebourg 2569f26805 fix lint 2022-09-29 11:27:14 +02:00
Max Lerebourg 773f001239 fix lint 2022-09-29 11:18:35 +02:00
Max Lerebourg 6b46c63287 fix logic 2022-09-29 11:16:52 +02:00
Max Lerebourg c8f59c2233 fix lint 2022-09-29 10:57:11 +02:00
Max Lerebourg 2110c70bbd fix lint 2022-09-29 10:52:29 +02:00
Max Lerebourg 00bbcfdb94 fix lint 2022-09-29 10:32:09 +02:00
Max Lerebourg a6012015e6 fix lint 2022-09-29 10:31:25 +02:00
Max Lerebourg feb40d9b5f fix lint 2022-09-29 10:08:07 +02:00
Max Lerebourg 6ab8ccb9db fix lint 2022-09-29 10:02:58 +02:00
Max Lerebourg dd36df5147 fix lint 2022-09-29 10:00:44 +02:00
Max Lerebourg ae959e4e0d fix lint 2022-09-29 09:55:17 +02:00
Max Lerebourg 7f5f69d134 fix lint 2022-09-29 09:52:24 +02:00
Max Lerebourg 1d7c5948f2 fix lint 2022-09-29 09:40:44 +02:00
Max Lerebourg 2559c71930 fix lint 2022-09-29 09:37:25 +02:00
Max Lerebourg 593681fd53 fix lint 2022-09-29 09:36:30 +02:00
Max Lerebourg c7714cb793 fix lint 2022-09-29 09:33:52 +02:00
Max Lerebourg 75267a1746 fix lint 2022-09-29 09:23:29 +02:00
Max Lerebourg 45eca85f3a fix lint 2022-09-29 09:18:39 +02:00
Max Lerebourg 1d9bd3fab0 fix lint 2022-09-29 09:16:03 +02:00
Max Lerebourg 0aff6e1789 fix lint 2022-09-29 09:15:14 +02:00
Max Lerebourg 0abf1b7390 fix main action 2022-09-29 09:13:04 +02:00
Max Lerebourg cb65e48a37 fix readme 8 2022-09-29 09:12:00 +02:00
Max Lerebourg cba7c1231f fix lint 2022-09-29 09:11:19 +02:00
Max Lerebourg da47ef320d fix readme 7 2022-09-29 09:00:20 +02:00
Max Lerebourg bbbb5acac5 fix readme 6 2022-09-29 08:59:28 +02:00
Max Lerebourg 6cf89bfc15 fix readme 5 2022-09-29 08:58:48 +02:00
Max Lerebourg f39317263f fix readme 4 2022-09-29 07:58:16 +02:00
Max Lerebourg 6dbd498212 fix readme 3 2022-09-29 07:54:11 +02:00
Max Lerebourg 7c2ac9830d 🍱 push .local 2022-09-29 07:45:23 +02:00
mathieuHaandMathieuHa b985f2d748 update main (#11)
* Update readme, add about sectioj

* Update readme about

* Change parameter with capitalized letters

* Add docker-compose local and docker-compose for real world utilization

* Update readme and .traefik

* update readme and docker-compose

* Update .traefik

Co-authored-by: MathieuHa <mathieu@hanotaux.fr>
2022-09-29 07:44:22 +02:00
Max Lerebourg a8758eb2e0 fix readme 2 2022-09-29 07:32:57 +02:00
Max Lerebourg aad92013d1 fix readme 2 2022-09-29 07:30:22 +02:00
Max Lerebourg 4843b86b45 fix readme 2022-09-29 07:19:36 +02:00
mathieuHaandMathieuHa 5eb4077133 update usage on readme, docker-compose and traefiker (#8)
* Update readme, add about sectioj

* Update readme about

* Change parameter with capitalized letters

* Add docker-compose local and docker-compose for real world utilization

* Update readme and .traefik

* update readme and docker-compose

* Update .traefik

Co-authored-by: MathieuHa <mathieu@hanotaux.fr>
2022-09-28 23:06:16 +02:00
mathieuHaandMathieuHa 57a97a0cf9 Update readme and solve traefiker error (#6)
* Update readme, add about sectioj

* Update readme about

* Change parameter with capitalized letters

* Add docker-compose local and docker-compose for real world utilization

* Update readme and .traefik

Co-authored-by: MathieuHa <mathieu@hanotaux.fr>
2022-09-28 22:16:03 +02:00
mathieuHaandMathieuHa 224669599e Updates .traefik with good name parameter (#4)
* Update readme, add about sectioj

* Update readme about

* Change parameter with capitalized letters

Co-authored-by: MathieuHa <mathieu@hanotaux.fr>
2022-09-28 21:39:15 +02:00
mathieuHa 71b5ac354b Merge pull request #2 from maxlerebourg/dev
initial documentation and review
2022-09-28 21:11:44 +02:00
MathieuHa 1098e5230e Add initial readme, update com on bouncer 2022-09-28 21:04:58 +02:00
MathieuHa 60b789ba9d Clean docker-compose, add test data in .traefik 2022-09-28 20:44:52 +02:00
MathieuHa 188aea2446 Review Code, add comments 2022-09-28 19:09:11 +02:00
Max Lerebourg 279c496fb3 clean up 2022-09-28 18:19:13 +02:00
Max Lerebourg 1d4dbe1cd5 🍱 finition 2022-09-28 18:02:43 +02:00
Max Lerebourg 5a370d9a88 add vendor 2022-09-28 17:52:24 +02:00
106 changed files with 8092 additions and 331 deletions
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 30 KiB

+35
View File
@@ -0,0 +1,35 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG] My short bug title"
labels: ''
assignees: ''
---
**Describe the bug** 🐛
A clear and concise description of what the bug is.
**Expected behavior** 👀
A clear and concise description of what you expected to happen.
**Context** 🔎
Before posting, make sure to use the `logLevel: DEBUG` of the plugin and see if you can fix the error yourself.
If not, post your plugin configuration file (anonimize IP and secret key) and a partial logs file where the error is encountered.
**Version (please complete the following information):**
- OS: [e.g. Ubuntu, Debian, Docker]
- Traefik version: [e.g. 2.9.*, 3.0.*]
- Plugin version: [e.g. 1.1.*]
- Redis ? : [e.g. 7.0.*]
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
<!---
If you like the plugin, please consider starring it, so you can get updates and we get some more visibility ✨
-->
+21
View File
@@ -0,0 +1,21 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE] My short feature title"
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.** 🐛
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.
<!---
If you like the plugin, please consider starring it, so you can get updates and we get some more visibility ✨
-->
-47
View File
@@ -1,47 +0,0 @@
name: Go Matrix
on: [push, pull_request]
jobs:
cross:
name: Go
runs-on: ${{ matrix.os }}
env:
CGO_ENABLED: 0
strategy:
matrix:
go-version: [ 1.17, 1.x ]
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
# https://github.com/marketplace/actions/setup-go-environment
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
# https://github.com/marketplace/actions/checkout
- name: Checkout code
uses: actions/checkout@v2
# https://github.com/marketplace/actions/cache
- name: Cache Go modules
uses: actions/cache@v2
with:
# In order:
# * Module download cache
# * Build cache (Linux)
# * Build cache (Mac)
# * Build cache (Windows)
path: |
~/go/pkg/mod
~/.cache/go-build
~/Library/Caches/go-build
%LocalAppData%\go-build
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-${{ matrix.go-version }}-go-
- name: Test
run: go test -v -cover ./...
+4 -4
View File
@@ -3,7 +3,7 @@ name: Main
on: on:
push: push:
branches: branches:
- master - main
pull_request: pull_request:
jobs: jobs:
@@ -12,9 +12,9 @@ jobs:
name: Main Process name: Main Process
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
GO_VERSION: 1.17 GO_VERSION: 1.22
GOLANGCI_LINT_VERSION: v1.46.2 GOLANGCI_LINT_VERSION: v1.57.2
YAEGI_VERSION: v0.13.0 YAEGI_VERSION: v0.16.1
CGO_ENABLED: 0 CGO_ENABLED: 0
defaults: defaults:
run: run:
+5 -1
View File
@@ -1,3 +1,7 @@
.idea/ .idea/
.DS_Store .DS_Store
golang-ttl-map ./config/
conf
db
logs
docker-compose.dev.yml
+42 -9
View File
@@ -1,17 +1,13 @@
run: run:
timeout: 3m timeout: 3m
skip-files: []
skip-dirs: []
linters-settings: linters-settings:
govet: govet:
enable-all: true enable-all: true
disable: disable:
- fieldalignment - fieldalignment
golint:
min-confidence: 0
gocyclo: gocyclo:
min-complexity: 12 min-complexity: 15
goconst: goconst:
min-len: 5 min-len: 5
min-occurrences: 4 min-occurrences: 4
@@ -25,15 +21,49 @@ linters-settings:
- FIXME - FIXME
gofumpt: gofumpt:
extra-rules: true extra-rules: true
depguard:
rules:
Main:
files:
- $all
- "!$test"
allow:
- $gostd
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/captcha
- github.com/leprosus/golang-ttl-map
- github.com/maxlerebourg/simpleredis
Test:
files:
- $test
allow:
- $gostd
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache
- github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/captcha
linters: linters:
enable-all: true enable-all: true
disable: disable:
- deadcode # deprecated
- exhaustivestruct # deprecated
- golint # deprecated
- ifshort # deprecated
- interfacer # deprecated - interfacer # deprecated
- maligned # deprecated - maligned # deprecated
- nosnakecase # deprecated
- scopelint # deprecated - scopelint # deprecated
- golint # deprecated - scopelint # deprecated
- exhaustivestruct # deprecated - structcheck # deprecated
- varcheck # deprecated
- sqlclosecheck # not relevant (SQL)
- rowserrcheck # not relevant (SQL)
- execinquery # not relevant (SQL)
- cyclop # duplicate of gocyclo - cyclop # duplicate of gocyclo
- bodyclose # Too many false positives: https://github.com/timakin/bodyclose/issues/30 - bodyclose # Too many false positives: https://github.com/timakin/bodyclose/issues/30
- dupl - dupl
@@ -52,12 +82,15 @@ linters:
- gomnd - gomnd
- forbidigo - forbidigo
- varnamelen - varnamelen
- wastedassign # is disabled because of generics
- gofumpt
- gci
issues: issues:
exclude-use-default: false exclude-use-default: false
max-per-linter: 0
max-same-issues: 0 max-same-issues: 0
exclude: [] exclude:
- "G402: TLS InsecureSkipVerify may be true."
exclude-rules: exclude-rules:
- path: (.+)_test.go - path: (.+)_test.go
linters: linters:
+4 -4
View File
@@ -4,9 +4,9 @@ iconPath: .assets/icon.png
import: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin import: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
summary: 'Add Request Header' summary: Middleware plugin which forwards the request IP to local Crowdsec agent, which can be used to allow/deny the request
testData: testData:
Headers: Enabled: false
X-Demo: test CrowdsecLapiKey: 40796d93c2958f9e58345514e67740e5
X-URL: '{{URL}}'
+94
View File
@@ -18,3 +18,97 @@ vendor:
clean: clean:
rm -rf ./vendor rm -rf ./vendor
run_dev:
docker compose -f docker-compose.dev.yml up -d --remove-orphans
run_local:
docker compose -f docker-compose.local.yml up -d --remove-orphans
run_behindproxy:
docker compose -f examples/behind-proxy/docker-compose.yml up -d --remove-orphans
run_cacheredis:
docker compose -f examples/redis-cache/docker-compose.yml up -d --remove-orphans
run_trustedips:
docker compose -f examples/trusted-ips/docker-compose.yml up -d --remove-orphans
run_binaryvm:
cd examples/binary-vm/ && sudo vagrant up
run_tlsauth:
docker compose -f examples/tls-auth/docker-compose.yml up -d --remove-orphans
run_appsec:
docker compose -f examples/appsec-enabled/docker-compose.yml up -d
run_captcha:
docker compose -f examples/captcha/docker-compose.yml up -d
run_custom_ban_page:
docker compose -f examples/custom-ban-page/docker-compose.yml up -d
run:
docker compose -f docker-compose.yml up -d --remove-orphans
restart_dev:
docker compose -f docker-compose.dev.yml restart
restart_local:
docker compose -f docker-compose.local.yml restart
restart:
docker compose -f docker-compose.yml restart
restart_behindproxy:
docker compose -f examples/behind-proxy/docker-compose.yml restart
restart_cacheredis:
docker compose -f examples/redis-cache/docker-compose.yml restart
restart_trustedips:
docker compose -f examples/trusted-ips/docker-compose.yml restart
restart_tlsauth:
docker compose -f examples/tls-auth/docker-compose.yml
restart_appsec:
docker compose -f examples/tls-auth/docker-compose.yml
restart_captcha:
docker compose -f examples/captcha/docker-compose.yml
restart_custombanpage:
docker compose -f examples/custom-ban-page/docker-compose.yml
show_logs:
docker compose -f docker-compose.yml restart
show_local_logs:
docker compose -f docker-compose.local.yml logs -f
show_dev_logs:
docker compose -f docker-compose.dev.yml logs -f
clean_all_docker:
docker compose -f examples/behind-proxy/docker-compose.yml down --remove-orphans
docker compose -f examples/redis-cache/docker-compose.yml down --remove-orphans
docker compose -f examples/trusted-ips/docker-compose.yml down --remove-orphans
docker compose -f examples/tls-auth/docker-compose.yml down --remove-orphans
docker compose -f examples/appsec-enabled/docker-compose.yml down --remove-orphans
docker compose -f examples/captcha/docker-compose.yml down --remove-orphans
docker compose -f examples/custom-ban-page/docker-compose.yml down --remove-orphans
docker compose -f docker-compose.local.yml down --remove-orphans
docker compose -f docker-compose.yml down --remove-orphans
clean_vagrant:
cd examples/binary-vm/ && sudo vagrant destroy -f
show_metrics:
docker exec crowdsec cscli metrics
show_decisions:
docker exec crowdsec cscli decisions list
+687
View File
@@ -0,0 +1,687 @@
![GitHub](https://img.shields.io/github/license/maxlerebourg/crowdsec-bouncer-traefik-plugin)
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/maxlerebourg/crowdsec-bouncer-traefik-plugin)
![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/maxlerebourg/crowdsec-bouncer-traefik-plugin)
[![Build Status](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/actions/workflows/go-cross.yml/badge.svg)](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/actions)
[![Go Report Card](https://goreportcard.com/badge/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin)](https://goreportcard.com/badge/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin)
# Crowdsec Bouncer Traefik plugin
> New! This plugin now supports [AppSec](https://doc.crowdsec.net/docs/next/appsec/intro/) 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](https://www.crowdsec.net/) 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.
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](https://doc.crowdsec.net/docs/next/appsec/intro/).
Remediation offered by [Crowdsec](https://docs.crowdsec.net/u/bouncers/intro) 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:
- [hcaptcha](https://www.hcaptcha.com/)
- [recaptcha](https://www.google.com/recaptcha/about/)
- [turnstile](https://www.cloudflare.com/products/turnstile/)
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:
<details><summary>Mode none workflow</summary>
> A Ban decision exists in CrowdsecLAPI
```mermaid
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
```mermaid
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
```
</details>
<details><summary>Mode live workflow</summary>
> A Ban decision exists in CrowdsecLAPI but not in cache
```mermaid
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
```mermaid
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
```
</details>
<details><summary>Mode stream workflow</summary>
> Cache Synchronization every UpdateIntervalSeconds
```mermaid
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
```mermaid
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
```mermaid
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
```
</details>
<details><summary>Mode alone Workflow</summary>
> Cache Synchronization every 2 hours to the Crowdsec Central API
```mermaid
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
```mermaid
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
```mermaid
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
```
</details>
<details><summary>Mode appsec workflow</summary>
> The request is detected as malicious
```mermaid
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
```mermaid
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
```
</details>
<details><summary>Captcha decision workflow</summary>
```mermaid
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
```
</details>
## Usage
To get started, use the `docker-compose.yml` file.
You can run it with:
```bash
make run
```
### Note
**/!\ 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*
Only one instance of the plugin is *possible*.
### Variables
- Enabled
- bool
- default: false
- Enable the plugin
- LogLevel
- string
- default: `INFO`, expected values are: `INFO`, `DEBUG`, `ERROR`, log are written to `stdout` / `stderr`
- 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. The scheme will be handled by the CrowdsecLapiScheme var.
- CrowdsecAppsecFailureBlock
- bool
- default: true
- Block request when Crowdsec Appsec Server have a [status 500](https://docs.crowdsec.net/docs/next/appsec/protocol#response-code).
- CrowdsecAppsecUnreachableBlock
- bool
- default: true
- Block request when Crowdsec Appsec Server is unreachable.
- CrowdsecLapiScheme
- string
- default: `http`, expected values are: `http`, `https`
- CrowdsecLapiHost
- string
- default: "crowdsec:8080"
- Crowdsec LAPI available on which host and port.
- 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 of the Crowdsec LAPI
- 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 cancelled (possible value of the header `ban` or `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 service
- RedisCachePassword
- string
- default: ""
- Password for the Redis service
- RedisCacheDatabase
- string
- default: ""
- Database selection for the Redis service
- 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
- Used only in `stream` and `alone` mode, the maximum number of time we can not reach Crowdsec before blocking traffic (set -1 to never block)
- DefaultDecisionSeconds
- int64
- default: 60
- Used only in `live` mode, maximum decision duration
- 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`
- 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
- CaptchaHTMLFilePath
- string
- default: /captcha.html
- Path where the captcha template is stored
- BanHTMLFilePath
- string
- default: ""
- Path where the ban html file is stored (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.
```yaml
# Static configuration
experimental:
plugins:
bouncer:
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
version: vX.Y.Z # To update
```
```yaml
# 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
updateIntervalSeconds: 60
updateMaxFailure: 0
defaultDecisionSeconds: 60
httpTimeoutSeconds: 10
crowdsecMode: live
crowdsecAppsecEnabled: false
crowdsecAppsecHost: crowdsec:7422
crowdsecAppsecFailureBlock: true
crowdsecAppsecUnreachableBlock: true
crowdsecLapiKey: privateKey-foo
crowdsecLapiKeyFile: /etc/traefik/cs-privateKey-foo
crowdsecLapiHost: crowdsec:8080
crowdsecLapiScheme: http
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:6379"
redisCachePassword: password
redisCacheDatabase: "5"
crowdsecLapiTLSCertificateAuthority: |-
-----BEGIN CERTIFICATE-----
MIIEBzCCAu+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
...
Q0veeNzBQXg1f/JxfeA39IDIX1kiCf71tGlT
-----END CERTIFICATE-----
crowdsecLapiTLSCertificateAuthorityFile: /etc/traefik/crowdsec-certs/ca.pem
crowdsecLapiTLSCertificateBouncer: |-
-----BEGIN CERTIFICATE-----
MIIEHjCCAwagAwIBAgIUOBTs1eqkaAUcPplztUr2xRapvNAwDQYJKoZIhvcNAQEL
...
RaXAnYYUVRblS1jmePemh388hFxbmrpG2pITx8B5FMULqHoj11o2Rl0gSV6tHIHz
N2U=
-----END CERTIFICATE-----
crowdsecLapiTLSCertificateBouncerFile: /etc/traefik/crowdsec-certs/bouncer.pem
crowdsecLapiTLSCertificateBouncerKey: |-
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAtYQnbJqifH+ZymePylDxGGLIuxzcAUU4/ajNj+qRAdI/Ux3d
...
ic5cDRo6/VD3CS3MYzyBcibaGaV34nr0G/pI+KEqkYChzk/PZRA=
-----END RSA PRIVATE KEY-----
crowdsecLapiTLSCertificateBouncerKeyFile: /etc/traefik/crowdsec-certs/bouncer-key.pem
captchaProvider: hcaptcha
captchaSiteKey: FIXME
captchaSecretKey: FIXME
captchaGracePeriodSeconds: 1800
captchaHTMLFilePath: /captcha.html
banHTMLFilePath: /ban.html
```
#### Fill variable with value of file
`CrowdsecLapiTlsCertificateBouncerKey`, `CrowdsecLapiTlsCertificateBouncer`, `CrowdsecLapiTlsCertificateAuthority`, `CrowdsecCapiMachineId`, `CrowdsecCapiPassword`, `CrowdsecLapiKey`, `CaptchaSiteKey` and `CaptchaSecretKey` 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](https://docs.crowdsec.net/docs/user_guides/lapi_mgmt)
```bash
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
```yaml
...
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:
```bash
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
To communicate with the LAPI in HTTPS you need to either accept any certificates by setting the `crowdsecLapiTLSInsecureVerify` to true or add the CA used by the server certificate of Crowdsec using `crowdsecLapiTLSCertificateAuthority` or `crowdsecLapiTLSCertificateAuthorityFile`.
Set the `crowdsecLapiScheme` to https.
Crowdsec must be listening in HTTPS for this to work.
Please see the [tls-auth example](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/tls-auth/README.md) or the official documentation: [docs.crowdsec.net/docs/local_api/tls_auth/](https://docs.crowdsec.net/docs/local_api/tls_auth/)
#### Manually add an IP to the blocklist (for testing purposes)
```bash
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](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/behind-proxy/README.md)
#### 2. With Redis as an external shared cache [examples/redis-cache/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/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](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/trusted-ips/README.md)
#### 4. Using Crowdsec and Traefik installed as binary in a single VM [examples/binary-vm/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/binary-vm/README.md)
#### 5. Using https communication and tls authentication with Crowdsec [examples/tls-auth/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/tls-auth/README.md)
#### 6. Using Crowdsec and Traefik in Kubernetes [examples/kubernetes/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/kubernetes/README.md)
#### 7. Using Traefik in standalone mode without Crowdsec [examples/standalone-mode/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/standalone-mode/README.md)
#### 8. Using Traefik with AppSec feature enabled [examples/appsec-enabled/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/appsec-enabled/README.md)
#### 9. Using Traefik with Captcha remediation feature enabled [examples/captcha/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/captcha/README.md)
#### 10. Using Traefik with Custom Ban HTML Page [examples/custom-ban-page/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/custom-ban-page/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](https://golang.org/doc/gopath_code.html#Workspaces), 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.
```bash
docker compose -f docker-compose.local.yml up -d
```
Equivalent to
```bash
make run_local
```
### About
[mathieuHa](https://github.com/mathieuHa) and [I](https://github.com/maxlerebourg) have been using Traefik since 2020 at [Primadviz](https://primadviz.com).
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](https://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](https://plugins.traefik.io) and be more performant.
+10 -1
View File
@@ -1,4 +1,13 @@
---
filenames: filenames:
- /var/log/traefik/*.log - /var/log/traefik/access.log
labels: labels:
type: traefik type: traefik
---
listen_addr: 0.0.0.0:7422
appsec_config: crowdsecurity/virtual-patching
name: myAppSecComponent
source: appsec
labels:
type: appsec
+329
View File
@@ -0,0 +1,329 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>CrowdSec Access Forbidden</title>
<meta content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
/*! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com*/
*,
:after,
:before {
border: 0 solid #e5e7eb;
box-sizing: border-box
}
:after,
:before {
--tw-content: ""
}
html {
-webkit-text-size-adjust: 100%;
font-feature-settings: normal;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4
}
body {
line-height: inherit;
margin: 0
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit
}
a {
color: inherit;
text-decoration: inherit
}
h1,
h2,
h3,
h4,
h5,
h6,
hr,
p,
pre {
margin: 0
}
*,
::backdrop,
:after,
:before {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #3b82f680;
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia:
}
.flex {
display: flex
}
.flex-wrap {
flex-wrap: wrap
}
.inline-flex {
display: inline-flex
}
.h-24 {
height: 6rem
}
.h-6 {
height: 1.5rem
}
.h-full {
height: 100%
}
.h-screen {
height: 100vh
}
.text-center {
text-align: center
}
.w-24 {
width: 6rem
}
.w-6 {
width: 1.5rem
}
.w-full {
width: 100%
}
.w-screen {
width: 100vw
}
.my-3 {
margin-top: 0.75rem;
margin-bottom: 0.75rem
}
.flex-col {
flex-direction: column
}
.items-center {
align-items: center
}
.justify-center {
justify-content: center
}
.justify-between {
justify-content: space-between
}
.space-y-1>:not([hidden])~:not([hidden]) {
--tw-space-y-reverse: 0;
margin-bottom: calc(.25rem*var(--tw-space-y-reverse));
margin-top: calc(.25rem*(1 - var(--tw-space-y-reverse)))
}
.space-y-4>:not([hidden])~:not([hidden]) {
--tw-space-y-reverse: 0;
margin-bottom: calc(1rem*var(--tw-space-y-reverse));
margin-top: calc(1rem*(1 - var(--tw-space-y-reverse)))
}
.rounded-xl {
border-radius: .75rem
}
.border-2 {
border-width: 2px
}
.border-black {
--tw-border-opacity: 1;
border-color: rgb(0 0 0/var(--tw-border-opacity))
}
.p-4 {
padding: 1rem
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem
}
.py-2 {
padding-bottom: .5rem;
padding-top: .5rem
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem
}
.text-sm {
font-size: .875rem;
line-height: 1.25rem
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem
}
.font-bold {
font-weight: 700
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255/var(--tw-text-opacity))
}
@media (min-width:640px) {
.sm\:w-2\/3 {
width: 66.666667%
}
}
@media (min-width:768px) {
.md\:flex-row {
flex-direction: row
}
}
@media (min-width:1024px) {
.lg\:w-1\/2 {
width: 50%
}
.lg\:text-3xl {
font-size: 1.875rem;
line-height: 2.25rem
}
.lg\:text-xl {
font-size: 1.25rem;
line-height: 1.75rem
}
}
@media (min-width:1280px) {
.xl\:text-4xl {
font-size: 2.25rem;
line-height: 2.5rem
}
}
</style>
</head>
<body class="h-screen w-screen p-4">
<div class="h-full w-full flex flex-col justify-center items-center">
<div class="border-2 border-black rounded-xl p-4 text-center w-full sm:w-2/3 lg:w-1/2">
<div class="flex flex-col items-center space-y-4">
<svg fill="black" class="h-24 w-24" aria-hidden="true" focusable="false" data-prefix="fas"
data-icon="exclamation-triangle" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"
class="warning">
<path
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z">
</path>
</svg>
<h1 class="text-2xl lg:text-3xl xl:text-4xl">CrowdSec Access Forbidden</h1>
</div>
<div class="flex justify-center flex-wrap">
<p class="my-3">This security check has been powered by</p>
<a href="https://crowdsec.net/" target="_blank" rel="noopener" class="inline-flex flex-col items-center">
<svg fill="black" width="33.92" height="33.76" viewBox="0 0 254.4 253.2">
<defs>
<clipPath id="a">
<path d="M0 52h84v201.2H0zm0 0" />
</clipPath>
<clipPath id="b">
<path d="M170 52h84.4v201.2H170zm0 0" />
</clipPath>
</defs>
<path
d="M59.3 128.4c1.4 2.3 2.5 4.6 3.4 7-1-4.1-2.3-8.1-4.3-12-3.1-6-7.8-5.8-10.7 0-2 4-3.2 8-4.3 12.1 1-2.4 2-4.8 3.4-7.1 3.4-5.8 8.8-6 12.5 0M207.8 128.4a42.9 42.9 0 013.4 7c-1-4.1-2.3-8.1-4.3-12-3.2-6-7.8-5.8-10.7 0-2 4-3.3 8-4.3 12.1.9-2.4 2-4.8 3.4-7.1 3.4-5.8 8.8-6 12.5 0M134.6 92.9c2 3.5 3.6 7 4.8 10.7-1.3-5.4-3-10.6-5.6-15.7-4-7.5-9.7-7.2-13.3 0a75.4 75.4 0 00-5.6 16c1.2-3.8 2.7-7.4 4.7-11 4.1-7.2 10.6-7.5 15 0M43.8 136.8c.9 4.6 3.7 8.3 7.3 9.2 0 2.7 0 5.5.2 8.2.3 3.3.4 6.6 1 9.6.3 2.3 1 2.2 1.3 0 .5-3 .6-6.3 1-9.6l.2-8.2c3.5-1 6.4-4.6 7.2-9.2a17.8 17.8 0 01-9 2.4c-3.5 0-6.6-1-9.2-2.4M192.4 136.8c.8 4.6 3.7 8.3 7.2 9.2 0 2.7 0 5.5.3 8.2.3 3.3.4 6.6 1 9.6.3 2.3.9 2.2 1.2 0 .6-3 .7-6.3 1-9.6.2-2.7.3-5.5.2-8.2 3.6-1 6.4-4.6 7.3-9.2a17.8 17.8 0 01-9.1 2.4c-3.4 0-6.6-1-9.1-2.4M138.3 104.6c-3.1 1.9-7 3-11.3 3-4.3 0-8.2-1.1-11.3-3 1 5.8 4.5 10.3 9 11.5 0 3.4 0 6.8.3 10.2.4 4.1.5 8.2 1.2 12 .4 2.9 1.2 2.7 1.6 0 .7-3.8.8-7.9 1.2-12 .3-3.4.3-6.8.3-10.2 4.5-1.2 8-5.7 9-11.5" />
<path
d="M51 146c0 2.7.1 5.5.3 8.2.3 3.3.4 6.6 1 9.6.3 2.3 1 2.2 1.3 0 .5-3 .6-6.3 1-9.6l.2-8.2c3.5-1 6.4-4.6 7.2-9.2a17.8 17.8 0 01-9 2.4c-3.5 0-6.6-1-9.2-2.4.9 4.6 3.7 8.3 7.3 9.2M143.9 105c-1.9-.4-3.5-1.2-4.9-2.3 1.4 5.6 2.5 11.3 4 17 1.2 5 2 10 2.4 15 .6 7.8-4.5 14.5-10.9 14.5h-15c-6.4 0-11.5-6.7-11-14.5.5-5 1.3-10 2.6-15 1.3-5.3 2.3-10.5 3.6-15.7-2.2 1.2-4.8 1.9-7.7 2-4.7.1-9.4-.3-14-1-4-.4-6.7-3-8-6.7-1.3-3.4-2-7-3.3-10.4-.5-1.5-1.6-2.8-2.4-4.2-.4-.6-.8-1.2-.9-1.8v-7.8a77 77 0 0124.5-3c6.1 0 12 1 17.8 3.2 4.7 1.7 9.7 1.8 14.4 0 9-3.4 18.2-3.8 27.5-3 4.9.5 9.8 1.6 14.8 2.4v8.2c0 .6-.3 1.5-.7 1.7-2 .9-2.2 2.7-2.7 4.5-.9 3.2-1.8 6.4-2.9 9.5a11 11 0 01-8.8 7.7 40.6 40.6 0 01-18.4-.2m29.4 80.6c-3.2-26.8-6.4-50-8.9-60.7a14.3 14.3 0 0014.1-14h.4a9 9 0 005.6-16.5 14.3 14.3 0 00-3.7-27.2 9 9 0 00-6.9-14.6c2.4-1.1 4.5-3 5.8-5 3.4-5.3 4-29-8-44.4-5-6.3-9.8-2.5-10 1.8-1 13.2-1.1 23-4.5 34.3a9 9 0 00-16-4.1 14.3 14.3 0 00-28.4 0 9 9 0 00-16 4.1c-3.4-11.2-3.5-21.1-4.4-34.3-.3-4.3-5.2-8-10-1.8-12 15.3-11.5 39-8.1 44.4 1.3 2 3.4 3.9 5.8 5a9 9 0 00-7 14.6 14.3 14.3 0 00-3.6 27.2A9 9 0 0075 111h.5a14.5 14.5 0 0014.3 14c-4 17.2-10 66.3-15 111.3l-1.3 13.4a1656.4 1656.4 0 01106.6 0l-1.4-12.7-5.4-51.3" />
<g clip-path="url(#a)">
<path
d="M83.5 136.6l-2.3.7c-5 1-9.8 1-14.8-.2-1.4-.3-2.7-1-3.8-1.9l3.1 13.7c1 4 1.7 8 2 12 .5 6.3-3.6 11.6-8.7 11.6H46.9c-5.1 0-9.2-5.3-8.7-11.6.3-4 1-8 2-12 1-4.2 1.8-8.5 2.9-12.6-1.8 1-3.9 1.5-6.3 1.6a71 71 0 01-11.1-.7 7.7 7.7 0 01-6.5-5.5c-1-2.7-1.6-5.6-2.6-8.3-.4-1.2-1.3-2.3-2-3.4-.2-.4-.6-1-.6-1.4v-6.3c6.4-2 13-2.6 19.6-2.5 4.9.1 9.6 1 14.2 2.6 3.9 1.4 7.9 1.5 11.7 0 1.8-.7 3.6-1.2 5.5-1.6a13 13 0 01-1.6-15.5A18.3 18.3 0 0159 73.1a11.5 11.5 0 00-17.4 8.1 7.2 7.2 0 00-12.9 3.3c-2.7-9-2.8-17-3.6-27.5-.2-3.4-4-6.5-8-1.4C7.5 67.8 7.9 86.9 10.6 91c1.1 1.7 2.8 3.1 4.7 4a7.2 7.2 0 00-5.6 11.7 11.5 11.5 0 00-2.9 21.9 7.2 7.2 0 004.5 13.2h.3c0 .6 0 1.1.2 1.7.9 5.4 5.6 9.5 11.3 9.5A1177.2 1177.2 0 0010 253.2c18.1-1.5 38.1-2.6 59.5-3.4.4-4.6.8-9.3 1.4-14 1.2-11.6 3.3-30.5 5.7-49.7 2.2-18 4.7-36.3 7-49.5" />
</g>
<g clip-path="url(#b)">
<path
d="M254.4 118.2c0-5.8-4.2-10.5-9.7-11.4a7.2 7.2 0 00-5.6-11.7c2-.9 3.6-2.3 4.7-4 2.7-4.2 3.1-23.3-6.5-35.5-4-5.1-7.8-2-8 1.4-.8 10.5-.9 18.5-3.6 27.5a7.2 7.2 0 00-12.8-3.3 11.5 11.5 0 00-17.8-7.9 18.4 18.4 0 01-4.5 22 13 13 0 01-1.3 15.2c2.4.5 4.8 1 7.1 2 3.8 1.3 7.8 1.4 11.6 0 7.2-2.8 14.6-3 22-2.4 4 .4 7.9 1.2 12 1.9l-.1 6.6c0 .5-.2 1.2-.5 1.3-1.7.7-1.8 2.2-2.2 3.7l-2.3 7.6a8.8 8.8 0 01-7 6.1c-5 1-10 1-14.9-.2-1.5-.3-2.8-1-3.9-1.9 1.2 4.5 2 9.1 3.2 13.7 1 4 1.6 8 2 12 .4 6.3-3.6 11.6-8.8 11.6h-12c-5.2 0-9.3-5.3-8.8-11.6.4-4 1-8 2-12 1-4.2 1.9-8.5 3-12.6-1.8 1-4 1.5-6.3 1.6-3.7 0-7.5-.3-11.2-.7a7.7 7.7 0 01-3.7-1.5c3.1 18.4 7.1 51.2 12.5 100.9l.6 5.3.8 7.9c21.4.7 41.5 1.9 59.7 3.4L243 243l-4.4-41.2a606 606 0 00-7-48.7 11.5 11.5 0 0011.2-11.2h.4a7.2 7.2 0 004.4-13.2c4-1.8 6.8-5.8 6.8-10.5" />
</g>
<path
d="M180 249.6h.4a6946 6946 0 00-7.1-63.9l5.4 51.3 1.4 12.6M164.4 125c2.5 10.7 5.7 33.9 8.9 60.7a570.9 570.9 0 00-8.9-60.7M74.8 236.3l-1.4 13.4 1.4-13.4" />
</svg>
<span>CrowdSec</span>
</a>
</div>
</div>
</div>
</body>
</html>
+532 -200
View File
@@ -1,100 +1,141 @@
// Package plugindemo a demo plugin. // Package crowdsec_bouncer_traefik_plugin implements a middleware that communicates with crowdsec.
package crowdsec_bouncer_traefik_plugin // It can cache results in memory or using redis, or even ask crowdsec for every requests.
package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/tls"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io"
"log"
"net"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"text/template" "text/template"
"time" "time"
ttl_map "github.com/leprosus/golang-ttl-map" cache "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache"
captcha "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/captcha"
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
) )
const ( const (
realIpHeader = "X-Real-Ip" crowdsecAppsecIPHeader = "X-Crowdsec-Appsec-Ip"
forwardHeader = "X-Forwarded-For" crowdsecAppsecURIHeader = "X-Crowdsec-Appsec-Uri"
crowdsecAuthHeader = "X-Api-Key" crowdsecAppsecHostHeader = "X-Crowdsec-Appsec-Host"
crowdsecRoute = "v1/decisions" crowdsecAppsecVerbHeader = "X-Crowdsec-Appsec-Verb"
crowdsecStreamRoute = "v1/decisions/stream" crowdsecAppsecHeader = "X-Crowdsec-Appsec-Api-Key"
cacheBannedValue = "t" crowdsecAppsecUserAgent = "X-Crowdsec-Appsec-User-Agent"
cacheNoBannedValue = "f" crowdsecLapiHeader = "X-Api-Key"
crowdsecLapiRoute = "v1/decisions"
crowdsecLapiStreamRoute = "v1/decisions/stream"
crowdsecCapiHost = "api.crowdsec.net"
crowdsecCapiHeader = "Authorization"
crowdsecCapiLoginRoute = "v2/watchers/login"
crowdsecCapiStreamRoute = "v2/decisions/stream"
cacheTimeoutKey = "updated"
) )
// var ipRegex = regexp.MustCompile(`\b\d+\.\d+\.\d+\.\d+\b`) //nolint:gochecknoglobals
var cache = ttl_map.New() var (
isStartup = true
isCrowdsecStreamHealthy = true
updateFailure = 0
ticker chan bool
)
type Config struct { // CreateConfig creates the default plugin configuration.
Enabled bool `json:"enabled,omitempty"` func CreateConfig() *configuration.Config {
CrowdsecMode string `json:"crowdsecMode,omitempty"` return configuration.New()
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
}
func CreateConfig() *Config {
return &Config{
Enabled: false,
CrowdsecMode: "stream",
CrowdsecLapiScheme: "http",
CrowdsecLapiHost: "crowdsec:8080",
CrowdsecLapiKey: "",
UpdateIntervalSeconds: 10,
DefaultDecisionSeconds: 10,
}
} }
// Bouncer a Bouncer struct.
type Bouncer struct { type Bouncer struct {
next http.Handler next http.Handler
name string name string
template *template.Template template *template.Template
enabled bool enabled bool
crowdsecStreamHealthy bool appsecEnabled bool
appsecHost string
appsecFailureBlock bool
appsecUnreachableBlock bool
crowdsecScheme string crowdsecScheme string
crowdsecHost string crowdsecHost string
crowdsecKey string crowdsecKey string
crowdsecMode string crowdsecMode string
crowdsecMachineID string
crowdsecPassword string
crowdsecScenarios []string
updateInterval int64 updateInterval int64
updateMaxFailure int
defaultDecisionTimeout int64 defaultDecisionTimeout int64
client *http.Client remediationCustomHeader string
forwardedCustomHeader string
crowdsecStreamRoute string
crowdsecHeader string
banTemplateString string
clientPoolStrategy *ip.PoolStrategy
serverPoolStrategy *ip.PoolStrategy
httpClient *http.Client
cacheClient *cache.Client
captchaClient *captcha.Client
log *logger.Log
} }
// New created a new Demo plugin. // New creates the crowdsec bouncer plugin.
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) { func New(_ context.Context, next http.Handler, config *configuration.Config, name string) (http.Handler, error) {
required := map[string]string{ log := logger.New(config.LogLevel)
"CrowdsecLapiScheme": config.CrowdsecLapiScheme, err := configuration.ValidateParams(config)
"CrowdsecLapiHost": config.CrowdsecLapiHost,
"CrowdsecLapiKey": config.CrowdsecLapiKey,
"CrowdsecMode": config.CrowdsecMode,
}
for val, key := range required {
if len(val) == 0 {
return nil, fmt.Errorf("%v cannot be empty", key)
}
}
if !contains([]string{"none", "live", "stream"}, config.CrowdsecMode) {
return nil, fmt.Errorf("CrowdsecMode must be one of: none, live or stream")
}
if !contains([]string{"http", "https"}, config.CrowdsecLapiScheme) {
return nil, fmt.Errorf("CrowdsecLapiScheme must be one of: http, https")
}
testUrl := url.URL{
Scheme: config.CrowdsecLapiScheme,
Host: config.CrowdsecLapiHost,
Path: crowdsecRoute,
}
_, err := http.NewRequest(http.MethodGet, testUrl.String(), nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("CrowdsecLapiScheme://CrowdsecLapiHost: '%v://%v' must be an URL", config.CrowdsecLapiScheme, config.CrowdsecLapiHost) log.Error("New:validateParams " + err.Error())
return nil, err
}
serverChecker, _ := ip.NewChecker(log, config.ForwardedHeadersTrustedIPs)
clientChecker, _ := ip.NewChecker(log, config.ClientTrustedIPs)
var tlsConfig *tls.Config
crowdsecStreamRoute := ""
crowdsecHeader := ""
if config.CrowdsecMode == configuration.AloneMode {
config.CrowdsecCapiMachineID, _ = configuration.GetVariable(config, "CrowdsecCapiMachineID")
config.CrowdsecCapiPassword, _ = configuration.GetVariable(config, "CrowdsecCapiPassword")
config.CrowdsecLapiHost = crowdsecCapiHost
config.CrowdsecLapiScheme = "https"
config.UpdateIntervalSeconds = 7200 // 2 hours
crowdsecStreamRoute = crowdsecCapiStreamRoute
crowdsecHeader = crowdsecCapiHeader
} else {
crowdsecStreamRoute = crowdsecLapiStreamRoute
crowdsecHeader = crowdsecLapiHeader
tlsConfig, err = configuration.GetTLSConfigCrowdsec(config, log)
if err != nil {
log.Error("New:getTLSConfigCrowdsec fail to get tlsConfig " + err.Error())
return nil, err
}
apiKey, errAPIKey := configuration.GetVariable(config, "CrowdsecLapiKey")
if errAPIKey != nil && len(tlsConfig.Certificates) == 0 {
log.Error("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup " + errAPIKey.Error())
return nil, err
}
config.CrowdsecLapiKey = apiKey
}
var banTemplateString string
if config.BanHTMLFilePath != "" {
var buf bytes.Buffer
banTemplate, _ := configuration.GetHTMLTemplate(config.BanHTMLFilePath)
err = banTemplate.Execute(&buf, nil)
if err != nil {
log.Error("New:banTemplate is bad formatted " + err.Error())
return nil, err
}
banTemplateString = buf.String()
} }
bouncer := &Bouncer{ bouncer := &Bouncer{
@@ -103,127 +144,173 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
template: template.New("CrowdsecBouncer").Delims("[[", "]]"), template: template.New("CrowdsecBouncer").Delims("[[", "]]"),
enabled: config.Enabled, enabled: config.Enabled,
crowdsecStreamHealthy: false,
crowdsecMode: config.CrowdsecMode, crowdsecMode: config.CrowdsecMode,
appsecEnabled: config.CrowdsecAppsecEnabled,
appsecHost: config.CrowdsecAppsecHost,
appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock,
crowdsecScheme: config.CrowdsecLapiScheme, crowdsecScheme: config.CrowdsecLapiScheme,
crowdsecHost: config.CrowdsecLapiHost, crowdsecHost: config.CrowdsecLapiHost,
crowdsecKey: config.CrowdsecLapiKey, crowdsecKey: config.CrowdsecLapiKey,
crowdsecMachineID: config.CrowdsecCapiMachineID,
crowdsecPassword: config.CrowdsecCapiPassword,
crowdsecScenarios: config.CrowdsecCapiScenarios,
updateInterval: config.UpdateIntervalSeconds, updateInterval: config.UpdateIntervalSeconds,
updateMaxFailure: config.UpdateMaxFailure,
remediationCustomHeader: config.RemediationHeadersCustomName,
forwardedCustomHeader: config.ForwardedHeadersCustomName,
defaultDecisionTimeout: config.DefaultDecisionSeconds, defaultDecisionTimeout: config.DefaultDecisionSeconds,
client: &http.Client{ banTemplateString: banTemplateString,
crowdsecStreamRoute: crowdsecStreamRoute,
crowdsecHeader: crowdsecHeader,
log: log,
serverPoolStrategy: &ip.PoolStrategy{
Checker: serverChecker,
},
clientPoolStrategy: &ip.PoolStrategy{
Checker: clientChecker,
},
httpClient: &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
MaxIdleConns: 10, MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second, IdleConnTimeout: 30 * time.Second,
TLSClientConfig: tlsConfig,
}, },
Timeout: 5 * time.Second, Timeout: time.Duration(config.HTTPTimeoutSeconds) * time.Second,
}, },
cacheClient: &cache.Client{},
captchaClient: &captcha.Client{},
} }
go handleStreamCache(bouncer, "true") if config.CrowdsecMode == configuration.AppsecMode {
return bouncer, nil
}
config.RedisCachePassword, _ = configuration.GetVariable(config, "RedisCachePassword")
bouncer.cacheClient.New(
log,
config.RedisCacheEnabled,
config.RedisCacheHost,
config.RedisCachePassword,
config.RedisCacheDatabase,
)
config.CaptchaSiteKey, _ = configuration.GetVariable(config, "CaptchaSiteKey")
config.CaptchaSecretKey, _ = configuration.GetVariable(config, "CaptchaSecretKey")
err = bouncer.captchaClient.New(
log,
bouncer.cacheClient,
&http.Client{
Transport: &http.Transport{MaxIdleConns: 10, IdleConnTimeout: 30 * time.Second},
Timeout: time.Duration(config.HTTPTimeoutSeconds) * time.Second,
},
config.CaptchaProvider,
config.CaptchaSiteKey,
config.CaptchaSecretKey,
config.RemediationHeadersCustomName,
config.CaptchaHTMLFilePath,
config.CaptchaGracePeriodSeconds,
)
if err != nil {
return nil, err
}
if (config.CrowdsecMode == configuration.StreamMode || config.CrowdsecMode == configuration.AloneMode) && ticker == nil {
if config.CrowdsecMode == configuration.AloneMode {
if err := getToken(bouncer); err != nil {
bouncer.log.Error("New:getToken " + err.Error())
return nil, err
}
}
handleStreamTicker(bouncer)
isStartup = false
ticker = startTicker(config, log, func() {
handleStreamTicker(bouncer)
})
}
bouncer.log.Debug("New initialized mode:" + config.CrowdsecMode)
return bouncer, nil return bouncer, nil
} }
func (a *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { // ServeHTTP principal function of plugin.
if !a.enabled { //
log.Printf("not enabled") //nolint:nestif
a.next.ServeHTTP(rw, req) func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if !bouncer.enabled {
bouncer.next.ServeHTTP(rw, req)
return return
} }
remoteHost, _, err := net.SplitHostPort(req.RemoteAddr) // Here we check for the trusted IPs in the forwardedCustomHeader
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.forwardedCustomHeader)
if err != nil { if err != nil {
log.Printf("failed to extract ip from remote address: %v", err) bouncer.log.Error(fmt.Sprintf("ServeHTTP:getRemoteIp ip:%s %s", remoteIP, err.Error()))
a.next.ServeHTTP(rw, req) handleBanServeHTTP(bouncer, rw)
return
}
isTrusted, err := bouncer.clientPoolStrategy.Checker.Contains(remoteIP)
if err != nil {
bouncer.log.Error(fmt.Sprintf("ServeHTTP:checkerContains ip:%s %s", remoteIP, err.Error()))
handleBanServeHTTP(bouncer, rw)
return
}
// if our IP is in the trusted list we bypass the next checks
bouncer.log.Debug(fmt.Sprintf("ServeHTTP ip:%s isTrusted:%v", remoteIP, isTrusted))
if isTrusted {
bouncer.next.ServeHTTP(rw, req)
return return
} }
if a.crowdsecMode == "stream" || a.crowdsecMode == "live" { if bouncer.crowdsecMode == configuration.AppsecMode {
isBanned, err := getDecision(remoteHost) handleNextServeHTTP(bouncer, remoteIP, rw, req)
if err == nil { return
if isBanned { }
rw.WriteHeader(http.StatusForbidden)
// TODO This should be simplified
if bouncer.crowdsecMode != configuration.NoneMode {
value, cacheErr := bouncer.cacheClient.Get(remoteIP)
if cacheErr != nil {
cacheErrString := cacheErr.Error()
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:Get ip:%s isBanned:false %s", remoteIP, cacheErrString))
if cacheErrString != cache.CacheMiss {
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s %s", remoteIP, cacheErrString))
handleBanServeHTTP(bouncer, rw)
return
}
} else { } else {
a.next.ServeHTTP(rw, req) bouncer.log.Debug(fmt.Sprintf("ServeHTTP ip:%s cache:hit isBanned:%v", remoteIP, value))
} if value == cache.NoBannedValue {
return handleNextServeHTTP(bouncer, remoteIP, rw, req)
}
}
if a.crowdsecMode == "stream" {
if a.crowdsecStreamHealthy {
a.next.ServeHTTP(rw, req)
} else { } else {
rw.WriteHeader(http.StatusForbidden) handleRemediationServeHTTP(bouncer, remoteIP, value, rw, req)
} }
return return
} }
}
if a.crowdsecMode == "none" || a.crowdsecMode == "live" { // Right here if we cannot join the stream we forbid the request to go on.
noneUrl := url.URL{ if bouncer.crowdsecMode == configuration.StreamMode || bouncer.crowdsecMode == configuration.AloneMode {
Scheme: a.crowdsecScheme, if isCrowdsecStreamHealthy {
Host: a.crowdsecHost, handleNextServeHTTP(bouncer, remoteIP, rw, req)
Path: crowdsecRoute, } else {
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteHost), bouncer.log.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s updateFailure:%d", remoteIP, updateFailure))
handleBanServeHTTP(bouncer, rw)
} }
req, _ := http.NewRequest(http.MethodGet, noneUrl.String(), nil) } else {
req.Header.Add(crowdsecAuthHeader, a.crowdsecKey) value, err := handleNoStreamCache(bouncer, remoteIP)
res, err := a.client.Do(req) if value == cache.NoBannedValue {
if err != nil { handleNextServeHTTP(bouncer, remoteIP, rw, req)
log.Printf("failed to get decision: %s", err) } else {
rw.WriteHeader(http.StatusForbidden) bouncer.log.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:%v %s", remoteIP, value, err.Error()))
return handleRemediationServeHTTP(bouncer, remoteIP, value, rw, req)
} }
defer res.Body.Close()
if res.StatusCode != 200 {
log.Printf("failed to get decision, status code: %d", res.StatusCode)
rw.WriteHeader(http.StatusForbidden)
return
} }
body, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Printf("failed to read body: %s", err)
rw.WriteHeader(http.StatusForbidden)
return
}
if !bytes.Equal(body, []byte("null")) {
var decisions []Decision
err = json.Unmarshal(body, &decisions)
if err != nil {
log.Printf("failed to parse body: %s", err)
rw.WriteHeader(http.StatusForbidden)
return
}
if len(decisions) == 0 {
if a.crowdsecMode == "live" {
setDecision(remoteHost, false, a.defaultDecisionTimeout)
}
a.next.ServeHTTP(rw, req)
return
}
duration, err := time.ParseDuration(decisions[0].Duration)
if err != nil {
log.Printf("failed to parse duration: %s", err)
rw.WriteHeader(http.StatusForbidden)
return
}
log.Printf("ip banned: %v", remoteHost)
rw.WriteHeader(http.StatusForbidden)
setDecision(remoteHost, true, int64(duration.Seconds()))
return
}
if a.crowdsecMode == "live" {
setDecision(remoteHost, false, a.defaultDecisionTimeout)
}
a.next.ServeHTTP(rw, req)
return
}
a.next.ServeHTTP(rw, req)
} }
// CUSTOM CODE // CUSTOM CODE.
// TODO place in another file.
// Decision Body returned from Crowdsec LAPI.
type Decision struct { type Decision struct {
Id int `json:"id"` ID int `json:"id"`
Origin string `json:"origin"` Origin string `json:"origin"`
Type string `json:"type"` Type string `json:"type"`
Scope string `json:"scope"` Scope string `json:"scope"`
@@ -233,77 +320,322 @@ type Decision struct {
Simulated bool `json:"simulated"` Simulated bool `json:"simulated"`
} }
// Stream Body returned from Crowdsec Stream LAPI.
type Stream struct { type Stream struct {
Deleted []Decision `json:"deleted"` Deleted []Decision `json:"deleted"`
New []Decision `json:"new"` New []Decision `json:"new"`
} }
func contains(source []string, target string) bool { // Login Body returned from Crowdsec Login CAPI.
for _, a := range source { type Login struct {
if a == target { Code int `json:"code"`
return true Token string `json:"token"`
} Expire string `json:"expire"`
}
return false
} }
func getDecision(clientIP string) (bool, error) { // To append Headers we need to call rw.WriteHeader after set any header.
isBanned, ok := cache.Get(clientIP) func handleBanServeHTTP(bouncer *Bouncer, rw http.ResponseWriter) {
if ok && len(isBanned.(string)) > 0 { if bouncer.banTemplateString == "" {
if isBanned == cacheNoBannedValue { rw.WriteHeader(http.StatusForbidden)
return false, nil
} else {
return true, nil
}
}
return false, fmt.Errorf("no data")
}
func setDecision(clientIP string, isBanned bool, duration int64) {
if isBanned {
cache.Set(clientIP, cacheBannedValue, duration)
} else {
cache.Set(clientIP, cacheNoBannedValue, duration)
}
}
func handleStreamCache(a *Bouncer, initialized string) {
time.AfterFunc(time.Duration(a.updateInterval) * time.Second, func () {
handleStreamCache(a, "false")
})
streamUrl := url.URL{
Scheme: a.crowdsecScheme,
Host: a.crowdsecHost,
Path: crowdsecStreamRoute,
RawQuery: fmt.Sprintf("startup=%s", initialized),
}
req, _ := http.NewRequest(http.MethodGet, streamUrl.String(), nil)
req.Header.Add(crowdsecAuthHeader, a.crowdsecKey)
res, err := a.client.Do(req)
if err != nil || res.StatusCode == http.StatusForbidden {
a.crowdsecStreamHealthy = false
return return
} }
defer res.Body.Close() rw.Header().Set("Content-Type", "text/html; charset=utf-8")
body, err := ioutil.ReadAll(res.Body) if bouncer.remediationCustomHeader != "" {
rw.Header().Set(bouncer.remediationCustomHeader, "ban")
}
rw.WriteHeader(http.StatusForbidden)
fmt.Fprint(rw, bouncer.banTemplateString)
}
func handleRemediationServeHTTP(bouncer *Bouncer, remoteIP, remediation string, rw http.ResponseWriter, req *http.Request) {
bouncer.log.Debug(fmt.Sprintf("handleRemediationServeHTTP ip:%s remediation:%s", remoteIP, remediation))
if bouncer.captchaClient.Valid && remediation == cache.CaptchaValue {
if bouncer.captchaClient.Check(remoteIP) {
handleNextServeHTTP(bouncer, remoteIP, rw, req)
return
}
bouncer.captchaClient.ServeHTTP(rw, req, remoteIP)
return
}
handleBanServeHTTP(bouncer, rw)
}
func handleNextServeHTTP(bouncer *Bouncer, remoteIP string, rw http.ResponseWriter, req *http.Request) {
if bouncer.appsecEnabled {
if err := appsecQuery(bouncer, remoteIP, req); err != nil {
bouncer.log.Debug(fmt.Sprintf("handleNextServeHTTP ip:%s isWaf:true %s", remoteIP, err.Error()))
handleBanServeHTTP(bouncer, rw)
return
}
}
bouncer.next.ServeHTTP(rw, req)
}
func handleStreamTicker(bouncer *Bouncer) {
if err := handleStreamCache(bouncer); err != nil {
bouncer.log.Debug(fmt.Sprintf("handleStreamTicker updateFailure:%d isCrowdsecStreamHealthy:%t %s", updateFailure, isCrowdsecStreamHealthy, err.Error()))
if bouncer.updateMaxFailure != -1 && updateFailure >= bouncer.updateMaxFailure && isCrowdsecStreamHealthy {
isCrowdsecStreamHealthy = false
bouncer.log.Error(fmt.Sprintf("handleStreamTicker:error updateFailure:%d %s", updateFailure, err.Error()))
}
updateFailure++
} else {
isCrowdsecStreamHealthy = true
updateFailure = 0
}
}
func startTicker(config *configuration.Config, log *logger.Log, work func()) chan bool {
ticker := time.NewTicker(time.Duration(config.UpdateIntervalSeconds) * time.Second)
stop := make(chan bool, 1)
go func() {
defer log.Debug("ticker:stopped")
for {
select {
case <-ticker.C:
go work()
case <-stop:
return
}
}
}()
return stop
}
// We are now in none or live mode.
func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
isLiveMode := bouncer.crowdsecMode == configuration.LiveMode
routeURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Host: bouncer.crowdsecHost,
Path: crowdsecLapiRoute,
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteIP),
}
body, err := crowdsecQuery(bouncer, routeURL.String(), false)
if err != nil { if err != nil {
a.crowdsecStreamHealthy = false return cache.BannedValue, err
return }
if bytes.Equal(body, []byte("null")) {
if isLiveMode {
bouncer.cacheClient.Set(remoteIP, cache.NoBannedValue, bouncer.defaultDecisionTimeout)
}
return cache.NoBannedValue, nil
}
var decisions []Decision
err = json.Unmarshal(body, &decisions)
if err != nil {
return cache.BannedValue, fmt.Errorf("handleNoStreamCache:parseBody %w", err)
}
if len(decisions) == 0 {
if isLiveMode {
bouncer.cacheClient.Set(remoteIP, cache.NoBannedValue, bouncer.defaultDecisionTimeout)
}
return cache.NoBannedValue, nil
}
var decision Decision
for _, d := range decisions {
decision = d
if decision.Type == "ban" {
break
}
}
duration, err := time.ParseDuration(decision.Duration)
if err != nil {
return cache.BannedValue, fmt.Errorf("handleNoStreamCache:parseDuration %w", err)
}
var value string
switch decision.Type {
case "ban":
value = cache.BannedValue
case "captcha":
value = cache.CaptchaValue
default:
bouncer.log.Debug("handleStreamCache:unknownType " + decision.Type)
}
if isLiveMode {
durationSecond := int64(duration.Seconds())
if bouncer.defaultDecisionTimeout < durationSecond {
durationSecond = bouncer.defaultDecisionTimeout
}
bouncer.cacheClient.Set(remoteIP, value, durationSecond)
}
return value, errors.New("handleNoStreamCache:banned")
}
func getToken(bouncer *Bouncer) error {
loginURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Host: bouncer.crowdsecHost,
Path: crowdsecCapiLoginRoute,
}
body, err := crowdsecQuery(bouncer, loginURL.String(), true)
if err != nil {
return err
}
var login Login
err = json.Unmarshal(body, &login)
if err != nil {
return fmt.Errorf("getToken:parsingBody %w", err)
}
if login.Code == 200 && len(login.Token) > 0 {
bouncer.crowdsecKey = login.Token
bouncer.log.Debug(fmt.Sprintf("getToken statusCode:%d", login.Code))
return nil
}
return fmt.Errorf("getToken statusCode:%d", login.Code)
}
func handleStreamCache(bouncer *Bouncer) error {
// TODO clean properly on exit.
// Instead of blocking the goroutine interval for all the secondary node,
// if the master service is shut down, other goroutine can take the lead
// because updated routine information is in the cache
_, err := bouncer.cacheClient.Get(cacheTimeoutKey)
if err == nil {
bouncer.log.Debug("handleStreamCache:alreadyUpdated")
return nil
}
if err.Error() != cache.CacheMiss {
return err
}
bouncer.cacheClient.Set(cacheTimeoutKey, cache.NoBannedValue, bouncer.updateInterval-1)
streamRouteURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Host: bouncer.crowdsecHost,
Path: bouncer.crowdsecStreamRoute,
RawQuery: fmt.Sprintf("startup=%t", !isCrowdsecStreamHealthy || isStartup),
}
body, err := crowdsecQuery(bouncer, streamRouteURL.String(), false)
if err != nil {
return err
} }
var stream Stream var stream Stream
err = json.Unmarshal(body, &stream) err = json.Unmarshal(body, &stream)
if err != nil { if err != nil {
a.crowdsecStreamHealthy = false return fmt.Errorf("handleStreamCache:parsingBody %w", err)
return
} }
for _, decision := range stream.New { for _, decision := range stream.New {
duration, err := time.ParseDuration(decision.Duration) duration, err := time.ParseDuration(decision.Duration)
if err == nil { if err == nil {
setDecision(decision.Value, true, int64(duration.Seconds())) var value string
switch decision.Type {
case "ban":
value = cache.BannedValue
case "captcha":
value = cache.CaptchaValue
default:
bouncer.log.Debug("handleStreamCache:unknownType " + decision.Type)
}
bouncer.cacheClient.Set(decision.Value, value, int64(duration.Seconds()))
} }
} }
for _, decision := range stream.Deleted { for _, decision := range stream.Deleted {
cache.Del(decision.Value) bouncer.cacheClient.Delete(decision.Value)
} }
a.crowdsecStreamHealthy = true bouncer.log.Debug("handleStreamCache:updated")
return nil
}
func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, error) {
var req *http.Request
if isPost {
data := []byte(fmt.Sprintf(
`{"machine_id": "%v","password": "%v","scenarios": ["%v"]}`,
bouncer.crowdsecMachineID,
bouncer.crowdsecPassword,
strings.Join(bouncer.crowdsecScenarios, `","`),
))
req, _ = http.NewRequest(http.MethodPost, stringURL, bytes.NewBuffer(data))
} else {
req, _ = http.NewRequest(http.MethodGet, stringURL, nil)
}
req.Header.Add(bouncer.crowdsecHeader, bouncer.crowdsecKey)
req.Header.Add("User-Agent", "Crowdsec-Bouncer-Traefik-Plugin/1.X.X")
res, err := bouncer.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("crowdsecQuery:unreachable url:%s %w", stringURL, err)
}
defer func() {
if err = res.Body.Close(); err != nil {
bouncer.log.Error("crowdsecQuery:closeBody " + err.Error())
}
}()
if res.StatusCode == http.StatusUnauthorized && bouncer.crowdsecMode == configuration.AloneMode {
if errToken := getToken(bouncer); errToken != nil {
return nil, fmt.Errorf("crowdsecQuery:renewToken url:%s %w", stringURL, errToken)
}
return crowdsecQuery(bouncer, stringURL, false)
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("crowdsecQuery url:%s, statusCode:%d", stringURL, res.StatusCode)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("crowdsecQuery:readBody %w", err)
}
return body, nil
}
func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
routeURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Host: bouncer.appsecHost,
Path: "/",
}
var req *http.Request
if httpReq.Body != nil && httpReq.ContentLength > 0 {
bodyBytes, err := io.ReadAll(httpReq.Body)
if err != nil {
return fmt.Errorf("appsecQuery:GetBody %w", err)
}
httpReq.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
req, _ = http.NewRequest(http.MethodPost, routeURL.String(), bytes.NewBuffer(bodyBytes))
} else {
req, _ = http.NewRequest(http.MethodGet, routeURL.String(), nil)
}
for key, headers := range httpReq.Header {
for _, value := range headers {
req.Header.Add(key, value)
}
}
req.Header.Set(crowdsecAppsecHeader, bouncer.crowdsecKey)
req.Header.Set(crowdsecAppsecIPHeader, ip)
req.Header.Set(crowdsecAppsecVerbHeader, httpReq.Method)
req.Header.Set(crowdsecAppsecHostHeader, httpReq.Host)
req.Header.Set(crowdsecAppsecURIHeader, httpReq.URL.String())
req.Header.Set(crowdsecAppsecUserAgent, httpReq.Header.Get("User-Agent"))
res, err := bouncer.httpClient.Do(req)
if err != nil {
bouncer.log.Error("appsecQuery:unreachable")
if bouncer.appsecUnreachableBlock {
return fmt.Errorf("appsecQuery:unreachable %w", err)
}
return nil
}
defer func() {
if err = res.Body.Close(); err != nil {
bouncer.log.Error("appsecQuery:closeBody " + err.Error())
}
}()
if res.StatusCode == http.StatusInternalServerError {
bouncer.log.Info("appsecQuery:failure")
if bouncer.appsecFailureBlock {
return errors.New("appsecQuery statusCode:500")
}
return nil
}
if res.StatusCode != http.StatusOK {
return fmt.Errorf("appsecQuery statusCode:%d", res.StatusCode)
}
if err != nil {
return fmt.Errorf("appsecQuery:readBody %w", err)
}
return nil
} }
+162 -23
View File
@@ -1,49 +1,188 @@
package crowdsec_bouncer_traefik_plugin_test package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
import ( import (
"context" "context"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect"
"testing" "testing"
"text/template"
"github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" cache "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache"
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
) )
func TestDemo(t *testing.T) { func TestServeHTTP(t *testing.T) {
cfg := crowdsec_bouncer_traefik_plugin.CreateConfig() cfg := CreateConfig()
cfg.CrowdsecLapiKey = "caca" cfg.CrowdsecLapiKey = "test"
// cfg.Headers["X-Method"] = "[[.Method]]"
// cfg.Headers["X-URL"] = "[[.URL]]"
// cfg.Headers["X-URL"] = "[[.URL]]"
// cfg.Headers["X-Demo"] = "test"
ctx := context.Background() ctx := context.Background()
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}) next := http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {})
handler, err := crowdsec_bouncer_traefik_plugin.New(ctx, next, cfg, "demo-plugin") handler, err := New(ctx, next, cfg, "demo-plugin")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
handler.ServeHTTP(recorder, req) handler.ServeHTTP(recorder, req)
// assertHeader(t, req, "X-Host", "localhost")
// assertHeader(t, req, "X-URL", "http://localhost")
// assertHeader(t, req, "X-Method", "GET")
// assertHeader(t, req, "X-Demo", "test")
} }
// func assertHeader(t *testing.T, req *http.Request, key, expected string) { func TestNew(t *testing.T) {
// t.Helper() type args struct {
ctx context.Context //nolint:containedctx
next http.Handler
config *configuration.Config
name string
}
tests := []struct {
name string
args args
want http.Handler
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := New(tt.args.ctx, tt.args.next, tt.args.config, tt.args.name)
if (err != nil) != tt.wantErr {
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("New() = %v, want %v", got, tt.want)
}
})
}
}
// if req.Header.Get(key) != expected { func TestBouncer_ServeHTTP(t *testing.T) {
// t.Errorf("invalid header value: %s", req.Header.Get(key)) type fields struct {
// } next http.Handler
// } name string
template *template.Template
enabled bool
crowdsecScheme string
crowdsecHost string
crowdsecKey string
crowdsecMode string
updateInterval int64
defaultDecisionTimeout int64
forwardedCustomHeader string
clientPoolStrategy *ip.PoolStrategy
serverPoolStrategy *ip.PoolStrategy
httpClient *http.Client
cacheClient *cache.Client
}
type args struct {
rw http.ResponseWriter
req *http.Request
}
tests := []struct {
name string
fields fields
args args
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(_ *testing.T) {
bouncer := &Bouncer{
next: tt.fields.next,
name: tt.fields.name,
template: tt.fields.template,
enabled: tt.fields.enabled,
crowdsecScheme: tt.fields.crowdsecScheme,
crowdsecHost: tt.fields.crowdsecHost,
crowdsecKey: tt.fields.crowdsecKey,
crowdsecMode: tt.fields.crowdsecMode,
updateInterval: tt.fields.updateInterval,
defaultDecisionTimeout: tt.fields.defaultDecisionTimeout,
forwardedCustomHeader: tt.fields.forwardedCustomHeader,
clientPoolStrategy: tt.fields.clientPoolStrategy,
serverPoolStrategy: tt.fields.serverPoolStrategy,
httpClient: tt.fields.httpClient,
cacheClient: tt.fields.cacheClient,
}
bouncer.ServeHTTP(tt.args.rw, tt.args.req)
})
}
}
func Test_handleNoStreamCache(t *testing.T) {
type args struct {
bouncer *Bouncer
remoteIP string
}
tests := []struct {
name string
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if _, err := handleNoStreamCache(tt.args.bouncer, tt.args.remoteIP); (err != nil) != tt.wantErr {
t.Errorf("handleNoStreamCache() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_handleStreamCache(t *testing.T) {
type args struct {
bouncer *Bouncer
}
tests := []struct {
name string
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := handleStreamCache(tt.args.bouncer)
if (err != nil) != tt.wantErr {
t.Errorf("handleStreamCache() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func Test_crowdsecQuery(t *testing.T) {
type args struct {
bouncer *Bouncer
stringURL string
isPost bool
}
tests := []struct {
name string
args args
want []byte
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := crowdsecQuery(tt.args.bouncer, tt.args.stringURL, tt.args.isPost)
if (err != nil) != tt.wantErr {
t.Errorf("crowdsecQuery() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("crowdsecQuery() = %v, want %v", got, tt.want)
}
})
}
}
+338
View File
@@ -0,0 +1,338 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>CrowdSec Captcha</title>
<meta content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
/*! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com*/
*,
:after,
:before {
border: 0 solid #e5e7eb;
box-sizing: border-box
}
:after,
:before {
--tw-content: ""
}
html {
-webkit-text-size-adjust: 100%;
font-feature-settings: normal;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4
}
body {
line-height: inherit;
margin: 0
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit
}
a {
color: inherit;
text-decoration: inherit
}
h1,
h2,
h3,
h4,
h5,
h6,
hr,
p,
pre {
margin: 0
}
*,
::backdrop,
:after,
:before {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #3b82f680;
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia:
}
.flex {
display: flex
}
.flex-wrap {
flex-wrap: wrap
}
.inline-flex {
display: inline-flex
}
.h-24 {
height: 6rem
}
.h-6 {
height: 1.5rem
}
.h-full {
height: 100%
}
.h-screen {
height: 100vh
}
.text-center {
text-align: center
}
.w-24 {
width: 6rem
}
.w-6 {
width: 1.5rem
}
.w-full {
width: 100%
}
.w-screen {
width: 100vw
}
.my-3 {
margin-top: 0.75rem;
margin-bottom: 0.75rem
}
.flex-col {
flex-direction: column
}
.items-center {
align-items: center
}
.justify-center {
justify-content: center
}
.justify-between {
justify-content: space-between
}
.space-y-1>:not([hidden])~:not([hidden]) {
--tw-space-y-reverse: 0;
margin-bottom: calc(.25rem*var(--tw-space-y-reverse));
margin-top: calc(.25rem*(1 - var(--tw-space-y-reverse)))
}
.space-y-4>:not([hidden])~:not([hidden]) {
--tw-space-y-reverse: 0;
margin-bottom: calc(1rem*var(--tw-space-y-reverse));
margin-top: calc(1rem*(1 - var(--tw-space-y-reverse)))
}
.rounded-xl {
border-radius: .75rem
}
.border-2 {
border-width: 2px
}
.border-black {
--tw-border-opacity: 1;
border-color: rgb(0 0 0/var(--tw-border-opacity))
}
.p-4 {
padding: 1rem
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem
}
.py-2 {
padding-bottom: .5rem;
padding-top: .5rem
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem
}
.text-sm {
font-size: .875rem;
line-height: 1.25rem
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem
}
.font-bold {
font-weight: 700
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255/var(--tw-text-opacity))
}
@media (min-width:640px) {
.sm\:w-2\/3 {
width: 66.666667%
}
}
@media (min-width:768px) {
.md\:flex-row {
flex-direction: row
}
}
@media (min-width:1024px) {
.lg\:w-1\/2 {
width: 50%
}
.lg\:text-3xl {
font-size: 1.875rem;
line-height: 2.25rem
}
.lg\:text-xl {
font-size: 1.25rem;
line-height: 1.75rem
}
}
@media (min-width:1280px) {
.xl\:text-4xl {
font-size: 2.25rem;
line-height: 2.5rem
}
}
</style>
<script src="{{ .FrontendJS }}" async defer></script>
</head>
<body class="h-screen w-screen p-4">
<div class="h-full w-full flex flex-col justify-center items-center">
<div class="border-2 border-black rounded-xl p-4 text-center w-full sm:w-2/3 lg:w-1/2">
<div class="flex flex-col items-center space-y-4">
<svg fill="black" class="h-24 w-24" aria-hidden="true" focusable="false" data-prefix="fas"
data-icon="exclamation-triangle" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"
class="warning">
<path
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z">
</path>
</svg>
<h1 class="text-2xl lg:text-3xl xl:text-4xl">CrowdSec Captcha</h1>
</div>
<form action="" method="POST" class="flex flex-col space-y-1" id="captcha-form">
<div id="captcha" class="{{ .FrontendKey }}" data-sitekey="{{ .SiteKey }}" data-callback="captchaCallback">
</div>
</form>
<div class="flex justify-center flex-wrap">
<p class="my-3">This security check has been powered by</p>
<a href="https://crowdsec.net/" target="_blank" rel="noopener" class="inline-flex flex-col items-center">
<svg fill="black" width="33.92" height="33.76" viewBox="0 0 254.4 253.2">
<defs>
<clipPath id="a">
<path d="M0 52h84v201.2H0zm0 0" />
</clipPath>
<clipPath id="b">
<path d="M170 52h84.4v201.2H170zm0 0" />
</clipPath>
</defs>
<path
d="M59.3 128.4c1.4 2.3 2.5 4.6 3.4 7-1-4.1-2.3-8.1-4.3-12-3.1-6-7.8-5.8-10.7 0-2 4-3.2 8-4.3 12.1 1-2.4 2-4.8 3.4-7.1 3.4-5.8 8.8-6 12.5 0M207.8 128.4a42.9 42.9 0 013.4 7c-1-4.1-2.3-8.1-4.3-12-3.2-6-7.8-5.8-10.7 0-2 4-3.3 8-4.3 12.1.9-2.4 2-4.8 3.4-7.1 3.4-5.8 8.8-6 12.5 0M134.6 92.9c2 3.5 3.6 7 4.8 10.7-1.3-5.4-3-10.6-5.6-15.7-4-7.5-9.7-7.2-13.3 0a75.4 75.4 0 00-5.6 16c1.2-3.8 2.7-7.4 4.7-11 4.1-7.2 10.6-7.5 15 0M43.8 136.8c.9 4.6 3.7 8.3 7.3 9.2 0 2.7 0 5.5.2 8.2.3 3.3.4 6.6 1 9.6.3 2.3 1 2.2 1.3 0 .5-3 .6-6.3 1-9.6l.2-8.2c3.5-1 6.4-4.6 7.2-9.2a17.8 17.8 0 01-9 2.4c-3.5 0-6.6-1-9.2-2.4M192.4 136.8c.8 4.6 3.7 8.3 7.2 9.2 0 2.7 0 5.5.3 8.2.3 3.3.4 6.6 1 9.6.3 2.3.9 2.2 1.2 0 .6-3 .7-6.3 1-9.6.2-2.7.3-5.5.2-8.2 3.6-1 6.4-4.6 7.3-9.2a17.8 17.8 0 01-9.1 2.4c-3.4 0-6.6-1-9.1-2.4M138.3 104.6c-3.1 1.9-7 3-11.3 3-4.3 0-8.2-1.1-11.3-3 1 5.8 4.5 10.3 9 11.5 0 3.4 0 6.8.3 10.2.4 4.1.5 8.2 1.2 12 .4 2.9 1.2 2.7 1.6 0 .7-3.8.8-7.9 1.2-12 .3-3.4.3-6.8.3-10.2 4.5-1.2 8-5.7 9-11.5" />
<path
d="M51 146c0 2.7.1 5.5.3 8.2.3 3.3.4 6.6 1 9.6.3 2.3 1 2.2 1.3 0 .5-3 .6-6.3 1-9.6l.2-8.2c3.5-1 6.4-4.6 7.2-9.2a17.8 17.8 0 01-9 2.4c-3.5 0-6.6-1-9.2-2.4.9 4.6 3.7 8.3 7.3 9.2M143.9 105c-1.9-.4-3.5-1.2-4.9-2.3 1.4 5.6 2.5 11.3 4 17 1.2 5 2 10 2.4 15 .6 7.8-4.5 14.5-10.9 14.5h-15c-6.4 0-11.5-6.7-11-14.5.5-5 1.3-10 2.6-15 1.3-5.3 2.3-10.5 3.6-15.7-2.2 1.2-4.8 1.9-7.7 2-4.7.1-9.4-.3-14-1-4-.4-6.7-3-8-6.7-1.3-3.4-2-7-3.3-10.4-.5-1.5-1.6-2.8-2.4-4.2-.4-.6-.8-1.2-.9-1.8v-7.8a77 77 0 0124.5-3c6.1 0 12 1 17.8 3.2 4.7 1.7 9.7 1.8 14.4 0 9-3.4 18.2-3.8 27.5-3 4.9.5 9.8 1.6 14.8 2.4v8.2c0 .6-.3 1.5-.7 1.7-2 .9-2.2 2.7-2.7 4.5-.9 3.2-1.8 6.4-2.9 9.5a11 11 0 01-8.8 7.7 40.6 40.6 0 01-18.4-.2m29.4 80.6c-3.2-26.8-6.4-50-8.9-60.7a14.3 14.3 0 0014.1-14h.4a9 9 0 005.6-16.5 14.3 14.3 0 00-3.7-27.2 9 9 0 00-6.9-14.6c2.4-1.1 4.5-3 5.8-5 3.4-5.3 4-29-8-44.4-5-6.3-9.8-2.5-10 1.8-1 13.2-1.1 23-4.5 34.3a9 9 0 00-16-4.1 14.3 14.3 0 00-28.4 0 9 9 0 00-16 4.1c-3.4-11.2-3.5-21.1-4.4-34.3-.3-4.3-5.2-8-10-1.8-12 15.3-11.5 39-8.1 44.4 1.3 2 3.4 3.9 5.8 5a9 9 0 00-7 14.6 14.3 14.3 0 00-3.6 27.2A9 9 0 0075 111h.5a14.5 14.5 0 0014.3 14c-4 17.2-10 66.3-15 111.3l-1.3 13.4a1656.4 1656.4 0 01106.6 0l-1.4-12.7-5.4-51.3" />
<g clip-path="url(#a)">
<path
d="M83.5 136.6l-2.3.7c-5 1-9.8 1-14.8-.2-1.4-.3-2.7-1-3.8-1.9l3.1 13.7c1 4 1.7 8 2 12 .5 6.3-3.6 11.6-8.7 11.6H46.9c-5.1 0-9.2-5.3-8.7-11.6.3-4 1-8 2-12 1-4.2 1.8-8.5 2.9-12.6-1.8 1-3.9 1.5-6.3 1.6a71 71 0 01-11.1-.7 7.7 7.7 0 01-6.5-5.5c-1-2.7-1.6-5.6-2.6-8.3-.4-1.2-1.3-2.3-2-3.4-.2-.4-.6-1-.6-1.4v-6.3c6.4-2 13-2.6 19.6-2.5 4.9.1 9.6 1 14.2 2.6 3.9 1.4 7.9 1.5 11.7 0 1.8-.7 3.6-1.2 5.5-1.6a13 13 0 01-1.6-15.5A18.3 18.3 0 0159 73.1a11.5 11.5 0 00-17.4 8.1 7.2 7.2 0 00-12.9 3.3c-2.7-9-2.8-17-3.6-27.5-.2-3.4-4-6.5-8-1.4C7.5 67.8 7.9 86.9 10.6 91c1.1 1.7 2.8 3.1 4.7 4a7.2 7.2 0 00-5.6 11.7 11.5 11.5 0 00-2.9 21.9 7.2 7.2 0 004.5 13.2h.3c0 .6 0 1.1.2 1.7.9 5.4 5.6 9.5 11.3 9.5A1177.2 1177.2 0 0010 253.2c18.1-1.5 38.1-2.6 59.5-3.4.4-4.6.8-9.3 1.4-14 1.2-11.6 3.3-30.5 5.7-49.7 2.2-18 4.7-36.3 7-49.5" />
</g>
<g clip-path="url(#b)">
<path
d="M254.4 118.2c0-5.8-4.2-10.5-9.7-11.4a7.2 7.2 0 00-5.6-11.7c2-.9 3.6-2.3 4.7-4 2.7-4.2 3.1-23.3-6.5-35.5-4-5.1-7.8-2-8 1.4-.8 10.5-.9 18.5-3.6 27.5a7.2 7.2 0 00-12.8-3.3 11.5 11.5 0 00-17.8-7.9 18.4 18.4 0 01-4.5 22 13 13 0 01-1.3 15.2c2.4.5 4.8 1 7.1 2 3.8 1.3 7.8 1.4 11.6 0 7.2-2.8 14.6-3 22-2.4 4 .4 7.9 1.2 12 1.9l-.1 6.6c0 .5-.2 1.2-.5 1.3-1.7.7-1.8 2.2-2.2 3.7l-2.3 7.6a8.8 8.8 0 01-7 6.1c-5 1-10 1-14.9-.2-1.5-.3-2.8-1-3.9-1.9 1.2 4.5 2 9.1 3.2 13.7 1 4 1.6 8 2 12 .4 6.3-3.6 11.6-8.8 11.6h-12c-5.2 0-9.3-5.3-8.8-11.6.4-4 1-8 2-12 1-4.2 1.9-8.5 3-12.6-1.8 1-4 1.5-6.3 1.6-3.7 0-7.5-.3-11.2-.7a7.7 7.7 0 01-3.7-1.5c3.1 18.4 7.1 51.2 12.5 100.9l.6 5.3.8 7.9c21.4.7 41.5 1.9 59.7 3.4L243 243l-4.4-41.2a606 606 0 00-7-48.7 11.5 11.5 0 0011.2-11.2h.4a7.2 7.2 0 004.4-13.2c4-1.8 6.8-5.8 6.8-10.5" />
</g>
<path
d="M180 249.6h.4a6946 6946 0 00-7.1-63.9l5.4 51.3 1.4 12.6M164.4 125c2.5 10.7 5.7 33.9 8.9 60.7a570.9 570.9 0 00-8.9-60.7M74.8 236.3l-1.4 13.4 1.4-13.4" />
</svg>
<span>CrowdSec</span>
</a>
</div>
</div>
</div>
<script>
function captchaCallback() {
setTimeout(() => document.querySelector('#captcha-form').submit(), 500);
}
</script>
</body>
</html>
+74
View File
@@ -0,0 +1,74 @@
services:
traefik:
image: "traefik:v3.0.0"
container_name: "traefik"
restart: unless-stopped
command:
- "--log.level=DEBUG"
- "--accesslog"
- "--accesslog.filepath=/var/log/traefik/access.log"
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-local:/var/log/traefik
- './ban.html:/ban.html:ro'
- './captcha.html:/captcha.html:ro'
- ./:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 8000:80
- 8080:8080
depends_on:
- crowdsec
whoami-foo:
image: traefik/whoami
container_name: "simple-service-foo"
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.router-foo.rule=PathPrefix(`/foo`)"
- "traefik.http.routers.router-foo.entrypoints=web"
- "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
whoami2:
image: traefik/whoami
container_name: "simple-service-bar"
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.router-bar.rule=PathPrefix(`/bar`)"
- "traefik.http.routers.router-bar.entrypoints=web"
- "traefik.http.routers.router-bar.middlewares=crowdsec@docker"
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsecenabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=stream"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5="
crowdsec:
image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK: 40796d93c2958f9e58345514e67740e5=
volumes:
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
- logs-local:/var/log/traefik:ro
- crowdsec-db-local:/var/lib/crowdsec/data/
- crowdsec-config-local:/etc/crowdsec/
labels:
- "traefik.enable=false"
volumes:
logs-local:
crowdsec-db-local:
crowdsec-config-local:
+47 -26
View File
@@ -1,58 +1,79 @@
version: "3.8"
services: services:
traefik: traefik:
image: "traefik:v2.8.5" image: "traefik:v3.0.0"
container_name: "traefik" container_name: "traefik"
restart: unless-stopped
command: command:
# - "--log.level=DEBUG"
- "--accesslog" - "--accesslog"
- "--accesslog.filepath=/var/log/traefik/traefik.log" - "--accesslog.filepath=/var/log/traefik/access.log"
- "--api.insecure=true" - "--api.insecure=true"
- "--providers.docker=true" - "--providers.docker=true"
- "--providers.docker.exposedbydefault=false" - "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80" - "--entrypoints.web.address=:80"
- "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" - "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.3.0"
volumes: volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro" - "/var/run/docker.sock:/var/run/docker.sock:ro"
# - './ban.html:/ban.html:ro'
# - './captcha.html:/captcha.html:ro'
- "logs:/var/log/traefik" - "logs:/var/log/traefik"
- ./:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
- ./golang-ttl-map:/plugins-local/src/github.com/leprosus/golang-ttl-map
ports: ports:
- 8000:80 - 8000:80
- 8080:8080 - 8080:8080
depends_on:
- 'crowdsec'
whoami: whoami1:
image: traefik/whoami image: traefik/whoami
container_name: "simple-service" container_name: "simple-service-foo"
restart: unless-stopped
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`localhost`)" # Definition of the router
- "traefik.http.routers.whoami.entrypoints=web" - "traefik.http.routers.router-foo.rule=Path(`/foo`)"
- "traefik.http.routers.whoami.middlewares=testo@docker" - "traefik.http.routers.router-foo.entrypoints=web"
- "traefik.http.middlewares.testo.plugin.bouncer.enabled=true" - "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
- "traefik.http.middlewares.testo.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5" # Definition of the service
# - "traefik.http.routers.whoami.middlewares=blacklist@docker" - "traefik.http.services.service-foo.loadbalancer.server.port=80"
# - "traefik.http.middlewares.blacklist.ipblacklist.sourcerange=127.0.0.1/32, 192.168.1.7"
whoami2:
image: traefik/whoami
container_name: "simple-service-bar"
restart: unless-stopped
labels:
- "traefik.enable=true"
# Definition of the router
- "traefik.http.routers.router-bar.rule=Path(`/bar`)"
- "traefik.http.routers.router-bar.entrypoints=web"
- "traefik.http.routers.router-bar.middlewares=crowdsec@docker"
# Definition of the service
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
# Definitin of the middleware
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
# crowdseclapikey is the key to authenticate to crowdsec
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-1="
# enable AppSec real time check
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsecenabled=true"
# forwardedheaderstrustedips should be the IP of the proxy that is in front of traefik (if any)
- "traefik.http.middlewares.crowdsec.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
crowdsec: crowdsec:
image: crowdsecurity/crowdsec:v1.4.1 image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec" container_name: "crowdsec"
command: rm -rf /etc/crowdsec/acquis.yaml restart: unless-stopped
environment: environment:
COLLECTIONS: crowdsecurity/traefik COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
CUSTOM_HOSTNAME: crowdsec CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK: 40796d93c2958f9e58345514e67740e5 # We need to register one api key per service we will use
depends_on: BOUNCER_KEY_TRAEFIK: FIXME-LAPI-KEY-1=
- 'traefik'
volumes: volumes:
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro - ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
- logs:/var/log/traefik - logs:/var/log/traefik:ro
- crowdsec-db:/var/lib/crowdsec/data/ - crowdsec-db:/var/lib/crowdsec/data/
- crowdsec-config:/etc/crowdsec/ - crowdsec-config:/etc/crowdsec/
ports: labels:
- "8083:8080" - "traefik.enable=false"
volumes: volumes:
logs: logs:
+28
View File
@@ -0,0 +1,28 @@
# Example
## Enabling AppSec WAF feature from crowdsec
You mostly need to configure Crowdsec for this to work by enabling virtual patching and configuring some custom rules.
In the example we use a whoami container protected by crowdsec with virtual patching enabled.
The Traefik instance just needs to know where appsec engine is located
```yaml
labels:
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecappsecenabled=true"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecappsechost=crowdsec:7422"
```
We can try to query normally the whoami server:
```bash
curl http://localhost:8000/foo
```
And then we verify that a malicious request will be blocked:
```bash
curl http://localhost:8000/foo/rpc2
```
You should get a 403 on http://localhost:8000/foo/rpc2
To play the demo environment run:
```bash
make run_appsec
```
+12
View File
@@ -0,0 +1,12 @@
filenames:
- /var/log/traefik/access.log
labels:
type: traefik
---
listen_addr: 0.0.0.0:7422
appsec_config: crowdsecurity/virtual-patching
name: myAppSecComponent
source: appsec
labels:
type: appsec
@@ -0,0 +1,72 @@
version: "3.8"
services:
traefik:
image: "traefik:v3.0.0"
container_name: "traefik"
restart: unless-stopped
command:
# - "--log.level=DEBUG"
- "--accesslog"
- "--accesslog.filepath=/var/log/traefik/access.log"
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.3.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-appsec-enabled:/var/log/traefik
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 8000:80
- 8080:8080
depends_on:
- crowdsec
whoami1:
image: traefik/whoami
container_name: "simple-service-foo"
restart: unless-stopped
labels:
- "traefik.enable=true"
# Definition of the router
- "traefik.http.routers.router-foo.rule=PathPrefix(`/foo`)"
- "traefik.http.routers.router-foo.entrypoints=web"
- "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
# Definition of the service
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
# Definition of the middleware
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
# Enable AppSec
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsecenabled=true"
# Define AppSec host and port informations
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsechost=crowdsec:7422"
crowdsec:
image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
volumes:
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
- logs-appsec-enabled:/var/log/traefik:ro
- crowdsec-db-appsec-enabled:/var/lib/crowdsec/data/
- crowdsec-config-appsec-enabled:/etc/crowdsec/
labels:
- "traefik.enable=false"
volumes:
logs-appsec-enabled:
crowdsec-db-appsec-enabled:
crowdsec-config-appsec-enabled:
+21
View File
@@ -0,0 +1,21 @@
# Example
## Behind another proxy service (ex: clouflare)
You need to configure your Traefik to trust Forwarded headers by your front proxy
In the example we use another instance of traefik with the container named cloudflare to simulate a front proxy
The "internal" Traefik instance is configured to trust the cloudflare forward headers
This helps Traefik choose the right IP of the client: see https://doc.traefik.io/traefik/routing/entrypoints/#forwarded-headers
```yaml
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
```
We configure the middleware to trust as well the IP:
```yaml
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
```
To play the demo environment run:
```bash
make run_behindproxy
```
@@ -0,0 +1,15 @@
http:
# Add the router
routers:
router0:
entryPoints:
- web
service: service-foo
rule: Path(`/foo`)
# Add the service
services:
service-foo:
loadBalancer:
servers:
- url: http://traefik/foo:80
+101
View File
@@ -0,0 +1,101 @@
services:
cloudflare:
image: "traefik:v3.0.0"
container_name: "cloudflare"
restart: unless-stopped
command:
# - "--log.level=DEBUG"
- "--accesslog"
- "--accesslog.filepath=/var/log/traefik/access.log"
- "--api.insecure=true"
- "--entrypoints.web.address=:80"
- "--providers.file.filename=/cloud.yaml"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./cloudflare-example.yaml:/cloud.yaml:ro
- logs-cloudflare:/var/log/traefik
ports:
- 80:80
- 8080:8080
traefik:
image: "traefik:v3.0.0"
container_name: "traefik"
restart: unless-stopped
command:
# - "--log.level=DEBUG"
- "--accesslog"
- "--accesslog.filepath=/var/log/traefik/access.log"
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.3.0"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-traefik:/var/log/traefik
ports:
- 90:80
- 9080:8080
depends_on:
- crowdsec
whoami-foo:
image: traefik/whoami
container_name: "simple-service-foo-cloudflare"
restart: unless-stopped
labels:
- "traefik.enable=true"
# Definition of the router
- "traefik.http.routers.router-foo.rule=Path(`/foo`)"
- "traefik.http.routers.router-foo.entrypoints=web"
- "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
# Definition of the service
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
# Definition of the middleware
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=live"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
whoami-bar:
image: traefik/whoami
container_name: "simple-service-bar-cloudflare"
restart: unless-stopped
labels:
- "traefik.enable=true"
# Definition of the router
- "traefik.http.routers.router-bar.rule=Path(`/bar`)"
- "traefik.http.routers.router-bar.entrypoints=web"
- "traefik.http.routers.router-bar.middlewares=crowdsec@docker"
# Definition of the service
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
# Definition of the middleware
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
crowdsec:
image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
volumes:
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
- logs-cloudflare:/var/log/traefik:ro
- crowdsec-db-cloudflare:/var/lib/crowdsec/data/
- crowdsec-config-cloudflare:/etc/crowdsec/
labels:
- "traefik.enable=false"
volumes:
logs-traefik:
logs-cloudflare:
crowdsec-db-cloudflare:
crowdsec-config-cloudflare:
+1
View File
@@ -0,0 +1 @@
.vagrant/
+62
View File
@@ -0,0 +1,62 @@
# Example
### Install vagrant
##### On linux
```bash
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.asc
echo "deb [ signed-by=/usr/share/keyrings/hashicorp-archive-keyring.asc ] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install vagrant
```
### Install libvirt
```bash
sudo apt install -y qemu-kvm virt-manager libvirt-daemon-system virtinst libvirt-clients bridge-utils
sudo systemctl enable --now libvirtd
sudo systemctl start libvirtd
sudo usermod -aG kvm $USER
sudo usermod -aG libvirt $USER
```
### Install the plugin vagrant-libvirt
```bash
vagrant plugin install vagrant-libvirt
```
#### Start the VM
```bash
sudo vagrant up --provider=libvirt
```
#### Destroy the VM
```bash
sudo vagrant destroy -f
```
#### SSH in the VM
```bash
sudo vagrant ssh
```
### Context
Traefik is installed as a systemd service.
It is configured with the dashboard activated and listening on port 8081 and port 80 for the web
Crowdsec is started and listening on port 8080.
Certificates are generated on the provision step of vagrant.
Whoami is installed as a systemd service.
It is configured to listen on port 9000.
Whoami is accessible from traefik on port 80 at any domain and path
For example: curl http://localhost:80/test
The Plugin / Bouncer use certificates to validate the server certificates and authenticates with the Crowdsec local api.
+83
View File
@@ -0,0 +1,83 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure("2") do |config|
# The most common configuration options are documented and commented below.
# For a complete reference, please see the online documentation at
# https://docs.vagrantup.com.
# Every Vagrant development environment requires a box. You can search for
# boxes at https://vagrantcloud.com/search.
config.vm.box = "generic/debian11"
# Disable automatic box update checking. If you disable this, then
# boxes will only be checked for updates when the user runs
# `vagrant box outdated`. This is not recommended.
# config.vm.box_check_update = false
# Create a forwarded port mapping which allows access to a specific port
# within the machine from a port on the host machine. In the example below,
# accessing "localhost:8080" will access port 80 on the guest machine.
# NOTE: This will enable public access to the opened port
# config.vm.network "forwarded_port", guest: 80, host: 8080
# Create a forwarded port mapping which allows access to a specific port
# within the machine from a port on the host machine and only allow access
# via 127.0.0.1 to disable public access
# config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"
config.vm.network "forwarded_port", guest: 8081, host: 8081
config.vm.network "forwarded_port", guest: 80, host: 80
# Create a private network, which allows host-only access to the machine
# using a specific IP.
# config.vm.network "private_network", ip: "192.168.33.10"
# Create a public network, which generally matched to bridged network.
# Bridged networks make the machine appear as another physical device on
# your network.
# config.vm.network "public_network"
# Share an additional folder to the guest VM. The first argument is
# the path on the host to the actual folder. The second argument is
# the path on the guest to mount the folder. And the optional third
# argument is a set of non-required options.
# Provider-specific configuration so you can fine-tune various
# backing providers for Vagrant. These expose provider-specific options.
# Example for VirtualBox:
#
config.vm.provider "libvirt" do |lv|
graphics_type = "none"
lv.cpus = 1
# lv.vm.network :private_network, :ip => "10.20.30.40"
# Customize the amount of memory on the VM:
lv.memory = "2048"
end
# config.vm.provider "virtualbox" do |vb|
# vb.gui = false
# vb.cpus = 1
# # vb.vm.network :private_network, :ip => "10.20.30.40"
# # Customize the amount of memory on the VM:
# vb.memory = "2048"
# end
config.vm.provision "file", source: "./files", destination: "/home/vagrant/vagrant_data"
config.vm.provision "file", source: "./scripts", destination: "/home/vagrant/scripts"
# config.vm.provision "file", source: "../../", destination: "/home/vagrant/plugin"
config.vm.provision "shell", path: "scripts/install_traefik.sh"
config.vm.provision "shell", path: "scripts/configure_traefik.sh"
config.vm.provision "shell", path: "scripts/install_whoami.sh"
config.vm.provision "shell", path: "scripts/install_crowdsec.sh"
config.vm.provision "shell", path: "scripts/configure_crowdsec_certs.sh"
end
@@ -0,0 +1,4 @@
filenames:
- /var/log/traefik/access.log
labels:
type: traefik
@@ -0,0 +1,16 @@
{
"CN": "myagent",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "FR",
"L": "Paris",
"O": "Crowdsec",
"OU": "agent-ou",
"ST": "France"
}
]
}
@@ -0,0 +1,17 @@
{
"CN": "whoami",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "FR",
"L": "Paris",
"O": "Crowdsec",
"OU": "bouncer-ou",
"ST": "France"
}
]
}
@@ -0,0 +1,16 @@
{
"CN": "CrowdSec Test CA",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "FR",
"L": "Paris",
"O": "Crowdsec",
"OU": "Crowdsec",
"ST": "France"
}
]
}
@@ -0,0 +1,19 @@
{
"CN": "CrowdSec Test CA Intermediate",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "FR",
"L": "Paris",
"O": "Crowdsec",
"OU": "Crowdsec Intermediate",
"ST": "France"
}
],
"ca": {
"expiry": "42720h"
}
}
@@ -0,0 +1,44 @@
{
"signing": {
"default": {
"expiry": "8760h"
},
"profiles": {
"intermediate_ca": {
"usages": [
"signing",
"digital signature",
"key encipherment",
"cert sign",
"crl sign",
"server auth",
"client auth"
],
"expiry": "8760h",
"ca_constraint": {
"is_ca": true,
"max_path_len": 0,
"max_path_len_zero": true
}
},
"server": {
"usages": [
"signing",
"digital signing",
"key encipherment",
"server auth"
],
"expiry": "8760h"
},
"client": {
"usages": [
"signing",
"digital signature",
"key encipherment",
"client auth"
],
"expiry": "8760h"
}
}
}
}
@@ -0,0 +1,20 @@
{
"CN": "localhost",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "FR",
"L": "Paris",
"O": "Crowdsec",
"OU": "Crowdsec Server",
"ST": "France"
}
],
"hosts": [
"127.0.0.1",
"localhost"
]
}
@@ -0,0 +1,62 @@
common:
daemonize: true
pid_dir: /var/run/
log_media: file
log_level: debug
log_dir: /var/log/
log_max_size: 20
compress_logs: true
log_max_files: 10
working_dir: .
config_paths:
config_dir: /etc/crowdsec/
data_dir: /var/lib/crowdsec/data/
simulation_path: /etc/crowdsec/simulation.yaml
hub_dir: /etc/crowdsec/hub/
index_path: /etc/crowdsec/hub/.index.json
notification_dir: /etc/crowdsec/notifications/
plugin_dir: /usr/lib/crowdsec/plugins/
crowdsec_service:
acquisition_path: /etc/crowdsec/acquis.yaml
acquisition_dir: /etc/crowdsec/acquis.d
parser_routines: 1
cscli:
output: human
color: auto
db_config:
log_level: info
type: sqlite
db_path: /var/lib/crowdsec/data/crowdsec.db
#max_open_conns: 100
#user:
#password:
#db_name:
#host:
#port:
flush:
max_items: 5000
max_age: 7d
plugin_config:
user: nobody # plugin process would be ran on behalf of this user
group: nogroup # plugin process would be ran on behalf of this group
api:
client:
insecure_skip_verify: false
credentials_path: /etc/crowdsec/local_api_credentials.yaml
server:
log_level: debug
listen_uri: 127.0.0.1:8080
profiles_path: /etc/crowdsec/profiles.yaml
console_path: /etc/crowdsec/console.yaml
online_client: # Central API credentials (to push signals and receive bad IPs)
credentials_path: /etc/crowdsec/online_api_credentials.yaml
trusted_ips: # IP ranges, or IPs which can have admin API access
- 127.0.0.1
tls:
cert_file: /etc/crowdsec/certs/server.pem #Server side cert
key_file: /etc/crowdsec/certs/server-key.pem #Server side key
ca_cert_path: /etc/crowdsec/certs/inter.pem #CA used to verify the client certs
bouncers_allowed_ou: #OU allowed for bouncers
- bouncer-ou
agents_allowed_ou: #OU allowed for agents
- agent-ou
@@ -0,0 +1,4 @@
url: https://localhost:8080
ca_cert_path: /etc/crowdsec/certs/inter.pem #CA to trust the server certificate
key_path: /etc/crowdsec/certs/agent-key.pem #Client key
cert_path: /etc/crowdsec/certs/agent.pem #Client cert
@@ -0,0 +1,31 @@
http:
routers:
to-whoami-http-service:
rule: "PathPrefix(`/`)"
service: whoami-service
middlewares:
- "crowdsec-whoami"
entryPoints:
- web
services:
whoami-service:
loadBalancer:
servers:
- url: "http://localhost:9000/"
middlewares:
crowdsec-whoami:
plugin:
bouncer:
enabled: true
crowdseclapikey: "whoami-demo"
updateintervalseconds: 60
crowdsecmode: live
loglevel: "DEBUG"
crowdsecLapiScheme: https
crowdsecLapiHost: localhost:8080
crowdsecLapiTLSInsecureVerify: false
crowdsecLapiTLSCertificateAuthorityFile: /etc/traefik/crowdsec-certs/inter.pem
crowdsecLapiTLSCertificateBouncerFile: /etc/traefik/crowdsec-certs/bouncer.pem
crowdsecLapiTLSCertificateBouncerKeyFile: /etc/traefik/crowdsec-certs/bouncer-key.pem
@@ -0,0 +1,40 @@
[Unit]
Description=traefik proxy
After=network-online.target
Wants=network-online.target systemd-networkd-wait-online.service
[Service]
Restart=on-abnormal
; User and group the process will run as.
User=traefik
Group=traefik
; Always set "-root" to something safe in case it gets forgotten in the traefikfile.
ExecStart=/usr/local/bin/traefik --configfile=/etc/traefik/traefik.yml
WorkingDirectory=/etc/traefik
; Limit the number of file descriptors; see `man systemd.exec` for more limit settings.
LimitNOFILE=1048576
; Use private /tmp and /var/tmp, which are discarded after traefik stops.
PrivateTmp=true
; Use a minimal /dev (May bring additional security if switched to 'true', but it may not work on Raspberry Pi's or other devices, so it has been disabled in this dist.)
PrivateDevices=false
; Hide /home, /root, and /run/user. Nobody will steal your SSH-keys.
ProtectHome=true
; Make /usr, /boot, /etc and possibly some more folders read-only.
;ProtectSystem=full
; … except /etc/ssl/traefik, because we want Letsencrypt-certificates there.
; This merely retains r/w access rights, it does not add any new. Must still be writable on the host!
;ReadWriteDirectories=/etc/traefik/acme
;ReadWriteDirectories=/etc/traefik/plugins-storage
; The following additional security directives only work with systemd v229 or later.
; They further restrict privileges that can be gained by traefik. Uncomment if you like.
; Note that you may have to add capabilities required by any plugins in use.
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,100 @@
################################################################
# Global configuration
################################################################
global:
checkNewVersion: false
sendAnonymousUsage: false
################################################################
# EntryPoints configuration
################################################################
entryPoints:
web:
address: :80
traefik:
address: :8081
################################################################
# Provider file configuration
################################################################
providers:
file:
directory: "/etc/traefik/conf"
################################################################
# Plugin configuration
################################################################
experimental:
plugins:
bouncer:
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
version: v1.1.6
################################################################
# Certificate Resolver
################################################################
serversTransport:
insecureSkipVerify: false
################################################################
# Traefik logs configuration
################################################################
# Traefik logs
# Enabled by default and log to stdout
log:
filePath: /var/log/traefik/traefik.log
level: DEBUG
# format: json
################################################################
# Access logs configuration
################################################################
# Enable access logs
# By default it will write to stdout and produce logs in the textual
# Common Log Format (CLF), extended with additional fields.
accessLog:
# Sets the file path for the access log. If not specified, stdout will be used.
# Intermediate directories are created if necessary.
filePath: /var/log/traefik/access.log
fields:
defaultMode: keep
names:
ClientUsername: keep
headers:
defaultMode: keep
# Format is either "json" or "common".
#
# Optional
# Default: "common"
#
# format: json
################################################################
# API and dashboard configuration
################################################################
# Enable API and dashboard
#
# Optional
#
api:
# Enable the API in insecure mode
#
# Optional
# Default: false
#
insecure: true
# Enabled Dashboard
#
# Optional
# Default: true
#
dashboard: true
+31
View File
@@ -0,0 +1,31 @@
[Unit]
Description=whoami web server
After=network-online.target
Wants=network-online.target systemd-networkd-wait-online.service
[Service]
Restart=on-abnormal
; User and group the process will run as.
User=whoami
Group=whoami
; Always set "-root" to something safe in case it gets forgotten in the traefikfile.
ExecStart=/usr/local/bin/whoami --port=9000
; Limit the number of file descriptors; see `man systemd.exec` for more limit settings.
LimitNOFILE=1048576
; Use private /tmp and /var/tmp, which are discarded after traefik stops.
PrivateTmp=true
; Use a minimal /dev (May bring additional security if switched to 'true', but it may not work on Raspberry Pi's or other devices, so it has been disabled in this dist.)
PrivateDevices=false
; Hide /home, /root, and /run/user. Nobody will steal your SSH-keys.
ProtectHome=true
; Make /usr, /boot, /etc and possibly some more folders read-only.
ProtectSystem=full
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,46 @@
#!/bin/bash
basepath="/home/vagrant/vagrant_data/crowdsec/certs"
VERSION=$(curl --silent "https://api.github.com/repos/cloudflare/cfssl/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
VNUMBER=${VERSION#"v"}
wget https://github.com/cloudflare/cfssl/releases/download/${VERSION}/cfssl_${VNUMBER}_linux_amd64 -O cfssl
chmod +x cfssl
sudo mv cfssl /usr/local/bin
VERSION=$(curl --silent "https://api.github.com/repos/cloudflare/cfssl/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
VNUMBER=${VERSION#"v"}
wget https://github.com/cloudflare/cfssl/releases/download/${VERSION}/cfssljson_${VNUMBER}_linux_amd64 -O cfssljson
chmod +x cfssljson
sudo mv cfssljson /usr/local/bin
cfssljson -version
mkdir -p /etc/crowdsec/certs
# Generate the CA
cfssl gencert --initca ${basepath}/ca.json 2>/dev/null | cfssljson --bare "/etc/crowdsec/certs/ca"
# Generate an intermediate certificate that will be used to sign the client certificates
cfssl gencert --initca ${basepath}/intermediate.json 2>/dev/null | cfssljson --bare "/etc/crowdsec/certs/inter"
cfssl sign -ca "/etc/crowdsec/certs/ca.pem" -ca-key "/etc/crowdsec/certs/ca-key.pem" -config ${basepath}/profiles.json -profile intermediate_ca "/etc/crowdsec/certs/inter.csr" 2>/dev/null | cfssljson --bare "/etc/crowdsec/certs/inter"
# Generate a server side certificate
cfssl gencert -ca "/etc/crowdsec/certs/inter.pem" -ca-key "/etc/crowdsec/certs/inter-key.pem" -config ${basepath}/profiles.json -profile=server ${basepath}/server.json 2>/dev/null | cfssljson --bare "/etc/crowdsec/certs/server"
# Generate a client certificate for the bouncer whoami
cfssl gencert -ca "/etc/crowdsec/certs/inter.pem" -ca-key "/etc/crowdsec/certs/inter-key.pem" -config ${basepath}/profiles.json -profile=client ${basepath}/bouncer.json 2>/dev/null | cfssljson --bare "/etc/crowdsec/certs/bouncer"
# Generate a client certificate for the agent
cfssl gencert -ca "/etc/crowdsec/certs/inter.pem" -ca-key "/etc/crowdsec/certs/inter-key.pem" -config ${basepath}/profiles.json -profile=client ${basepath}/agent.json 2>/dev/null | cfssljson --bare "/etc/crowdsec/certs/agent"
cp /home/vagrant/vagrant_data/crowdsec/config/config.yaml /etc/crowdsec/config.yaml.local
cp /home/vagrant/vagrant_data/crowdsec/config/local_api_credentials.yaml /etc/crowdsec/
chmod +r /etc/crowdsec/config.yaml
chmod +r /etc/crowdsec/local_api_credentials.yaml
systemctl restart crowdsec
mkdir /etc/traefik/crowdsec-certs
cp /etc/crowdsec/certs/inter.pem /etc/traefik/crowdsec-certs/inter.pem
cp /etc/crowdsec/certs/bouncer.pem /etc/traefik/crowdsec-certs/bouncer.pem
cp /etc/crowdsec/certs/bouncer-key.pem /etc/traefik/crowdsec-certs/bouncer-key.pem
chown -R traefik:traefik /etc/traefik/crowdsec-certs/
systemctl restart traefik
@@ -0,0 +1,11 @@
#!/bin/bash
sudo cp /home/vagrant/vagrant_data/traefik/traefik.yml /etc/traefik/traefik.yml
sudo cp -a /home/vagrant/vagrant_data/traefik/conf /etc/traefik/
sudo chown -R traefik:traefik /etc/traefik
sudo mkdir /var/log/traefik
sudo chown -R traefik:traefik /var/log/traefik
sudo systemctl restart traefik.service
sudo systemctl status traefik.service
@@ -0,0 +1,5 @@
#!/bin/bash
DEBIAN_FRONTEND=noninteractive curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | sudo bash
DEBIAN_FRONTEND=noninteractive sudo apt install crowdsec -y
sudo cp /home/vagrant/vagrant_data/crowdsec/acquis.yaml /etc/crowdsec/acquis.yaml
@@ -0,0 +1,32 @@
#!/bin/bash
DEBIAN_FRONTEND=noninteractive sudo apt-get update && sudo apt-get install wget -y
# DEBIAN_FRONTEND=noninteractive sudo apt-get upgrade -y --assume-yes
wget -O traefik.tar.gz "https://github.com/traefik/traefik/releases/download/v3.0.0/traefik_v3.0.0_linux_amd64.tar.gz"
tar -zxvf traefik.tar.gz
# inspired from https://gist.github.com/ubergesundheit/7c9d875befc2d7bfd0bf43d8b3862d85
sudo mv ./traefik /usr/local/bin/
sudo chown root:root /usr/local/bin/traefik
sudo chmod 755 /usr/local/bin/traefik
sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/traefik
sudo groupadd -g 321 traefik
sudo useradd \
-g traefik --no-user-group \
--home-dir /var/www --no-create-home \
--shell /usr/sbin/nologin \
--system --uid 321 traefik
sudo mkdir /etc/traefik
sudo mkdir /etc/traefik/acme
sudo mkdir /etc/traefik/plugins-storage
sudo chown -R root:root /etc/traefik
sudo chown -R traefik:traefik /etc/traefik/acme
sudo chown -R traefik:traefik /etc/traefik/plugins-storage
sudo cp /home/vagrant/vagrant_data/traefik/traefik.service /etc/systemd/system/
sudo chown root:root /etc/systemd/system/traefik.service
sudo chmod 644 /etc/systemd/system/traefik.service
sudo systemctl daemon-reload
sudo systemctl start traefik.service
sudo systemctl enable traefik.service
@@ -0,0 +1,23 @@
#!/bin/bash
DEBIAN_FRONTEND=noninteractive sudo apt-get update && apt-get install wget -y
wget -O whoami.tar.gz "https://github.com/traefik/whoami/releases/download/v1.10.1/whoami_v1.10.1_linux_amd64.tar.gz"
tar -zxvf whoami.tar.gz
# inspired from https://gist.github.com/ubergesundheit/7c9d875befc2d7bfd0bf43d8b3862d85
sudo mv ./whoami /usr/local/bin/
sudo chown root:root /usr/local/bin/whoami
sudo chmod 755 /usr/local/bin/whoami
sudo groupadd -g 322 whoami
sudo useradd \
-g whoami --no-user-group \
--home-dir /var/www --no-create-home \
--shell /usr/sbin/nologin \
--system --uid 322 whoami
sudo cp /home/vagrant/vagrant_data/whoami.service /etc/systemd/system/
sudo chown root:root /etc/systemd/system/whoami.service
sudo chmod 644 /etc/systemd/system/whoami.service
sudo systemctl daemon-reload
sudo systemctl start whoami.service
sudo systemctl enable whoami.service
+161
View File
@@ -0,0 +1,161 @@
# Example
## Enabling catpcha response from crowdsec
Crowdsec support 3 remediations solutions `ban`, `captcha`, and `throttle`.
This plugins support the `ban` and `captcha` remediation.
### Traefik configuration
The minimal configuration is defined below.
For now 3 captcha providers are supported:
- [hcaptcha](https://www.hcaptcha.com/)
- [recaptcha](https://www.google.com/recaptcha/about/)
- [turnstile](https://www.cloudflare.com/fr-fr/products/turnstile/)
```yaml
labels:
# Choose captcha provider
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaProvider=hcaptcha"
# Define captcha site key
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaSiteKey=FIXME"
# Define captcha secret key
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaSecretKey=FIXME"
# Define captcha grade period seconds
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaGracePeriodSeconds=1800"
# Define captcha HTML file path
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaHTMLFilePath=/captcha.html"
```
The captcha HTML file must be present in the Traefik container (bind mounted or added during a custom build).
It is not directly accessible from Traefik even when importing the plugin, so [download](https://raw.githubusercontent.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/master/captcha.html) it locally to expose it to Traefik.
```yaml
...
traefik:
image: "traefik:v3.0.0"
volumes:
- './captcha.html:/captcha.html'
...
```
### Crowdsec configuration
Crowdsec by default will take the ban action on suspicious activity detected in logs.
To instruct Crowdsec to use captcha remediation, change the `/etc/crowdsec/profiles.yaml`.
2 modes are supported:
- Always return a captcha decision
- Return a captcha decision the first X times and then a ban decision.
The second mode could be used to prevent repeated malicious activity.
More information is available on configuring Crowdsec in the [official documentation](https://docs.crowdsec.net/docs/next/profiles/captcha_profile/).
```yaml
...
crowdsec:
image: crowdsecurity/crowdsec:v1.6.1-2
volumes:
# For captcha and ban mixed decision
- './profiles.yaml:/etc/crowdsec/profiles.yaml:ro'
# For captcha only remediation
# - './profiles_captcha_only.yaml:/etc/crowdsec/profiles.yaml:ro'
...
```
## Exemple navigation
We can try to query normally the whoami server:
```bash
curl http://localhost:8000/foo
```
We can try to ban ourself
```bash
docker exec crowdsec cscli decisions add --ip 10.0.0.20 -d 4h --type captcha
```
![image decision captcha](image_decision_captcha.png)
We will see in the browser the captcha validation page:
![image captcha validation](image_captcha_validation.png)
To play the demo environment run:
```bash
make run_captcha
```
> Note, if we are banned with a "ban" decision from crowdsec a captcha will not be asked and you will have to wait for the decision to expire or remove it manually.
```bash
docker exec crowdsec cscli decisions add --ip 10.0.0.10 -d 10m --type ban
```
## Captcha Workflow
> Context: The user has no decision attached to his IP
```mermaid
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
```
> Context: The user has a captcha decision attached to his IP
```mermaid
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
```
> Context: The user has a ban decision attached to his IP
```mermaid
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
TraefikPlugin->>User: No, HTTP 403
```
## Captcha Configuration:
- Recaptcha
Choose v2 (challenge) and configure the domain to protect:
![Recaptcha image](image_recaptcha.png)
- Turnstile
TODO
- Hcatpcha
TODO
+4
View File
@@ -0,0 +1,4 @@
filenames:
- /var/log/traefik/access.log
labels:
type: traefik
+338
View File
@@ -0,0 +1,338 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>CrowdSec Captcha</title>
<meta content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
/*! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com*/
*,
:after,
:before {
border: 0 solid #e5e7eb;
box-sizing: border-box
}
:after,
:before {
--tw-content: ""
}
html {
-webkit-text-size-adjust: 100%;
font-feature-settings: normal;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4
}
body {
line-height: inherit;
margin: 0
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit
}
a {
color: inherit;
text-decoration: inherit
}
h1,
h2,
h3,
h4,
h5,
h6,
hr,
p,
pre {
margin: 0
}
*,
::backdrop,
:after,
:before {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #3b82f680;
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia:
}
.flex {
display: flex
}
.flex-wrap {
flex-wrap: wrap
}
.inline-flex {
display: inline-flex
}
.h-24 {
height: 6rem
}
.h-6 {
height: 1.5rem
}
.h-full {
height: 100%
}
.h-screen {
height: 100vh
}
.text-center {
text-align: center
}
.w-24 {
width: 6rem
}
.w-6 {
width: 1.5rem
}
.w-full {
width: 100%
}
.w-screen {
width: 100vw
}
.my-3 {
margin-top: 0.75rem;
margin-bottom: 0.75rem
}
.flex-col {
flex-direction: column
}
.items-center {
align-items: center
}
.justify-center {
justify-content: center
}
.justify-between {
justify-content: space-between
}
.space-y-1>:not([hidden])~:not([hidden]) {
--tw-space-y-reverse: 0;
margin-bottom: calc(.25rem*var(--tw-space-y-reverse));
margin-top: calc(.25rem*(1 - var(--tw-space-y-reverse)))
}
.space-y-4>:not([hidden])~:not([hidden]) {
--tw-space-y-reverse: 0;
margin-bottom: calc(1rem*var(--tw-space-y-reverse));
margin-top: calc(1rem*(1 - var(--tw-space-y-reverse)))
}
.rounded-xl {
border-radius: .75rem
}
.border-2 {
border-width: 2px
}
.border-black {
--tw-border-opacity: 1;
border-color: rgb(0 0 0/var(--tw-border-opacity))
}
.p-4 {
padding: 1rem
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem
}
.py-2 {
padding-bottom: .5rem;
padding-top: .5rem
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem
}
.text-sm {
font-size: .875rem;
line-height: 1.25rem
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem
}
.font-bold {
font-weight: 700
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255/var(--tw-text-opacity))
}
@media (min-width:640px) {
.sm\:w-2\/3 {
width: 66.666667%
}
}
@media (min-width:768px) {
.md\:flex-row {
flex-direction: row
}
}
@media (min-width:1024px) {
.lg\:w-1\/2 {
width: 50%
}
.lg\:text-3xl {
font-size: 1.875rem;
line-height: 2.25rem
}
.lg\:text-xl {
font-size: 1.25rem;
line-height: 1.75rem
}
}
@media (min-width:1280px) {
.xl\:text-4xl {
font-size: 2.25rem;
line-height: 2.5rem
}
}
</style>
<script src="{{ .FrontendJS }}" async defer></script>
</head>
<body class="h-screen w-screen p-4">
<div class="h-full w-full flex flex-col justify-center items-center">
<div class="border-2 border-black rounded-xl p-4 text-center w-full sm:w-2/3 lg:w-1/2">
<div class="flex flex-col items-center space-y-4">
<svg fill="black" class="h-24 w-24" aria-hidden="true" focusable="false" data-prefix="fas"
data-icon="exclamation-triangle" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"
class="warning">
<path
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z">
</path>
</svg>
<h1 class="text-2xl lg:text-3xl xl:text-4xl">CrowdSec Captcha</h1>
</div>
<form action="" method="POST" class="flex flex-col space-y-1" id="captcha-form">
<div id="captcha" class="{{ .FrontendKey }}" data-sitekey="{{ .SiteKey }}" data-callback="captchaCallback">
</div>
</form>
<div class="flex justify-center flex-wrap">
<p class="my-3">This security check has been powered by</p>
<a href="https://crowdsec.net/" target="_blank" rel="noopener" class="inline-flex flex-col items-center">
<svg fill="black" width="33.92" height="33.76" viewBox="0 0 254.4 253.2">
<defs>
<clipPath id="a">
<path d="M0 52h84v201.2H0zm0 0" />
</clipPath>
<clipPath id="b">
<path d="M170 52h84.4v201.2H170zm0 0" />
</clipPath>
</defs>
<path
d="M59.3 128.4c1.4 2.3 2.5 4.6 3.4 7-1-4.1-2.3-8.1-4.3-12-3.1-6-7.8-5.8-10.7 0-2 4-3.2 8-4.3 12.1 1-2.4 2-4.8 3.4-7.1 3.4-5.8 8.8-6 12.5 0M207.8 128.4a42.9 42.9 0 013.4 7c-1-4.1-2.3-8.1-4.3-12-3.2-6-7.8-5.8-10.7 0-2 4-3.3 8-4.3 12.1.9-2.4 2-4.8 3.4-7.1 3.4-5.8 8.8-6 12.5 0M134.6 92.9c2 3.5 3.6 7 4.8 10.7-1.3-5.4-3-10.6-5.6-15.7-4-7.5-9.7-7.2-13.3 0a75.4 75.4 0 00-5.6 16c1.2-3.8 2.7-7.4 4.7-11 4.1-7.2 10.6-7.5 15 0M43.8 136.8c.9 4.6 3.7 8.3 7.3 9.2 0 2.7 0 5.5.2 8.2.3 3.3.4 6.6 1 9.6.3 2.3 1 2.2 1.3 0 .5-3 .6-6.3 1-9.6l.2-8.2c3.5-1 6.4-4.6 7.2-9.2a17.8 17.8 0 01-9 2.4c-3.5 0-6.6-1-9.2-2.4M192.4 136.8c.8 4.6 3.7 8.3 7.2 9.2 0 2.7 0 5.5.3 8.2.3 3.3.4 6.6 1 9.6.3 2.3.9 2.2 1.2 0 .6-3 .7-6.3 1-9.6.2-2.7.3-5.5.2-8.2 3.6-1 6.4-4.6 7.3-9.2a17.8 17.8 0 01-9.1 2.4c-3.4 0-6.6-1-9.1-2.4M138.3 104.6c-3.1 1.9-7 3-11.3 3-4.3 0-8.2-1.1-11.3-3 1 5.8 4.5 10.3 9 11.5 0 3.4 0 6.8.3 10.2.4 4.1.5 8.2 1.2 12 .4 2.9 1.2 2.7 1.6 0 .7-3.8.8-7.9 1.2-12 .3-3.4.3-6.8.3-10.2 4.5-1.2 8-5.7 9-11.5" />
<path
d="M51 146c0 2.7.1 5.5.3 8.2.3 3.3.4 6.6 1 9.6.3 2.3 1 2.2 1.3 0 .5-3 .6-6.3 1-9.6l.2-8.2c3.5-1 6.4-4.6 7.2-9.2a17.8 17.8 0 01-9 2.4c-3.5 0-6.6-1-9.2-2.4.9 4.6 3.7 8.3 7.3 9.2M143.9 105c-1.9-.4-3.5-1.2-4.9-2.3 1.4 5.6 2.5 11.3 4 17 1.2 5 2 10 2.4 15 .6 7.8-4.5 14.5-10.9 14.5h-15c-6.4 0-11.5-6.7-11-14.5.5-5 1.3-10 2.6-15 1.3-5.3 2.3-10.5 3.6-15.7-2.2 1.2-4.8 1.9-7.7 2-4.7.1-9.4-.3-14-1-4-.4-6.7-3-8-6.7-1.3-3.4-2-7-3.3-10.4-.5-1.5-1.6-2.8-2.4-4.2-.4-.6-.8-1.2-.9-1.8v-7.8a77 77 0 0124.5-3c6.1 0 12 1 17.8 3.2 4.7 1.7 9.7 1.8 14.4 0 9-3.4 18.2-3.8 27.5-3 4.9.5 9.8 1.6 14.8 2.4v8.2c0 .6-.3 1.5-.7 1.7-2 .9-2.2 2.7-2.7 4.5-.9 3.2-1.8 6.4-2.9 9.5a11 11 0 01-8.8 7.7 40.6 40.6 0 01-18.4-.2m29.4 80.6c-3.2-26.8-6.4-50-8.9-60.7a14.3 14.3 0 0014.1-14h.4a9 9 0 005.6-16.5 14.3 14.3 0 00-3.7-27.2 9 9 0 00-6.9-14.6c2.4-1.1 4.5-3 5.8-5 3.4-5.3 4-29-8-44.4-5-6.3-9.8-2.5-10 1.8-1 13.2-1.1 23-4.5 34.3a9 9 0 00-16-4.1 14.3 14.3 0 00-28.4 0 9 9 0 00-16 4.1c-3.4-11.2-3.5-21.1-4.4-34.3-.3-4.3-5.2-8-10-1.8-12 15.3-11.5 39-8.1 44.4 1.3 2 3.4 3.9 5.8 5a9 9 0 00-7 14.6 14.3 14.3 0 00-3.6 27.2A9 9 0 0075 111h.5a14.5 14.5 0 0014.3 14c-4 17.2-10 66.3-15 111.3l-1.3 13.4a1656.4 1656.4 0 01106.6 0l-1.4-12.7-5.4-51.3" />
<g clip-path="url(#a)">
<path
d="M83.5 136.6l-2.3.7c-5 1-9.8 1-14.8-.2-1.4-.3-2.7-1-3.8-1.9l3.1 13.7c1 4 1.7 8 2 12 .5 6.3-3.6 11.6-8.7 11.6H46.9c-5.1 0-9.2-5.3-8.7-11.6.3-4 1-8 2-12 1-4.2 1.8-8.5 2.9-12.6-1.8 1-3.9 1.5-6.3 1.6a71 71 0 01-11.1-.7 7.7 7.7 0 01-6.5-5.5c-1-2.7-1.6-5.6-2.6-8.3-.4-1.2-1.3-2.3-2-3.4-.2-.4-.6-1-.6-1.4v-6.3c6.4-2 13-2.6 19.6-2.5 4.9.1 9.6 1 14.2 2.6 3.9 1.4 7.9 1.5 11.7 0 1.8-.7 3.6-1.2 5.5-1.6a13 13 0 01-1.6-15.5A18.3 18.3 0 0159 73.1a11.5 11.5 0 00-17.4 8.1 7.2 7.2 0 00-12.9 3.3c-2.7-9-2.8-17-3.6-27.5-.2-3.4-4-6.5-8-1.4C7.5 67.8 7.9 86.9 10.6 91c1.1 1.7 2.8 3.1 4.7 4a7.2 7.2 0 00-5.6 11.7 11.5 11.5 0 00-2.9 21.9 7.2 7.2 0 004.5 13.2h.3c0 .6 0 1.1.2 1.7.9 5.4 5.6 9.5 11.3 9.5A1177.2 1177.2 0 0010 253.2c18.1-1.5 38.1-2.6 59.5-3.4.4-4.6.8-9.3 1.4-14 1.2-11.6 3.3-30.5 5.7-49.7 2.2-18 4.7-36.3 7-49.5" />
</g>
<g clip-path="url(#b)">
<path
d="M254.4 118.2c0-5.8-4.2-10.5-9.7-11.4a7.2 7.2 0 00-5.6-11.7c2-.9 3.6-2.3 4.7-4 2.7-4.2 3.1-23.3-6.5-35.5-4-5.1-7.8-2-8 1.4-.8 10.5-.9 18.5-3.6 27.5a7.2 7.2 0 00-12.8-3.3 11.5 11.5 0 00-17.8-7.9 18.4 18.4 0 01-4.5 22 13 13 0 01-1.3 15.2c2.4.5 4.8 1 7.1 2 3.8 1.3 7.8 1.4 11.6 0 7.2-2.8 14.6-3 22-2.4 4 .4 7.9 1.2 12 1.9l-.1 6.6c0 .5-.2 1.2-.5 1.3-1.7.7-1.8 2.2-2.2 3.7l-2.3 7.6a8.8 8.8 0 01-7 6.1c-5 1-10 1-14.9-.2-1.5-.3-2.8-1-3.9-1.9 1.2 4.5 2 9.1 3.2 13.7 1 4 1.6 8 2 12 .4 6.3-3.6 11.6-8.8 11.6h-12c-5.2 0-9.3-5.3-8.8-11.6.4-4 1-8 2-12 1-4.2 1.9-8.5 3-12.6-1.8 1-4 1.5-6.3 1.6-3.7 0-7.5-.3-11.2-.7a7.7 7.7 0 01-3.7-1.5c3.1 18.4 7.1 51.2 12.5 100.9l.6 5.3.8 7.9c21.4.7 41.5 1.9 59.7 3.4L243 243l-4.4-41.2a606 606 0 00-7-48.7 11.5 11.5 0 0011.2-11.2h.4a7.2 7.2 0 004.4-13.2c4-1.8 6.8-5.8 6.8-10.5" />
</g>
<path
d="M180 249.6h.4a6946 6946 0 00-7.1-63.9l5.4 51.3 1.4 12.6M164.4 125c2.5 10.7 5.7 33.9 8.9 60.7a570.9 570.9 0 00-8.9-60.7M74.8 236.3l-1.4 13.4 1.4-13.4" />
</svg>
<span>CrowdSec</span>
</a>
</div>
</div>
</div>
<script>
function captchaCallback() {
setTimeout(() => document.querySelector('#captcha-form').submit(), 500);
}
</script>
</body>
</html>
+80
View File
@@ -0,0 +1,80 @@
services:
traefik:
image: "traefik:v3.0.0"
container_name: "traefik"
restart: unless-stopped
command:
# - "--log.level=DEBUG"
- "--accesslog"
- "--accesslog.filepath=/var/log/traefik/access.log"
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.3.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-captcha-enabled:/var/log/traefik
- './captcha.html:/captcha.html'
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 8000:80
- 8080:8080
depends_on:
- crowdsec
whoami1:
image: traefik/whoami
container_name: "simple-service-foo"
restart: unless-stopped
labels:
- "traefik.enable=true"
# Definition of the router
- "traefik.http.routers.router-foo.rule=PathPrefix(`/foo`)"
- "traefik.http.routers.router-foo.entrypoints=web"
- "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
# Definition of the service
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
# Definition of the middleware
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
# Choose captcha provider
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaProvider=hcaptcha"
# Define captcha site key
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaSiteKey=FIXME"
# Define captcha secret key
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaSecretKey=FIXME"
# Define captcha grade period seconds
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaGracePeriodSeconds=20"
# Define captcha HTML file path
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaHTMLFilePath=/captcha.html"
crowdsec:
image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
volumes:
# For captcha and ban mixed decision
- './profiles.yaml:/etc/crowdsec/profiles.yaml:ro'
# For captcha only remediation
# - './profiles_captcha_only.yaml:/etc/crowdsec/profiles.yaml:ro'
- './acquis.yaml:/etc/crowdsec/acquis.yaml:ro'
- logs-captcha-enabled:/var/log/traefik:ro
- crowdsec-db-captcha-enabled:/var/lib/crowdsec/data/
- crowdsec-config-captcha-enabled:/etc/crowdsec/
labels:
- "traefik.enable=false"
volumes:
logs-captcha-enabled:
crowdsec-db-captcha-enabled:
crowdsec-config-captcha-enabled:
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

+17
View File
@@ -0,0 +1,17 @@
name: captcha_remediation
filters:
- Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.GetScenario() contains "http" && GetDecisionsSinceCount(Alert.GetValue(), "24h") <= 3
## Same as above but only 3 captcha decision per 24 hours before ban
decisions:
- type: captcha
duration: 4h
on_success: break
---
name: default_ip_remediation
filters:
- Alert.Remediation == true && Alert.GetScope() == "Ip"
decisions:
- type: ban
duration: 4h
#duration_expr: "Sprintf('%dh', (GetDecisionsCount(Alert.GetValue()) + 1) * 4)"
on_success: break
@@ -0,0 +1,17 @@
name: captcha_remediation
filters:
- Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.GetScenario() contains "http"
## Any scenario with http in its name will trigger a captcha challenge
decisions:
- type: captcha
duration: 4h
on_success: break
---
name: default_ip_remediation
filters:
- Alert.Remediation == true && Alert.GetScope() == "Ip"
decisions:
- type: ban
duration: 4h
#duration_expr: "Sprintf('%dh', (GetDecisionsCount(Alert.GetValue()) + 1) * 4)"
on_success: break
+47
View File
@@ -0,0 +1,47 @@
# Example
## Adding a custom ban page
Traefik can return a custom HTML ban page along with the 403 HTTP response code.
This can be usefull as some browser (Firefox for instance) return a 403 blank webpage and we can mistake a server/reverse-proxy error with a ban from Crowdsec.
### Traefik configuration
```yaml
labels:
# Define ban HTML file path
- "traefik.http.middlewares.crowdsec.plugin.bouncer.banHtmlFilePath=/ban.html"
```
The ban HTML file must be present in the Traefik container (bind mounted or added during a custom build).
It is not directly accessible from Traefik even when importing the plugin, so [download](https://raw.githubusercontent.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/master/ban.html) it locally to expose it to Traefik.
```yaml
...
traefik:
image: "traefik:v2.11.0"
volumes:
- './ban.html:/ban.html'
...
```
## Exemple navigation
We can try to query normally the whoami server:
```bash
curl http://localhost:8000/foo
```
We can try to ban ourself
```bash
docker exec crowdsec cscli decisions add --ip 10.0.0.20 -d 4h --type ban
```
![image decision ban](image_decision_ban.png)
We will see in the browser the ban custom page:
To play the demo environment run:
```bash
make run_custom_ban_page
```
+4
View File
@@ -0,0 +1,4 @@
filenames:
- /var/log/traefik/access.log
labels:
type: traefik
+330
View File
@@ -0,0 +1,330 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>CrowdSec Access Forbidden</title>
<meta content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
/*! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com*/
*,
:after,
:before {
border: 0 solid #e5e7eb;
box-sizing: border-box
}
:after,
:before {
--tw-content: ""
}
html {
-webkit-text-size-adjust: 100%;
font-feature-settings: normal;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4
}
body {
line-height: inherit;
margin: 0
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit
}
a {
color: inherit;
text-decoration: inherit
}
h1,
h2,
h3,
h4,
h5,
h6,
hr,
p,
pre {
margin: 0
}
*,
::backdrop,
:after,
:before {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: #3b82f680;
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia:
}
.flex {
display: flex
}
.flex-wrap {
flex-wrap: wrap
}
.inline-flex {
display: inline-flex
}
.h-24 {
height: 6rem
}
.h-6 {
height: 1.5rem
}
.h-full {
height: 100%
}
.h-screen {
height: 100vh
}
.text-center {
text-align: center
}
.w-24 {
width: 6rem
}
.w-6 {
width: 1.5rem
}
.w-full {
width: 100%
}
.w-screen {
width: 100vw
}
.my-3 {
margin-top: 0.75rem;
margin-bottom: 0.75rem
}
.flex-col {
flex-direction: column
}
.items-center {
align-items: center
}
.justify-center {
justify-content: center
}
.justify-between {
justify-content: space-between
}
.space-y-1>:not([hidden])~:not([hidden]) {
--tw-space-y-reverse: 0;
margin-bottom: calc(.25rem*var(--tw-space-y-reverse));
margin-top: calc(.25rem*(1 - var(--tw-space-y-reverse)))
}
.space-y-4>:not([hidden])~:not([hidden]) {
--tw-space-y-reverse: 0;
margin-bottom: calc(1rem*var(--tw-space-y-reverse));
margin-top: calc(1rem*(1 - var(--tw-space-y-reverse)))
}
.rounded-xl {
border-radius: .75rem
}
.border-2 {
border-width: 2px
}
.border-black {
--tw-border-opacity: 1;
border-color: rgb(0 0 0/var(--tw-border-opacity))
}
.p-4 {
padding: 1rem
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem
}
.py-2 {
padding-bottom: .5rem;
padding-top: .5rem
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem
}
.text-sm {
font-size: .875rem;
line-height: 1.25rem
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem
}
.font-bold {
font-weight: 700
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255/var(--tw-text-opacity))
}
@media (min-width:640px) {
.sm\:w-2\/3 {
width: 66.666667%
}
}
@media (min-width:768px) {
.md\:flex-row {
flex-direction: row
}
}
@media (min-width:1024px) {
.lg\:w-1\/2 {
width: 50%
}
.lg\:text-3xl {
font-size: 1.875rem;
line-height: 2.25rem
}
.lg\:text-xl {
font-size: 1.25rem;
line-height: 1.75rem
}
}
@media (min-width:1280px) {
.xl\:text-4xl {
font-size: 2.25rem;
line-height: 2.5rem
}
}
</style>
</head>
<body class="h-screen w-screen p-4">
<div class="h-full w-full flex flex-col justify-center items-center">
<div class="border-2 border-black rounded-xl p-4 text-center w-full sm:w-2/3 lg:w-1/2">
<div class="flex flex-col items-center space-y-4">
<svg fill="black" class="h-24 w-24" aria-hidden="true" focusable="false" data-prefix="fas"
data-icon="exclamation-triangle" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"
class="warning">
<path
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z">
</path>
</svg>
<h1 class="text-2xl lg:text-3xl xl:text-4xl">CrowdSec Access Forbidden</h1>
</div>
<div class="flex justify-center flex-wrap">
<p class="my-3">This security check has been powered by</p>
<a href="https://crowdsec.net/" target="_blank" rel="noopener" class="inline-flex flex-col items-center">
<svg fill="black" width="33.92" height="33.76" viewBox="0 0 254.4 253.2">
<defs>
<clipPath id="a">
<path d="M0 52h84v201.2H0zm0 0" />
</clipPath>
<clipPath id="b">
<path d="M170 52h84.4v201.2H170zm0 0" />
</clipPath>
</defs>
<path
d="M59.3 128.4c1.4 2.3 2.5 4.6 3.4 7-1-4.1-2.3-8.1-4.3-12-3.1-6-7.8-5.8-10.7 0-2 4-3.2 8-4.3 12.1 1-2.4 2-4.8 3.4-7.1 3.4-5.8 8.8-6 12.5 0M207.8 128.4a42.9 42.9 0 013.4 7c-1-4.1-2.3-8.1-4.3-12-3.2-6-7.8-5.8-10.7 0-2 4-3.3 8-4.3 12.1.9-2.4 2-4.8 3.4-7.1 3.4-5.8 8.8-6 12.5 0M134.6 92.9c2 3.5 3.6 7 4.8 10.7-1.3-5.4-3-10.6-5.6-15.7-4-7.5-9.7-7.2-13.3 0a75.4 75.4 0 00-5.6 16c1.2-3.8 2.7-7.4 4.7-11 4.1-7.2 10.6-7.5 15 0M43.8 136.8c.9 4.6 3.7 8.3 7.3 9.2 0 2.7 0 5.5.2 8.2.3 3.3.4 6.6 1 9.6.3 2.3 1 2.2 1.3 0 .5-3 .6-6.3 1-9.6l.2-8.2c3.5-1 6.4-4.6 7.2-9.2a17.8 17.8 0 01-9 2.4c-3.5 0-6.6-1-9.2-2.4M192.4 136.8c.8 4.6 3.7 8.3 7.2 9.2 0 2.7 0 5.5.3 8.2.3 3.3.4 6.6 1 9.6.3 2.3.9 2.2 1.2 0 .6-3 .7-6.3 1-9.6.2-2.7.3-5.5.2-8.2 3.6-1 6.4-4.6 7.3-9.2a17.8 17.8 0 01-9.1 2.4c-3.4 0-6.6-1-9.1-2.4M138.3 104.6c-3.1 1.9-7 3-11.3 3-4.3 0-8.2-1.1-11.3-3 1 5.8 4.5 10.3 9 11.5 0 3.4 0 6.8.3 10.2.4 4.1.5 8.2 1.2 12 .4 2.9 1.2 2.7 1.6 0 .7-3.8.8-7.9 1.2-12 .3-3.4.3-6.8.3-10.2 4.5-1.2 8-5.7 9-11.5" />
<path
d="M51 146c0 2.7.1 5.5.3 8.2.3 3.3.4 6.6 1 9.6.3 2.3 1 2.2 1.3 0 .5-3 .6-6.3 1-9.6l.2-8.2c3.5-1 6.4-4.6 7.2-9.2a17.8 17.8 0 01-9 2.4c-3.5 0-6.6-1-9.2-2.4.9 4.6 3.7 8.3 7.3 9.2M143.9 105c-1.9-.4-3.5-1.2-4.9-2.3 1.4 5.6 2.5 11.3 4 17 1.2 5 2 10 2.4 15 .6 7.8-4.5 14.5-10.9 14.5h-15c-6.4 0-11.5-6.7-11-14.5.5-5 1.3-10 2.6-15 1.3-5.3 2.3-10.5 3.6-15.7-2.2 1.2-4.8 1.9-7.7 2-4.7.1-9.4-.3-14-1-4-.4-6.7-3-8-6.7-1.3-3.4-2-7-3.3-10.4-.5-1.5-1.6-2.8-2.4-4.2-.4-.6-.8-1.2-.9-1.8v-7.8a77 77 0 0124.5-3c6.1 0 12 1 17.8 3.2 4.7 1.7 9.7 1.8 14.4 0 9-3.4 18.2-3.8 27.5-3 4.9.5 9.8 1.6 14.8 2.4v8.2c0 .6-.3 1.5-.7 1.7-2 .9-2.2 2.7-2.7 4.5-.9 3.2-1.8 6.4-2.9 9.5a11 11 0 01-8.8 7.7 40.6 40.6 0 01-18.4-.2m29.4 80.6c-3.2-26.8-6.4-50-8.9-60.7a14.3 14.3 0 0014.1-14h.4a9 9 0 005.6-16.5 14.3 14.3 0 00-3.7-27.2 9 9 0 00-6.9-14.6c2.4-1.1 4.5-3 5.8-5 3.4-5.3 4-29-8-44.4-5-6.3-9.8-2.5-10 1.8-1 13.2-1.1 23-4.5 34.3a9 9 0 00-16-4.1 14.3 14.3 0 00-28.4 0 9 9 0 00-16 4.1c-3.4-11.2-3.5-21.1-4.4-34.3-.3-4.3-5.2-8-10-1.8-12 15.3-11.5 39-8.1 44.4 1.3 2 3.4 3.9 5.8 5a9 9 0 00-7 14.6 14.3 14.3 0 00-3.6 27.2A9 9 0 0075 111h.5a14.5 14.5 0 0014.3 14c-4 17.2-10 66.3-15 111.3l-1.3 13.4a1656.4 1656.4 0 01106.6 0l-1.4-12.7-5.4-51.3" />
<g clip-path="url(#a)">
<path
d="M83.5 136.6l-2.3.7c-5 1-9.8 1-14.8-.2-1.4-.3-2.7-1-3.8-1.9l3.1 13.7c1 4 1.7 8 2 12 .5 6.3-3.6 11.6-8.7 11.6H46.9c-5.1 0-9.2-5.3-8.7-11.6.3-4 1-8 2-12 1-4.2 1.8-8.5 2.9-12.6-1.8 1-3.9 1.5-6.3 1.6a71 71 0 01-11.1-.7 7.7 7.7 0 01-6.5-5.5c-1-2.7-1.6-5.6-2.6-8.3-.4-1.2-1.3-2.3-2-3.4-.2-.4-.6-1-.6-1.4v-6.3c6.4-2 13-2.6 19.6-2.5 4.9.1 9.6 1 14.2 2.6 3.9 1.4 7.9 1.5 11.7 0 1.8-.7 3.6-1.2 5.5-1.6a13 13 0 01-1.6-15.5A18.3 18.3 0 0159 73.1a11.5 11.5 0 00-17.4 8.1 7.2 7.2 0 00-12.9 3.3c-2.7-9-2.8-17-3.6-27.5-.2-3.4-4-6.5-8-1.4C7.5 67.8 7.9 86.9 10.6 91c1.1 1.7 2.8 3.1 4.7 4a7.2 7.2 0 00-5.6 11.7 11.5 11.5 0 00-2.9 21.9 7.2 7.2 0 004.5 13.2h.3c0 .6 0 1.1.2 1.7.9 5.4 5.6 9.5 11.3 9.5A1177.2 1177.2 0 0010 253.2c18.1-1.5 38.1-2.6 59.5-3.4.4-4.6.8-9.3 1.4-14 1.2-11.6 3.3-30.5 5.7-49.7 2.2-18 4.7-36.3 7-49.5" />
</g>
<g clip-path="url(#b)">
<path
d="M254.4 118.2c0-5.8-4.2-10.5-9.7-11.4a7.2 7.2 0 00-5.6-11.7c2-.9 3.6-2.3 4.7-4 2.7-4.2 3.1-23.3-6.5-35.5-4-5.1-7.8-2-8 1.4-.8 10.5-.9 18.5-3.6 27.5a7.2 7.2 0 00-12.8-3.3 11.5 11.5 0 00-17.8-7.9 18.4 18.4 0 01-4.5 22 13 13 0 01-1.3 15.2c2.4.5 4.8 1 7.1 2 3.8 1.3 7.8 1.4 11.6 0 7.2-2.8 14.6-3 22-2.4 4 .4 7.9 1.2 12 1.9l-.1 6.6c0 .5-.2 1.2-.5 1.3-1.7.7-1.8 2.2-2.2 3.7l-2.3 7.6a8.8 8.8 0 01-7 6.1c-5 1-10 1-14.9-.2-1.5-.3-2.8-1-3.9-1.9 1.2 4.5 2 9.1 3.2 13.7 1 4 1.6 8 2 12 .4 6.3-3.6 11.6-8.8 11.6h-12c-5.2 0-9.3-5.3-8.8-11.6.4-4 1-8 2-12 1-4.2 1.9-8.5 3-12.6-1.8 1-4 1.5-6.3 1.6-3.7 0-7.5-.3-11.2-.7a7.7 7.7 0 01-3.7-1.5c3.1 18.4 7.1 51.2 12.5 100.9l.6 5.3.8 7.9c21.4.7 41.5 1.9 59.7 3.4L243 243l-4.4-41.2a606 606 0 00-7-48.7 11.5 11.5 0 0011.2-11.2h.4a7.2 7.2 0 004.4-13.2c4-1.8 6.8-5.8 6.8-10.5" />
</g>
<path
d="M180 249.6h.4a6946 6946 0 00-7.1-63.9l5.4 51.3 1.4 12.6M164.4 125c2.5 10.7 5.7 33.9 8.9 60.7a570.9 570.9 0 00-8.9-60.7M74.8 236.3l-1.4 13.4 1.4-13.4" />
</svg>
<span>CrowdSec</span>
</a>
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,67 @@
services:
traefik:
image: "traefik:v3.0.0"
container_name: "traefik"
restart: unless-stopped
command:
# - "--log.level=DEBUG"
- "--accesslog"
- "--accesslog.filepath=/var/log/traefik/access.log"
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.3.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-custom-ban-page-enabled:/var/log/traefik
- './ban.html:/ban.html'
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 8000:80
- 8080:8080
depends_on:
- crowdsec
whoami1:
image: traefik/whoami
container_name: "simple-service-foo"
restart: unless-stopped
labels:
- "traefik.enable=true"
# Definition of the router
- "traefik.http.routers.router-foo.rule=PathPrefix(`/foo`)"
- "traefik.http.routers.router-foo.entrypoints=web"
- "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
# Definition of the service
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
# Definition of the middleware
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
# Define ban HTML file path
- "traefik.http.middlewares.crowdsec.plugin.bouncer.banHtmlFilePath=/ban.html"
crowdsec:
image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
volumes:
- './acquis.yaml:/etc/crowdsec/acquis.yaml:ro'
- logs-custom-ban-page-enabled:/var/log/traefik:ro
- crowdsec-db-custom-ban-page-enabled:/var/lib/crowdsec/data/
- crowdsec-config-custom-ban-page-enabled:/etc/crowdsec/
labels:
- "traefik.enable=false"
volumes:
logs-custom-ban-page-enabled:
crowdsec-db-custom-ban-page-enabled:
crowdsec-config-custom-ban-page-enabled:
Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

+108
View File
@@ -0,0 +1,108 @@
### Kubernetes Example
#### Official docs
##### Install Kubernetes on Docker Desktop
Install Docker Desktop
[https://www.docker.com/products/docker-desktop/](https://www.docker.com/products/docker-desktop/)
In settings, click on `Kubernetes` menu, and click `Enable Kubernetes`, then `Apply and Restart`.
In case of any issue, you can reset the cluster from this menu and the button `Reset Kubernetes Cluster`.
##### Install Kubernetes on Minikube
Install Minikube
```bash
curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 \
&& chmod +x minikube
sudo mkdir -p /usr/local/bin/
sudo install minikube /usr/local/bin/
minikube start
```
##### Install Traefik
[getting-started/install-traefik/#use-the-helm-chart](https://doc.traefik.io/traefik/getting-started/install-traefik/#use-the-helm-chart)
```bash
helm repo add traefik https://traefik.github.io/charts
helm repo update
kubectl create ns traefik
helm upgrade --version v28.0.0 --install --namespace=traefik \
--values=./traefik/values.yml \
traefik traefik/traefik
```
_A [bug](https://github.com/traefik/traefik-helm-chart/commit/e7ce1b410c858642069033305eb6362f26689f16) has been fixed in chart 26.1.0 that could prevent plugin to be loaded_
_v28.0.0 of the Traefik helm chart is only compatible with v3 of Traefik_
#### View the Traefik dashboard
> Port forward the dashboard:
```bash
kubectl --namespace=traefik port-forward $(kubectl get pods --namespace=traefik --selector "app.kubernetes.io/name=traefik" --output=name) 9000:9000
```
Access the dashboard with: [localhost:9000/dashboard/#/](http://localhost:9000/dashboard/#/)
#### Install the plugin
```bash
kubectl apply -f traefik/plugin.yml
```
#### Install Whoami
```bash
kubectl apply -f whoami/whoami.yml
kubectl apply -f whoami/whoami-services.yml
kubectl apply -f whoami/whoami-ingress.yml
```
#### Access Whoami
> Port forward web port of Traefik
```bash
kubectl --namespace=traefik port-forward $(kubectl get pods --namespace=traefik --selector "app.kubernetes.io/name=traefik" --output=name) 8000:8000
```
Access the whoami with: [localhost:8000/](http://localhost:8000/)
#### Install Crowdsec
[helm/crowdsec/crowdsec](https://artifacthub.io/packages/helm/crowdsec/crowdsec)
```bash
helm repo add crowdsec https://crowdsecurity.github.io/helm-charts
helm repo update
kubectl create ns crowdsec
helm upgrade --install --namespace=crowdsec \
--values=./crowdsec/values.yml \
crowdsec crowdsec/crowdsec
```
#### Read Traefik Logs
```bash
kubectl get pod --namespace traefik
kubectl logs $(kubectl get pods --namespace=traefik --selector "app.kubernetes.io/name=traefik" --output=name) --namespace traefik -f
```
#### Use CSCLI in Crowdsec container
```bash
kubectl -n crowdsec exec -it $(kubectl get pods -n crowdsec --selector "k8s-app=crowdsec,type=lapi" --output=name) bash
```
#### Shell in Traefik container
```bash
kubectl -n traefik exec -it $(kubectl get pods -n traefik --selector "app.kubernetes.io/name=traefik" --output=name) sh
```
+14
View File
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: crowdsec
namespace: crowdsec
spec:
ports:
- name: web
port: 8080
targetPort: web
selector:
app: crowdsec
+25
View File
@@ -0,0 +1,25 @@
image:
tag: v1.6.1-2
agent:
acquisition:
- namespace: traefik
podName: traefik-*
program: traefik
env:
- name: COLLECTIONS
value: "crowdsecurity/traefik"
lapi:
dashboard:
enabled: false
env:
- name: ENROLL_KEY
value: 40796d93c2958f9e58345514e67740e5
- name: ENROLL_INSTANCE_NAME
value: k8s_cluster
- name: ENROLL_TAGS
value: "k8s linux test"
# If it's a test, we don't want to share signals with CrowdSec so disable the Online API.
- name: DISABLE_ONLINE_API
value: "true"
+14
View File
@@ -0,0 +1,14 @@
# dashboard.yaml
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: dashboard
spec:
entryPoints:
- web
routes:
- match: Host(`traefik.localhost`) && (PathPrefix(`/dashboard`) || PathPrefix(`/api`))
kind: Rule
services:
- name: api@internal
kind: TraefikService
+14
View File
@@ -0,0 +1,14 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: bouncer
namespace: traefik
spec:
plugin:
bouncer:
CrowdsecLapiKey: 40796d93c2958f9e58345514e67740e5
Enabled: "true"
logLevel: DEBUG
crowdsecMode: live
crowdsecLapiHost: crowdsec-service.crowdsec.svc.cluster.local:8080
crowdsecLapiScheme: http
+17
View File
@@ -0,0 +1,17 @@
image:
tag: v3.0.0
logs:
general:
level: DEBUG
access:
enabled: true
fields:
headers:
defaultmode: keep
experimental:
plugins:
bouncer:
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
version: "v1.3.0"
@@ -0,0 +1,20 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: whoami
namespace: traefik
annotations:
traefik.ingress.kubernetes.io/router.middlewares: traefik-bouncer@kubernetescrd
traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: whoami
port:
name: web
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: whoami
namespace: traefik
spec:
ports:
- name: web
port: 80
targetPort: web
selector:
app: whoami
+24
View File
@@ -0,0 +1,24 @@
kind: Deployment
apiVersion: apps/v1
metadata:
name: whoami
namespace: traefik
labels:
app: whoami
spec:
replicas: 1
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: traefik/whoami
ports:
- name: web
containerPort: 80
+13
View File
@@ -0,0 +1,13 @@
# Example
## With Redis as an external shared cache
The plugin must be configured to connect to a redis instance
```yaml
redisCacheHost: "redis:6379"
```
Here **redis** is the hostname of a container located in the same network as Traefik and **6379** is the default port of redis
To play the demo environment run:
```bash
make run_cacheredis
```
+4
View File
@@ -0,0 +1,4 @@
filenames:
- /var/log/traefik/access.log
labels:
type: traefik
+112
View File
@@ -0,0 +1,112 @@
services:
traefik:
image: "traefik:v3.0.0"
container_name: "traefik"
restart: unless-stopped
command:
# - "--log.level=DEBUG"
- "--accesslog"
- "--accesslog.filepath=/var/log/traefik/access.log"
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.3.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-redis:/var/log/traefik
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 80:80
- 8080:8080
depends_on:
- crowdsec
# - redis-insecure
- redis-secure
# Either use secure, or insecure but do not mix both
# whoami-redis-insecure:
# image: traefik/whoami
# container_name: "simple-service-foo"
# restart: unless-stopped
# labels:
# - "traefik.enable=true"
# # Definition of the router
# - "traefik.http.routers.router-foo.rule=Path(`/redis-insecure`)"
# - "traefik.http.routers.router-foo.entrypoints=web"
# - "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
# # Definition of the service
# - "traefik.http.services.service-foo.loadbalancer.server.port=80"
# # Definition of the middleware
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.rediscacheenabled=true"
# # Contact redis-unsecure without a password
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.rediscachehost=redis-insecure:6379"
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
whoami-redis-secure:
image: traefik/whoami
container_name: "simple-service-bar-redis"
restart: unless-stopped
labels:
- "traefik.enable=true"
# Definition of the router
- "traefik.http.routers.router-bar.rule=Path(`/redis-secure`)"
- "traefik.http.routers.router-bar.entrypoints=web"
- "traefik.http.routers.router-bar.middlewares=crowdsec@docker"
# Definition of the service
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
# Definition of the middleware
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.rediscacheenabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.rediscachepassword=FIXME"
# Contact redis-secure with password
- "traefik.http.middlewares.crowdsec.plugin.bouncer.rediscachehost=redis-secure:6379"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
crowdsec:
image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
volumes:
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
- logs-redis:/var/log/traefik:ro
- crowdsec-db-redis:/var/lib/crowdsec/data/
- crowdsec-config-redis:/etc/crowdsec/
labels:
- "traefik.enable=false"
redis-secure:
image: "redis:7.0.12-alpine"
container_name: "redis-secure"
hostname: redis-secure
restart: unless-stopped
command: "redis-server --save 60 1 --loglevel debug --requirepass FIXME"
volumes:
- redis-secure-data:/data
# redis-insecure:
# image: "redis:7.0.12-alpine"
# container_name: "redis-insecure"
# hostname: redis-unsecure
# restart: unless-stopped
# command: "redis-server --save 60 1 --loglevel debug"
# volumes:
# - redis-unsecure-data:/data
volumes:
logs-redis:
crowdsec-db-redis:
crowdsec-config-redis:
redis-unsecure-data:
redis-secure-data:
+24
View File
@@ -0,0 +1,24 @@
#### Generate CAPI credentials (only for `alone` mode)
You need to create a crowdsec API credentials for the CAPI.
You can follow the documentation here: https://docs.crowdsec.net/docs/central_api/intro
```bash
curl -X POST "https://api.crowdsec.net/v2/watchers" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"password\": \"PASSWORD\", \"machine_id\": \"LOGIN\"}"
```
These CAPI credentials must be set in your docker-compose.yml or in your config files
```yaml
...
whoami:
labels:
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecMode=alone"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecCapiMachineId=LOGIN"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecCapiPassword=PASSWORD"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecCapiScenarios=crowdsecurity/http-generic-bf,crowdsecurity/http-xss-probing,..."
```
You can then run all the containers:
```bash
docker-compose up -d
```
@@ -0,0 +1,43 @@
services:
traefik:
image: "traefik:v3.0.0"
container_name: "traefik"
restart: unless-stopped
command:
# - "--log.level=DEBUG"
- "--accesslog"
- "--accesslog.filepath=/var/log/traefik/access.log"
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.3.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 80:80
- 8080:8080
whoami-foo:
image: traefik/whoami
container_name: "simple-service-foo-alone"
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.router-foo.rule=Path(`/foo`)"
- "traefik.http.routers.router-foo.entrypoints=web"
- "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=alone"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiMachineId=FIXME"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiPassword=FIXME"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/sshd,crowdsecurity/asterisk_bf,crowdsecurity/asterisk_user_enum,crowdsecurity/base-http-scenarios"
+16
View File
@@ -0,0 +1,16 @@
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y curl wget
RUN VERSION=$(curl --silent "https://api.github.com/repos/cloudflare/cfssl/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') && VNUMBER=${VERSION#"v"} && wget https://github.com/cloudflare/cfssl/releases/download/${VERSION}/cfssl_${VNUMBER}_linux_amd64 -O cfssl && chmod +x cfssl && mv cfssl /usr/local/bin
RUN VERSION=$(curl --silent "https://api.github.com/repos/cloudflare/cfssl/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') && \
VNUMBER=${VERSION#"v"} && \
wget https://github.com/cloudflare/cfssl/releases/download/${VERSION}/cfssljson_${VNUMBER}_linux_amd64 -O cfssljson && \
chmod +x cfssljson && \
mv cfssljson /usr/local/bin && \
cfssljson -version
COPY gencerts.sh /gencerts.sh
RUN chmod +x /gencerts.sh
CMD [ "/gencerts.sh" ]
+92
View File
@@ -0,0 +1,92 @@
# Example
## Using https communication and tls authentication with Crowdsec
##### Summary
This example demonstrates the use of https between the Traefik plugin and the Crowdsec LAPI.
It is possible to communicate with the LAPI in https and still authenticate with API key.
You can add the client TLS certificate generated to authenticate without any Token in the plugin.
However, note that it is not possible to authenticate with TLS client certificate without https setup for the LAPI.
##### Details
Simple HTTPS communication: It is possible to talk to Crowdsec LAPI which is configured with a self-signed certificate
In that case the setting **crowdsecLapiTLSInsecureVerify** must be set to true.
It is recommended to validate the certificate presented by Crowdsec LAPI using the Certificate Authority which created it.
You can provide the Certificate Authority using:
* A file path readable by Traefik
```yaml
http:
middlewares:
crowdsec:
plugin:
bouncer:
crowdsecLapiTlsCertificateAuthorityFile: /etc/traefik/certs/crowdsecCA.pem
```
* The PEM encoded certificate as a text variable
In the static file configuration of Traefik
```yaml
http:
middlewares:
crowdsec:
plugin:
bouncer:
crowdsecLapiTlsCertificateAuthority: |-
-----BEGIN CERTIFICATE-----
MIIEBzCCAu+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK
...
C6qNieSwcvWL7C03ri0DefTQMY54r5wP33QU5hJ71JoaZI3YTeT0Nf+NRL4hM++w
Q0veeNzBQXg1f/JxfeA39IDIX1kiCf71tGlT
-----END CERTIFICATE-----
```
In a dynamic configuration of a provider (ex docker) as a Label
```yaml
services:
whoami-foo:
image: traefik/whoami
labels:
- |
traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdsecLapiTlsCertificateAuthority=
-----BEGIN CERTIFICATE-----
MIIEBzCCAu+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK
...
C6qNieSwcvWL7C03ri0DefTQMY54r5wP33QU5hJ71JoaZI3YTeT0Nf+NRL4hM++w
Q0veeNzBQXg1f/JxfeA39IDIX1kiCf71tGlT
-----END CERTIFICATE-----
```
The example tls-auth presents 2 services, foo and bar which comes with the bouncer.
At startup, certificates are created in a shared docker volume by a sidecar container which exits after.
Traefik will use client and CA certificates.
The Bouncer will use server and CA certificates.
The service `whoami-foo` will authenticate with an **API key** over HTTPS after verifying the server certificate with CA.
The service `whoami-bar` will authenticate with a **client certificate** signed by the CA.
Access to a route that communicate via https and authenticate with API-key:
```
curl http://localhost:8000/foo
```
Access to a route that communicate via https and authenticate with a client certificate:
```
curl http://localhost:8000/bar
```
Access to the traefik dashboard
```
curl http://localhost:8080/dashboard/#/
```
To play the demo environnement run:
```bash
make run_tlsauth
```
Note:
> Traefik need to be restart if certificates are regenerated after his launch
+4
View File
@@ -0,0 +1,4 @@
filenames:
- /var/log/traefik/access.log
labels:
type: traefik
+62
View File
@@ -0,0 +1,62 @@
common:
daemonize: false
pid_dir: /var/run/
log_media: stdout
log_level: info
log_dir: /var/log/
working_dir: .
config_paths:
config_dir: /etc/crowdsec/
data_dir: /var/lib/crowdsec/data/
simulation_path: /etc/crowdsec/simulation.yaml
hub_dir: /etc/crowdsec/hub/
index_path: /etc/crowdsec/hub/.index.json
notification_dir: /etc/crowdsec/notifications/
plugin_dir: /usr/local/lib/crowdsec/plugins/
crowdsec_service:
acquisition_path: /etc/crowdsec/acquis.yaml
parser_routines: 1
plugin_config:
user: nobody
group: nobody
cscli:
output: human
db_config:
log_level: info
type: sqlite
db_path: /var/lib/crowdsec/data/crowdsec.db
#user:
#password:
#db_name:
#host:
#port:
flush:
max_items: 5000
max_age: 7d
api:
client:
insecure_skip_verify: false
credentials_path: /etc/crowdsec/local_api_credentials.yaml
server:
log_level: info
listen_uri: 0.0.0.0:8080
profiles_path: /etc/crowdsec/profiles.yaml
trusted_ips: # IP ranges, or IPs which can have admin API access
- 127.0.0.1
- ::1
online_client: # Central API credentials (to push signals and receive bad IPs)
#credentials_path: /etc/crowdsec/online_api_credentials.yaml
tls:
cert_file: /etc/crowdsec/certs/server.pem #Server side cert
key_file: /etc/crowdsec/certs/server-key.pem #Server side key
ca_cert_path: /etc/crowdsec/certs/inter.pem #CA used to verify the client certs
bouncers_allowed_ou: #OU allowed for bouncers
- bouncer-ou
agents_allowed_ou: #OU allowed for agents
- agent-ou
prometheus:
enabled: true
level: full
listen_addr: 0.0.0.0
listen_port: 6060
@@ -0,0 +1,4 @@
url: https://localhost:8080
ca_cert_path: /etc/crowdsec/certs/inter.pem #CA to trust the server certificate
key_path: /etc/crowdsec/certs/agent-key.pem #Client key
cert_path: /etc/crowdsec/certs/agent.pem #Client cert
+114
View File
@@ -0,0 +1,114 @@
services:
traefik:
image: "traefik:v3.0.0"
container_name: "traefik"
restart: unless-stopped
command:
# - "--log.level=DEBUG"
- "--accesslog"
- "--accesslog.filepath=/var/log/traefik/access.log"
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.3.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./LAPIKEY:/etc/traefik/LAPIKEY:ro
- logs-tls-auth:/var/log/traefik
- crowdsec-certs-tls-auth:/etc/traefik/crowdsec-certs
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 8000:80
- 8080:8080
depends_on:
- crowdsec
- gencert
# Use HTTPS scheme but with lapikey authentication
# whoami-foo:
# image: traefik/whoami
# container_name: "simple-service-foo"
# restart: unless-stopped
# labels:
# - "traefik.enable=true"
# - "traefik.http.routers.router-foo.rule=Path(`/foo`)"
# - "traefik.http.routers.router-foo.entrypoints=web"
# - "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
# - "traefik.http.services.service-foo.loadbalancer.server.port=80"
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapischeme=https"
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateAuthorityFile=/etc/traefik/crowdsec-certs/inter.pem"
# Use HTTPS scheme with TLS cert authentication
whoami-bar:
image: traefik/whoami
container_name: "simple-service-bar"
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.router-bar.rule=Path(`/bar`)"
- "traefik.http.routers.router-bar.entrypoints=web"
- "traefik.http.routers.router-bar.middlewares=crowdsec@docker"
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapischeme=https"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateAuthorityFile=/etc/traefik/crowdsec-certs/inter.pem"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateBouncerFile=/etc/traefik/crowdsec-certs/bouncer.pem"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateBouncerKeyFile=/etc/traefik/crowdsec-certs/bouncer-key.pem"
crowdsec:
image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
CUSTOM_HOSTNAME: crowdsec
# whoami-foo is authenticating with api key over https
# whoami-bar is authenticating with tls cert over https
BOUNCER_KEY_TRAEFIK_FOO: 40796d93c2958f9e58345514e67740e5
LOCAL_API_URL: https://127.0.0.1:8080
USE_TLS: "true"
CERT_FILE: "/etc/crowdsec/certs/server.pem"
KEY_FILE: "/etc/crowdsec/certs/server-key.pem"
CACERT_FILE: "/etc/crowdsec/certs/inter.pem"
# CLIENT_CERT_FILE: "/etc/crowdsec/certs/bouncer.pem"
# CLIENT_CERT_FILE: "/etc/crowdsec/certs/bouncer.pem"
AGENTS_ALLOWED_OU: "agent-ou"
BOUNCERS_ALLOWED_OU: "bouncer-ou"
LEVEL_DEBUG: "true"
# Disabled because it restart in loop otherwise
# DISABLE_AGENT: "true"
# Disabled for the examples
DISABLE_ONLINE_API: "true"
volumes:
- ./config/acquis.yaml:/etc/crowdsec/acquis.yaml
# - ./config/config.yaml:/etc/crowdsec/config_local.yaml
# - ./config/local_api_credentials.yaml:/etc/crowdsec/local_api_credentials.yaml:ro
- crowdsec-certs-tls-auth:/etc/crowdsec/certs/:ro
- logs-tls-auth:/var/log/traefik:ro
- crowdsec-db-tls-auth:/var/lib/crowdsec/data/
- crowdsec-config-tls-auth:/etc/crowdsec/
labels:
- "traefik.enable=false"
depends_on:
- gencert
gencert:
build: .
volumes:
- crowdsec-certs-tls-auth:/out
- ./in:/in:ro
volumes:
logs-tls-auth:
crowdsec-db-tls-auth:
crowdsec-config-tls-auth:
crowdsec-certs-tls-auth:
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
stdout=/out/res.log
cfssl gencert --initca /in/ca.json 2>${stdout} | cfssljson --bare "/out/ca" && \
# Generate an intermediate certificate that will be used to sign the client certificates
cfssl gencert --initca /in/intermediate.json 2>${stdout} | cfssljson --bare "/out/inter" && \
cfssl sign -ca "/out/ca.pem" -ca-key "/out/ca-key.pem" -config /in/profiles.json -profile intermediate_ca "/out/inter.csr" 2>${stdout} | cfssljson --bare "/out/inter" && \
# Generate a server side certificate
cfssl gencert -ca "/out/inter.pem" -ca-key "/out/inter-key.pem" -config /in/profiles.json -profile=server /in/server.json 2>${stdout} | cfssljson --bare "/out/server" && \
# Generate a client certificate for the bouncer whoami
cfssl gencert -ca "/out/inter.pem" -ca-key "/out/inter-key.pem" -config /in/profiles.json -profile=client /in/bouncer.json 2>${stdout} | cfssljson --bare "/out/bouncer" && \
# Generate a client certificate for the agent
cfssl gencert -ca "/out/inter.pem" -ca-key "/out/inter-key.pem" -config /in/profiles.json -profile=client /in/agent.json 2>${stdout} | cfssljson --bare "/out/agent"
+16
View File
@@ -0,0 +1,16 @@
{
"CN": "myagent",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "FR",
"L": "Paris",
"O": "Crowdsec",
"OU": "agent-ou",
"ST": "France"
}
]
}
+21
View File
@@ -0,0 +1,21 @@
{
"CN": "crowdsec",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "FR",
"L": "Paris",
"O": "Crowdsec",
"OU": "bouncer-ou",
"ST": "France"
}
],
"hosts": [
"127.0.0.1",
"localhost",
"crowdsec"
]
}
+16
View File
@@ -0,0 +1,16 @@
{
"CN": "CrowdSec Test CA",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "FR",
"L": "Paris",
"O": "Crowdsec",
"OU": "Crowdsec",
"ST": "France"
}
]
}
+19
View File
@@ -0,0 +1,19 @@
{
"CN": "CrowdSec Test CA Intermediate",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "FR",
"L": "Paris",
"O": "Crowdsec",
"OU": "Crowdsec Intermediate",
"ST": "France"
}
],
"ca": {
"expiry": "42720h"
}
}
+44
View File
@@ -0,0 +1,44 @@
{
"signing": {
"default": {
"expiry": "8760h"
},
"profiles": {
"intermediate_ca": {
"usages": [
"signing",
"digital signature",
"key encipherment",
"cert sign",
"crl sign",
"server auth",
"client auth"
],
"expiry": "8760h",
"ca_constraint": {
"is_ca": true,
"max_path_len": 0,
"max_path_len_zero": true
}
},
"server": {
"usages": [
"signing",
"digital signing",
"key encipherment",
"server auth"
],
"expiry": "8760h"
},
"client": {
"usages": [
"signing",
"digital signature",
"key encipherment",
"client auth"
],
"expiry": "8760h"
}
}
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"CN": "crowdsec",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "FR",
"L": "Paris",
"O": "Crowdsec",
"OU": "Crowdsec Server",
"ST": "France"
}
],
"hosts": [
"127.0.0.1",
"localhost",
"crowdsec"
]
}
+39
View File
@@ -0,0 +1,39 @@
# Example
## Using Trusted IP (ex: LAN OR VPN) that won't get filtered by crowdsec
You need to configure your Traefik to trust Forwarded headers by your front proxy
In the example we use a whoami container protected by crowdsec, and we ban our IP before allowing using TrustedIPs
If you are using another proxy in front, you need to add its IP in the trusted IP for the forwarded headers.
This helps Traefik choose the right IP of the client: see https://doc.traefik.io/traefik/routing/entrypoints/#forwarded-headers
The "internal" Traefik instance is configured to trust the forward headers
```yaml
- "--entrypoints.web.forwardedheaders.trustedips=172.21.0.5"
```
We configure the middleware to trust as well as the IP of the intermediate proxy if needed:
```yaml
- "traefik.http.middlewares.crowdsec.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
```
Add your IP to the ban list
```bash
docker exec crowdsec cscli decisions add --ip 10.0.10.30 -d 10m
```
You should get a 403 on http://localhost/foo
> Replace *10.0.10.30* by your IP
Add the IPs that will not be filtered by the plugin
```yaml
- "traefik.http.middlewares.crowdsec.plugin.bouncer.clientTrustedips=10.0.10.30/32"
```
> Replace *10.0.10.30/32* by your IP or IP range, so it's not getting checked against ban cache of crowdsec
You should get a 200 on http://localhost/foo even if you are on the ban cache
To play the demo environment run:
```bash
make run_trustedips
```
+4
View File
@@ -0,0 +1,4 @@
filenames:
- /var/log/traefik/access.log
labels:
type: traefik
+86
View File
@@ -0,0 +1,86 @@
services:
traefik:
image: "traefik:v3.0.0"
container_name: "traefik"
restart: unless-stopped
command:
# - "--log.level=DEBUG"
- "--accesslog"
- "--accesslog.filepath=/var/log/traefik/access.log"
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.3.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-trustedips:/var/log/traefik
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 80:80
- 8080:8080
depends_on:
- crowdsec
whoami1:
image: traefik/whoami
container_name: "simple-service-foo-trustedips"
restart: unless-stopped
labels:
- "traefik.enable=true"
# Definition of the router
- "traefik.http.routers.router-foo.rule=Path(`/foo`)"
- "traefik.http.routers.router-foo.entrypoints=web"
- "traefik.http.routers.router-foo.middlewares=crowdsec-foo@docker"
# Definition of the service
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
# Definition of the middleware
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
# Replace 10.0.10.30/32 by your IP range which is "trusted"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.clienttrustedips=10.0.10.30/32"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
whoami2:
image: traefik/whoami
container_name: "simple-service-bar-trustedips"
restart: unless-stopped
labels:
- "traefik.enable=true"
# Definition of the router
- "traefik.http.routers.router-bar.rule=Path(`/bar`)"
- "traefik.http.routers.router-bar.entrypoints=web"
- "traefik.http.routers.router-bar.middlewares=crowdsec-bar@docker"
# Definition of the service
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
# Definition of the middleware
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
# Replace 10.0.10.30/32 by your IP range which is "trusted"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.clienttrustedips=10.0.10.30/32"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
crowdsec:
image: crowdsecurity/crowdsec:v1.6.1-2
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
volumes:
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
- logs-trustedips:/var/log/traefik:ro
- crowdsec-db-trustedips:/var/lib/crowdsec/data/
- crowdsec-config-trustedips:/etc/crowdsec/
labels:
- "traefik.enable=false"
volumes:
logs-trustedips:
crowdsec-db-trustedips:
crowdsec-config-trustedips:
+5 -2
View File
@@ -1,5 +1,8 @@
module github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin module github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
go 1.17 go 1.22
require github.com/leprosus/golang-ttl-map v1.1.7 require (
github.com/leprosus/golang-ttl-map v1.1.7
github.com/maxlerebourg/simpleredis v1.0.11
)
+2
View File
@@ -1,2 +1,4 @@
github.com/leprosus/golang-ttl-map v1.1.7 h1:cF4AAFDDnJTFSV+/42sKLhmMluvLdRlCGS2UaifH6UM= github.com/leprosus/golang-ttl-map v1.1.7 h1:cF4AAFDDnJTFSV+/42sKLhmMluvLdRlCGS2UaifH6UM=
github.com/leprosus/golang-ttl-map v1.1.7/go.mod h1:4QWHJPeVBbrkhOhXdhCv9IEiyj/YzkO04/iexy4vSe0= github.com/leprosus/golang-ttl-map v1.1.7/go.mod h1:4QWHJPeVBbrkhOhXdhCv9IEiyj/YzkO04/iexy4vSe0=
github.com/maxlerebourg/simpleredis v1.0.11 h1:B33TUeIrHtJH2/Qj2bRdU+UZ1BvZwFyP55JWMxHirWg=
github.com/maxlerebourg/simpleredis v1.0.11/go.mod h1:lT4LX02SOsE9PxUcSrz1QW5ZnO86gPbaiYBxmtcXEls=
+122
View File
@@ -0,0 +1,122 @@
// Package cache implements utility routines for manipulating cache.
// It supports currently local file and redis cache.
package cache
import (
"errors"
"fmt"
ttl_map "github.com/leprosus/golang-ttl-map"
simpleredis "github.com/maxlerebourg/simpleredis"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
)
const (
// BannedValue Banned string.
BannedValue = "t"
// NoBannedValue No banned string.
NoBannedValue = "f"
// CaptchaValue Need captcha string.
CaptchaValue = "c"
// CaptchaDoneValue Captcha done string.
CaptchaDoneValue = "d"
// CacheMiss error string when cache is miss.
CacheMiss = "cache:miss"
)
//nolint:gochecknoglobals
var (
redis simpleredis.SimpleRedis
cache = ttl_map.New()
)
type localCache struct{}
func (localCache) get(key string) (string, error) {
value, isCached := cache.Get(key)
valueString, isValid := value.(string)
if isCached && isValid && len(valueString) > 0 {
return valueString, nil
}
return "", errors.New(CacheMiss)
}
func (localCache) set(key, value string, duration int64) {
cache.Set(key, value, duration)
}
func (localCache) delete(key string) {
cache.Del(key)
}
type redisCache struct {
log *logger.Log
}
func (redisCache) get(key string) (string, error) {
value, err := redis.Get(key)
valueString := string(value)
if err == nil && len(valueString) > 0 {
return valueString, nil
}
if err.Error() == simpleredis.RedisMiss {
return "", errors.New(CacheMiss)
}
return "", err
}
func (rc redisCache) set(key, value string, duration int64) {
if err := redis.Set(key, []byte(value), duration); err != nil {
rc.log.Error("cache:setDecisionRedisCache" + err.Error())
}
}
func (rc redisCache) delete(key string) {
if err := redis.Del(key); err != nil {
rc.log.Error("cache:deleteDecisionRedisCache " + err.Error())
}
}
type cacheInterface interface {
set(key, value string, duration int64)
get(key string) (string, error)
delete(key string)
}
// Client Cache client.
type Client struct {
cache cacheInterface
log *logger.Log
}
// New Initialize cache client.
func (c *Client) New(log *logger.Log, isRedis bool, host, pass, database string) {
c.log = log
if isRedis {
redis.Init(host, pass, database)
c.cache = &redisCache{log: log}
} else {
c.cache = &localCache{}
}
c.log.Debug(fmt.Sprintf("cache:New initialized isRedis:%v", isRedis))
}
// Delete delete decision in cache.
func (c *Client) Delete(key string) {
c.log.Debug(fmt.Sprintf("cache:Delete key:%v", key))
c.cache.delete(key)
}
// Get check in the cache if the IP has the banned / not banned value.
// Otherwise return with an error to add the IP in cache if we are on.
func (c *Client) Get(key string) (string, error) {
c.log.Debug(fmt.Sprintf("cache:Get key:%v", key))
return c.cache.get(key)
}
// Set update the cache with the IP as key and the value banned / not banned.
func (c *Client) Set(key string, value string, duration int64) {
c.log.Debug(fmt.Sprintf("cache:Set key:%v value:%v duration:%vs", key, value, duration))
c.cache.set(key, value, duration)
}
+124
View File
@@ -0,0 +1,124 @@
// Package cache implements utility routines for manipulating cache.
// It supports currently local file and redis cache.
package cache
import (
"testing"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
)
func Test_Get(t *testing.T) {
IPInCache := "10.0.0.10"
IPNotInCache := "10.0.0.20"
client := &Client{cache: &localCache{}, log: logger.New("INFO")}
client.Set(IPInCache, BannedValue, 10)
type args struct {
clientIP string
}
tests := []struct {
name string
args args
want string
wantErr bool
valueErr string
}{
{name: "Fetch Known valid IP", args: args{clientIP: IPInCache}, want: BannedValue, wantErr: false, valueErr: ""},
{name: "Fetch Unknown valid IP", args: args{clientIP: IPNotInCache}, want: "", wantErr: true, valueErr: CacheMiss},
{name: "Fetch invalid value", args: args{clientIP: "test"}, want: "", wantErr: true, valueErr: CacheMiss},
{name: "Fetch empty value", args: args{clientIP: ""}, want: "", wantErr: true, valueErr: CacheMiss},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := client.Get(tt.args.clientIP)
if (err != nil) != tt.wantErr {
t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Get() = %v, want %v", got, tt.want)
return
}
if tt.valueErr != "" && tt.valueErr != err.Error() {
t.Errorf("Get() err = %v, want %v", err.Error(), tt.valueErr)
}
})
}
}
func Test_Set(t *testing.T) {
client := &Client{cache: &localCache{}, log: logger.New("INFO")}
IPInCache := "10.0.0.11"
type args struct {
clientIP string
value string
duration int64
}
tests := []struct {
name string
args args
want string
wantErr bool
valueErr string
}{
{name: "Set valid IP in local cache for 0 sec", args: args{clientIP: IPInCache, value: BannedValue, duration: 0}, want: "", wantErr: true, valueErr: CacheMiss},
{name: "Set valid IP in local cache for 10 sec", args: args{clientIP: IPInCache, value: BannedValue, duration: 10}, want: BannedValue, wantErr: false, valueErr: ""},
{name: "Set valid IP in local cache for 10 sec", args: args{clientIP: IPInCache, value: NoBannedValue, duration: 10}, want: NoBannedValue, wantErr: false, valueErr: ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client.Set(tt.args.clientIP, tt.args.value, tt.args.duration)
got, err := client.Get(tt.args.clientIP)
if (err != nil) != tt.wantErr {
t.Errorf("Set() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Set() = %v, want %v", got, tt.want)
return
}
if tt.valueErr != "" && tt.valueErr != err.Error() {
t.Errorf("Set() err = %v, want %v", err.Error(), tt.valueErr)
}
})
}
}
func Test_Delete(t *testing.T) {
IPInCache := "10.0.0.12"
IPNotInCache := "10.0.0.22"
client := &Client{cache: &localCache{}, log: logger.New("INFO")}
client.Set(IPInCache, BannedValue, 10)
type args struct {
clientIP string
}
tests := []struct {
name string
args args
want string
wantErr bool
valueErr string
}{
{name: "Delete Known valid IP", args: args{clientIP: IPInCache}, want: "", wantErr: true, valueErr: CacheMiss},
{name: "Delete Unknown valid IP", args: args{clientIP: IPNotInCache}, want: "", wantErr: true, valueErr: CacheMiss},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client.Delete(tt.args.clientIP)
got, err := client.Get(tt.args.clientIP)
if (err != nil) != tt.wantErr {
t.Errorf("Delete() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Delete() = %v, want %v", got, tt.want)
return
}
if tt.valueErr != "" && tt.valueErr != err.Error() {
t.Errorf("Delete() err = %v, want %v", err.Error(), tt.valueErr)
}
})
}
}
+152
View File
@@ -0,0 +1,152 @@
// Package captcha implements utility for captcha management.
package captcha
import (
"encoding/json"
"fmt"
"html/template"
"net/http"
"net/url"
"strings"
cache "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache"
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
)
// Client Captcha client.
type Client struct {
Valid bool
provider string
siteKey string
secretKey string
remediationCustomHeader string
gracePeriodSeconds int64
captchaTemplate *template.Template
cacheClient *cache.Client
httpClient *http.Client
log *logger.Log
}
type infoProvider struct {
js string
key string
validate string
}
var (
//nolint:gochecknoglobals
captcha = map[string]infoProvider{
configuration.HcaptchaProvider: {
js: "https://hcaptcha.com/1/api.js",
key: "h-captcha",
validate: "https://api.hcaptcha.com/siteverify",
},
configuration.RecaptchaProvider: {
js: "https://www.google.com/recaptcha/api.js",
key: "g-recaptcha",
validate: "https://www.google.com/recaptcha/api/siteverify",
},
configuration.TurnstileProvider: {
js: "https://challenges.cloudflare.com/turnstile/v0/api.js",
key: "cf-turnstile",
validate: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
},
}
)
// New Initialize captcha client.
func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *http.Client, provider, siteKey, secretKey, remediationCustomHeader, captchaTemplatePath string, gracePeriodSeconds int64) error {
c.Valid = provider != ""
if !c.Valid {
return nil
}
c.siteKey = siteKey
c.secretKey = secretKey
c.provider = provider
c.remediationCustomHeader = remediationCustomHeader
html, _ := configuration.GetHTMLTemplate(captchaTemplatePath)
c.captchaTemplate = html
c.gracePeriodSeconds = gracePeriodSeconds
c.log = log
c.httpClient = httpClient
c.cacheClient = cacheClient
return nil
}
// ServeHTTP Handle captcha html page or validation.
func (c *Client) ServeHTTP(rw http.ResponseWriter, r *http.Request, remoteIP string) {
valid, err := c.Validate(r)
if err != nil {
c.log.Info("captcha:ServeHTTP:validate " + err.Error())
rw.WriteHeader(http.StatusBadRequest)
return
}
if valid {
c.log.Debug("captcha:ServeHTTP captcha:valid")
c.cacheClient.Set(remoteIP+"_captcha", cache.CaptchaDoneValue, c.gracePeriodSeconds)
http.Redirect(rw, r, r.URL.String(), http.StatusFound)
return
}
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
if c.remediationCustomHeader != "" {
rw.Header().Set(c.remediationCustomHeader, "captcha")
}
rw.WriteHeader(http.StatusOK)
err = c.captchaTemplate.Execute(rw, map[string]string{
"SiteKey": c.siteKey,
"FrontendJS": captcha[c.provider].js,
"FrontendKey": captcha[c.provider].key,
})
if err != nil {
c.log.Info("captcha:ServeHTTP captchaTemplateServe " + err.Error())
}
}
// Check Verify if the captcha is already done.
func (c *Client) Check(remoteIP string) bool {
value, _ := c.cacheClient.Get(remoteIP + "_captcha")
passed := value == cache.CaptchaDoneValue
c.log.Debug(fmt.Sprintf("captcha:Check ip:%s pass:%v", remoteIP, passed))
return passed
}
type responseProvider struct {
Success bool `json:"success"`
}
// Validate Verify the captcha from provider API.
func (c *Client) Validate(r *http.Request) (bool, error) {
if r.Method != http.MethodPost {
c.log.Debug("captcha:Validate invalid method: " + r.Method)
return false, nil
}
var response = r.FormValue(captcha[c.provider].key + "-response")
if response == "" {
c.log.Debug("captcha:Validate no captcha response found in request")
return false, nil
}
var body = url.Values{}
body.Add("secret", c.secretKey)
body.Add("response", response)
res, err := c.httpClient.PostForm(captcha[c.provider].validate, body)
if err != nil {
return false, err
}
defer func() {
if err = res.Body.Close(); err != nil {
c.log.Error("captcha:Validate " + err.Error())
}
}()
if !strings.Contains(res.Header.Get("content-type"), "application/json") {
c.log.Debug("captcha:Validate responseType:noJson")
return false, nil
}
var captchaResponse responseProvider
err = json.NewDecoder(res.Body).Decode(&captchaResponse)
if err != nil {
return false, err
}
c.log.Debug(fmt.Sprintf("captcha:Validate success:%v", captchaResponse.Success))
return captchaResponse.Success, nil
}
+392
View File
@@ -0,0 +1,392 @@
// Package configuration implements plugin Config, default Config values and validation param functions.
package configuration
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"html/template"
"net/http"
"net/url"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
)
// Enums for crowdsec mode.
const (
AloneMode = "alone"
StreamMode = "stream"
LiveMode = "live"
NoneMode = "none"
AppsecMode = "appsec"
HTTPS = "https"
HTTP = "http"
HcaptchaProvider = "hcaptcha"
RecaptchaProvider = "recaptcha"
TurnstileProvider = "turnstile"
)
// Config the plugin configuration.
type Config struct {
Enabled bool `json:"enabled,omitempty"`
LogLevel string `json:"logLevel,omitempty"`
CrowdsecMode string `json:"crowdsecMode,omitempty"`
CrowdsecAppsecEnabled bool `json:"crowdsecAppsecEnabled,omitempty"`
CrowdsecAppsecHost string `json:"crowdsecAppsecHost,omitempty"`
CrowdsecAppsecFailureBlock bool `json:"crowdsecAppsecFailureBlock,omitempty"`
CrowdsecAppsecUnreachableBlock bool `json:"crowdsecAppsecUnreachableBlock,omitempty"`
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
CrowdsecLapiKeyFile string `json:"crowdsecLapiKeyFile,omitempty"`
CrowdsecLapiTLSInsecureVerify bool `json:"crowdsecLapiTlsInsecureVerify,omitempty"`
CrowdsecLapiTLSCertificateAuthority string `json:"crowdsecLapiTlsCertificateAuthority,omitempty"`
CrowdsecLapiTLSCertificateAuthorityFile string `json:"crowdsecLapiTlsCertificateAuthorityFile,omitempty"`
CrowdsecLapiTLSCertificateBouncer string `json:"crowdsecLapiTlsCertificateBouncer,omitempty"`
CrowdsecLapiTLSCertificateBouncerFile string `json:"crowdsecLapiTlsCertificateBouncerFile,omitempty"`
CrowdsecLapiTLSCertificateBouncerKey string `json:"crowdsecLapiTlsCertificateBouncerKey,omitempty"`
CrowdsecLapiTLSCertificateBouncerKeyFile string `json:"crowdsecLapiTlsCertificateBouncerKeyFile,omitempty"`
CrowdsecCapiMachineID string `json:"crowdsecCapiMachineId,omitempty"`
CrowdsecCapiMachineIDFile string `json:"crowdsecCapiMachineIdFile,omitempty"`
CrowdsecCapiPassword string `json:"crowdsecCapiPassword,omitempty"`
CrowdsecCapiPasswordFile string `json:"crowdsecCapiPasswordFile,omitempty"`
CrowdsecCapiScenarios []string `json:"crowdsecCapiScenarios,omitempty"`
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
UpdateMaxFailure int `json:"updateMaxFailure,omitempty"`
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
RemediationHeadersCustomName string `json:"remediationHeadersCustomName,omitempty"`
ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"`
ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"`
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
RedisCacheEnabled bool `json:"redisCacheEnabled,omitempty"`
RedisCacheHost string `json:"redisCacheHost,omitempty"`
RedisCachePassword string `json:"redisCachePassword,omitempty"`
RedisCachePasswordFile string `json:"redisCachePasswordFile,omitempty"`
RedisCacheDatabase string `json:"redisCacheDatabase,omitempty"`
BanHTMLFilePath string `json:"banHtmlFilePath,omitempty"`
CaptchaHTMLFilePath string `json:"captchaHtmlFilePath,omitempty"`
CaptchaProvider string `json:"captchaProvider,omitempty"`
CaptchaSiteKey string `json:"captchaSiteKey,omitempty"`
CaptchaSiteKeyFile string `json:"captchaSiteKeyFile,omitempty"`
CaptchaSecretKey string `json:"captchaSecretKey,omitempty"`
CaptchaSecretKeyFile string `json:"captchaSecretKeyFile,omitempty"`
CaptchaGracePeriodSeconds int64 `json:"captchaGracePeriodSeconds,omitempty"`
}
func contains(source []string, target string) bool {
for _, item := range source {
if item == target {
return true
}
}
return false
}
// New creates the default plugin configuration.
func New() *Config {
return &Config{
Enabled: false,
LogLevel: "INFO",
CrowdsecMode: LiveMode,
CrowdsecAppsecEnabled: false,
CrowdsecAppsecHost: "crowdsec:7422",
CrowdsecAppsecFailureBlock: true,
CrowdsecAppsecUnreachableBlock: true,
CrowdsecLapiScheme: HTTP,
CrowdsecLapiHost: "crowdsec:8080",
CrowdsecLapiKey: "",
CrowdsecLapiTLSInsecureVerify: false,
UpdateIntervalSeconds: 60,
UpdateMaxFailure: 0,
DefaultDecisionSeconds: 60,
HTTPTimeoutSeconds: 10,
CaptchaProvider: "",
CaptchaSiteKey: "",
CaptchaSecretKey: "",
CaptchaGracePeriodSeconds: 1800,
CaptchaHTMLFilePath: "/captcha.html",
BanHTMLFilePath: "",
RemediationHeadersCustomName: "",
ForwardedHeadersCustomName: "X-Forwarded-For",
ForwardedHeadersTrustedIPs: []string{},
ClientTrustedIPs: []string{},
RedisCacheEnabled: false,
RedisCacheHost: "redis:6379",
RedisCachePassword: "",
RedisCacheDatabase: "",
}
}
// GetVariable get variable from file and after in the variables gave by user.
func GetVariable(config *Config, key string) (string, error) {
value := ""
object := reflect.Indirect(reflect.ValueOf(config))
field := object.FieldByName(key + "File")
// Here linter say you should simplify this code, but lets not, performance is important not clarity and complexity
fp := field.String()
if fp != "" {
file, err := os.Stat(fp)
if err != nil {
return value, fmt.Errorf("%s:%s invalid path %w", key, fp, err)
}
if file.IsDir() {
return value, fmt.Errorf("%s:%s path must be a file", key, fp)
}
fileValue, err := os.ReadFile(filepath.Clean(fp))
if err != nil {
return value, fmt.Errorf("%s:%s read file path failed %w", key, fp, err)
}
value = string(fileValue)
return strings.TrimSpace(value), nil
}
field = object.FieldByName(key)
value = field.String()
return strings.TrimSpace(value), nil
}
// GetHTMLTemplate get compiled HTML template.
func GetHTMLTemplate(path string) (*template.Template, error) {
var err error
if path == "" {
return nil, errors.New("no html template provided")
}
//nolint:gosec
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
html := string(b)
compiledTemplate, err := template.New("html").Parse(html)
if err != nil {
return nil, fmt.Errorf("impossible to compile html template: %w", err)
}
return compiledTemplate, nil
}
// ValidateParams validate all the param gave by user.
//
//nolint:gocyclo,gocognit
func ValidateParams(config *Config) error {
if err := validateParamsRequired(config); err != nil {
return err
}
if err := validateParamsIPs(config.ForwardedHeadersTrustedIPs, "ForwardedHeadersTrustedIPs"); err != nil {
return err
}
if err := validateParamsIPs(config.ClientTrustedIPs, "ClientTrustedIPs"); err != nil {
return err
}
if _, err := GetVariable(config, "RedisCachePassword"); err != nil {
return err
}
if config.CrowdsecMode == AloneMode {
if _, err := GetVariable(config, "CrowdsecCapiMachineID"); err != nil {
return err
}
if _, err := GetVariable(config, "CrowdsecCapiPassword"); err != nil {
return err
}
return nil
}
if config.CaptchaProvider != "" {
if _, err := GetVariable(config, "CaptchaSiteKey"); err != nil {
return err
}
if _, err := GetVariable(config, "CaptchaSecretKey"); err != nil {
return err
}
if _, err := GetHTMLTemplate(config.CaptchaHTMLFilePath); err != nil {
return err
}
}
if config.BanHTMLFilePath != "" {
if _, err := GetHTMLTemplate(config.BanHTMLFilePath); err != nil {
return err
}
}
if err := validateURL("CrowdsecLapi", config.CrowdsecLapiScheme, config.CrowdsecLapiHost); err != nil {
return err
}
if err := validateURL("CrowdsecAppsec", config.CrowdsecLapiScheme, config.CrowdsecAppsecHost); err != nil {
return err
}
lapiKey, err := GetVariable(config, "CrowdsecLapiKey")
if err != nil {
return err
}
certBouncer, err := GetVariable(config, "CrowdsecLapiTLSCertificateBouncer")
if err != nil {
return err
}
certBouncerKey, err := GetVariable(config, "CrowdsecLapiTLSCertificateBouncerKey")
if err != nil {
return err
}
// We need to either have crowdsecLapiKey defined or the BouncerCert and Bouncerkey
if lapiKey == "" && (certBouncer == "" || certBouncerKey == "") {
return errors.New("CrowdsecLapiKey || (CrowdsecLapiTLSCertificateBouncer && CrowdsecLapiTLSCertificateBouncerKey): cannot be all empty")
} else if lapiKey != "" && (certBouncer == "" || certBouncerKey == "") {
lapiKey = strings.TrimSpace(lapiKey)
if err = validateParamsAPIKey(lapiKey); err != nil {
return err
}
}
// Case https to contact Crowdsec LAPI and certificate must be provided
if config.CrowdsecLapiScheme == HTTPS && !config.CrowdsecLapiTLSInsecureVerify {
if err = validateParamsTLS(config); err != nil {
return err
}
}
return nil
}
func validateURL(variable, scheme, host string) error {
// This only check that the format of the URL scheme://host is correct and do not make requests
testURL := url.URL{Scheme: scheme, Host: host}
if _, err := http.NewRequest(http.MethodGet, testURL.String(), nil); err != nil {
return fmt.Errorf("CrowdsecLapiScheme://%sHost: '%v://%v' must be an URL", variable, scheme, host)
}
return nil
}
// validHeaderFieldByte reports whether b is a valid byte in a header
// field name. RFC 7230 says:
// valid ! # $ % & ' * + - . ^ _ ` | ~ DIGIT ALPHA
// See https://httpwg.github.io/specs/rfc7230.html#rule.token.separators
func validateParamsAPIKey(lapiKey string) error {
reg := regexp.MustCompile("^[a-zA-Z0-9 !#$%&'*+-.^_`|~=/]*$")
if !reg.MatchString(lapiKey) {
return fmt.Errorf("CrowdsecLapiKey doesn't valid this regexp: '/%s/'", reg.String())
}
return nil
}
func validateParamsTLS(config *Config) error {
certAuth, err := GetVariable(config, "CrowdsecLapiTLSCertificateAuthority")
if err != nil {
return err
}
if certAuth == "" {
return errors.New("CrowdsecLapiTLSCertificateAuthority must be specified when CrowdsecLapiScheme='https' and CrowdsecLapiTLSInsecureVerify=false")
}
tlsConfig := new(tls.Config)
tlsConfig.RootCAs = x509.NewCertPool()
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuth)) {
return errors.New("failed parsing pem file")
}
return nil
}
func validateParamsIPs(listIP []string, key string) error {
if len(listIP) > 0 {
if _, err := ip.NewChecker(logger.New("INFO"), listIP); err != nil {
return fmt.Errorf("%s must be a list of IP/CIDR :%w", key, err)
}
}
return nil
}
func validateParamsRequired(config *Config) error {
requiredStrings := map[string]string{
"CrowdsecLapiScheme": config.CrowdsecLapiScheme,
"CrowdsecLapiHost": config.CrowdsecLapiHost,
"CrowdsecMode": config.CrowdsecMode,
}
for key, val := range requiredStrings {
if len(val) == 0 {
return fmt.Errorf("%v: cannot be empty", key)
}
}
requiredInt := map[string]int64{
"UpdateIntervalSeconds": config.UpdateIntervalSeconds,
"DefaultDecisionSeconds": config.DefaultDecisionSeconds,
"HTTPTimeoutSeconds": config.HTTPTimeoutSeconds,
"CaptchaGracePeriodSeconds": config.CaptchaGracePeriodSeconds,
}
for key, val := range requiredInt {
if val < 1 {
return fmt.Errorf("%v: cannot be less than 1", key)
}
}
if config.UpdateMaxFailure < -1 {
return errors.New("UpdateMaxFailure: cannot be less than -1")
}
if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode, AppsecMode}, config.CrowdsecMode) {
return errors.New("CrowdsecMode: must be one of 'none', 'live', 'stream', 'alone' or 'appsec'")
}
if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) {
return errors.New("CrowdsecLapiScheme: must be one of 'http' or 'https'")
}
if !contains([]string{"", HcaptchaProvider, RecaptchaProvider, TurnstileProvider}, config.CaptchaProvider) {
return errors.New("CaptchaProvider: must be one of 'hcaptcha', 'recaptcha' or 'turnstile'")
}
return nil
}
// GetTLSConfigCrowdsec get TLS config from Config.
//
//nolint:nestif
func GetTLSConfigCrowdsec(config *Config, log *logger.Log) (*tls.Config, error) {
tlsConfig := new(tls.Config)
tlsConfig.RootCAs = x509.NewCertPool()
//nolint:gocritic
if config.CrowdsecLapiScheme != HTTPS {
log.Debug("getTLSConfigCrowdsec:CrowdsecLapiScheme https:no")
return tlsConfig, nil
} else if config.CrowdsecLapiTLSInsecureVerify {
tlsConfig.InsecureSkipVerify = true
log.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSInsecureVerify tlsInsecure:true")
// If we return here and still want to use client auth this won't work
// return tlsConfig, nil
} else {
certAuthority, err := GetVariable(config, "CrowdsecLapiTLSCertificateAuthority")
if err != nil {
return nil, err
}
if len(certAuthority) > 0 {
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuthority)) {
// here we return because if CrowdsecLapiTLSInsecureVerify is false
// and CA not load, we can't communicate with https
return nil, errors.New("getTLSConfigCrowdsec:cannot load CA and verify cert is enabled")
}
log.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSCertificateAuthority CA added successfully")
}
}
certBouncer, err := GetVariable(config, "CrowdsecLapiTLSCertificateBouncer")
if err != nil {
return nil, err
}
certBouncerKey, err := GetVariable(config, "CrowdsecLapiTLSCertificateBouncerKey")
if err != nil {
return nil, err
}
if certBouncer == "" || certBouncerKey == "" {
return tlsConfig, nil
}
clientCert, err := tls.X509KeyPair([]byte(certBouncer), []byte(certBouncerKey))
if err != nil {
return nil, fmt.Errorf("getTLSClientConfigCrowdsec impossible to generate ClientCert %w", err)
}
tlsConfig.Certificates = append(tlsConfig.Certificates, clientCert)
return tlsConfig, nil
}
+246
View File
@@ -0,0 +1,246 @@
package configuration
import (
"crypto/tls"
"reflect"
"testing"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
)
func getMinimalConfig() *Config {
cfg := New()
cfg.CrowdsecLapiKey = "test"
return cfg
}
func Test_contains(t *testing.T) {
type args struct {
source []string
target string
}
tests := []struct {
name string
args args
want bool
}{
{name: "Contain in the list", args: args{source: []string{"a", "b"}, target: "a"}, want: true},
{name: "Contain not in the list", args: args{source: []string{"a", "b"}, target: "c"}, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := contains(tt.args.source, tt.args.target); got != tt.want {
t.Errorf("contains() = %v, want %v", got, tt.want)
}
})
}
}
func Test_GetVariable(t *testing.T) {
cfg1 := New()
cfg1.CrowdsecLapiKey = "test"
cfg2 := New()
cfg2.CrowdsecLapiKeyFile = "../../tests/.keytest"
cfg3 := New()
cfg3.CrowdsecLapiKeyFile = "../../tests/.bad"
type args struct {
config *Config
key string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{name: "Validate a key string", args: args{config: cfg1, key: "CrowdsecLapiKey"}, want: "test", wantErr: false},
{name: "Validate a key file", args: args{config: cfg2, key: "CrowdsecLapiKey"}, want: "test", wantErr: false},
{name: "Not validate an invalid file", args: args{config: cfg3, key: "CrowdsecLapiKey"}, want: "", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetVariable(tt.args.config, tt.args.key)
if (err != nil) != tt.wantErr {
t.Errorf("getVariable() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("getVariable() = %v, want %v", got, tt.want)
}
})
}
}
func Test_ValidateParams(t *testing.T) {
cfg1 := New()
cfg1.CrowdsecLapiKey = "test\n\n"
cfg2 := New()
cfg2.CrowdsecLapiKey = "test@"
cfg3 := getMinimalConfig()
cfg3.CrowdsecMode = "bad"
cfg4 := getMinimalConfig()
cfg4.UpdateIntervalSeconds = 0
cfg5 := getMinimalConfig()
cfg5.ClientTrustedIPs = []string{0: "bad"}
cfg6 := getMinimalConfig()
cfg6.CrowdsecLapiScheme = HTTPS
cfg6.CrowdsecLapiTLSInsecureVerify = true
cfg8 := getMinimalConfig()
cfg8.CrowdsecLapiScheme = HTTPS
type args struct {
config *Config
}
tests := []struct {
name string
args args
wantErr bool
}{
{name: "Validate minimal config", args: args{config: getMinimalConfig()}, wantErr: false},
{name: "Validate a non trimed crowdsec lapi key", args: args{config: cfg1}, wantErr: false},
{name: "Not validate unauthorized character in crowdsec lapi key", args: args{config: cfg2}, wantErr: true},
{name: "Not validate an absent crowdsec lapi key", args: args{config: New()}, wantErr: true},
{name: "Not validate a not listed item", args: args{config: cfg3}, wantErr: true},
{name: "Not validate a bad number", args: args{config: cfg4}, wantErr: true},
{name: "Not validate a bad clients ips", args: args{config: cfg5}, wantErr: true},
// HTTPS enabled
{name: "Validate https config with insecure verify", args: args{config: cfg6}, wantErr: false},
{name: "Not validate https without cert authority", args: args{config: cfg8}, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := ValidateParams(tt.args.config); (err != nil) != tt.wantErr {
t.Errorf("validateParams() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_validateParamsTLS(t *testing.T) {
type args struct {
config *Config
}
tests := []struct {
name string
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validateParamsTLS(tt.args.config); (err != nil) != tt.wantErr {
t.Errorf("validateParamsTLS() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_validateParamsIPs(t *testing.T) {
type args struct {
listIP []string
key string
}
tests := []struct {
name string
args args
wantErr bool
}{
{name: "Not validate a non ip", args: args{listIP: []string{0: "bad"}}, wantErr: true},
{name: "Not validate localhost", args: args{listIP: []string{0: "localhost"}}, wantErr: true},
{name: "Not validate a weird ip", args: args{listIP: []string{0: "0.0.0.0/89"}}, wantErr: true},
{name: "Not validate a weird ip 2", args: args{listIP: []string{0: "0.0.0.256/12"}}, wantErr: true},
{name: "Validate an ip not trimmed", args: args{listIP: []string{0: " 0.0.0.0/0"}}, wantErr: false},
{name: "Validate an ip", args: args{listIP: []string{0: "0.0.0.0/12"}}, wantErr: false},
{name: "Validate an ip list", args: args{listIP: []string{0: "0.0.0.0/0", 1: "1.1.1.1/1"}}, wantErr: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validateParamsIPs(tt.args.listIP, tt.args.key); (err != nil) != tt.wantErr {
t.Errorf("validateParamsIPs() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_validateParamsRequired(t *testing.T) {
cfg2 := getMinimalConfig()
cfg2.CrowdsecLapiScheme = "bad"
cfg3 := getMinimalConfig()
cfg3.CrowdsecMode = "bad"
cfg4 := getMinimalConfig()
cfg4.UpdateIntervalSeconds = 0
cfg5 := getMinimalConfig()
cfg5.DefaultDecisionSeconds = 0
type args struct {
config *Config
}
tests := []struct {
name string
args args
wantErr bool
}{
{name: "Validate minimal config", args: args{config: getMinimalConfig()}, wantErr: false},
{name: "Not validate a bad crowdsec scheme", args: args{config: cfg2}, wantErr: true},
{name: "Not validate a bad crowdsec mode", args: args{config: cfg3}, wantErr: true},
{name: "Not validate a bad update interval seconds", args: args{config: cfg4}, wantErr: true},
{name: "Not validate a bad default decision seconds", args: args{config: cfg5}, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validateParamsRequired(tt.args.config); (err != nil) != tt.wantErr {
t.Errorf("validateParamsRequired() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_validateParamsAPIKey(t *testing.T) {
type args struct {
lapiKey string
}
tests := []struct {
name string
args args
wantErr bool
}{
{name: "Validate all the valid characters", args: args{lapiKey: "test!#$%&'*+-.^_`|~"}, wantErr: false},
{name: "Not validate a @", args: args{lapiKey: "test@"}, wantErr: true},
{name: "Not validate a (", args: args{lapiKey: "test("}, wantErr: true},
{name: "Not validate a [", args: args{lapiKey: "test["}, wantErr: true},
{name: "Not validate a ?", args: args{lapiKey: "test?"}, wantErr: true},
{name: "Not validate a \\n, (must be trimed before)", args: args{lapiKey: "test\n"}, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validateParamsAPIKey(tt.args.lapiKey); (err != nil) != tt.wantErr {
t.Errorf("validateParamsAPIKey() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_GetTLSConfigCrowdsec(t *testing.T) {
type args struct {
config *Config
}
tests := []struct {
name string
args args
want *tls.Config
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetTLSConfigCrowdsec(tt.args.config, logger.New("INFO"))
if (err != nil) != tt.wantErr {
t.Errorf("getTLSConfigCrowdsec() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("getTLSConfigCrowdsec() = %v, want %v", got, tt.want)
}
})
}
}
+130
View File
@@ -0,0 +1,130 @@
// Package ip implements utility routines to manipulate IP and CIDR.
// It allows searching an IP on a list, and find if an IP is part of a list of CIDR.
package ip
import (
"errors"
"fmt"
"net"
"net/http"
"strings"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
)
// CHECKER
// Checker allows to check that addresses are in a trusted IPs.
type Checker struct {
authorizedIPs []*net.IP
authorizedIPsNet []*net.IPNet
}
// NewChecker builds a new Checker given a list of CIDR-Strings to trusted IPs.
func NewChecker(log *logger.Log, trustedIPs []string) (*Checker, error) {
checker := &Checker{}
for _, ipMaskRaw := range trustedIPs {
ipMask := strings.TrimSpace(ipMaskRaw)
if ipAddr := net.ParseIP(ipMask); ipAddr != nil {
checker.authorizedIPs = append(checker.authorizedIPs, &ipAddr)
log.Debug(fmt.Sprintf("IP %v is trusted", ipAddr))
continue
}
_, ipAddr, err := net.ParseCIDR(ipMask)
if err != nil {
return nil, fmt.Errorf("parsing CIDR trusted IPs %s: %w", ipAddr, err)
}
checker.authorizedIPsNet = append(checker.authorizedIPsNet, ipAddr)
log.Debug(fmt.Sprintf("IP network %v is trusted", ipAddr))
}
return checker, nil
}
// Contains checks if provided address is in the trusted IPs.
func (ip *Checker) Contains(addr string) (bool, error) {
if len(addr) == 0 {
return false, errors.New("Contains:noAddress")
}
ipAddr, err := parseIP(addr)
if err != nil {
return false, fmt.Errorf("Contains:parseAddress addr:%s %w", addr, err)
}
return ip.ContainsIP(ipAddr), nil
}
// ContainsIP checks if provided address is in the trusted IPs.
func (ip *Checker) ContainsIP(addr net.IP) bool {
for _, authorizedIP := range ip.authorizedIPs {
if authorizedIP.Equal(addr) {
return true
}
}
for _, authorizedNet := range ip.authorizedIPsNet {
if authorizedNet.Contains(addr) {
return true
}
}
return false
}
func parseIP(addr string) (net.IP, error) {
userIP := net.ParseIP(addr)
if userIP == nil {
return nil, fmt.Errorf("parseIP:parseAddress %s", addr)
}
return userIP, nil
}
// STRATEGY
// PoolStrategy is a strategy based on an IP Checker.
// It allows to check whether addresses are in a given pool of IPs.
type PoolStrategy struct {
Checker *Checker
}
// GetIP checks the list of Forwarded IPs (most recent first) against the
// Checker pool of IPs. It returns the first IP that is not in the pool, or the
// empty string otherwise.
func (s *PoolStrategy) getIP(req *http.Request, customHeader string) string {
if s.Checker == nil {
return ""
}
xff := req.Header.Get(customHeader)
xffs := strings.Split(xff, ",")
for i := len(xffs) - 1; i >= 0; i-- {
xffTrimmed := strings.TrimSpace(xffs[i])
if len(xffTrimmed) == 0 {
continue
}
if contain, _ := s.Checker.Contains(xffTrimmed); !contain {
return xffTrimmed
}
}
return ""
}
// GetRemoteIP It returns the first IP that is not in the pool, or the empty string otherwise.
func GetRemoteIP(req *http.Request, strategy *PoolStrategy, customHeader string) (string, error) {
remoteIP := strategy.getIP(req, customHeader)
if len(remoteIP) != 0 {
return remoteIP, nil
}
remoteIP, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
return "", fmt.Errorf("GetRemoteIP:extractIP: %w", err)
}
return remoteIP, nil
}
+48
View File
@@ -0,0 +1,48 @@
// Package logger implements utility routines to write to stdout and stderr.
// It supports debug, info and error level
package logger
import (
"io"
"log"
"os"
)
// Log Logger struct.
type Log struct {
logError *log.Logger
logInfo *log.Logger
logDebug *log.Logger
}
// New Set Default log level to info in case log level to defined.
func New(logLevel string) *Log {
logError := log.New(io.Discard, "ERROR: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime)
logInfo := log.New(io.Discard, "INFO: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime)
logDebug := log.New(io.Discard, "DEBUG: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime)
logError.SetOutput(os.Stderr)
logInfo.SetOutput(os.Stdout)
if logLevel == "DEBUG" {
logDebug.SetOutput(os.Stdout)
}
return &Log{
logError: logError,
logInfo: logInfo,
logDebug: logDebug,
}
}
// Info log to Stdout.
func (l *Log) Info(str string) {
l.logInfo.Printf(str)
}
// Debug log to Stdout.
func (l *Log) Debug(str string) {
l.logDebug.Printf(str)
}
// Error log to Stderr.
func (l *Log) Error(str string) {
l.logError.Printf(str)
}
+1
View File
@@ -0,0 +1 @@
test
+2
View File
@@ -0,0 +1,2 @@
.idea
*.iml
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{one line to give the program's name and a brief idea of what it does.}
Copyright (C) {year} {name of author}
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
{project} Copyright (C) {year} {fullname}
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

Some files were not shown because too many files have changed in this diff Show More