Compare commits

...
85 Commits
Author SHA1 Message Date
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
62 changed files with 3270 additions and 283 deletions
+38
View File
@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
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.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+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:
+2 -1
View File
@@ -1,6 +1,7 @@
.idea/
.DS_Store
config
./config/
conf
db
logs
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
+320 -30
View File
@@ -6,13 +6,14 @@
# Crowdsec Bouncer Traefik plugin
This plugins aims to implement a Crowdsec Bouncer into a traefik plugin.
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 treafik 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.
@@ -24,7 +25,10 @@ There are 3 operating modes (CrowdsecMode) for this plugin:
| 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. |
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).
## Usage
@@ -36,40 +40,77 @@ 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.
**/!\ Since Release 1.1.0, the cache is no longer duplicated but shared by all services**
*This lowers the overhead of the cache in memory and the numbers of cache to fetch it from crowdsec in situations with many services*
At each start of synchronisation, the middleware will wait a random number of seconds to avoid simultaneous calls to crowdsec.
### Variables
- Enabled
- bool
- default: false
- enable the plugin
- LogLevel
- string
- default: `INFO`, expected values are: `INFO`, `DEBUG`
- CrowdsecMode
- string
- default: `live`, expected value are: `none`, `live`, `stream`
- default: `live`, expected values are: `none`, `live`, `stream`
- 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**.
- 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
- UpdateIntervalSeconds
- int64
- default: 60
- Used only in `stream` mode, interval between fetching blacklisted IPs from LAPI
- Used only in `stream` mode, the interval between requests to fetch blacklisted IPs from LAPI
- DefaultDecisionSeconds
- int64
- default: 60
- Used only in `live` mode, decision duration of accepted IPs
- 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
### Configuration
@@ -110,17 +151,64 @@ 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
crowdsecLapiTLSInsecureVerify: false
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"
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
```
These are the default values of the plugin except for LapiKey.
#### Fill variable with value of file
`CrowdsecLapiTlsCertificateBouncerKey`, `CrowdsecLapiTlsCertificateBouncer`, `CrowdsecLapiTlsCertificateAuthority` 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 need to generate a crowdsec API key for the LAPI.
You can generate a crowdsec API key for the LAPI.
You can follow the documentation here: https://docs.crowdsec.net/docs/user_guides/lapi_mgmt/
```bash
@@ -128,7 +216,7 @@ 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:
@@ -143,16 +231,34 @@ crowdsec:
...
```
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 or the official documentation: [https://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
```
@@ -161,7 +267,7 @@ docker exec crowdsec cscli decisions remove --ip 10.0.0.10
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:
@@ -180,19 +286,203 @@ 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 look below for information
```bash
docker-compose -f docker-compose.local.yml up -d
```
Equivalent to
```bash
make run_local
```
### Examples
1. 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
```
2. 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
```
3. 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. Using Crowdsec and Traefik installed as binary in a single VM
Please see details in `exemples/binary-vm/README.md`
To play the demo environment run:
```bash
make run_binaryvm
```
5. 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.
The example is detailed below and will be placed in the `examples/tls-auth/README.md` file.
##### 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"
### 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.
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: 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 and be more performant.
+161 -179
View File
@@ -1,4 +1,6 @@
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"
@@ -6,76 +8,75 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"math/rand"
"net"
"net/http"
"net/url"
"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"
simpleredis "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/simpleredis"
)
const (
streamMode = "stream"
liveMode = "live"
noneMode = "none"
crowdsecLapiHeader = "X-Api-Key"
crowdsecCapiHeader = "Authorization"
crowdsecLapiRoute = "v1/decisions"
crowdsecLapiStreamRoute = "v1/decisions/stream"
cacheBannedValue = "t"
cacheNoBannedValue = "f"
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"`
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
}
//nolint:gochecknoglobals
var (
isCrowdsecStreamHealthy = false
ticker chan bool
)
// CreateConfig creates the default plugin configuration.
func CreateConfig() *Config {
return &Config{
Enabled: false,
CrowdsecMode: liveMode,
CrowdsecLapiScheme: "http",
CrowdsecLapiHost: "crowdsec:8080",
CrowdsecLapiKey: "",
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
updateInterval int64
defaultDecisionTimeout int64
customHeader string
clientPoolStrategy *ip.PoolStrategy
serverPoolStrategy *ip.PoolStrategy
client *http.Client
cache *ttl_map.Heap
}
// 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.Info(fmt.Sprintf("New:validateParams %s", err.Error()))
return nil, err
}
serverChecker, _ := ip.NewChecker(config.ForwardedHeadersTrustedIPs)
clientChecker, _ := ip.NewChecker(config.ClientTrustedIPs)
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, err := configuration.GetVariable(config, "CrowdsecLapiKey")
if err != nil && len(tlsConfig.Certificates) == 0 {
logger.Error(fmt.Sprintf("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup %s", err.Error()))
return nil, err
}
@@ -85,56 +86,81 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
template: template.New("CrowdsecBouncer").Delims("[[", "]]"),
enabled: config.Enabled,
crowdsecStreamHealthy: false,
crowdsecMode: config.CrowdsecMode,
crowdsecScheme: config.CrowdsecLapiScheme,
crowdsecHost: config.CrowdsecLapiHost,
crowdsecKey: config.CrowdsecLapiKey,
crowdsecKey: apiKey,
updateInterval: config.UpdateIntervalSeconds,
customHeader: config.ForwardedHeadersCustomName,
defaultDecisionTimeout: config.DefaultDecisionSeconds,
serverPoolStrategy: &ip.PoolStrategy{
Checker: serverChecker,
},
clientPoolStrategy: &ip.PoolStrategy{
Checker: clientChecker,
},
client: &http.Client{
Transport: &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
TLSClientConfig: tlsConfig,
},
Timeout: 5 * time.Second,
Timeout: 2 * time.Second,
},
cache: ttl_map.New(),
}
if config.CrowdsecMode == streamMode {
go func() {
rand.Seed(time.Now().UnixNano())
timeout := rand.Int63n(30)
logger(fmt.Sprintf("Wait: %v", timeout))
time.Sleep(time.Duration(timeout) * time.Second)
go handleStreamCache(bouncer)
ticker := time.NewTicker(time.Duration(config.UpdateIntervalSeconds) * time.Second)
for range ticker.C {
if config.RedisCacheEnabled {
cache.InitRedisClient(config.RedisCacheHost)
}
if config.CrowdsecMode == configuration.StreamMode && ticker == nil {
ticker = startTicker(config, func() {
handleStreamCache(bouncer)
})
go handleStreamCache(bouncer)
}
}()
}
return bouncer, nil
}
// ServeHTTP principal function of plugin.
//
//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))
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 bouncer.crowdsecMode == streamMode || bouncer.crowdsecMode == liveMode {
isBanned, err := getDecision(bouncer, remoteHost)
if err == nil {
// TODO This should be simplified
if bouncer.crowdsecMode != configuration.NoneMode {
isBanned, erro := cache.GetDecision(remoteIP)
if erro != nil {
logger.Debug(fmt.Sprintf("ServeHTTP:getDecision ip:%s %s", remoteIP, erro.Error()))
if erro.Error() == simpleredis.RedisUnreachable {
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 {
@@ -145,14 +171,21 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
// Right here if we cannot join the stream we forbid the request to go on.
if bouncer.crowdsecMode == streamMode {
if bouncer.crowdsecStreamHealthy {
if bouncer.crowdsecMode == configuration.StreamMode {
if isCrowdsecStreamHealthy {
bouncer.next.ServeHTTP(rw, req)
} else {
logger.Error(fmt.Sprintf("ServeHTTP:isCrowdsecStreamHealthy ip:%s", remoteIP))
rw.WriteHeader(http.StatusForbidden)
}
} else {
handleNoStreamCache(bouncer, rw, req, remoteHost)
err = handleNoStreamCache(bouncer, remoteIP)
if err != nil {
logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s %s", remoteIP, err.Error()))
rw.WriteHeader(http.StatusForbidden)
} else {
bouncer.next.ServeHTTP(rw, req)
}
}
}
@@ -177,178 +210,127 @@ type Stream struct {
New []Decision `json:"new"`
}
// Login Body returned from Crowdsec Login CAPI.
type Login struct {
Code int `json:"code"`
Token string `json:"token"`
Expire string `json:"expire"`
}
func logger(str string) {
log.Printf("Crowdsec Bouncer Traefik Plugin - %s", str)
}
func contains(source []string, target string) bool {
for _, item := range source {
if item == 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(bouncer *Bouncer, clientIP string) (bool, error) {
banned, isCached := bouncer.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(bouncer *Bouncer, clientIP string, isBanned bool, duration int64) {
if bouncer.crowdsecMode == noneMode {
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
}
if isBanned {
logger(fmt.Sprintf("%v banned", clientIP))
bouncer.cache.Set(clientIP, cacheBannedValue, duration)
} else {
bouncer.cache.Set(clientIP, cacheNoBannedValue, duration)
}
}()
return stop
}
func handleNoStreamCache(bouncer *Bouncer, rw http.ResponseWriter, req *http.Request, remoteHost string) {
// We are now in none or live mode.
func handleNoStreamCache(bouncer *Bouncer, remoteIP string) error {
isLiveMode := bouncer.crowdsecMode == configuration.LiveMode
routeURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Host: bouncer.crowdsecHost,
Path: crowdsecLapiRoute,
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteHost),
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteIP),
}
body, err := crowdsecQuery(bouncer, routeURL.String())
if err != nil {
return err
}
body := crowdsecQuery(bouncer, routeURL.String())
if bytes.Equal(body, []byte("null")) {
setDecision(bouncer, remoteHost, false, bouncer.defaultDecisionTimeout)
bouncer.next.ServeHTTP(rw, req)
return
if isLiveMode {
cache.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(bouncer, remoteHost, false, bouncer.defaultDecisionTimeout)
bouncer.next.ServeHTTP(rw, req)
return
if isLiveMode {
cache.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(bouncer, remoteHost, true, int64(duration.Seconds()))
if isLiveMode {
cache.SetDecision(remoteIP, true, int64(duration.Seconds()))
}
return fmt.Errorf("handleNoStreamCache:banned")
}
func handleStreamCache(bouncer *Bouncer) {
// 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 := cache.GetDecision(cacheTimeoutKey)
if err == nil {
logger.Debug("handleStreamCache:alreadyUpdated")
return
}
cache.SetDecision(cacheTimeoutKey, false, bouncer.updateInterval-1)
streamRouteURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Host: bouncer.crowdsecHost,
Path: crowdsecLapiStreamRoute,
RawQuery: fmt.Sprintf("startup=%t", !bouncer.crowdsecStreamHealthy),
RawQuery: fmt.Sprintf("startup=%t", !isCrowdsecStreamHealthy),
}
body := crowdsecQuery(bouncer, streamRouteURL.String())
var stream Stream
err := json.Unmarshal(body, &stream)
body, err := crowdsecQuery(bouncer, streamRouteURL.String())
if err != nil {
logger(fmt.Sprintf("error while parsing body: %s", err))
bouncer.crowdsecStreamHealthy = false
logger.Error(err.Error())
isCrowdsecStreamHealthy = false
return
}
var stream Stream
err = json.Unmarshal(body, &stream)
if err != nil {
logger.Error(fmt.Sprintf("handleStreamCache:parsingBody %s", err.Error()))
isCrowdsecStreamHealthy = false
return
}
for _, decision := range stream.New {
duration, err := time.ParseDuration(decision.Duration)
if err == nil {
setDecision(bouncer, decision.Value, true, int64(duration.Seconds()))
cache.SetDecision(decision.Value, true, int64(duration.Seconds()))
}
}
for _, decision := range stream.Deleted {
bouncer.cache.Del(decision.Value)
cache.DeleteDecision(decision.Value)
}
bouncer.crowdsecStreamHealthy = true
isCrowdsecStreamHealthy = true
}
func crowdsecQuery(bouncer *Bouncer, stringURL string) []byte {
func crowdsecQuery(bouncer *Bouncer, stringURL string) ([]byte, error) {
var req *http.Request
req, _ = http.NewRequest(http.MethodGet, stringURL, nil)
req.Header.Add(crowdsecLapiHeader, bouncer.crowdsecKey)
res, err := bouncer.client.Do(req)
if err != nil {
logger(fmt.Sprintf("error while fetching %v: %s", stringURL, err))
bouncer.crowdsecStreamHealthy = false
return nil
return nil, fmt.Errorf("crowdsecQuery url:%s %w", stringURL, err)
}
if res.StatusCode != http.StatusOK {
logger(fmt.Sprintf("error while fetching %v, status code: %d", stringURL, res.StatusCode))
bouncer.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))
}
}(res.Body)
body, err := ioutil.ReadAll(res.Body)
if err != nil {
logger(fmt.Sprintf("error while reading body: %s", err))
bouncer.crowdsecStreamHealthy = false
return nil
}
return body
defer func() {
if err = res.Body.Close(); err != nil {
logger.Error(fmt.Sprintf("crowdsecQuery:closeBody %s", err.Error()))
}
}()
body, err := io.ReadAll(res.Body)
func validateParams(config *Config) error {
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}, 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
}
+152 -3
View File
@@ -1,13 +1,18 @@
package crowdsec_bouncer_traefik_plugin
package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
import (
"context"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"text/template"
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) {
func TestServeHTTP(t *testing.T) {
cfg := CreateConfig()
cfg.CrowdsecLapiKey = "test"
@@ -20,7 +25,6 @@ func TestCrowdSec(t *testing.T) {
}
recorder := httptest.NewRecorder()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil)
if err != nil {
t.Fatal(err)
@@ -28,3 +32,148 @@ 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
client *http.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,
client: tt.fields.client,
}
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
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
handleStreamCache(tt.args.bouncer)
})
}
}
func Test_crowdsecQuery(t *testing.T) {
type args struct {
bouncer *Bouncer
stringURL string
}
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)
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)
}
})
}
}
+22 -32
View File
@@ -2,10 +2,11 @@ version: "3.8"
services:
traefik:
image: "traefik:v2.8.8"
image: "traefik:v2.9.4"
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
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
CUSTOM_HOSTNAME: crowdsec
+27 -20
View File
@@ -2,10 +2,10 @@ version: "3.8"
services:
traefik:
image: "traefik:v2.8.8"
image: "traefik:v2.9.4"
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.3"
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
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
CUSTOM_HOSTNAME: crowdsec
+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,111 @@
version: "3.8"
services:
cloudflare:
image: "traefik:v2.9.4"
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"
- "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
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.4"
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.3"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-traefik:/var/log/traefik
ports:
- 90:80
- 9080: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"
# 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"
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"
# 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.1
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/
+60
View File
@@ -0,0 +1,60 @@
### 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.
+81
View File
@@ -0,0 +1,81 @@
# -*- 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 "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.5
################################################################
# 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.
@@ -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
curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | sudo bash
sudo apt install crowdsec -y
sudo cp /home/vagrant/vagrant_data/crowdsec/acquis.yaml /etc/crowdsec/acquis.yaml
@@ -0,0 +1,31 @@
#!/bin/bash
sudo apt-get update && apt-get install wget -y && apt-get upgrade -y
wget -O traefik.tar.gz "https://github.com/traefik/traefik/releases/download/v2.9.5/traefik_v2.9.5_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
sudo apt-get update && apt-get install wget -y
wget -O whoami.tar.gz "https://github.com/traefik/whoami/releases/download/v1.8.7/whoami_v1.8.7_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
+4
View File
@@ -0,0 +1,4 @@
filenames:
- /var/log/traefik/access.log
labels:
type: traefik
@@ -0,0 +1,101 @@
version: "3.8"
services:
traefik:
image: "traefik:v2.9.4"
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.4"
# - "--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
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"
# 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"
- "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"
# 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.loglevel=DEBUG"
crowdsec:
image: crowdsecurity/crowdsec:v1.4.1
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:
image: "redis:7.0.5-alpine"
container_name: "redis"
restart: unless-stopped
command: "redis-server --save 60 1"
volumes:
- redis-data:/data
ports:
- 6379:6379
volumes:
logs-redis:
crowdsec-db-redis:
crowdsec-config-redis:
redis-data:
+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" ]
+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,108 @@
version: "3.8"
services:
traefik:
image: "traefik:v2.9.4"
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.5"
- "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock: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.3
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"
]
}
+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.4"
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.3"
# - "--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.1
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:
+1 -1
View File
@@ -1,5 +1,5 @@
module github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
go 1.17
go 1.19
require github.com/leprosus/golang-ttl-map v1.1.7
+107
View File
@@ -0,0 +1,107 @@
// 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"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
simpleredis "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/simpleredis"
)
const (
cacheBannedValue = "t"
cacheNoBannedValue = "f"
)
//nolint:gochecknoglobals
var (
cache = ttl_map.New()
redis simpleredis.SimpleRedis
redisEnabled = false
)
// FileSystem Cache
func getDecisionLocalCache(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("cache:miss")
}
func setDecisionLocalCache(clientIP string, value string, duration int64) {
cache.Set(clientIP, value, duration)
}
func deleteDecisionLocalCache(clientIP string) {
cache.Del(clientIP)
}
// Redis Cache
func getDecisionRedisCache(clientIP string) (bool, error) {
banned, err := redis.Get(clientIP)
bannedString := string(banned)
if err == nil && len(bannedString) > 0 {
return bannedString == cacheBannedValue, nil
}
return false, err
}
func setDecisionRedisCache(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 deleteDecisionRedisCache(clientIP string) {
if err := redis.Del(clientIP); err != nil {
logger.Error(fmt.Sprintf("cache:deleteDecisionRedisCache %s", err.Error()))
}
}
// DeleteDecision delete decision in cache.
func DeleteDecision(clientIP string) {
if redisEnabled {
deleteDecisionRedisCache(clientIP)
} else {
deleteDecisionLocalCache(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 GetDecision(clientIP string) (bool, error) {
if redisEnabled {
return getDecisionRedisCache(clientIP)
}
return getDecisionLocalCache(clientIP)
}
// SetDecision update the cache with the IP as key and the value banned / not banned.
func SetDecision(clientIP string, isBanned bool, duration int64) {
var value string
if isBanned {
logger.Debug(fmt.Sprintf("cache:SetDecision ip:%v banned", clientIP))
value = cacheBannedValue
} else {
value = cacheNoBannedValue
}
if redisEnabled {
setDecisionRedisCache(clientIP, value, duration)
} else {
setDecisionLocalCache(clientIP, value, duration)
}
}
// InitRedisClient loads variables.
func InitRedisClient(host string) {
redisEnabled = true
redis.Init(host)
logger.Debug("cache:InitRedisClient redis:initialized")
}
+226
View File
@@ -0,0 +1,226 @@
// Package cache implements utility routines for manipulating cache.
// It supports currently local file and redis cache.
package cache
import (
"testing"
)
func Test_getDecisionLocalCache(t *testing.T) {
IPInCache := "10.0.0.10"
IPNotInCache := "10.0.0.20"
setDecisionLocalCache(IPInCache, "t", 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: "zaeaea"}, 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 := getDecisionLocalCache(tt.args.clientIP)
if (err != nil) != tt.wantErr {
t.Errorf("getDecisionLocalCache() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("getDecisionLocalCache() = %v, want %v", got, tt.want)
return
}
if tt.valueErr != "" && tt.valueErr != err.Error() {
t.Errorf("getDecisionLocalCache() err = %v, want %v", err.Error(), tt.valueErr)
}
})
}
}
func Test_setDecisionLocalCache(t *testing.T) {
IPInCache := "10.0.0.10"
type args struct {
clientIP string
value string
duration int64
}
tests := []struct {
name string
args args
}{
{name: "Set valid IP in local cache as t", args: args{clientIP: IPInCache, value: "t", duration: 0}},
{name: "Set valid IP in local cache as f", args: args{clientIP: IPInCache, value: "f", duration: 0}},
{name: "Set valid IP in local cache as empty str", args: args{clientIP: IPInCache, value: "", duration: 0}},
{name: "Set valid IP in local cache as f for -1 sec", args: args{clientIP: IPInCache, value: "f", duration: -1}},
{name: "Set valid IP in local cache as f for 10 sec", args: args{clientIP: IPInCache, value: "f", duration: 10}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
setDecisionLocalCache(tt.args.clientIP, tt.args.value, tt.args.duration)
})
}
}
func Test_deleteDecisionLocalCache(t *testing.T) {
type args struct {
clientIP string
}
tests := []struct {
name string
args args
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
deleteDecisionLocalCache(tt.args.clientIP)
})
}
}
func Test_getDecisionRedisCache(t *testing.T) {
type args struct {
clientIP string
}
tests := []struct {
name string
args args
want bool
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getDecisionRedisCache(tt.args.clientIP)
if (err != nil) != tt.wantErr {
t.Errorf("getDecisionRedisCache() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("getDecisionRedisCache() = %v, want %v", got, tt.want)
}
})
}
}
func Test_setDecisionRedisCache(t *testing.T) {
type args struct {
clientIP string
value string
duration int64
}
tests := []struct {
name string
args args
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
setDecisionRedisCache(tt.args.clientIP, tt.args.value, tt.args.duration)
})
}
}
func Test_deleteDecisionRedisCache(t *testing.T) {
type args struct {
clientIP string
}
tests := []struct {
name string
args args
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
deleteDecisionRedisCache(tt.args.clientIP)
})
}
}
func TestDeleteDecision(t *testing.T) {
type args struct {
clientIP string
}
tests := []struct {
name string
args args
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
DeleteDecision(tt.args.clientIP)
})
}
}
func TestGetDecision(t *testing.T) {
type args struct {
clientIP string
}
tests := []struct {
name string
args args
want bool
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := 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)
}
})
}
}
func TestSetDecision(t *testing.T) {
type args struct {
clientIP string
isBanned bool
duration int64
}
tests := []struct {
name string
args args
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
SetDecision(tt.args.clientIP, tt.args.isBanned, tt.args.duration)
})
}
}
func TestInitRedisClient(t *testing.T) {
type args struct {
host string
}
tests := []struct {
name string
args args
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
InitRedisClient(tt.args.host)
})
}
}
+259
View File
@@ -0,0 +1,259 @@
// 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"
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 (
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"`
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"`
}
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",
}
}
// 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 value, nil
}
field = object.FieldByName(key)
value = field.String()
return value, nil
}
// ValidateParams validate all the param gave by user.
func ValidateParams(config *Config) error {
if err := validateParamsRequired(config); err != nil {
return err
}
// 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)
}
if err := validateParamsIPs(config.ForwardedHeadersTrustedIPs, "ForwardedHeadersTrustedIPs"); err != nil {
return err
}
if err := validateParamsIPs(config.ClientTrustedIPs, "ClientTrustedIPs"); err != nil {
return err
}
lapiKey, err := GetVariable(config, "CrowdsecLapiKey")
if err != nil {
return err
}
certBouncer, err := GetVariable(config, "CrowdsecLapiTLSCertificateBouncer")
if err != nil {
return err
}
certBouncerKey, err := GetVariable(config, "CrowdsecLapiTLSCertificateBouncerKey")
if err != nil {
return err
}
// We need to either have crowdsecLapiKey defined or the BouncerCert and Bouncerkey
if lapiKey == "" && (certBouncer == "" || certBouncerKey == "") {
return fmt.Errorf("CrowdsecLapiKey || (CrowdsecLapiTLSCertificateBouncer && CrowdsecLapiTLSCertificateBouncerKey): cannot be both empty")
}
// Case https to contact Crowdsec LAPI and certificate must be provided
if config.CrowdsecLapiScheme == HTTPS && !config.CrowdsecLapiTLSInsecureVerify {
err = validateParamsTLS(config)
if err != nil {
return err
}
}
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}, 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'")
}
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 not https")
return tlsConfig, nil
} else if config.CrowdsecLapiTLSInsecureVerify {
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSInsecureVerify is 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
}
+212
View File
@@ -0,0 +1,212 @@
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) {
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: "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", args: args{listIP: []string{0: "0.0.0.0/12"}}, wantErr: false},
{name: "Validate a 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_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)
}
})
}
}
+128
View File
@@ -0,0 +1,128 @@
// 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 _, ipMask := range trustedIPs {
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)
}
+141
View File
@@ -0,0 +1,141 @@
// 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"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
)
// Error strings for redis.
const (
RedisUnreachable = "redis:unreachable"
RedisMiss = "redis:miss"
RedisTimeout = "redis:timeout"
)
// 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 {
redisHost 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 {
logger.Error(fmt.Sprintf("redis:%s %s", method, err.Error()))
} else {
logger.Debug(fmt.Sprintf("redis:%s", method))
}
}
func askRedis(hostnamePort string, cmd RedisCmd, channel chan RedisCmd) {
dialer := net.Dialer{Timeout: 2 * time.Second}
conn, err := dialer.Dial("tcp", hostnamePort)
if err != nil {
channel <- RedisCmd{Error: fmt.Errorf(RedisUnreachable)}
return
}
defer func() {
if err := conn.Close(); err != nil {
logger.Error(fmt.Sprintf("redis:connClose %s", err.Error()))
}
}()
writer := textproto.NewWriter(bufio.NewWriter(conn))
reader := textproto.NewReader(bufio.NewReader(conn))
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()
if string(read) != "$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(redisHost string) {
sr.redisHost = redisHost
}
// Get fetches the value for key name in redis.
func (sr *SimpleRedis) Get(name string) ([]byte, error) {
redisCmd := RedisCmd{
Command: "GET",
Name: name,
}
channel := make(chan RedisCmd)
go askRedis(sr.redisHost, redisCmd, 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 {
redisCmd := RedisCmd{
Command: "SET",
Name: name,
Data: data,
Duration: duration,
}
go askRedis(sr.redisHost, redisCmd, nil)
return nil
}
// Del removes the key name in redis.
func (sr *SimpleRedis) Del(name string) error {
redisCmd := RedisCmd{
Command: "DEL",
Name: name,
}
go askRedis(sr.redisHost, redisCmd, nil)
return nil
}
+1
View File
@@ -0,0 +1 @@
test