Compare commits

...
114 Commits
Author SHA1 Message Date
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
81 changed files with 3986 additions and 426 deletions
+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 ✨
-->
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
go-version: [ 1.17, 1.x ]
go-version: [ 1.19, 1.x ]
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
+3 -3
View File
@@ -12,9 +12,9 @@ jobs:
name: Main Process
runs-on: ubuntu-latest
env:
GO_VERSION: 1.17
GOLANGCI_LINT_VERSION: v1.46.2
YAEGI_VERSION: v0.13.0
GO_VERSION: 1.19
GOLANGCI_LINT_VERSION: v1.50.0
YAEGI_VERSION: v0.14.2
CGO_ENABLED: 0
defaults:
run:
+3 -2
View File
@@ -1,6 +1,7 @@
.idea/
.DS_Store
config
./config/
conf
db
logs
docker-compose.dev.yml
docker-compose.dev.yml
+17 -4
View File
@@ -11,7 +11,7 @@ linters-settings:
golint:
min-confidence: 0
gocyclo:
min-complexity: 12
min-complexity: 15
goconst:
min-len: 5
min-occurrences: 4
@@ -29,11 +29,20 @@ linters-settings:
linters:
enable-all: true
disable:
- deadcode # deprecated
- exhaustivestruct # deprecated
- golint # deprecated
- ifshort # deprecated
- interfacer # deprecated
- maligned # deprecated
- nosnakecase # deprecated
- scopelint # deprecated
- golint # deprecated
- exhaustivestruct # deprecated
- scopelint # deprecated
- structcheck # deprecated
- varcheck # deprecated
- sqlclosecheck # not relevant (SQL)
- rowserrcheck # not relevant (SQL)
- execinquery # not relevant (SQL)
- cyclop # duplicate of gocyclo
- bodyclose # Too many false positives: https://github.com/timakin/bodyclose/issues/30
- dupl
@@ -52,12 +61,16 @@ linters:
- gomnd
- forbidigo
- varnamelen
- wastedassign # is disabled because of generics
- gofumpt
- gci
issues:
exclude-use-default: false
max-per-linter: 0
max-same-issues: 0
exclude: []
exclude:
- "G402: TLS InsecureSkipVerify may be true."
exclude-rules:
- path: (.+)_test.go
linters:
+51 -3
View File
@@ -25,18 +25,66 @@ run_dev:
run_local:
docker-compose -f docker-compose.local.yml up -d --remove-orphans
run_behindproxy:
docker-compose -f exemples/behind-proxy/docker-compose.cloudflare.yml up -d --remove-orphans
run_cacheredis:
docker-compose -f exemples/redis-cache/docker-compose.redis.yml up -d --remove-orphans
run_trustedips:
docker-compose -f exemples/trusted-ips/docker-compose.trusted.yml up -d --remove-orphans
run_binaryvm:
cd exemples/binary-vm/ && sudo vagrant up
run_tlsauth:
docker-compose -f exemples/tls-auth/docker-compose.tls-auth.yml down && docker-compose -f exemples/tls-auth/docker-compose.tls-auth.yml up -d && docker-compose -f exemples/tls-auth/docker-compose.tls-auth.yml restart && docker-compose -f exemples/tls-auth/docker-compose.tls-auth.yml logs -f
run:
docker-compose -f docker-compose.yml up -d --remove-orphans
restart_docker_dev:
restart_dev:
docker-compose -f docker-compose.dev.yml restart
restart_docker_local:
restart_local:
docker-compose -f docker-compose.local.yml restart
restart_docker:
restart:
docker-compose -f docker-compose.yml restart
restart_behindproxy:
docker-compose -f exemples/behind-proxy/docker-compose.cloudflare.yml restart
restart_cacheredis:
docker-compose -f exemples/redis-cache/docker-compose.redis.yml restart
restart_trustedips:
docker-compose -f exemples/trusted-ips/docker-compose.trusted.yml restart
restart_tlsauth:
docker-compose -f exemples/tls-auth/docker-compose.tls-auth.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 exemples/behind-proxy/docker-compose.cloudflare.yml down --remove-orphans
docker-compose -f exemples/redis-cache/docker-compose.redis.yml down --remove-orphans
docker-compose -f exemples/trusted-ips/docker-compose.trusted.yml down --remove-orphans
docker-compose -f exemples/tls-auth/docker-compose.tls-auth.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 exemples/binary-vm/ && sudo vagrant destroy -f
show_metrics:
docker exec crowdsec cscli metrics
+186 -81
View File
@@ -6,15 +6,16 @@
# Crowdsec Bouncer Traefik plugin
This plugins aims to implement a Crowdsec Bouncer into a traefik plugin.
> [CrowdSec](https://www.crowdsec.net/) is an open-source and collaborative IPS (Intrusion Prevention System) and a security suite.
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 treafik to authorize or block requests from IP based and their reputation and behavior.
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 IP banned from the crowdsec network.
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 against your website.
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.
There are 4 operating modes (CrowdsecMode) for this plugin:
@@ -23,9 +24,12 @@ There are 4 operating modes (CrowdsecMode) for this plugin:
| 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 localy banned IP, but can work without a crowdsec service|
| 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 localy banned IP, but can work without a crowdsec service. |
The recommanded mode for performance is the streaming mode, decisions are updated every 60 sec by default and that's the only communication between traefik and crowdsec. Every requests that happens hits the cache for quick decisions.
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 using the filesystem, or a separate Redis instance.
Support for Redis is currently in beta (requires version 7.0.X of Redis).
## Usage
@@ -37,49 +41,93 @@ make run
```
### Note
Each middleware in traefik has it's own data and is instanciated by service.
This means if there are 10 services protected by the bouncer in streaming alone or live mode, the cache will be duplicated to all 10 services.
This is because traefik does not allow plugins to store data locally that can be consummed.
The synchronisation with the crowdsec service will happen also 10 times in the period selected.
It should be taken into account when fixing this period so each middleware has time to sync data from crowdsec.
At each start of synchronisation, the middleware will wait a random number of seconds to avoid simultaneous calls to crowdsec.
**/!\ 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*
### Variables
- Enabled
- bool
- default: false
- enable the plugin
- LogLevel
- string
- default: `INFO`, expected values are: `INFO`, `DEBUG`
- CrowdsecMode
- string
- default: `stream`, expected value are: `none`, `live`, `stream`, `alone`
- default: `live`, expected values are: `none`, `live`, `stream`, `alone`
- CrowdsecLapiScheme
- string
- default: `http`, expected value are: `http`, `https`
- default: `http`, expected values are: `http`, `https`
- CrowdsecLapiHost
- string
- default: "crowdsec:8080"
- Crowdsec LAPI available on which host.
- Crowdsec LAPI available on which host and port.
- CrowdsecLapiKey
- string
- Crowdsec LAPI generated key for the bouncer : **must be unique by service**.
- CrowdsecCapiLogin
- default: ""
- Crowdsec LAPI key for the bouncer : **must be unique by service**.
- 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)
- ForwardedHeadersTrustedIPs
- []string
- default: []
- List of IPs of trusted Proxies that are in front of traefik (ex: Cloudflare)
- ForwardedHeadersCustomName
- string
- default: "X-Forwarded-For"
- Name of the header where the real IP of the client should be retrieved
- RedisCacheEnabled
- bool
- default: false
- enable Redis cache instead of filesystem 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
- UpdateIntervalSeconds
- int64
- default: 60
- Used only in `stream` mode, the interval between requests to fetch blacklisted IPs from LAPI
- DefaultDecisionSeconds
- int64
- default: 60
- Used only in `live` mode, maximum decision duration
- CrowdsecCapiMachineId
- string
- Used only in `alone` mode, login for Crowdsec CAPI
- CrowdsecCapiPwd
- CrowdsecCapiPassword
- string
- Used only in `alone` mode, password for Crowdsec CAPI
- CrowdsecCapiScenarios
- []string
- Used only in `alone` mode, scenarios for Crowdsec CAPI
- UpdateIntervalSeconds
- int64
- default: 60
- Used only in `stream` mode, interval between fetching blacklisted IPs from LAPI
- DefaultDecisionSeconds
- int64
- default: 60
- Used only in `live` mode, decision duration of accepted IPs
### Configuration
@@ -94,6 +142,7 @@ experimental:
plugins:
bouncer:
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
version: vX.Y.Z # To update
```
```yaml
@@ -102,7 +151,7 @@ experimental:
http:
routers:
my-router:
rule: host(`woami.localhost`)
rule: host(`whoami.localhost`)
service: service-foo
entryPoints:
- web
@@ -120,32 +169,79 @@ http:
plugin:
bouncer:
enabled: false
logLevel: DEBUG
updateIntervalSeconds: 60
defaultDecisionSeconds: 60
crowdsecMode: stream
crowdsecLapiKey: privateKey
crowdsecMode: live
crowdsecLapiKey: privateKey-foo
crowdsecLapiKeyFile: /etc/traefik/cs-privateKey-foo
crowdsecLapiHost: crowdsec:8080
crowdsecLapiScheme: http
crowdsecCapiLogin: login
crowdsecCapiPwd: password
crowdsecLapiTLSInsecureVerify: false
crowdsecCapiMachineId: login
crowdsecCapiPassword: password
crowdsecCapiScenarios:
- scenario1
- scenario2
- 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
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
```
Except for the crowdsecLapiKey, crowdsecCapiLogin, crowdsecCapiPwd, crowdsecCapiScenarios, these are the default value of the plugin.
#### Generate LAPI KEY (exept for `alone` mode)
You need to generate a crowdsec API key for the LAPI.
You can follow the documentation here: https://docs.crowdsec.net/docs/user_guides/lapi_mgmt/
#### Fill variable with value of file
`CrowdsecLapiTlsCertificateBouncerKey`, `CrowdsecLapiTlsCertificateBouncer`, `CrowdsecLapiTlsCertificateAuthority`, `CrowdsecCapiMachineId`, `CrowdsecCapiPassword` and `CrowdsecLapiKey` 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-test.yml
This LAPI key must be set where is noted FIXME-LAPI-KEY in the docker-compose.yml
```yaml
...
whoami:
@@ -160,55 +256,59 @@ crowdsec:
...
```
You can then run all the containers:
```bash
docker-compose up -d
```
#### 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
...
traefik:
command:
...
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.0.0"
...
whoami:
labels:
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapilogin=LOGIN"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapipwd=PASSWORD"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapiscenarios=scenario1, scenario2, ..."
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
```
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
```
#### Add manually an IP to the blocklist (testing purpose)
#### Use certificates to authenticate with CrowdSec
You can follow the example in `exemples/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 `exemples/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 exemple](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/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 # this will be effective 4h
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
```
### Exemples
#### 1. Behind another proxy service (ex: clouflare) [exemples/behind-proxy/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/behind-proxy/README.md)
#### 2. With Redis as an external shared cache [exemples/redis-cache/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/redis-cache/README.md)
#### 3. Using Trusted IP (ex: LAN OR VPN) that won't get filtered by crowdsec [exemples/trusted-ips/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/trusted-ips/README.md)
#### 4. Using Crowdsec and Traefik installed as binary in a single VM [exemples/binary-vm/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/binary-vm/README.md)
#### 5. Using https communication and tls authentication with Crowdsec [exemples/tls-auth/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/tls-auth/README.md)
#### 6. Using Crowdsec and Traefik in Kubernetes [exemples/kubernetes/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/kubernetes/README.md)
#### 7. Using Traefik in standalone mode without Crowdsec [exemples/standalone-mode/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/standalone-mode/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 `./plugins-local` 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:
@@ -227,19 +327,24 @@ The source code of the plugin should be organized as follows:
└── vendor/*
```
For local developpement a docker-compose.local.yml is provided and reproduce the directory layout needed by traefik. This works once you have generated and filled your LAPI-KEY (crowdsecLapiKey), if not look below for informations
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
Me and [mathieuHa](https://github.com/mathieuHa) have been using traefik since 2020 at [Primadviz](https://primadviz.com).
We come from web developper and security engineer background and wanted to add the power of a very promesing technology (Crowdsec) into the edge router we love.
[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 run into this project: https://github.com/fbonalair/traefik-crowdsec-bouncer
It was using traefik and forward auth middleware to verify every requests.
They had to go through a webserver which then contacts of another webservice (the crowdsec LAPI) to make a decision based on the source IP.
We initially proposed some improvement by implementing a streaming mode and a local cache.
With the Traefik hackathon we deciced to implement our solution directly as a traefik plugin which could be found by every one on plugins.traefik.io and be more performant.
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.
+1 -1
View File
@@ -1,4 +1,4 @@
filenames:
- /var/log/traefik/access.log
labels:
type: traefik
type: traefik
+271 -266
View File
@@ -1,177 +1,240 @@
package crowdsec_bouncer_traefik_plugin
// Package crowdsec_bouncer_traefik_plugin implements a middleware that communicates with crowdsec.
// It can cache results to filesystem or redis, or even ask crowdsec for every requests.
package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"math/rand"
"net"
"net/http"
"net/url"
"strings"
"text/template"
"time"
ttl_map "github.com/leprosus/golang-ttl-map"
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"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
)
const (
aloneMode = "alone"
streamMode = "stream"
liveMode = "live"
noneMode = "none"
crowdsecLapiHeader = "X-Api-Key"
crowdsecCapiHeader = "Authorization"
crowdsecLapiRoute = "v1/decisions"
crowdsecLapiStreamRoute = "v1/decisions/stream"
crowdsecCapiLogin = "v2/watchers/login"
crowdsecCapiDecisions = "v2/decisions/stream"
cacheBannedValue = "t"
cacheNoBannedValue = "f"
crowdsecCapiStreamRoute = "v2/decisions/stream"
cacheTimeoutKey = "updated"
)
// Config the plugin configuration.
type Config struct {
Enabled bool `json:"enabled,omitempty"`
CrowdsecMode string `json:"crowdsecMode,omitempty"`
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
CrowdsecCapiLogin string `json:"crowdsecCapiLogin,omitempty"`
CrowdsecCapiPwd string `json:"crowdsecCapiPwd,omitempty"`
CrowdsecCapiScenarios []string `json:"crowdsecCapiScenarios,omitempty"`
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
}
//nolint:gochecknoglobals
var (
isStartup = true
isCrowdsecStreamHealthy = true
ticker chan bool
)
// CreateConfig creates the default plugin configuration.
func CreateConfig() *Config {
return &Config{
Enabled: false,
CrowdsecMode: streamMode,
CrowdsecLapiScheme: "http",
CrowdsecLapiHost: "crowdsec:8080",
CrowdsecLapiKey: "",
CrowdsecCapiLogin: "",
CrowdsecCapiPwd: "",
CrowdsecCapiScenarios: []string{},
UpdateIntervalSeconds: 60,
DefaultDecisionSeconds: 60,
}
func CreateConfig() *configuration.Config {
return configuration.New()
}
// Bouncer a Bouncer plugin.
// Bouncer a Bouncer struct.
type Bouncer struct {
next http.Handler
name string
template *template.Template
enabled bool
crowdsecStreamHealthy bool
crowdsecScheme string
crowdsecHost string
crowdsecKey string
crowdsecMode string
crowdsecMachineID string
crowdsecPassword string
crowdsecScenarios []string
updateInterval int64
defaultDecisionTimeout int64
crowdsecLogin string
crowdsecPwd string
crowdsecScenarios []string
client *http.Client
cache *ttl_map.Heap
customHeader string
crowdsecStreamRoute string
crowdsecHeader string
clientPoolStrategy *ip.PoolStrategy
serverPoolStrategy *ip.PoolStrategy
httpClient *http.Client
cacheClient *cache.Client
}
// New creates the crowdsec bouncer plugin.
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
err := validateParams(config)
func New(ctx context.Context, next http.Handler, config *configuration.Config, name string) (http.Handler, error) {
logger.Init(config.LogLevel)
err := configuration.ValidateParams(config)
if err != nil {
logger.Error(fmt.Sprintf("New:validateParams %s", err.Error()))
return nil, err
}
serverChecker, _ := ip.NewChecker(config.ForwardedHeadersTrustedIPs)
clientChecker, _ := ip.NewChecker(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 = "api.crowdsec.net"
config.CrowdsecLapiScheme = "https"
config.UpdateIntervalSeconds = 7200
crowdsecStreamRoute = crowdsecCapiStreamRoute
crowdsecHeader = crowdsecCapiHeader
} else {
crowdsecStreamRoute = crowdsecLapiStreamRoute
crowdsecHeader = crowdsecLapiHeader
tlsConfig, err = configuration.GetTLSConfigCrowdsec(config)
if err != nil {
logger.Error(fmt.Sprintf("New:getTLSConfigCrowdsec fail to get tlsConfig %s", err.Error()))
return nil, err
}
apiKey, errAPIKey := configuration.GetVariable(config, "CrowdsecLapiKey")
if errAPIKey != nil && len(tlsConfig.Certificates) == 0 {
logger.Error(fmt.Sprintf("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup %s", errAPIKey.Error()))
return nil, err
}
config.CrowdsecLapiKey = apiKey
}
bouncer := &Bouncer{
next: next,
name: name,
template: template.New("CrowdsecBouncer").Delims("[[", "]]"),
enabled: config.Enabled,
crowdsecStreamHealthy: false,
crowdsecMode: config.CrowdsecMode,
crowdsecScheme: config.CrowdsecLapiScheme,
crowdsecHost: config.CrowdsecLapiHost,
crowdsecKey: config.CrowdsecLapiKey,
crowdsecLogin: config.CrowdsecCapiLogin,
crowdsecPwd: config.CrowdsecCapiPwd,
crowdsecMachineID: config.CrowdsecCapiMachineID,
crowdsecPassword: config.CrowdsecCapiPassword,
crowdsecScenarios: config.CrowdsecCapiScenarios,
updateInterval: config.UpdateIntervalSeconds,
customHeader: config.ForwardedHeadersCustomName,
defaultDecisionTimeout: config.DefaultDecisionSeconds,
client: &http.Client{
crowdsecStreamRoute: crowdsecStreamRoute,
crowdsecHeader: crowdsecHeader,
serverPoolStrategy: &ip.PoolStrategy{
Checker: serverChecker,
},
clientPoolStrategy: &ip.PoolStrategy{
Checker: clientChecker,
},
httpClient: &http.Client{
Transport: &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
TLSClientConfig: tlsConfig,
},
Timeout: 5 * time.Second,
Timeout: 10 * time.Second,
},
cache: ttl_map.New(),
cacheClient: &cache.Client{},
}
if config.CrowdsecMode == streamMode || config.CrowdsecMode == aloneMode {
go func() {
rand.Seed(time.Now().UnixNano())
timeout := rand.Int63n(30)
logger(fmt.Sprintf("Wait: %v", timeout))
time.Sleep(time.Duration(timeout) * time.Second)
if config.CrowdsecMode == aloneMode {
getToken(bouncer)
config.RedisCachePassword, _ = configuration.GetVariable(config, "RedisCachePassword")
bouncer.cacheClient.New(
config.RedisCacheEnabled,
config.RedisCacheHost,
config.RedisCachePassword,
config.RedisCacheDatabase,
)
if (config.CrowdsecMode == configuration.StreamMode || config.CrowdsecMode == configuration.AloneMode) && ticker == nil {
if config.CrowdsecMode == configuration.AloneMode {
if err := getToken(bouncer); err != nil {
logger.Error(fmt.Sprintf("New:getToken %s", err.Error()))
return nil, err
}
go handleStreamCache(bouncer)
ticker := time.NewTicker(time.Duration(config.UpdateIntervalSeconds) * time.Second)
for range ticker.C {
go handleStreamCache(bouncer)
}
}()
}
handleStreamTicker(bouncer)
isStartup = false
ticker = startTicker(config, func() {
handleStreamTicker(bouncer)
})
}
logger.Debug(fmt.Sprintf("New initialized mode:%s", config.CrowdsecMode))
return bouncer, nil
}
// ServeHTTP principal function of plugin.
func (a *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if !a.enabled {
a.next.ServeHTTP(rw, req)
//
//nolint:nestif
func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if !bouncer.enabled {
bouncer.next.ServeHTTP(rw, req)
return
}
// TODO Make sur remote address does not include the port.
remoteHost, _, err := net.SplitHostPort(req.RemoteAddr)
// Here we check for the trusted IPs in the customHeader
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.customHeader)
if err != nil {
logger(fmt.Sprintf("failed to extract ip from remote address: %v", err))
a.next.ServeHTTP(rw, req)
logger.Error(fmt.Sprintf("ServeHTTP:getRemoteIp ip:%s %s", remoteIP, err.Error()))
rw.WriteHeader(http.StatusForbidden)
return
}
isTrusted, err := bouncer.clientPoolStrategy.Checker.Contains(remoteIP)
if err != nil {
logger.Error(fmt.Sprintf("ServeHTTP:checkerContains ip:%s %s", remoteIP, err.Error()))
rw.WriteHeader(http.StatusForbidden)
return
}
// if our IP is in the trusted list we bypass the next checks
logger.Debug(fmt.Sprintf("ServeHTTP ip:%s isTrusted:%v", remoteIP, isTrusted))
if isTrusted {
bouncer.next.ServeHTTP(rw, req)
return
}
if a.crowdsecMode == streamMode || a.crowdsecMode == aloneMode || a.crowdsecMode == liveMode {
isBanned, err := getDecision(a, remoteHost)
if err == nil {
// TODO This should be simplified
if bouncer.crowdsecMode != configuration.NoneMode {
isBanned, cacheErr := bouncer.cacheClient.GetDecision(remoteIP)
if cacheErr != nil {
errString := cacheErr.Error()
logger.Debug(fmt.Sprintf("ServeHTTP:getDecision ip:%s isBanned:false %s", remoteIP, errString))
if errString != cache.CacheMiss {
logger.Error(fmt.Sprintf("ServeHTTP:getDecision ip:%s %s", remoteIP, errString))
rw.WriteHeader(http.StatusForbidden)
return
}
} else {
logger.Debug(fmt.Sprintf("ServeHTTP ip:%s cache:hit isBanned:%v", remoteIP, isBanned))
if isBanned {
rw.WriteHeader(http.StatusForbidden)
} else {
a.next.ServeHTTP(rw, req)
bouncer.next.ServeHTTP(rw, req)
}
return
}
}
// Right here if we cannot join the stream we forbid the request to go on.
if a.crowdsecMode == streamMode || a.crowdsecMode == aloneMode {
if a.crowdsecStreamHealthy {
a.next.ServeHTTP(rw, req)
if bouncer.crowdsecMode == configuration.StreamMode || bouncer.crowdsecMode == configuration.AloneMode {
if isCrowdsecStreamHealthy {
bouncer.next.ServeHTTP(rw, req)
} else {
logger.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s", remoteIP))
rw.WriteHeader(http.StatusForbidden)
}
} else {
handleNoStreamCache(a, rw, req, remoteHost)
err = handleNoStreamCache(bouncer, remoteIP)
if err != nil {
logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:true %s", remoteIP, err.Error()))
rw.WriteHeader(http.StatusForbidden)
} else {
logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:false", remoteIP))
bouncer.next.ServeHTTP(rw, req)
}
}
}
@@ -203,239 +266,181 @@ type Login struct {
Expire string `json:"expire"`
}
func logger(str string) {
log.Printf("Crowdsec Bouncer Traefik Plugin - %s", str)
}
func contains(source []string, target string) bool {
for _, a := range source {
if a == target {
return true
}
}
return false
}
// Get Decision 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 getDecision(a *Bouncer, clientIP string) (bool, error) {
banned, isCached := a.cache.Get(clientIP)
bannedString, isValid := banned.(string)
if isCached && isValid && len(bannedString) > 0 {
return bannedString == cacheBannedValue, nil
}
return false, fmt.Errorf("no cache data")
}
func setDecision(a *Bouncer, clientIP string, isBanned bool, duration int64) {
if a.crowdsecMode == noneMode {
return
}
if isBanned {
logger(fmt.Sprintf("%v banned", clientIP))
a.cache.Set(clientIP, cacheBannedValue, duration)
func handleStreamTicker(bouncer *Bouncer) {
if err := handleStreamCache(bouncer); err != nil {
isCrowdsecStreamHealthy = false
logger.Error(err.Error())
} else {
a.cache.Set(clientIP, cacheNoBannedValue, duration)
isCrowdsecStreamHealthy = true
}
}
func handleNoStreamCache(a *Bouncer, rw http.ResponseWriter, req *http.Request, remoteHost string) {
// We are now in none or live mode.
func startTicker(config *configuration.Config, work func()) chan bool {
ticker := time.NewTicker(time.Duration(config.UpdateIntervalSeconds) * time.Second)
stop := make(chan bool, 1)
go func() {
defer logger.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) error {
isLiveMode := bouncer.crowdsecMode == configuration.LiveMode
routeURL := url.URL{
Scheme: a.crowdsecScheme,
Host: a.crowdsecHost,
Scheme: bouncer.crowdsecScheme,
Host: bouncer.crowdsecHost,
Path: crowdsecLapiRoute,
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteHost),
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteIP),
}
body, err := crowdsecQuery(bouncer, routeURL.String(), false)
if err != nil {
return err
}
body := crowdsecQuery(a, routeURL.String(), false)
if bytes.Equal(body, []byte("null")) {
setDecision(a, remoteHost, false, a.defaultDecisionTimeout)
a.next.ServeHTTP(rw, req)
return
if isLiveMode {
bouncer.cacheClient.SetDecision(remoteIP, false, bouncer.defaultDecisionTimeout)
}
return nil
}
var decisions []Decision
err := json.Unmarshal(body, &decisions)
err = json.Unmarshal(body, &decisions)
if err != nil {
logger(fmt.Sprintf("failed to parse body: %s", err))
rw.WriteHeader(http.StatusForbidden)
return
return fmt.Errorf("handleNoStreamCache:parseBody %w", err)
}
if len(decisions) == 0 {
setDecision(a, remoteHost, false, a.defaultDecisionTimeout)
a.next.ServeHTTP(rw, req)
return
if isLiveMode {
bouncer.cacheClient.SetDecision(remoteIP, false, bouncer.defaultDecisionTimeout)
}
return nil
}
rw.WriteHeader(http.StatusForbidden)
duration, err := time.ParseDuration(decisions[0].Duration)
if err != nil {
logger(fmt.Sprintf("failed to parse duration: %s", err))
return
return fmt.Errorf("handleNoStreamCache:parseDuration %w", err)
}
setDecision(a, remoteHost, true, int64(duration.Seconds()))
if isLiveMode {
durationSecond := int64(duration.Seconds())
if bouncer.defaultDecisionTimeout < durationSecond {
durationSecond = bouncer.defaultDecisionTimeout
}
bouncer.cacheClient.SetDecision(remoteIP, true, durationSecond)
}
return fmt.Errorf("handleNoStreamCache:banned")
}
func handleStreamCache(a *Bouncer) {
// TODO clean properly on exit.
var rawQuery string
var path string
if a.crowdsecMode == aloneMode {
rawQuery = ""
path = crowdsecCapiDecisions
} else {
rawQuery = fmt.Sprintf("startup=%t", !a.crowdsecStreamHealthy)
path = crowdsecLapiStreamRoute
func getToken(bouncer *Bouncer) error {
loginURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Host: bouncer.crowdsecHost,
Path: crowdsecCapiLogin,
}
streamRouteURL := url.URL{
Scheme: a.crowdsecScheme,
Host: a.crowdsecHost,
Path: path,
RawQuery: rawQuery,
}
body := crowdsecQuery(a, streamRouteURL.String(), false)
var stream Stream
err := json.Unmarshal(body, &stream)
body, err := crowdsecQuery(bouncer, loginURL.String(), true)
if err != nil {
logger(fmt.Sprintf("error while parsing body: %s", err))
a.crowdsecStreamHealthy = false
return
return err
}
var login Login
err = json.Unmarshal(body, &login)
if err != nil {
isCrowdsecStreamHealthy = false
return fmt.Errorf("getToken:parsingBody %w", err)
}
if login.Code == 200 && len(login.Token) > 0 {
bouncer.crowdsecKey = login.Token
logger.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.GetDecision(cacheTimeoutKey)
if err == nil {
logger.Debug("handleStreamCache:alreadyUpdated")
return nil
}
if err.Error() != cache.CacheMiss {
return err
}
bouncer.cacheClient.SetDecision(cacheTimeoutKey, false, 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
err = json.Unmarshal(body, &stream)
if err != nil {
return fmt.Errorf("handleStreamCache:parsingBody %w", err)
}
for _, decision := range stream.New {
duration, err := time.ParseDuration(decision.Duration)
if err == nil {
setDecision(a, decision.Value, true, int64(duration.Seconds()))
bouncer.cacheClient.SetDecision(decision.Value, true, int64(duration.Seconds()))
}
}
for _, decision := range stream.Deleted {
a.cache.Del(decision.Value)
bouncer.cacheClient.DeleteDecision(decision.Value)
}
a.crowdsecStreamHealthy = true
logger.Debug("handleStreamCache:updated")
isCrowdsecStreamHealthy = true
return nil
}
func getToken(a *Bouncer) {
loginURL := url.URL{
Scheme: a.crowdsecScheme,
Host: a.crowdsecHost,
Path: crowdsecCapiLogin,
}
body := crowdsecQuery(a, loginURL.String(), true)
var login Login
err := json.Unmarshal(body, &login)
if err != nil {
logger(fmt.Sprintf("error while parsing body: %s", err))
a.crowdsecStreamHealthy = false
return
}
if login.Code == 200 && len(login.Token) > 0 {
a.crowdsecKey = login.Token
}
}
func crowdsecQuery(a *Bouncer, stringURL string, isPost bool) []byte {
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"]}`,
a.crowdsecLogin,
a.crowdsecPwd,
strings.Join(a.crowdsecScenarios, `","`),
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)
}
if a.crowdsecMode == aloneMode {
req.Header.Add(crowdsecCapiHeader, a.crowdsecKey)
} else {
req.Header.Add(crowdsecLapiHeader, a.crowdsecKey)
}
res, err := a.client.Do(req)
req.Header.Add(bouncer.crowdsecHeader, bouncer.crowdsecKey)
res, err := bouncer.httpClient.Do(req)
if err != nil {
logger(fmt.Sprintf("error while fetching %v: %s", stringURL, err))
a.crowdsecStreamHealthy = false
return nil
return nil, fmt.Errorf("crowdsecQuery url:%s %w", stringURL, err)
}
if res.StatusCode == http.StatusUnauthorized && a.crowdsecMode == aloneMode {
oldToken := a.crowdsecKey
getToken(a)
if oldToken == a.crowdsecKey {
a.crowdsecStreamHealthy = false
return nil
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(a, stringURL, false)
return crowdsecQuery(bouncer, stringURL, false)
}
if res.StatusCode != http.StatusOK {
logger(fmt.Sprintf("error while fetching %v, status code: %d", stringURL, res.StatusCode))
a.crowdsecStreamHealthy = false
return nil
return nil, fmt.Errorf("crowdsecQuery url:%s, statusCode:%d", stringURL, res.StatusCode)
}
defer func(body io.ReadCloser) {
err = body.Close()
if err != nil {
logger(fmt.Sprintf("failed to close body reader: %s", err))
defer func() {
if err = res.Body.Close(); err != nil {
logger.Error(fmt.Sprintf("crowdsecQuery:closeBody %s", err.Error()))
}
}(res.Body)
body, err := ioutil.ReadAll(res.Body)
if err != nil {
logger(fmt.Sprintf("error while reading body: %s", err))
a.crowdsecStreamHealthy = false
return nil
}
return body
}
}()
body, err := io.ReadAll(res.Body)
func validateParams(config *Config) error {
var requiredStrings map[string]string
if config.CrowdsecMode == aloneMode {
requiredStrings = map[string]string{
"CrowdsecCapiLogin": config.CrowdsecLapiScheme,
"CrowdsecCapiPwd": config.CrowdsecLapiHost,
}
for _, val := range config.CrowdsecCapiScenarios {
if len(val) == 0 {
return fmt.Errorf("CrowdsecCapiScenarios: one or more scenario are empty")
}
}
config.UpdateIntervalSeconds = 7200
config.CrowdsecLapiKey = ""
config.CrowdsecLapiScheme = "https"
config.CrowdsecLapiHost = "api.crowdsec.net"
} else {
requiredStrings = map[string]string{
"CrowdsecLapiScheme": config.CrowdsecLapiScheme,
"CrowdsecLapiHost": config.CrowdsecLapiHost,
"CrowdsecLapiKey": config.CrowdsecLapiKey,
"CrowdsecMode": config.CrowdsecMode,
}
requiredInt := map[string]int64{
"UpdateIntervalSeconds": config.UpdateIntervalSeconds,
"DefaultDecisionSeconds": config.DefaultDecisionSeconds,
}
for key, val := range requiredInt {
if val < 1 {
return fmt.Errorf("%v: cannot be less than 1", key)
}
}
}
for key, val := range requiredStrings {
if len(val) == 0 {
return fmt.Errorf("%v: cannot be empty", key)
}
}
if !contains([]string{noneMode, liveMode, streamMode, aloneMode}, config.CrowdsecMode) {
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live' or 'stream'")
}
if !contains([]string{"http", "https"}, config.CrowdsecLapiScheme) {
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'http' or 'https'")
}
testURL := url.URL{
Scheme: config.CrowdsecLapiScheme,
Host: config.CrowdsecLapiHost,
}
_, err := http.NewRequest(http.MethodGet, testURL.String(), nil)
if err != nil {
return fmt.Errorf("CrowdsecLapiScheme://CrowdsecLapiHost: '%v://%v' must be an URL", config.CrowdsecLapiScheme, config.CrowdsecLapiHost)
return nil, fmt.Errorf("crowdsecQuery:readBody %w", err)
}
return nil
return body, nil
}
+163 -7
View File
@@ -1,28 +1,31 @@
package crowdsec_bouncer_traefik_plugin_test
package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
import (
"context"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"text/template"
crowdsec_bouncer_traefik_plugin "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 TestCrowdSec(t *testing.T) {
cfg := crowdsec_bouncer_traefik_plugin.CreateConfig()
cfg.CrowdsecLapiKey = "caca"
func TestServeHTTP(t *testing.T) {
cfg := CreateConfig()
cfg.CrowdsecLapiKey = "test"
ctx := context.Background()
next := http.HandlerFunc(func(rw http.ResponseWriter, req *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 {
t.Fatal(err)
}
recorder := httptest.NewRecorder()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil)
if err != nil {
t.Fatal(err)
@@ -30,3 +33,156 @@ func TestCrowdSec(t *testing.T) {
handler.ServeHTTP(recorder, req)
}
func TestNew(t *testing.T) {
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)
}
})
}
}
func TestBouncer_ServeHTTP(t *testing.T) {
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
customHeader 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(t *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,
customHeader: tt.fields.customHeader,
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)
}
})
}
}
+24 -34
View File
@@ -2,10 +2,11 @@ version: "3.8"
services:
traefik:
image: "traefik:v2.8.8"
image: "traefik:v2.9.10"
container_name: "traefik"
restart: unless-stopped
command:
# - "--log.level=DEBUG"
- "--log.level=DEBUG"
- "--accesslog"
- "--accesslog.filepath=/var/log/traefik/access.log"
- "--api.insecure=true"
@@ -18,53 +19,42 @@ services:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-local:/var/log/traefik
- ./:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
labels:
- "traefik.enable=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapilogin=3829a6c9870e4726a377d8951ebb64a1m8psGvMaq1ykJ3zX"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapipwd=Q5pgN8bRNInHGdx6QCksdPOJVLeLQ7ipJntSeuP3r8088zXzRVs4G8liXAKfI1k6"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/http-backdoors-attempts,baudneo/zoneminder-bf"
ports:
- 80:80
- 8080:8080
depends_on:
- crowdsec
whoami1:
whoami-foo:
image: traefik/whoami
container_name: "simple-service1"
container_name: "simple-service-foo"
restart: unless-stopped
labels:
- "traefik.enable=true"
# Definition of the router
- "traefik.http.routers.router1.rule=Host(`localhost`) && Path(`/foo`)"
- "traefik.http.routers.router1.entrypoints=web"
- "traefik.http.routers.router1.middlewares=crowdsec1@docker"
# Definition of the service
- "traefik.http.services.service1.loadbalancer.server.port=80"
# Definitin of the middleware
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.enabled=true"
# crowdseclapikey must be uniq to the middleware attached to the service
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.routers.router-foo.rule=Path(`/foo`)"
- "traefik.http.routers.router-foo.entrypoints=web"
- "traefik.http.routers.router-foo.middlewares=crowdsec-foo@docker"
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
whoami2:
image: traefik/whoami
container_name: "simple-service2"
container_name: "simple-service-bar"
restart: unless-stopped
labels:
- "traefik.enable=true"
# Definition of the router
- "traefik.http.routers.router2.rule=Host(`localhost`) && Path(`/bar`)"
- "traefik.http.routers.router2.entrypoints=web"
- "traefik.http.routers.router2.middlewares=crowdsec2@docker"
# Definition of the service
- "traefik.http.services.service2.loadbalancer.server.port=80"
# Definitin of the middleware
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.enabled=true"
# crowdseclapikey must be uniq to the middleware attached to the service
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.crowdseclapikey=44c36dac5c4140af9f06f397508e82c7"
- "traefik.http.routers.router-bar.rule=Path(`/bar`)"
- "traefik.http.routers.router-bar.entrypoints=web"
- "traefik.http.routers.router-bar.middlewares=crowdsec-bar@docker"
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapikey=44c36dac5c4140af9f06f397508e82c7"
crowdsec:
image: crowdsecurity/crowdsec:v1.4.1
image: crowdsecurity/crowdsec:v1.4.6
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
CUSTOM_HOSTNAME: crowdsec
@@ -80,4 +70,4 @@ services:
volumes:
logs-local:
crowdsec-db-local:
crowdsec-config-local:
crowdsec-config-local:
+29 -22
View File
@@ -2,10 +2,10 @@ version: "3.8"
services:
traefik:
image: "traefik:v2.8.8"
image: "traefik:v2.9.10"
container_name: "traefik"
restart: unless-stopped
command:
# - "--log.level=DEBUG"
- "--accesslog"
- "--accesslog.filepath=/var/log/traefik/access.log"
- "--api.insecure=true"
@@ -14,7 +14,7 @@ services:
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.0.6"
- "--experimental.plugins.bouncer.version=v1.1.11"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "logs:/var/log/traefik"
@@ -26,39 +26,46 @@ services:
whoami1:
image: traefik/whoami
container_name: "simple-service-1"
container_name: "simple-service-foo"
restart: unless-stopped
labels:
- "traefik.enable=true"
# Definition of the router
- "traefik.http.routers.router1.rule=Host(`localhost`) && Path(`/foo`)"
- "traefik.http.routers.router1.entrypoints=web"
- "traefik.http.routers.router1.middlewares=crowdsec2@docker"
- "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.service1.loadbalancer.server.port=80"
# Definitin of the middleware
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.enabled=true"
# crowdseclapikey must be uniq to the middleware attached to the service
- "traefik.http.middlewares.crowdsec1.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-1"
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
# Definition of the middleware
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.enabled=true"
# crowdseclapikey must be unique to the middleware attached to the service
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-1"
# forwardedheaderstrustedips should be the IP of the proxy that is in front of traefik (if any)
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
whoami2:
image: traefik/whoami
container_name: "simple-service-2"
container_name: "simple-service-bar"
restart: unless-stopped
labels:
- "traefik.enable=true"
# Definition of the router
- "traefik.http.routers.router2.rule=Host(`localhost`) && Path(`/bar`)"
- "traefik.http.routers.router2.entrypoints=web"
- "traefik.http.routers.router2.middlewares=crowdsec2@docker"
- "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.service2.loadbalancer.server.port=80"
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
# Definitin of the middleware
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.enabled=true"
# crowdseclapikey must be uniq to the middleware attached to the service
- "traefik.http.middlewares.crowdsec2.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-2"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.enabled=true"
# crowdseclapikey must be unique to the middleware attached to the service
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-2"
# forwardedheaderstrustedips should be the IP of the proxy that is in front of traefik (if any)
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
crowdsec:
image: crowdsecurity/crowdsec:v1.4.1
image: crowdsecurity/crowdsec:v1.4.6
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
CUSTOM_HOSTNAME: crowdsec
@@ -76,4 +83,4 @@ services:
volumes:
logs:
crowdsec-db:
crowdsec-config:
crowdsec-config:
+21
View File
@@ -0,0 +1,21 @@
# Exemple
## 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
```
+4
View File
@@ -0,0 +1,4 @@
filenames:
- /var/log/traefik/access.log
labels:
type: traefik
@@ -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
@@ -0,0 +1,110 @@
version: "3.8"
services:
cloudflare:
image: "traefik:v2.9.10"
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-exemple.yaml:/cloud.yaml:ro
- logs-cloudflare:/var/log/traefik
ports:
- 80:80
- 8080:8080
traefik:
image: "traefik:v2.9.10"
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.1.11"
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"
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"
# Definitin of the middleware
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.enabled=true"
# crowdseclapikey must be uniq to the middleware attached to the service
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdsecmode=live"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.loglevel=DEBUG"
whoami-bar:
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-bar@docker"
# Definition of the service
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
# Definitin of the middleware
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.enabled=true"
# crowdseclapikey must be uniq to the middleware attached to the service
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapikey=44c36dac5c4140af9f06f397508e82c7"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecmode=live"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.loglevel=DEBUG"
crowdsec:
image: crowdsecurity/crowdsec:v1.4.6
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK_DEV_1: 40796d93c2958f9e58345514e67740e5
BOUNCER_KEY_TRAEFIK_DEV_2: 44c36dac5c4140af9f06f397508e82c7
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 @@
# Exemple
### 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/v2.9.10/traefik_v2.9.10_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.9.0/whoami_v1.9.0_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
+91
View File
@@ -0,0 +1,91 @@
### Kubernetes Exemple
#### 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 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 --install --namespace=traefik \
--values=./traefik/values.yml \
traefik traefik/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.4.6
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.containo.us/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.containo.us/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
+19
View File
@@ -0,0 +1,19 @@
image:
tag: v2.9.10
logs:
general:
level: DEBUG
access:
enabled: true
fields:
headers:
defaultmode: keep
experimental:
plugins:
enabled: true
additionalArguments:
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.11"
@@ -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 @@
# Exemple
## 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
@@ -0,0 +1,116 @@
version: "3.8"
services:
traefik:
image: "traefik:v2.9.10"
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.1.11"
# - "--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
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-foo@docker"
# Definition of the service
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
# Definition of the middleware
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.enabled=true"
# crowdseclapikey must be uniq to the middleware attached to the service
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.rediscacheenabled=true"
# Contact redis-unsecure without a password
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.rediscachehost=redis-insecure:6379"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.loglevel=DEBUG"
whoami-redis-secure:
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(`/redis-secure`)"
- "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-bar.plugin.bouncer.enabled=true"
# crowdseclapikey must be uniq to the middleware attached to the service
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapikey=44c36dac5c4140af9f06f397508e82c7"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.rediscacheenabled=true"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.rediscachepassword=FIXME"
# Contact redis-secure with password
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.rediscachehost=redis-secure:6379"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.loglevel=DEBUG"
crowdsec:
image: crowdsecurity/crowdsec:v1.4.6
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK_DEV_1: 40796d93c2958f9e58345514e67740e5
BOUNCER_KEY_TRAEFIK_DEV_2: 44c36dac5c4140af9f06f397508e82c7
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.9-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.9-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,45 @@
version: "3.8"
services:
traefik:
image: "traefik:v2.9.10"
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.1.11"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
ports:
- 80:80
- 8080:8080
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-foo@docker"
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.enabled=true"
# - "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdsecmode=alone"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.CrowdsecCapiMachineId=logincacacalfkrjebfreifgzfblezgyfoerxsqxsqxsqxsr"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.CrowdsecCapiPassword=Password2"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/sshd,crowdsecurity/asterisk_bf,crowdsecurity/asterisk_user_enum,crowdsecurity/base-http-scenarios"
volumes:
logs-local:
+16
View File
@@ -0,0 +1,16 @@
FROM ubuntu:22.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" ]
+93
View File
@@ -0,0 +1,93 @@
# Exemple
## 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:80/foo
```
Access to a route that communicate via https and authenticate with a client certificate:
```
curl http://localhost:80/bar
```
Access to the traefik dashboard
```
curl http://localhost:8080/dashboard/#/
```
To play the demo environnement run:
```bash
make run_tlsauth
```
Note:
> This example is still in Beta and use a new version of Crowdsec (v1.4.3) at time of writing
A functionnality has been disabled in Crowdsec in order to make the example work DISABLE_AGENT: "true"
+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
@@ -0,0 +1,109 @@
version: "3.8"
services:
traefik:
image: "traefik:v2.9.10"
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.1.11"
# - "--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:
- 80: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=Path(`/foo`)"
- "traefik.http.routers.router-foo.entrypoints=web"
- "traefik.http.routers.router-foo.middlewares=crowdsec-foo@docker"
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapischeme=https"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdsecLapiTLSCertificateAuthorityFile=/etc/traefik/crowdsec-certs/inter.pem"
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-bar@docker"
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapischeme=https"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecLapiTLSCertificateAuthorityFile=/etc/traefik/crowdsec-certs/inter.pem"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecLapiTLSCertificateBouncerFile=/etc/traefik/crowdsec-certs/bouncer.pem"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecLapiTLSCertificateBouncerKeyFile=/etc/traefik/crowdsec-certs/bouncer-key.pem"
crowdsec:
image: crowdsecurity/crowdsec:v1.4.6
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"
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"
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 @@
# Exemple
## 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
@@ -0,0 +1,91 @@
version: "3.8"
services:
traefik:
image: "traefik:v2.9.10"
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.1.11"
# - "--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"
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-foo.plugin.bouncer.enabled=true"
# crowdseclapikey must be uniq to the middleware attached to the service
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
# Replace 10.0.10.30/32 by your IP range which is "trusted"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.clienttrustedips=10.0.10.30/32"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.loglevel=DEBUG"
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-bar@docker"
# Definition of the service
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
# Definition of the middleware
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.enabled=true"
# crowdseclapikey must be uniq to the middleware attached to the service
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapikey=44c36dac5c4140af9f06f397508e82c7"
# Replace 10.0.10.30/32 by your IP range which is "trusted"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.clienttrustedips=10.0.10.30/32"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.loglevel=DEBUG"
crowdsec:
image: crowdsecurity/crowdsec:v1.4.6
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK_DEV_1: 40796d93c2958f9e58345514e67740e5
BOUNCER_KEY_TRAEFIK_DEV_2: 44c36dac5c4140af9f06f397508e82c7
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
go 1.17
go 1.19
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.7
)
+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/go.mod h1:4QWHJPeVBbrkhOhXdhCv9IEiyj/YzkO04/iexy4vSe0=
github.com/maxlerebourg/simpleredis v1.0.7 h1:d53p3GOIgQtxxWuqsWMGTJ0dZjP8UhiDX8L1q2QZ/vY=
github.com/maxlerebourg/simpleredis v1.0.7/go.mod h1:/DH8zOK6kDskSqoX/m5CJJdNGfkIQZd/ERBJgytDDSk=
+118
View File
@@ -0,0 +1,118 @@
// Package cache implements utility routines for manipulating cache.
// It supports currently local file and redis cache.
package cache
import (
"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 (
cacheBannedValue = "t"
cacheNoBannedValue = "f"
)
// CacheMiss error string when cache is miss.
const CacheMiss = "cache:miss"
//nolint:gochecknoglobals
var (
redis simpleredis.SimpleRedis
cache = ttl_map.New()
)
type localCache struct{}
func (localCache) getDecision(clientIP string) (bool, error) {
banned, isCached := cache.Get(clientIP)
bannedString, isValid := banned.(string)
if isCached && isValid && len(bannedString) > 0 {
return bannedString == cacheBannedValue, nil
}
return false, fmt.Errorf(CacheMiss)
}
func (localCache) setDecision(clientIP string, value string, duration int64) {
cache.Set(clientIP, value, duration)
}
func (localCache) deleteDecision(clientIP string) {
cache.Del(clientIP)
}
type redisCache struct{}
func (redisCache) getDecision(clientIP string) (bool, error) {
banned, err := redis.Get(clientIP)
bannedString := string(banned)
if err == nil && len(bannedString) > 0 {
return bannedString == cacheBannedValue, nil
}
if err.Error() == simpleredis.RedisMiss {
return false, fmt.Errorf(CacheMiss)
}
return false, err
}
func (redisCache) setDecision(clientIP string, value string, duration int64) {
if err := redis.Set(clientIP, []byte(value), duration); err != nil {
logger.Error(fmt.Sprintf("cache:setDecisionRedisCache %s", err.Error()))
}
}
func (redisCache) deleteDecision(clientIP string) {
if err := redis.Del(clientIP); err != nil {
logger.Error(fmt.Sprintf("cache:deleteDecisionRedisCache %s", err.Error()))
}
}
type cacheInterface interface {
setDecision(clientIP string, value string, duration int64)
getDecision(clientIP string) (bool, error)
deleteDecision(clientIP string)
}
// Client Cache client.
type Client struct {
cache cacheInterface
}
// New Initialize cache client.
func (client *Client) New(isRedis bool, host, pass, database string) {
if isRedis {
redis.Init(host, pass, database)
client.cache = &redisCache{}
} else {
client.cache = &localCache{}
}
logger.Debug(fmt.Sprintf("cache:New initialized isRedis:%v", isRedis))
}
// DeleteDecision delete decision in cache.
func (client *Client) DeleteDecision(clientIP string) {
logger.Debug(fmt.Sprintf("cache:DeleteDecision ip:%v", clientIP))
client.cache.deleteDecision(clientIP)
}
// GetDecision 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 (client *Client) GetDecision(clientIP string) (bool, error) {
logger.Debug(fmt.Sprintf("cache:GetDecision ip:%v", clientIP))
return client.cache.getDecision(clientIP)
}
// SetDecision update the cache with the IP as key and the value banned / not banned.
func (client *Client) SetDecision(clientIP string, isBanned bool, duration int64) {
logger.Debug(fmt.Sprintf("cache:SetDecision ip:%v isBanned:%v duration:%vs", clientIP, isBanned, duration))
var value string
if isBanned {
value = cacheBannedValue
} else {
value = cacheNoBannedValue
}
client.cache.setDecision(clientIP, value, duration)
}
+102
View File
@@ -0,0 +1,102 @@
// Package cache implements utility routines for manipulating cache.
// It supports currently local file and redis cache.
package cache
import (
"testing"
)
func Test_GetDecision(t *testing.T) {
IPInCache := "10.0.0.10"
IPNotInCache := "10.0.0.20"
client := &Client{cache: &localCache{}}
client.SetDecision(IPInCache, true, 10)
type args struct {
clientIP string
}
tests := []struct {
name string
args args
want bool
wantErr bool
valueErr string
}{
{name: "Fetch Known valid IP", args: args{clientIP: IPInCache}, want: true, wantErr: false, valueErr: ""},
{name: "Fetch Unknown valid IP", args: args{clientIP: IPNotInCache}, want: false, wantErr: true, valueErr: "cache:miss"},
{name: "Fetch invalid value", args: args{clientIP: "test"}, want: false, wantErr: true, valueErr: "cache:miss"},
{name: "Fetch empty value", args: args{clientIP: ""}, want: false, wantErr: true, valueErr: "cache:miss"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := client.GetDecision(tt.args.clientIP)
if (err != nil) != tt.wantErr {
t.Errorf("GetDecision() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("GetDecision() = %v, want %v", got, tt.want)
return
}
if tt.valueErr != "" && tt.valueErr != err.Error() {
t.Errorf("GetDecision() err = %v, want %v", err.Error(), tt.valueErr)
}
})
}
}
func Test_SetDecision(t *testing.T) {
client := &Client{cache: &localCache{}}
IPInCache := "10.0.0.11"
type args struct {
clientIP string
value bool
duration int64
}
tests := []struct {
name string
args args
want bool
}{
{name: "Set valid IP in local cache for 0 sec", args: args{clientIP: IPInCache, value: true, duration: 0}, want: false},
{name: "Set valid IP in local cache for 10 sec", args: args{clientIP: IPInCache, value: true, duration: 10}, want: true},
{name: "Set valid IP in local cache for 10 sec", args: args{clientIP: IPInCache, value: false, duration: 10}, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client.SetDecision(tt.args.clientIP, tt.args.value, tt.args.duration)
got, _ := client.GetDecision(tt.args.clientIP)
if got != tt.want {
t.Errorf("SetDecision() = %v, want %v", got, tt.want)
return
}
})
}
}
func Test_DeleteDecision(t *testing.T) {
IPInCache := "10.0.0.12"
IPNotInCache := "10.0.0.22"
client := &Client{cache: &localCache{}}
client.SetDecision(IPInCache, true, 10)
type args struct {
clientIP string
}
tests := []struct {
name string
args args
want bool
}{
{name: "Delete Known valid IP", args: args{clientIP: IPInCache}, want: false},
{name: "Delete Unknown valid IP", args: args{clientIP: IPNotInCache}, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client.DeleteDecision(tt.args.clientIP)
got, _ := client.GetDecision(tt.args.clientIP)
if got != tt.want {
t.Errorf("DeleteDecision() = %v, want %v", got, tt.want)
return
}
})
}
}
+304
View File
@@ -0,0 +1,304 @@
// Package configuration implements plugin Config, default Config values and validation param functions.
package configuration
import (
"crypto/tls"
"crypto/x509"
"fmt"
"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"
HTTPS = "https"
HTTP = "http"
)
// Config the plugin configuration.
type Config struct {
Enabled bool `json:"enabled,omitempty"`
LogLevel string `json:"logLevel,omitempty"`
CrowdsecMode string `json:"crowdsecMode,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"`
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
ForwardedHeadersCustomName string `json:"forwardedheaderscustomheader,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"`
}
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,
CrowdsecLapiScheme: HTTP,
CrowdsecLapiHost: "crowdsec:8080",
CrowdsecLapiKey: "",
CrowdsecLapiTLSInsecureVerify: false,
UpdateIntervalSeconds: 60,
DefaultDecisionSeconds: 60,
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(fmt.Sprintf("%sFile", key))
// 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
}
// 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
}
// This only check that the format of the URL scheme:// is correct and do not make requests
testURL := url.URL{
Scheme: config.CrowdsecLapiScheme,
Host: config.CrowdsecLapiHost,
}
if _, err := http.NewRequest(http.MethodGet, testURL.String(), nil); err != nil {
return fmt.Errorf("CrowdsecLapiScheme://CrowdsecLapiHost: '%v://%v' must be an URL", config.CrowdsecLapiScheme, config.CrowdsecLapiHost)
}
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 fmt.Errorf("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
}
// 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.Match([]byte(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 fmt.Errorf("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 fmt.Errorf("failed parsing pem file")
}
return nil
}
func validateParamsIPs(listIP []string, key string) error {
if len(listIP) > 0 {
if _, err := ip.NewChecker(listIP); err != nil {
return fmt.Errorf("%s must be a list of IP/CIDR :%w", key, err)
}
} else {
logger.Debug(fmt.Sprintf("No IP provided for %s", key))
}
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,
}
for key, val := range requiredInt {
if val < 1 {
return fmt.Errorf("%v: cannot be less than 1", key)
}
}
if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode}, config.CrowdsecMode) {
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live', 'stream' or 'alone'")
}
if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) {
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'http' or 'https'")
}
return nil
}
// GetTLSConfigCrowdsec get TLS config from Config.
func GetTLSConfigCrowdsec(config *Config) (*tls.Config, error) {
tlsConfig := new(tls.Config)
tlsConfig.RootCAs = x509.NewCertPool()
//nolint:gocritic
if config.CrowdsecLapiScheme != HTTPS {
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiScheme https:no")
return tlsConfig, nil
} else if config.CrowdsecLapiTLSInsecureVerify {
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSInsecureVerify tlsInsecure:true")
tlsConfig.InsecureSkipVerify = 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
}
cert := []byte(certAuthority)
if !tlsConfig.RootCAs.AppendCertsFromPEM(cert) {
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSCertificateAuthority read cert failed")
// here we return because if CrowdsecLapiTLSInsecureVerify is false
// and CA not load, we can't communicate with https
return nil, fmt.Errorf("getTLSConfigCrowdsec:cannot load CA and verify cert is enabled")
}
}
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
}
+244
View File
@@ -0,0 +1,244 @@
package configuration
import (
"crypto/tls"
"reflect"
"testing"
)
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 trimed", 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)
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)
}
})
}
}
+129
View File
@@ -0,0 +1,129 @@
// 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 (
"fmt"
"net"
"net/http"
"strings"
"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(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)
logger.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)
logger.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, fmt.Errorf("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
}
+39
View File
@@ -0,0 +1,39 @@
// Package logger implements utility routines to write to stdout and stderr.
// It supports debug, info and error level
package logger
import (
"io"
"log"
"os"
)
var (
loggerInfo = log.New(io.Discard, "INFO: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime) //nolint:gochecknoglobals
loggerDebug = log.New(io.Discard, "DEBUG: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime) //nolint:gochecknoglobals
loggerError = log.New(io.Discard, "ERROR: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime) //nolint:gochecknoglobals
)
// Init Set Default log level to info in case log level to defined.
func Init(logLevel string) {
loggerError.SetOutput(os.Stderr)
loggerInfo.SetOutput(os.Stdout)
if logLevel == "DEBUG" {
loggerDebug.SetOutput(os.Stdout)
}
}
// Info log to Stdout.
func Info(str string) {
loggerInfo.Printf(str)
}
// Debug log to Stdout.
func Debug(str string) {
loggerDebug.Printf(str)
}
// Error log to Stderr.
func Error(str string) {
loggerError.Printf(str)
}
+1
View File
@@ -0,0 +1 @@
test
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+31
View File
@@ -0,0 +1,31 @@
# simpleredis
Minimal go redis with only `get`, `set` and `delete` operation.
It supports password authentication with redis.
With **NO** external dependencies.
## Example
```go
import simpleredis "github.com/maxlerebourg/simpleredis"
var redis simpleredis.SimpleRedis
redis.Init("redis:6379", "", "") // redisHost, redisPass, redisDatabase
err := redis.Set("test", []bytes("whatever"), 60), // Set key "test" with "whatever" for 60 seconds
if err != nil {
...
}
val, err := redis.Get("test") // get key test
if err != nil {
// err could be only redis:unreachable, redis:miss or redis:timeout available in simpleredis.RedisUnreachable
...
}
err = redis.Del("test")
if err != nil {
...
}
```
## Author
Max Lerebourg @ [Primadviz.com](https://primadviz.com)
Mathieu Hanotaux
+176
View File
@@ -0,0 +1,176 @@
// Package simpleredis implements utility routines for interacting.
// It supports currently the following operations: GET, SET, DELETE,
// and support timetoleave for keys.
package simpleredis
import (
"bufio"
"fmt"
"net"
"net/textproto"
"strings"
"time"
)
// Error strings for redis.
const (
RedisUnreachable = "redis:unreachable"
RedisMiss = "redis:miss"
RedisTimeout = "redis:timeout"
RedisNoAuth = "redis:noauth"
)
// A redisCmd is used to communicate with redis at low level using commands.
type redisCmd struct {
Command string
Name string
Data []byte
Duration int64
Error error
}
// A SimpleRedis is used to communicate with redis.
type SimpleRedis struct {
host string
pass string
database string
}
func genRedisArray(params ...[]byte) []byte {
MSG := ""
for cntr := 0; cntr < len(params); cntr++ {
MSG = strings.Join([]string{MSG, string(params[cntr])}, " ")
}
MSG = strings.Trim(MSG, " ")
MSG = strings.Join([]string{MSG, "\r\n"}, "")
return []byte(MSG)
}
func send(wr *textproto.Writer, method string, data []byte) {
if err := wr.PrintfLine(string(data)); err != nil {
fmt.Printf("redis:%s %s", method, err.Error())
}
}
func (sr *SimpleRedis) waitRedis(reader *textproto.Reader, channel chan redisCmd) {
for {
select {
case <-time.After(time.Second * 1):
channel <- redisCmd{Error: fmt.Errorf(RedisTimeout)}
return
default:
read, _ := reader.ReadLineBytes()
if string(read) != "+OK" {
channel <- redisCmd{Error: fmt.Errorf(RedisNoAuth)}
return
}
}
// breaks out of for
break
}
}
func (sr *SimpleRedis) askRedis(cmd redisCmd, channel chan redisCmd) {
dialer := net.Dialer{Timeout: 2 * time.Second}
conn, err := dialer.Dial("tcp", sr.host)
if err != nil {
channel <- redisCmd{Error: fmt.Errorf(RedisUnreachable)}
return
}
defer func() {
if err := conn.Close(); err != nil {
fmt.Printf("redis:connClose %s", err.Error())
}
}()
writer := textproto.NewWriter(bufio.NewWriter(conn))
reader := textproto.NewReader(bufio.NewReader(conn))
if sr.pass != "" {
data := genRedisArray([]byte("AUTH"), []byte(sr.pass))
send(writer, "auth", data)
sr.waitRedis(reader, channel)
}
if sr.database != "" {
data := genRedisArray([]byte("SELECT"), []byte(sr.database))
send(writer, "select", data)
sr.waitRedis(reader, channel)
}
switch cmd.Command {
case "SET":
data := genRedisArray([]byte("SET"), []byte(cmd.Name), cmd.Data, []byte("EX"), []byte(fmt.Sprintf("%d", cmd.Duration)))
send(writer, "set", data)
case "DEL":
data := genRedisArray([]byte("DEL"), []byte(cmd.Name))
send(writer, "del", data)
case "GET":
data := genRedisArray([]byte("GET"), []byte(cmd.Name))
send(writer, "get", data)
for {
select {
case <-time.After(time.Second * 1):
channel <- redisCmd{Error: fmt.Errorf(RedisTimeout)}
return
default:
read, _ := reader.ReadLineBytes()
str := string(read)
if strings.Contains(str, "-NOAUTH") {
channel <- redisCmd{Error: fmt.Errorf(RedisNoAuth)}
return
} else if str != "$1" {
channel <- redisCmd{Error: fmt.Errorf(RedisMiss)}
return
}
read, _ = reader.ReadLineBytes()
channel <- redisCmd{Data: read}
return
}
}
}
}
// Init sets the redisHost used to connect to redis.
func (sr *SimpleRedis) Init(host, pass, database string) {
sr.host = host
sr.pass = pass
sr.database = database
}
// Get fetches the value for key name in redis.
func (sr *SimpleRedis) Get(name string) ([]byte, error) {
cmd := redisCmd{
Command: "GET",
Name: name,
}
channel := make(chan redisCmd)
go sr.askRedis(cmd, channel)
resp := <-channel
if resp.Error != nil {
return nil, resp.Error
}
return resp.Data, nil
}
// Set updates the value for key name in redis with value data for duration.
func (sr *SimpleRedis) Set(name string, data []byte, duration int64) error {
cmd := redisCmd{
Command: "SET",
Name: name,
Data: data,
Duration: duration,
}
go sr.askRedis(cmd, nil)
return nil
}
// Del removes the key name in redis.
func (sr *SimpleRedis) Del(name string) error {
cmd := redisCmd{
Command: "DEL",
Name: name,
}
go sr.askRedis(cmd, nil)
return nil
}
+3
View File
@@ -1,3 +1,6 @@
# github.com/leprosus/golang-ttl-map v1.1.7
## explicit; go 1.15
github.com/leprosus/golang-ttl-map
# github.com/maxlerebourg/simpleredis v1.0.7
## explicit; go 1.19
github.com/maxlerebourg/simpleredis