Compare commits

..
21 Commits
Author SHA1 Message Date
mathieuHa c3e0c2d4c3 🐛 Update documentation that help avoid a bug in traefik chart (#135)
* 🐛 Update documentation that help avoid a bug in traefik chart

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

* 🍱 fix test

* 🍱 fix lint

* 🍱 fix lint

* 🍱 fix lint

* 🍱 fix lint

* 🍱 fix lint

* 🍱 fix test

* 🍱 fix test

* 🍱 fix lint + test

* 🍱 fix test

* 🍱 fix test

* 🍱 fix test

* 🍱 fix lint

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

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

* 🐛 lint

* 🐛 fix lint

* 🐛 fix lint

* 🐛 fix lint

* fix: comments

* 🐛 lint and doc

* 🐛 fix comment and lint

* 📝 Start documentation for appsec with exemple

* 📝 Fix readme typos and update example

* 🚨 Fix Lint

---------

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

* Exemples -> Examples

* Uppercase

* Uppercase v2

* Uppercase v3

* Uppercases

* Another one

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

* 🥅 catch error from sscan

*  fix ugly code

* 🐛 clean code

*  fix type + wrong type assertion

* 🚨 fix lint go simplify

* 📝 Update readme with new variable

* 🍱 fix variable naming

* 🍱 revert vendor file

* 🍱 fix lint

---------

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

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

* 📝 update docs

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

* 📝 update doc

* 📝 documentation

* 📝 documentation

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

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

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

* fix: lint

* fix: lint
2023-03-01 14:18:19 +01:00
80 changed files with 837 additions and 413 deletions
+2
View File
@@ -30,4 +30,6 @@ Steps to reproduce the behavior:
3. Scroll down to '....'
4. See error
<!---
If you like the plugin, please consider starring it, so you can get updates and we get some more visibility ✨
-->
@@ -16,4 +16,6 @@ A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.
<!---
If you like the plugin, please consider starring it, so you can get updates and we get some more visibility ✨
-->
+32 -25
View File
@@ -20,69 +20,76 @@ clean:
rm -rf ./vendor
run_dev:
docker-compose -f docker-compose.dev.yml up -d --remove-orphans
docker compose -f docker-compose.dev.yml up -d --remove-orphans
run_local:
docker-compose -f docker-compose.local.yml up -d --remove-orphans
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
docker compose -f examples/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
docker compose -f examples/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
docker compose -f examples/trusted-ips/docker-compose.trusted.yml up -d --remove-orphans
run_binaryvm:
cd exemples/binary-vm/ && sudo vagrant up
cd examples/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
docker compose -f examples/tls-auth/docker-compose.tls-auth.yml down && docker compose -f examples/tls-auth/docker-compose.tls-auth.yml up -d && docker compose -f examples/tls-auth/docker-compose.tls-auth.yml restart && docker compose -f examples/tls-auth/docker-compose.tls-auth.yml logs -f
run_appsec:
docker compose -f examples/appsec-enabled/docker-compose.appsec-enabled.yml up -d
run:
docker-compose -f docker-compose.yml up -d --remove-orphans
docker compose -f docker-compose.yml up -d --remove-orphans
restart_dev:
docker-compose -f docker-compose.dev.yml restart
docker compose -f docker-compose.dev.yml restart
restart_local:
docker-compose -f docker-compose.local.yml restart
docker compose -f docker-compose.local.yml restart
restart:
docker-compose -f docker-compose.yml restart
docker compose -f docker-compose.yml restart
restart_behindproxy:
docker-compose -f exemples/behind-proxy/docker-compose.cloudflare.yml restart
docker compose -f examples/behind-proxy/docker-compose.cloudflare.yml restart
restart_cacheredis:
docker-compose -f exemples/redis-cache/docker-compose.redis.yml restart
docker compose -f examples/redis-cache/docker-compose.redis.yml restart
restart_trustedips:
docker-compose -f exemples/trusted-ips/docker-compose.trusted.yml restart
docker compose -f examples/trusted-ips/docker-compose.trusted.yml restart
restart_tlsauth:
docker-compose -f exemples/tls-auth/docker-compose.tls-auth.yml
docker compose -f examples/tls-auth/docker-compose.tls-auth.yml
restart_appsec:
docker compose -f examples/tls-auth/docker-compose.appsec-enabled.yml
show_logs:
docker-compose -f docker-compose.yml restart
docker compose -f docker-compose.yml restart
show_local_logs:
docker-compose -f docker-compose.local.yml logs -f
docker compose -f docker-compose.local.yml logs -f
show_dev_logs:
docker-compose -f docker-compose.dev.yml logs -f
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
docker compose -f examples/behind-proxy/docker-compose.cloudflare.yml down --remove-orphans
docker compose -f examples/redis-cache/docker-compose.redis.yml down --remove-orphans
docker compose -f examples/trusted-ips/docker-compose.trusted.yml down --remove-orphans
docker compose -f examples/tls-auth/docker-compose.tls-auth.yml down --remove-orphans
docker compose -f examples/appsec-enabled/docker-compose.appsec-enabled.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
cd examples/binary-vm/ && sudo vagrant destroy -f
show_metrics:
+64 -15
View File
@@ -6,6 +6,8 @@
# Crowdsec Bouncer Traefik plugin
> New! This plugin now supports [AppSec](https://doc.crowdsec.net/docs/next/appsec/intro/) feature including virtual patching and capabilities support for your legacy ModSecurity rules.
This plugin aims to implement a Crowdsec Bouncer in a Traefik plugin.
> [CrowdSec](https://www.crowdsec.net/) is an open-source and collaborative IPS (Intrusion Prevention System) and a security suite.
@@ -17,6 +19,16 @@ The Crowdsec utility will provide the community blocklist which contains highly
When used with Crowdsec it will leverage the local API which will analyze Traefik logs and take decisions on the requests made by users/bots. Malicious actors will be banned based on patterns used against your website.
Appsec feature is supported from plugin version 1.2.0 and Crowdsec 1.6.0.
The AppSec Component offers:
- Low-effort virtual patching capabilities.
- Support for your legacy ModSecurity rules.
- Combining classic WAF benefits with advanced CrowdSec features for otherwise difficult advanced behavior detection.
More information on appsec in the [Crowdsec Documentation](https://doc.crowdsec.net/docs/next/appsec/intro/).
There are 4 operating modes (CrowdsecMode) for this plugin:
| Mode | Description |
@@ -24,12 +36,13 @@ There are 4 operating modes (CrowdsecMode) for this plugin:
| none | If the client IP is on ban list, it will get a http code 403 response. Otherwise, request will continue as usual. All request call the Crowdsec LAPI |
| live | If the client IP is on ban list, it will get a http code 403 response. Otherwise, request will continue as usual. The bouncer can leverage use of a local cache in order to reduce the number of requests made to the Crowdsec LAPI. It will keep in cache the status for each IP that makes queries. |
| stream | Stream Streaming mode allows you to keep in the local cache only the Banned IPs, every requests that does not hit the cache is authorized. Every minute, the cache is updated with news from the Crowdsec LAPI. |
| alone | Standalone mode, similar to the streaming mode but the blacklisted IPs are fetched on the CAPI. Every 2 hours, the cache is updated with news from the Crowdsec CAPI. It does not include any localy banned IP, but can work without a crowdsec service. |
| alone | Standalone mode, similar to the streaming mode but the blacklisted IPs are fetched on the CAPI. Every 2 hours, the cache is updated with news from the Crowdsec CAPI. It does not include any locally banned IP, but can work without a crowdsec service. |
| appsec | Disable Crowdsec IP checking but apply Crowdsec Appsec checking. This mode is intended to be used when Crowdsec IP checking is applied at the Firewall Level. |
The `streaming mode` is recommended for performance, decisions are updated every 60 sec by default and that's the only communication between Traefik and Crowdsec. Every request that happens hits the cache for quick decisions.
The cache can be local to Traefik using the filesystem, or a separate Redis instance.
Support for Redis is currently in beta (requires version 7.0.X of Redis).
## Usage
@@ -44,18 +57,31 @@ make run
**/!\ Cache is shared by all services**
*This means if an IP is banned, all services which are protected by an instance of the plugin will deny requests from that IP*
Only one instance of the plugin is *possible*.
### Variables
- Enabled
- bool
- default: false
- enable the plugin
- Enable the plugin
- LogLevel
- string
- default: `INFO`, expected values are: `INFO`, `DEBUG`
- CrowdsecMode
- string
- default: `live`, expected values are: `none`, `live`, `stream`, `alone`
- default: `live`, expected values are: `none`, `live`, `stream`, `alone`, `appsec`
- CrowdsecAppsecEnabled
- bool
- default: false
- Enable Crowdsec Appsec Server (WAF).
- CrowdsecAppsecHost
- string
- default: "crowdsec:7422"
- Crowdsec Appsec Server available on which host and port. The scheme will be handled by the CrowdsecLapiScheme var.
- CrowdsecAppsecFailureBlock
- bool
- default: true
- Block request when Crowdsec Appsec Server have a [status 500](https://docs.crowdsec.net/docs/next/appsec/protocol#response-code).
- CrowdsecLapiScheme
- string
- default: `http`, expected values are: `http`, `https`
@@ -103,6 +129,18 @@ make run
- string
- default: "redis:6379"
- hostname and port for the Redis service
- RedisCachePassword
- string
- default: ""
- Password for the Redis service
- RedisCacheDatabase
- string
- default: ""
- Database selection for the Redis service
- HTTPTimeoutSeconds
- int64
- default: 10
- Default timeout in seconds for contacting Crowdsec LAPI
- UpdateIntervalSeconds
- int64
- default: 60
@@ -134,6 +172,7 @@ experimental:
plugins:
bouncer:
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
version: vX.Y.Z # To update
```
```yaml
@@ -163,7 +202,11 @@ http:
logLevel: DEBUG
updateIntervalSeconds: 60
defaultDecisionSeconds: 60
httpTimeoutSeconds: 10
crowdsecMode: live
crowdsecAppsecEnabled: false
crowdsecAppsecHost: crowdsec:7422
crowdsecAppsecFailureBlock: true
crowdsecLapiKey: privateKey-foo
crowdsecLapiKeyFile: /etc/traefik/cs-privateKey-foo
crowdsecLapiHost: crowdsec:8080
@@ -183,6 +226,8 @@ http:
forwardedHeadersCustomName: X-Custom-Header
redisCacheEnabled: false
redisCacheHost: "redis:6379"
redisCachePassword: password
redisCacheDatabase: "5"
crowdsecLapiTLSCertificateAuthority: |-
-----BEGIN CERTIFICATE-----
MIIEBzCCAu+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
@@ -255,10 +300,10 @@ docker-compose up -d
#### 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.
You can follow the example in `examples/tls-auth` to view how to authenticate with client certificates with the LAPI.
In that case, communications with the LAPI must go through HTTPS.
A script is available to generate certificates in `exemples/tls-auth/gencerts.sh` and must be in the same directory as the inputs for the PKI creation.
A script is available to generate certificates in `examples/tls-auth/gencerts.sh` and must be in the same directory as the inputs for the PKI creation.
#### Use HTTPS to communicate with the LAPI
@@ -266,7 +311,7 @@ To communicate with the LAPI in HTTPS you need to either accept any certificates
Set the `crowdsecLapiScheme` to https.
Crowdsec must be listening in HTTPS for this to work.
Please see the [tls-auth exemple](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/tls-auth/README.md) or the official documentation: [docs.crowdsec.net/docs/local_api/tls_auth/](https://docs.crowdsec.net/docs/local_api/tls_auth/)
Please see the [tls-auth example](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/tls-auth/README.md) or the official documentation: [docs.crowdsec.net/docs/local_api/tls_auth/](https://docs.crowdsec.net/docs/local_api/tls_auth/)
#### Manually add an IP to the blocklist (for testing purposes)
@@ -276,21 +321,25 @@ docker exec crowdsec cscli decisions add --ip 10.0.0.10 -d 10m # this will be ef
docker exec crowdsec cscli decisions remove --ip 10.0.0.10
```
### Exemples
### Examples
#### 1. Behind another proxy service (ex: clouflare) [exemples/behind-proxy/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/behind-proxy/README.md)
#### 1. Behind another proxy service (ex: clouflare) [examples/behind-proxy/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/behind-proxy/README.md)
#### 2. With Redis as an external shared cache [exemples/redis-cache/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/redis-cache/README.md)
#### 2. With Redis as an external shared cache [examples/redis-cache/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/redis-cache/README.md)
#### 3. Using Trusted IP (ex: LAN OR VPN) that won't get filtered by crowdsec [exemples/trusted-ips/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/trusted-ips/README.md)
#### 3. Using Trusted IP (ex: LAN OR VPN) that won't get filtered by crowdsec [examples/trusted-ips/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/trusted-ips/README.md)
#### 4. Using Crowdsec and Traefik installed as binary in a single VM [exemples/binary-vm/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/binary-vm/README.md)
#### 4. Using Crowdsec and Traefik installed as binary in a single VM [examples/binary-vm/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/binary-vm/README.md)
#### 5. Using https communication and tls authentication with Crowdsec [exemples/tls-auth/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/tls-auth/README.md)
#### 5. Using https communication and tls authentication with Crowdsec [examples/tls-auth/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/tls-auth/README.md)
#### 6. Using Crowdsec and Traefik in Kubernetes [exemples/kubernetes/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/kubernetes/README.md)
#### 6. Using Crowdsec and Traefik in Kubernetes [examples/kubernetes/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/kubernetes/README.md)
#### 7. Using Traefik in standalone mode without Crowdsec [examples/standalone-mode/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/standalone-mode/README.md)
#### 8. Using Traefik with AppSec feature enabled [examples/appsec-enabled/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/appsec-enabled/README.md)
#### 7. Using Traefik in standalone mode without Crowdsec [exemples/standalone-mode/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/standalone-mode/README.md)
### Local Mode
+9
View File
@@ -1,4 +1,13 @@
---
filenames:
- /var/log/traefik/access.log
labels:
type: traefik
---
listen_addr: 0.0.0.0:7422
appsec_config: crowdsecurity/virtual-patching
name: myAppSecComponent
source: appsec
labels:
type: appsec
+160 -57
View File
@@ -22,13 +22,19 @@ import (
)
const (
crowdsecLapiHeader = "X-Api-Key"
crowdsecCapiHeader = "Authorization"
crowdsecLapiRoute = "v1/decisions"
crowdsecLapiStreamRoute = "v1/decisions/stream"
crowdsecCapiLogin = "v2/watchers/login"
crowdsecCapiStreamRoute = "v2/decisions/stream"
cacheTimeoutKey = "updated"
crowdsecAppsecIPHeader = "X-Crowdsec-Appsec-Ip"
crowdsecAppsecURIHeader = "X-Crowdsec-Appsec-Uri"
crowdsecAppsecHostHeader = "X-Crowdsec-Appsec-Host"
crowdsecAppsecVerbHeader = "X-Crowdsec-Appsec-Verb"
crowdsecAppsecHeader = "X-Crowdsec-Appsec-Api-Key"
crowdsecLapiHeader = "X-Api-Key"
crowdsecLapiRoute = "v1/decisions"
crowdsecLapiStreamRoute = "v1/decisions/stream"
crowdsecCapiHost = "api.crowdsec.net"
crowdsecCapiHeader = "Authorization"
crowdsecCapiLoginRoute = "v2/watchers/login"
crowdsecCapiStreamRoute = "v2/decisions/stream"
cacheTimeoutKey = "updated"
)
//nolint:gochecknoglobals
@@ -50,6 +56,9 @@ type Bouncer struct {
template *template.Template
enabled bool
appsecEnabled bool
appsecHost string
appsecFailureBlock bool
crowdsecScheme string
crowdsecHost string
crowdsecKey string
@@ -66,19 +75,20 @@ type Bouncer struct {
serverPoolStrategy *ip.PoolStrategy
httpClient *http.Client
cacheClient *cache.Client
log *logger.Log
}
// New creates the crowdsec bouncer plugin.
func New(ctx context.Context, next http.Handler, config *configuration.Config, name string) (http.Handler, error) {
logger.Init(config.LogLevel)
log := logger.New(config.LogLevel)
err := configuration.ValidateParams(config)
if err != nil {
logger.Error(fmt.Sprintf("New:validateParams %s", err.Error()))
log.Error(fmt.Sprintf("New:validateParams %s", err.Error()))
return nil, err
}
serverChecker, _ := ip.NewChecker(config.ForwardedHeadersTrustedIPs)
clientChecker, _ := ip.NewChecker(config.ClientTrustedIPs)
serverChecker, _ := ip.NewChecker(log, config.ForwardedHeadersTrustedIPs)
clientChecker, _ := ip.NewChecker(log, config.ClientTrustedIPs)
var tlsConfig *tls.Config
crowdsecStreamRoute := ""
@@ -86,22 +96,22 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
if config.CrowdsecMode == configuration.AloneMode {
config.CrowdsecCapiMachineID, _ = configuration.GetVariable(config, "CrowdsecCapiMachineID")
config.CrowdsecCapiPassword, _ = configuration.GetVariable(config, "CrowdsecCapiPassword")
config.CrowdsecLapiHost = "api.crowdsec.net"
config.CrowdsecLapiHost = crowdsecCapiHost
config.CrowdsecLapiScheme = "https"
config.UpdateIntervalSeconds = 7200
config.UpdateIntervalSeconds = 7200 // 2 hours
crowdsecStreamRoute = crowdsecCapiStreamRoute
crowdsecHeader = crowdsecCapiHeader
} else {
crowdsecStreamRoute = crowdsecLapiStreamRoute
crowdsecHeader = crowdsecLapiHeader
tlsConfig, err = configuration.GetTLSConfigCrowdsec(config)
tlsConfig, err = configuration.GetTLSConfigCrowdsec(config, log)
if err != nil {
logger.Error(fmt.Sprintf("New:getTLSConfigCrowdsec fail to get tlsConfig %s", err.Error()))
log.Error(fmt.Sprintf("New:getTLSConfigCrowdsec fail to get tlsConfig %s", err.Error()))
return nil, err
}
apiKey, errAPIKey := configuration.GetVariable(config, "CrowdsecLapiKey")
if errAPIKey != nil && len(tlsConfig.Certificates) == 0 {
logger.Error(fmt.Sprintf("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup %s", errAPIKey.Error()))
log.Error(fmt.Sprintf("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup %s", errAPIKey.Error()))
return nil, err
}
config.CrowdsecLapiKey = apiKey
@@ -114,6 +124,9 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
enabled: config.Enabled,
crowdsecMode: config.CrowdsecMode,
appsecEnabled: config.CrowdsecAppsecEnabled,
appsecHost: config.CrowdsecAppsecHost,
appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
crowdsecScheme: config.CrowdsecLapiScheme,
crowdsecHost: config.CrowdsecLapiHost,
crowdsecKey: config.CrowdsecLapiKey,
@@ -125,6 +138,7 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
defaultDecisionTimeout: config.DefaultDecisionSeconds,
crowdsecStreamRoute: crowdsecStreamRoute,
crowdsecHeader: crowdsecHeader,
log: log,
serverPoolStrategy: &ip.PoolStrategy{
Checker: serverChecker,
},
@@ -137,27 +151,36 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
IdleConnTimeout: 30 * time.Second,
TLSClientConfig: tlsConfig,
},
Timeout: 10 * time.Second,
Timeout: time.Duration(config.HTTPTimeoutSeconds) * time.Second,
},
cacheClient: &cache.Client{},
}
bouncer.cacheClient.New(config.RedisCacheEnabled, config.RedisCacheHost)
if config.CrowdsecMode == configuration.AppsecMode {
return bouncer, nil
}
config.RedisCachePassword, _ = configuration.GetVariable(config, "RedisCachePassword")
bouncer.cacheClient.Init(
log,
config.RedisCacheEnabled,
config.RedisCacheHost,
config.RedisCachePassword,
config.RedisCacheDatabase,
)
if (config.CrowdsecMode == configuration.StreamMode || config.CrowdsecMode == configuration.AloneMode) && ticker == nil {
if config.CrowdsecMode == configuration.AloneMode {
err = getToken(bouncer)
if err != nil {
logger.Error(fmt.Sprintf("New:getToken %s", err.Error()))
if err := getToken(bouncer); err != nil {
bouncer.log.Error(fmt.Sprintf("New:getToken %s", err.Error()))
return nil, err
}
}
ticker = startTicker(config, func() {
handleStreamCache(bouncer)
})
handleStreamCache(bouncer)
handleStreamTicker(bouncer)
isStartup = false
ticker = startTicker(config, log, func() {
handleStreamTicker(bouncer)
})
}
logger.Debug(fmt.Sprintf("New initialized mode:%s", config.CrowdsecMode))
bouncer.log.Debug(fmt.Sprintf("New initialized mode:%s", config.CrowdsecMode))
return bouncer, nil
}
@@ -174,40 +197,45 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// Here we check for the trusted IPs in the customHeader
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.customHeader)
if err != nil {
logger.Error(fmt.Sprintf("ServeHTTP:getRemoteIp ip:%s %s", remoteIP, err.Error()))
bouncer.log.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()))
bouncer.log.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))
bouncer.log.Debug(fmt.Sprintf("ServeHTTP ip:%s isTrusted:%v", remoteIP, isTrusted))
if isTrusted {
bouncer.next.ServeHTTP(rw, req)
return
}
if bouncer.crowdsecMode == configuration.AppsecMode {
handleNextServeHTTP(bouncer, remoteIP, rw, req)
return
}
// TODO This should be simplified
if bouncer.crowdsecMode != configuration.NoneMode {
isBanned, cacheErr := bouncer.cacheClient.GetDecision(remoteIP)
if cacheErr != nil {
errString := cacheErr.Error()
logger.Debug(fmt.Sprintf("ServeHTTP:getDecision ip:%s isBanned:false %s", remoteIP, errString))
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:getDecision ip:%s isBanned:false %s", remoteIP, errString))
if errString != cache.CacheMiss {
logger.Error(fmt.Sprintf("ServeHTTP:getDecision ip:%s %s", remoteIP, errString))
bouncer.log.Error(fmt.Sprintf("ServeHTTP:getDecision ip:%s %s", remoteIP, errString))
rw.WriteHeader(http.StatusForbidden)
return
}
} else {
logger.Debug(fmt.Sprintf("ServeHTTP ip:%s cache:hit isBanned:%v", remoteIP, isBanned))
bouncer.log.Debug(fmt.Sprintf("ServeHTTP ip:%s cache:hit isBanned:%v", remoteIP, isBanned))
if isBanned {
rw.WriteHeader(http.StatusForbidden)
} else {
bouncer.next.ServeHTTP(rw, req)
handleNextServeHTTP(bouncer, remoteIP, rw, req)
}
return
}
@@ -216,19 +244,18 @@ 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 == configuration.StreamMode || bouncer.crowdsecMode == configuration.AloneMode {
if isCrowdsecStreamHealthy {
bouncer.next.ServeHTTP(rw, req)
handleNextServeHTTP(bouncer, remoteIP, rw, req)
} else {
logger.Error(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s", remoteIP))
bouncer.log.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s", remoteIP))
rw.WriteHeader(http.StatusForbidden)
}
} else {
err = handleNoStreamCache(bouncer, remoteIP)
if err != nil {
logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:true %s", remoteIP, err.Error()))
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:true %s", remoteIP, err.Error()))
rw.WriteHeader(http.StatusForbidden)
} else {
logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:false", remoteIP))
bouncer.next.ServeHTTP(rw, req)
handleNextServeHTTP(bouncer, remoteIP, rw, req)
}
}
}
@@ -261,11 +288,32 @@ type Login struct {
Expire string `json:"expire"`
}
func startTicker(config *configuration.Config, work func()) chan bool {
func handleNextServeHTTP(bouncer *Bouncer, remoteIP string, rw http.ResponseWriter, req *http.Request) {
if bouncer.appsecEnabled {
err := appsecQuery(bouncer, remoteIP, req)
if err != nil {
bouncer.log.Debug(fmt.Sprintf("handleNextServeHTTP ip:%s isWaf:true %s", remoteIP, err.Error()))
rw.WriteHeader(http.StatusForbidden)
return
}
}
bouncer.next.ServeHTTP(rw, req)
}
func handleStreamTicker(bouncer *Bouncer) {
if err := handleStreamCache(bouncer); err != nil {
isCrowdsecStreamHealthy = false
bouncer.log.Error(err.Error())
} else {
isCrowdsecStreamHealthy = true
}
}
func startTicker(config *configuration.Config, log *logger.Log, work func()) chan bool {
ticker := time.NewTicker(time.Duration(config.UpdateIntervalSeconds) * time.Second)
stop := make(chan bool, 1)
go func() {
defer logger.Debug("ticker:stopped")
defer log.Debug("ticker:stopped")
for {
select {
case <-ticker.C:
@@ -328,7 +376,7 @@ func getToken(bouncer *Bouncer) error {
loginURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Host: bouncer.crowdsecHost,
Path: crowdsecCapiLogin,
Path: crowdsecCapiLoginRoute,
}
body, err := crowdsecQuery(bouncer, loginURL.String(), true)
if err != nil {
@@ -342,21 +390,24 @@ func getToken(bouncer *Bouncer) error {
}
if login.Code == 200 && len(login.Token) > 0 {
bouncer.crowdsecKey = login.Token
logger.Debug(fmt.Sprintf("getToken statusCode:%d", login.Code))
bouncer.log.Debug(fmt.Sprintf("getToken statusCode:%d", login.Code))
return nil
}
return fmt.Errorf("getToken statusCode:%d", login.Code)
}
func handleStreamCache(bouncer *Bouncer) {
func handleStreamCache(bouncer *Bouncer) error {
// TODO clean properly on exit.
// Instead of blocking the goroutine interval for all the secondary node,
// if the master service is shut down, other goroutine can take the lead
// because updated routine information is in the cache
_, err := bouncer.cacheClient.GetDecision(cacheTimeoutKey)
if err == nil {
logger.Debug("handleStreamCache:alreadyUpdated")
return
bouncer.log.Debug("handleStreamCache:alreadyUpdated")
return nil
}
if err.Error() != cache.CacheMiss {
return err
}
bouncer.cacheClient.SetDecision(cacheTimeoutKey, false, bouncer.updateInterval-1)
streamRouteURL := url.URL{
@@ -367,16 +418,12 @@ func handleStreamCache(bouncer *Bouncer) {
}
body, err := crowdsecQuery(bouncer, streamRouteURL.String(), false)
if err != nil {
logger.Error(err.Error())
isCrowdsecStreamHealthy = false
return
return err
}
var stream Stream
err = json.Unmarshal(body, &stream)
if err != nil {
logger.Error(fmt.Sprintf("handleStreamCache:parsingBody %s", err.Error()))
isCrowdsecStreamHealthy = false
return
return fmt.Errorf("handleStreamCache:parsingBody %w", err)
}
for _, decision := range stream.New {
duration, err := time.ParseDuration(decision.Duration)
@@ -387,8 +434,9 @@ func handleStreamCache(bouncer *Bouncer) {
for _, decision := range stream.Deleted {
bouncer.cacheClient.DeleteDecision(decision.Value)
}
logger.Debug("handleStreamCache:updated")
bouncer.log.Debug("handleStreamCache:updated")
isCrowdsecStreamHealthy = true
return nil
}
func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, error) {
@@ -409,6 +457,11 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, err
if err != nil {
return nil, fmt.Errorf("crowdsecQuery url:%s %w", stringURL, err)
}
defer func() {
if err = res.Body.Close(); err != nil {
bouncer.log.Error(fmt.Sprintf("crowdsecQuery:closeBody %s", err.Error()))
}
}()
if res.StatusCode == http.StatusUnauthorized && bouncer.crowdsecMode == configuration.AloneMode {
if errToken := getToken(bouncer); errToken != nil {
return nil, fmt.Errorf("crowdsecQuery:renewToken url:%s %w", stringURL, errToken)
@@ -418,11 +471,6 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, err
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("crowdsecQuery url:%s, statusCode:%d", stringURL, res.StatusCode)
}
defer func() {
if err = res.Body.Close(); err != nil {
logger.Error(fmt.Sprintf("crowdsecQuery:closeBody %s", err.Error()))
}
}()
body, err := io.ReadAll(res.Body)
if err != nil {
@@ -430,3 +478,58 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, err
}
return body, nil
}
func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
routeURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Host: bouncer.appsecHost,
Path: "/",
}
var req *http.Request
if httpReq.Body != nil && httpReq.ContentLength > 0 {
bodyBytes, err := io.ReadAll(httpReq.Body)
if err != nil {
return fmt.Errorf("appsecQuery:GetBody %w", err)
}
httpReq.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
req, _ = http.NewRequest(http.MethodPost, routeURL.String(), bytes.NewBuffer(bodyBytes))
} else {
req, _ = http.NewRequest(http.MethodGet, routeURL.String(), nil)
}
for key, headers := range httpReq.Header {
for _, value := range headers {
req.Header.Add(key, value)
}
}
req.Header.Set(crowdsecAppsecHeader, bouncer.crowdsecKey)
req.Header.Set(crowdsecAppsecIPHeader, ip)
req.Header.Set(crowdsecAppsecVerbHeader, httpReq.Method)
req.Header.Set(crowdsecAppsecHostHeader, httpReq.Host)
req.Header.Set(crowdsecAppsecURIHeader, httpReq.URL.Path)
res, err := bouncer.httpClient.Do(req)
if err != nil {
return fmt.Errorf("appsecQuery %w", err)
}
defer func() {
if err = res.Body.Close(); err != nil {
bouncer.log.Error(fmt.Sprintf("appsecQuery:closeBody %s", err.Error()))
}
}()
if res.StatusCode == http.StatusInternalServerError {
bouncer.log.Debug("crowdsecQuery statusCode:500")
if bouncer.appsecFailureBlock {
return fmt.Errorf("appsecQuery statusCode:%d", res.StatusCode)
}
return nil
}
if res.StatusCode != http.StatusOK {
return fmt.Errorf("appsecQuery statusCode:%d", res.StatusCode)
}
if err != nil {
return fmt.Errorf("appsecQuery:readBody %w", err)
}
return nil
}
+8 -3
View File
@@ -142,14 +142,19 @@ func Test_handleStreamCache(t *testing.T) {
bouncer *Bouncer
}
tests := []struct {
name string
args args
name string
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
handleStreamCache(tt.args.bouncer)
err := handleStreamCache(tt.args.bouncer)
if (err != nil) != tt.wantErr {
t.Errorf("handleStreamCache() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
+12 -10
View File
@@ -2,7 +2,7 @@ version: "3.8"
services:
traefik:
image: "traefik:v2.9.6"
image: "traefik:v2.10.4"
container_name: "traefik"
restart: unless-stopped
command:
@@ -20,7 +20,7 @@ services:
- logs-local:/var/log/traefik
- ./:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 80:80
- 8000:80
- 8080:8080
depends_on:
- crowdsec
@@ -31,12 +31,12 @@ services:
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.router-foo.rule=Path(`/foo`)"
- "traefik.http.routers.router-foo.rule=PathPrefix(`/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"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5="
whoami2:
image: traefik/whoami
@@ -44,22 +44,23 @@ services:
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.router-bar.rule=Path(`/bar`)"
- "traefik.http.routers.router-bar.rule=PathPrefix(`/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"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecappsecenabled=true"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5="
crowdsec:
image: crowdsecurity/crowdsec:v1.4.1
image: crowdsecurity/crowdsec:dev
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching
CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK_1: 40796d93c2958f9e58345514e67740e5
BOUNCER_KEY_TRAEFIK_2: 44c36dac5c4140af9f06f397508e82c7
BOUNCER_KEY_TRAEFIK: 40796d93c2958f9e58345514e67740e5=
volumes:
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
- logs-local:/var/log/traefik:ro
@@ -67,6 +68,7 @@ services:
- crowdsec-config-local:/etc/crowdsec/
labels:
- "traefik.enable=false"
volumes:
logs-local:
crowdsec-db-local:
+9 -7
View File
@@ -2,7 +2,7 @@ version: "3.8"
services:
traefik:
image: "traefik:v2.9.6"
image: "traefik:v2.10.4"
container_name: "traefik"
restart: unless-stopped
command:
@@ -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.1.8"
- "--experimental.plugins.bouncer.version=v1.1.15"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "logs:/var/log/traefik"
@@ -39,7 +39,8 @@ services:
# 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"
- "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"
@@ -58,20 +59,21 @@ services:
# Definitin of the middleware
- "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"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-1="
# enable AppSec real time check
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecappsecenabled=true"
# forwardedheaderstrustedips should be the IP of the proxy that is in front of traefik (if any)
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
crowdsec:
image: crowdsecurity/crowdsec:v1.4.1
image: crowdsecurity/crowdsec:v1.5.3
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
CUSTOM_HOSTNAME: crowdsec
# We need to register one api key per service we will use
BOUNCER_KEY_TRAEFIK_1: FIXME-LAPI-KEY-1
BOUNCER_KEY_TRAEFIK_2: FIXME-LAPI-KEY-2
BOUNCER_KEY_TRAEFIK: FIXME-LAPI-KEY-1=
volumes:
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
- logs:/var/log/traefik:ro
+28
View File
@@ -0,0 +1,28 @@
# Example
## Enabling AppSec WAF feature from crowdsec
You mostly need to configure Crowdsec for this to work by enabling virtual patching and configuring some custom rules.
In the example we use a whoami container protected by crowdsec with virtual patching enabled.
The Traefik instance just needs to know where appsec engine is located
```yaml
labels:
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecappsecenabled=true"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecappsechost=crowdsec:7422"
```
We can try to query normally the whoami server:
```bash
curl http://localhost:8000/foo
```
And then we verify that a malicious request will be blocked:
```bash
curl http://localhost:8000/foo/rpc2
```
You should get a 403 on http://localhost:8000/foo/rpc2
To play the demo environment run:
```bash
make run_appsec
```
+12
View File
@@ -0,0 +1,12 @@
filenames:
- /var/log/traefik/access.log
labels:
type: traefik
---
listen_addr: 0.0.0.0:7422
appsec_config: crowdsecurity/virtual-patching
name: myAppSecComponent
source: appsec
labels:
type: appsec
@@ -0,0 +1,72 @@
version: "3.8"
services:
traefik:
image: "traefik:v2.10.7"
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.2.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-appsec-enabled:/var/log/traefik
- ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 8000:80
- 8080:8080
depends_on:
- crowdsec
whoami1:
image: traefik/whoami
container_name: "simple-service-foo"
restart: unless-stopped
labels:
- "traefik.enable=true"
# Definition of the router
- "traefik.http.routers.router-foo.rule=PathPrefix(`/foo`)"
- "traefik.http.routers.router-foo.entrypoints=web"
- "traefik.http.routers.router-foo.middlewares=crowdsec-foo@docker"
# Definition of the service
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
# Definition of the middleware
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
# Enable AppSec
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsecenabled=true"
# Define AppSec host and port informations
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsechost=crowdsec:7422"
crowdsec:
image: crowdsecurity/crowdsec:v1.6.0
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
volumes:
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
- logs-appsec-enabled:/var/log/traefik:ro
- crowdsec-db-appsec-enabled:/var/lib/crowdsec/data/
- crowdsec-config-appsec-enabled:/etc/crowdsec/
labels:
- "traefik.enable=false"
volumes:
logs-appsec-enabled:
crowdsec-db-appsec-enabled:
crowdsec-config-appsec-enabled:
@@ -1,4 +1,4 @@
# Exemple
# Example
## Behind another proxy service (ex: clouflare)
You need to configure your Traefik to trust Forwarded headers by your front proxy
@@ -2,7 +2,7 @@ version: "3.8"
services:
cloudflare:
image: "traefik:v2.9.6"
image: "traefik:v2.10.4"
container_name: "cloudflare"
restart: unless-stopped
command:
@@ -12,17 +12,16 @@ services:
- "--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
- ./cloudflare-example.yaml:/cloud.yaml:ro
- logs-cloudflare:/var/log/traefik
ports:
- 80:80
- 8080:8080
traefik:
image: "traefik:v2.9.6"
image: "traefik:v2.10.4"
container_name: "traefik"
restart: unless-stopped
command:
@@ -36,7 +35,7 @@ services:
- "--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.7"
- "--experimental.plugins.bouncer.version=v1.1.13"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-traefik:/var/log/traefik
@@ -46,7 +45,7 @@ services:
depends_on:
- crowdsec
whoami1:
whoami-foo:
image: traefik/whoami
container_name: "simple-service-foo"
restart: unless-stopped
@@ -58,15 +57,14 @@ services:
- "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"
# Definition of the middleware
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=live"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
whoami2:
whoami-bar:
image: traefik/whoami
container_name: "simple-service-bar"
restart: unless-stopped
@@ -78,24 +76,22 @@ services:
- "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
# 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.crowdsecmode=live"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=live"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
crowdsec:
image: crowdsecurity/crowdsec:v1.4.3
image: crowdsecurity/crowdsec:v1.5.2
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
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
volumes:
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
- logs-cloudflare:/var/log/traefik:ro
@@ -1,4 +1,4 @@
# Exemple
# Example
### Install vagrant
@@ -2,7 +2,7 @@
DEBIAN_FRONTEND=noninteractive sudo apt-get update && sudo apt-get install wget -y
# DEBIAN_FRONTEND=noninteractive sudo apt-get upgrade -y --assume-yes
wget -O traefik.tar.gz "https://github.com/traefik/traefik/releases/download/v2.9.6/traefik_v2.9.6_linux_amd64.tar.gz"
wget -O traefik.tar.gz "https://github.com/traefik/traefik/releases/download/v2.10.4/traefik_v2.10.4_linux_amd64.tar.gz"
tar -zxvf traefik.tar.gz
# inspired from https://gist.github.com/ubergesundheit/7c9d875befc2d7bfd0bf43d8b3862d85
sudo mv ./traefik /usr/local/bin/
@@ -1,7 +1,7 @@
#!/bin/bash
DEBIAN_FRONTEND=noninteractive sudo apt-get update && apt-get install wget -y
wget -O whoami.tar.gz "https://github.com/traefik/whoami/releases/download/v1.8.7/whoami_v1.8.7_linux_amd64.tar.gz"
wget -O whoami.tar.gz "https://github.com/traefik/whoami/releases/download/v1.9.0/whoami_v1.9.0_linux_amd64.tar.gz"
tar -zxvf whoami.tar.gz
# inspired from https://gist.github.com/ubergesundheit/7c9d875befc2d7bfd0bf43d8b3862d85
sudo mv ./whoami /usr/local/bin/
@@ -1,4 +1,4 @@
### Kubernetes Exemple
### Kubernetes Example
#### Official docs
@@ -11,6 +11,19 @@ Install Docker Desktop
In settings, click on `Kubernetes` menu, and click `Enable Kubernetes`, then `Apply and Restart`.
In case of any issue, you can reset the cluster from this menu and the button `Reset Kubernetes Cluster`.
##### Install Kubernetes on Minikube
Install Minikube
```bash
curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 \
&& chmod +x minikube
sudo mkdir -p /usr/local/bin/
sudo install minikube /usr/local/bin/
minikube start
```
##### Install Traefik
[getting-started/install-traefik/#use-the-helm-chart](https://doc.traefik.io/traefik/getting-started/install-traefik/#use-the-helm-chart)
@@ -19,11 +32,13 @@ In case of any issue, you can reset the cluster from this menu and the button `R
helm repo add traefik https://traefik.github.io/charts
helm repo update
kubectl create ns traefik
helm upgrade --install --namespace=traefik \
helm upgrade --version v26.1.0 --install --namespace=traefik \
--values=./traefik/values.yml \
traefik traefik/traefik
```
_A [bug](https://github.com/traefik/traefik-helm-chart/commit/e7ce1b410c858642069033305eb6362f26689f16) has been fixed in chart 26.1.0 that could prevent plugin to be loaded_
#### View the Traefik dashboard
> Port forward the dashboard:
@@ -88,4 +103,4 @@ kubectl -n crowdsec exec -it $(kubectl get pods -n crowdsec --selector "k8s-app=
```bash
kubectl -n traefik exec -it $(kubectl get pods -n traefik --selector "app.kubernetes.io/name=traefik" --output=name) sh
```
```
@@ -1,5 +1,5 @@
image:
tag: v1.4.4-rc1
tag: v1.6.0
agent:
acquisition:
@@ -22,4 +22,4 @@ lapi:
value: "k8s linux test"
# If it's a test, we don't want to share signals with CrowdSec so disable the Online API.
- name: DISABLE_ONLINE_API
value: true
value: "true"
+17
View File
@@ -0,0 +1,17 @@
image:
tag: v2.11
logs:
general:
level: DEBUG
access:
enabled: true
fields:
headers:
defaultmode: keep
experimental:
plugins:
bouncer:
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
version: "v1.2.0"
@@ -1,4 +1,4 @@
# Exemple
# Example
## With Redis as an external shared cache
The plugin must be configured to connect to a redis instance
@@ -0,0 +1,114 @@
version: "3.8"
services:
traefik:
image: "traefik:v2.10.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.13"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-redis:/var/log/traefik
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 80:80
- 8080:8080
depends_on:
- crowdsec
# - redis-insecure
- redis-secure
# Either use secure, or insecure but do not mix both
# whoami-redis-insecure:
# image: traefik/whoami
# container_name: "simple-service-foo"
# restart: unless-stopped
# labels:
# - "traefik.enable=true"
# # Definition of the router
# - "traefik.http.routers.router-foo.rule=Path(`/redis-insecure`)"
# - "traefik.http.routers.router-foo.entrypoints=web"
# - "traefik.http.routers.router-foo.middlewares=crowdsec-foo@docker"
# # Definition of the service
# - "traefik.http.services.service-foo.loadbalancer.server.port=80"
# # Definition of the middleware
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.rediscacheenabled=true"
# # Contact redis-unsecure without a password
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.rediscachehost=redis-insecure:6379"
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
whoami-redis-secure:
image: traefik/whoami
container_name: "simple-service-bar"
restart: unless-stopped
labels:
- "traefik.enable=true"
# Definition of the router
- "traefik.http.routers.router-bar.rule=Path(`/redis-secure`)"
- "traefik.http.routers.router-bar.entrypoints=web"
- "traefik.http.routers.router-bar.middlewares=crowdsec-bar@docker"
# Definition of the service
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
# Definition of the middleware
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.rediscacheenabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.rediscachepassword=FIXME"
# Contact redis-secure with password
- "traefik.http.middlewares.crowdsec.plugin.bouncer.rediscachehost=redis-secure:6379"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
crowdsec:
image: crowdsecurity/crowdsec:v1.5.2
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
volumes:
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
- logs-redis:/var/log/traefik:ro
- crowdsec-db-redis:/var/lib/crowdsec/data/
- crowdsec-config-redis:/etc/crowdsec/
labels:
- "traefik.enable=false"
redis-secure:
image: "redis:7.0.12-alpine"
container_name: "redis-secure"
hostname: redis-secure
restart: unless-stopped
command: "redis-server --save 60 1 --loglevel debug --requirepass FIXME"
volumes:
- redis-secure-data:/data
# redis-insecure:
# image: "redis:7.0.12-alpine"
# container_name: "redis-insecure"
# hostname: redis-unsecure
# restart: unless-stopped
# command: "redis-server --save 60 1 --loglevel debug"
# volumes:
# - redis-unsecure-data:/data
volumes:
logs-redis:
crowdsec-db-redis:
crowdsec-config-redis:
redis-unsecure-data:
redis-secure-data:
@@ -2,7 +2,7 @@ version: "3.8"
services:
traefik:
image: "traefik:v2.9.6"
image: "traefik:v2.10.4"
container_name: "traefik"
restart: unless-stopped
command:
@@ -15,7 +15,7 @@ services:
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.7"
- "--experimental.plugins.bouncer.version=v1.1.13"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
ports:
@@ -1,4 +1,4 @@
# Exemple
# Example
## Using https communication and tls authentication with Crowdsec
##### Summary
@@ -2,7 +2,7 @@ version: "3.8"
services:
traefik:
image: "traefik:v2.9.6"
image: "traefik:v2.10.4"
container_name: "traefik"
restart: unless-stopped
command:
@@ -15,14 +15,14 @@ services:
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.7"
- "--experimental.plugins.bouncer.version=v1.1.13"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./LAPIKEY:/etc/traefik/LAPIKEY:ro
- logs-tls-auth:/var/log/traefik
- crowdsec-certs-tls-auth:/etc/traefik/crowdsec-certs
- ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 80:80
@@ -30,22 +30,24 @@ services:
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"
# Use HTTPS scheme but with lapikey authentication
# whoami-foo:
# image: traefik/whoami
# container_name: "simple-service-foo"
# restart: unless-stopped
# labels:
# - "traefik.enable=true"
# - "traefik.http.routers.router-foo.rule=Path(`/foo`)"
# - "traefik.http.routers.router-foo.entrypoints=web"
# - "traefik.http.routers.router-foo.middlewares=crowdsec-foo@docker"
# - "traefik.http.services.service-foo.loadbalancer.server.port=80"
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapischeme=https"
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateAuthorityFile=/etc/traefik/crowdsec-certs/inter.pem"
# Use HTTPS scheme with TLS cert authentication
whoami-bar:
image: traefik/whoami
container_name: "simple-service-bar"
@@ -56,15 +58,15 @@ services:
- "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"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapischeme=https"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateAuthorityFile=/etc/traefik/crowdsec-certs/inter.pem"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateBouncerFile=/etc/traefik/crowdsec-certs/bouncer.pem"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiTLSCertificateBouncerKeyFile=/etc/traefik/crowdsec-certs/bouncer-key.pem"
crowdsec:
image: crowdsecurity/crowdsec:v1.4.3
image: crowdsecurity/crowdsec:v1.5.2
container_name: "crowdsec"
restart: unless-stopped
environment:
@@ -1,4 +1,4 @@
# Exemple
# Example
## Using Trusted IP (ex: LAN OR VPN) that won't get filtered by crowdsec
You need to configure your Traefik to trust Forwarded headers by your front proxy
@@ -36,4 +36,4 @@ 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
```
```
@@ -2,7 +2,7 @@ version: "3.8"
services:
traefik:
image: "traefik:v2.9.6"
image: "traefik:v2.10.4"
container_name: "traefik"
restart: unless-stopped
command:
@@ -15,12 +15,12 @@ services:
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.7"
- "--experimental.plugins.bouncer.version=v1.1.13"
# - "--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
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 80:80
- 8080:8080
@@ -40,12 +40,11 @@ services:
# 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.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
# Replace 10.0.10.30/32 by your IP range which is "trusted"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.clienttrustedips=10.0.10.30/32"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.clienttrustedips=10.0.10.30/32"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
whoami2:
image: traefik/whoami
@@ -60,23 +59,21 @@ services:
# 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.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
# Replace 10.0.10.30/32 by your IP range which is "trusted"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.clienttrustedips=10.0.10.30/32"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.clienttrustedips=10.0.10.30/32"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
crowdsec:
image: crowdsecurity/crowdsec:v1.4.3
image: crowdsecurity/crowdsec:v1.5.2
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
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
volumes:
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
- logs-trustedips:/var/log/traefik:ro
-19
View File
@@ -1,19 +0,0 @@
image:
tag: v2.9.6
logs:
general:
level: DEBUG
access:
enabled: true
fields:
headers:
defaultmode: keep
experimental:
plugins:
enabled: true
additionalArguments:
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.1.7-beta1"
@@ -1,101 +0,0 @@
version: "3.8"
services:
traefik:
image: "traefik:v2.9.6"
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.7"
# - "--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.3
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:
+1 -1
View File
@@ -4,5 +4,5 @@ go 1.19
require (
github.com/leprosus/golang-ttl-map v1.1.7
github.com/maxlerebourg/simpleredis v1.0.3
github.com/maxlerebourg/simpleredis v1.0.9
)
+2 -2
View File
@@ -1,4 +1,4 @@
github.com/leprosus/golang-ttl-map v1.1.7 h1:cF4AAFDDnJTFSV+/42sKLhmMluvLdRlCGS2UaifH6UM=
github.com/leprosus/golang-ttl-map v1.1.7/go.mod h1:4QWHJPeVBbrkhOhXdhCv9IEiyj/YzkO04/iexy4vSe0=
github.com/maxlerebourg/simpleredis v1.0.3 h1:VhXq9bVytWDqD/TS/GjHKayvQb/VUeEql5F+yUbdOiI=
github.com/maxlerebourg/simpleredis v1.0.3/go.mod h1:/DH8zOK6kDskSqoX/m5CJJdNGfkIQZd/ERBJgytDDSk=
github.com/maxlerebourg/simpleredis v1.0.9 h1:aj1hKaYPeOVE4Ksu3TV/zsreUDDWOpKXBAvoFysiqII=
github.com/maxlerebourg/simpleredis v1.0.9/go.mod h1:/DH8zOK6kDskSqoX/m5CJJdNGfkIQZd/ERBJgytDDSk=
+24 -20
View File
@@ -44,7 +44,9 @@ func (localCache) deleteDecision(clientIP string) {
cache.Del(clientIP)
}
type redisCache struct{}
type redisCache struct {
log *logger.Log
}
func (redisCache) getDecision(clientIP string) (bool, error) {
banned, err := redis.Get(clientIP)
@@ -58,15 +60,15 @@ func (redisCache) getDecision(clientIP string) (bool, error) {
return false, err
}
func (redisCache) setDecision(clientIP string, value string, duration int64) {
func (rc redisCache) setDecision(clientIP string, value string, duration int64) {
if err := redis.Set(clientIP, []byte(value), duration); err != nil {
logger.Error(fmt.Sprintf("cache:setDecisionRedisCache %s", err.Error()))
rc.log.Error(fmt.Sprintf("cache:setDecisionRedisCache %s", err.Error()))
}
}
func (redisCache) deleteDecision(clientIP string) {
func (rc redisCache) deleteDecision(clientIP string) {
if err := redis.Del(clientIP); err != nil {
logger.Error(fmt.Sprintf("cache:deleteDecisionRedisCache %s", err.Error()))
rc.log.Error(fmt.Sprintf("cache:deleteDecisionRedisCache %s", err.Error()))
}
}
@@ -79,40 +81,42 @@ type cacheInterface interface {
// Client Cache client.
type Client struct {
cache cacheInterface
log *logger.Log
}
// New Initialize cache client.
func (client *Client) New(isRedis bool, host string) {
// Init Initialize cache client.
func (c *Client) Init(log *logger.Log, isRedis bool, host, pass, database string) {
c.log = log
if isRedis {
redis.Init(host)
client.cache = &redisCache{}
redis.Init(host, pass, database)
c.cache = &redisCache{log: log}
} else {
client.cache = &localCache{}
c.cache = &localCache{}
}
logger.Debug(fmt.Sprintf("cache:New initialized isRedis:%v", isRedis))
c.log.Debug(fmt.Sprintf("cache:New initialized isRedis:%v", isRedis))
}
// DeleteDecision delete decision in cache.
func (client *Client) DeleteDecision(clientIP string) {
logger.Debug(fmt.Sprintf("cache:DeleteDecision ip:%v", clientIP))
client.cache.deleteDecision(clientIP)
func (c *Client) DeleteDecision(clientIP string) {
c.log.Debug(fmt.Sprintf("cache:DeleteDecision ip:%v", clientIP))
c.cache.deleteDecision(clientIP)
}
// GetDecision check in the cache if the IP has the banned / not banned value.
// Otherwise return with an error to add the IP in cache if we are on.
func (client *Client) GetDecision(clientIP string) (bool, error) {
logger.Debug(fmt.Sprintf("cache:GetDecision ip:%v", clientIP))
return client.cache.getDecision(clientIP)
func (c *Client) GetDecision(clientIP string) (bool, error) {
c.log.Debug(fmt.Sprintf("cache:GetDecision ip:%v", clientIP))
return c.cache.getDecision(clientIP)
}
// SetDecision update the cache with the IP as key and the value banned / not banned.
func (client *Client) SetDecision(clientIP string, isBanned bool, duration int64) {
logger.Debug(fmt.Sprintf("cache:SetDecision ip:%v isBanned:%v duration:%vs", clientIP, isBanned, duration))
func (c *Client) SetDecision(clientIP string, isBanned bool, duration int64) {
c.log.Debug(fmt.Sprintf("cache:SetDecision ip:%v isBanned:%v duration:%vs", clientIP, isBanned, duration))
var value string
if isBanned {
value = cacheBannedValue
} else {
value = cacheNoBannedValue
}
client.cache.setDecision(clientIP, value, duration)
c.cache.setDecision(clientIP, value, duration)
}
+5 -3
View File
@@ -4,12 +4,14 @@ package cache
import (
"testing"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
)
func Test_GetDecision(t *testing.T) {
IPInCache := "10.0.0.10"
IPNotInCache := "10.0.0.20"
client := &Client{cache: &localCache{}}
client := &Client{cache: &localCache{}, log: logger.New("INFO")}
client.SetDecision(IPInCache, true, 10)
type args struct {
clientIP string
@@ -45,7 +47,7 @@ func Test_GetDecision(t *testing.T) {
}
func Test_SetDecision(t *testing.T) {
client := &Client{cache: &localCache{}}
client := &Client{cache: &localCache{}, log: logger.New("INFO")}
IPInCache := "10.0.0.11"
type args struct {
clientIP string
@@ -76,7 +78,7 @@ func Test_SetDecision(t *testing.T) {
func Test_DeleteDecision(t *testing.T) {
IPInCache := "10.0.0.12"
IPNotInCache := "10.0.0.22"
client := &Client{cache: &localCache{}}
client := &Client{cache: &localCache{}, log: logger.New("INFO")}
client.SetDecision(IPInCache, true, 10)
type args struct {
clientIP string
+51 -23
View File
@@ -23,6 +23,7 @@ const (
StreamMode = "stream"
LiveMode = "live"
NoneMode = "none"
AppsecMode = "appsec"
HTTPS = "https"
HTTP = "http"
)
@@ -32,6 +33,9 @@ type Config struct {
Enabled bool `json:"enabled,omitempty"`
LogLevel string `json:"logLevel,omitempty"`
CrowdsecMode string `json:"crowdsecMode,omitempty"`
CrowdsecAppsecEnabled bool `json:"crowdsecAppsecEnabled,omitempty"`
CrowdsecAppsecHost string `json:"crowdsecAppsecHost,omitempty"`
CrowdsecAppsecFailureBlock bool `json:"crowdsecAppsecFailureBlock,omitempty"`
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
@@ -50,11 +54,15 @@ type Config struct {
CrowdsecCapiScenarios []string `json:"crowdsecCapiScenarios,omitempty"`
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
ForwardedHeadersCustomName string `json:"forwardedheaderscustomheader,omitempty"`
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"`
ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"`
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
RedisCacheEnabled bool `json:"redisCacheEnabled,omitempty"`
RedisCacheHost string `json:"redisCacheHost,omitempty"`
RedisCachePassword string `json:"redisCachePassword,omitempty"`
RedisCachePasswordFile string `json:"redisCachePasswordFile,omitempty"`
RedisCacheDatabase string `json:"redisCacheDatabase,omitempty"`
}
func contains(source []string, target string) bool {
@@ -72,17 +80,23 @@ func New() *Config {
Enabled: false,
LogLevel: "INFO",
CrowdsecMode: LiveMode,
CrowdsecAppsecEnabled: false,
CrowdsecAppsecHost: "crowdsec:7422",
CrowdsecAppsecFailureBlock: true,
CrowdsecLapiScheme: HTTP,
CrowdsecLapiHost: "crowdsec:8080",
CrowdsecLapiKey: "",
CrowdsecLapiTLSInsecureVerify: false,
UpdateIntervalSeconds: 60,
DefaultDecisionSeconds: 60,
HTTPTimeoutSeconds: 10,
ForwardedHeadersCustomName: "X-Forwarded-For",
ForwardedHeadersTrustedIPs: []string{},
ClientTrustedIPs: []string{},
RedisCacheEnabled: false,
RedisCacheHost: "redis:6379",
RedisCachePassword: "",
RedisCacheDatabase: "",
}
}
@@ -115,7 +129,7 @@ func GetVariable(config *Config, key string) (string, error) {
// ValidateParams validate all the param gave by user.
//
//nolint:gocyclo
//nolint:gocyclo,gocognit
func ValidateParams(config *Config) error {
if err := validateParamsRequired(config); err != nil {
return err
@@ -128,6 +142,10 @@ func ValidateParams(config *Config) error {
return err
}
if _, err := GetVariable(config, "RedisCachePassword"); err != nil {
return err
}
if config.CrowdsecMode == AloneMode {
if _, err := GetVariable(config, "CrowdsecCapiMachineID"); err != nil {
return err
@@ -138,13 +156,12 @@ func ValidateParams(config *Config) error {
return nil
}
// This only check that the format of the URL scheme:// is correct and do not make requests
testURL := url.URL{
Scheme: config.CrowdsecLapiScheme,
Host: config.CrowdsecLapiHost,
if err := validateURL("CrowdsecLapi", config.CrowdsecLapiScheme, config.CrowdsecLapiHost); err != nil {
return err
}
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 := validateURL("CrowdsecAppsec", config.CrowdsecLapiScheme, config.CrowdsecAppsecHost); err != nil {
return err
}
lapiKey, err := GetVariable(config, "CrowdsecLapiKey")
@@ -179,12 +196,21 @@ func ValidateParams(config *Config) error {
return nil
}
func validateURL(variable, scheme, host string) error {
// This only check that the format of the URL scheme://host is correct and do not make requests
testURL := url.URL{Scheme: scheme, Host: host}
if _, err := http.NewRequest(http.MethodGet, testURL.String(), nil); err != nil {
return fmt.Errorf("CrowdsecLapiScheme://%sHost: '%v://%v' must be an URL", variable, scheme, host)
}
return nil
}
// validHeaderFieldByte reports whether b is a valid byte in a header
// field name. RFC 7230 says:
// valid ! # $ % & ' * + - . ^ _ ` | ~ DIGIT ALPHA
// See https://httpwg.github.io/specs/rfc7230.html#rule.token.separators
func validateParamsAPIKey(lapiKey string) error {
reg := regexp.MustCompile("^[a-zA-Z0-9 !#$%&'*+-.^_`|~]*$")
reg := regexp.MustCompile("^[a-zA-Z0-9 !#$%&'*+-.^_`|~=/]*$")
if !reg.Match([]byte(lapiKey)) {
return fmt.Errorf("CrowdsecLapiKey doesn't valid this regexp: '/%s/'", reg.String())
}
@@ -209,11 +235,9 @@ func validateParamsTLS(config *Config) error {
func validateParamsIPs(listIP []string, key string) error {
if len(listIP) > 0 {
if _, err := ip.NewChecker(listIP); err != nil {
if _, err := ip.NewChecker(logger.New("INFO"), 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
}
@@ -232,14 +256,15 @@ func validateParamsRequired(config *Config) error {
requiredInt := map[string]int64{
"UpdateIntervalSeconds": config.UpdateIntervalSeconds,
"DefaultDecisionSeconds": config.DefaultDecisionSeconds,
"HTTPTimeoutSeconds": config.HTTPTimeoutSeconds,
}
for key, val := range requiredInt {
if val < 1 {
return fmt.Errorf("%v: cannot be less than 1", key)
}
}
if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode}, config.CrowdsecMode) {
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live', 'stream' or 'alone'")
if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode, AppsecMode}, config.CrowdsecMode) {
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live', 'stream', 'alone' or 'appsec'")
}
if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) {
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'http' or 'https'")
@@ -248,16 +273,18 @@ func validateParamsRequired(config *Config) error {
}
// GetTLSConfigCrowdsec get TLS config from Config.
func GetTLSConfigCrowdsec(config *Config) (*tls.Config, error) {
//
//nolint:nestif
func GetTLSConfigCrowdsec(config *Config, log *logger.Log) (*tls.Config, error) {
tlsConfig := new(tls.Config)
tlsConfig.RootCAs = x509.NewCertPool()
//nolint:gocritic
if config.CrowdsecLapiScheme != HTTPS {
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiScheme https:no")
log.Debug("getTLSConfigCrowdsec:CrowdsecLapiScheme https:no")
return tlsConfig, nil
} else if config.CrowdsecLapiTLSInsecureVerify {
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSInsecureVerify tlsInsecure:true")
tlsConfig.InsecureSkipVerify = true
log.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSInsecureVerify tlsInsecure:true")
// If we return here and still want to use client auth this won't work
// return tlsConfig, nil
} else {
@@ -265,12 +292,13 @@ func GetTLSConfigCrowdsec(config *Config) (*tls.Config, error) {
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")
if len(certAuthority) > 0 {
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuthority)) {
// here we return because if CrowdsecLapiTLSInsecureVerify is false
// and CA not load, we can't communicate with https
return nil, fmt.Errorf("getTLSConfigCrowdsec:cannot load CA and verify cert is enabled")
}
log.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSCertificateAuthority CA added successfully")
}
}
+4 -2
View File
@@ -4,6 +4,8 @@ import (
"crypto/tls"
"reflect"
"testing"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
)
func getMinimalConfig() *Config {
@@ -147,7 +149,7 @@ func Test_validateParamsIPs(t *testing.T) {
{name: "Not validate localhost", args: args{listIP: []string{0: "localhost"}}, wantErr: true},
{name: "Not validate a weird ip", args: args{listIP: []string{0: "0.0.0.0/89"}}, wantErr: true},
{name: "Not validate a weird ip 2", args: args{listIP: []string{0: "0.0.0.256/12"}}, wantErr: true},
{name: "Validate an ip not trimed", args: args{listIP: []string{0: " 0.0.0.0/0"}}, wantErr: false},
{name: "Validate an ip not trimmed", args: args{listIP: []string{0: " 0.0.0.0/0"}}, wantErr: false},
{name: "Validate an ip", args: args{listIP: []string{0: "0.0.0.0/12"}}, wantErr: false},
{name: "Validate an ip list", args: args{listIP: []string{0: "0.0.0.0/0", 1: "1.1.1.1/1"}}, wantErr: false},
}
@@ -231,7 +233,7 @@ func Test_GetTLSConfigCrowdsec(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetTLSConfigCrowdsec(tt.args.config)
got, err := GetTLSConfigCrowdsec(tt.args.config, logger.New("INFO"))
if (err != nil) != tt.wantErr {
t.Errorf("getTLSConfigCrowdsec() error = %v, wantErr %v", err, tt.wantErr)
return
+4 -4
View File
@@ -8,7 +8,7 @@ import (
"net/http"
"strings"
"github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
)
// CHECKER
@@ -20,14 +20,14 @@ type Checker struct {
}
// NewChecker builds a new Checker given a list of CIDR-Strings to trusted IPs.
func NewChecker(trustedIPs []string) (*Checker, error) {
func NewChecker(log *logger.Log, trustedIPs []string) (*Checker, error) {
checker := &Checker{}
for _, ipMaskRaw := range trustedIPs {
ipMask := strings.TrimSpace(ipMaskRaw)
if ipAddr := net.ParseIP(ipMask); ipAddr != nil {
checker.authorizedIPs = append(checker.authorizedIPs, &ipAddr)
logger.Debug(fmt.Sprintf("IP %v is trusted", ipAddr))
log.Debug(fmt.Sprintf("IP %v is trusted", ipAddr))
continue
}
@@ -36,7 +36,7 @@ func NewChecker(trustedIPs []string) (*Checker, error) {
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))
log.Debug(fmt.Sprintf("IP network %v is trusted", ipAddr))
}
return checker, nil
+25 -16
View File
@@ -8,32 +8,41 @@ import (
"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
)
// Log Logger struct.
type Log struct {
logError *log.Logger
logInfo *log.Logger
logDebug *log.Logger
}
// 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)
// New Set Default log level to info in case log level to defined.
func New(logLevel string) *Log {
logError := log.New(io.Discard, "ERROR: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime)
logInfo := log.New(io.Discard, "INFO: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime)
logDebug := log.New(io.Discard, "DEBUG: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime)
logError.SetOutput(os.Stderr)
logInfo.SetOutput(os.Stdout)
if logLevel == "DEBUG" {
loggerDebug.SetOutput(os.Stdout)
logDebug.SetOutput(os.Stdout)
}
return &Log{
logError: logError,
logInfo: logInfo,
logDebug: logDebug,
}
}
// Info log to Stdout.
func Info(str string) {
loggerInfo.Printf(str)
func (l *Log) Info(str string) {
l.logInfo.Printf(str)
}
// Debug log to Stdout.
func Debug(str string) {
loggerDebug.Printf(str)
func (l *Log) Debug(str string) {
l.logDebug.Printf(str)
}
// Error log to Stderr.
func Error(str string) {
loggerError.Printf(str)
func (l *Log) Error(str string) {
l.logError.Printf(str)
}
+30 -1
View File
@@ -1,2 +1,31 @@
# simpleredis
Minimal go redis with only get, set and delete operation
Minimal go redis with only `get`, `set` and `delete` operation.
It supports password authentication with redis.
With **NO** external dependencies.
## Example
```go
import simpleredis "github.com/maxlerebourg/simpleredis"
var redis simpleredis.SimpleRedis
redis.Init("redis:6379", "", "") // redisHost, redisPass, redisDatabase
err := redis.Set("test", []bytes("whatever"), 60), // Set key "test" with "whatever" for 60 seconds
if err != nil {
...
}
val, err := redis.Get("test") // get key test
if err != nil {
// err could be only redis:unreachable, redis:miss or redis:timeout available in simpleredis.RedisUnreachable
...
}
err = redis.Del("test")
if err != nil {
...
}
```
## Author
Max Lerebourg @ [Primadviz.com](https://primadviz.com)
Mathieu Hanotaux
+60 -24
View File
@@ -17,10 +17,12 @@ const (
RedisUnreachable = "redis:unreachable"
RedisMiss = "redis:miss"
RedisTimeout = "redis:timeout"
RedisNoAuth = "redis:noauth"
RedisIssue = "redis:issue?"
)
// A RedisCmd is used to communicate with redis at low level using commands.
type RedisCmd struct {
// A redisCmd is used to communicate with redis at low level using commands.
type redisCmd struct {
Command string
Name string
Data []byte
@@ -30,7 +32,9 @@ type RedisCmd struct {
// A SimpleRedis is used to communicate with redis.
type SimpleRedis struct {
redisHost string
host string
pass string
database string
}
func genRedisArray(params ...[]byte) []byte {
@@ -49,12 +53,29 @@ func send(wr *textproto.Writer, method string, data []byte) {
}
}
func askRedis(hostnamePort string, cmd RedisCmd, channel chan RedisCmd) {
func (sr *SimpleRedis) waitRedis(reader *textproto.Reader, channel chan redisCmd) {
for {
select {
case <-time.After(time.Second * 1):
channel <- redisCmd{Error: fmt.Errorf(RedisTimeout)}
return
default:
read, _ := reader.ReadLineBytes()
if string(read) != "+OK" {
channel <- redisCmd{Error: fmt.Errorf(RedisNoAuth)}
return
}
}
// breaks out of for
break
}
}
func (sr *SimpleRedis) askRedis(cmd redisCmd, channel chan redisCmd) redisCmd {
dialer := net.Dialer{Timeout: 2 * time.Second}
conn, err := dialer.Dial("tcp", hostnamePort)
conn, err := dialer.Dial("tcp", sr.host)
if err != nil {
channel <- RedisCmd{Error: fmt.Errorf(RedisUnreachable)}
return
return redisCmd{Error: fmt.Errorf(RedisUnreachable)}
}
defer func() {
if err := conn.Close(); err != nil {
@@ -65,6 +86,18 @@ func askRedis(hostnamePort string, cmd RedisCmd, channel chan RedisCmd) {
writer := textproto.NewWriter(bufio.NewWriter(conn))
reader := textproto.NewReader(bufio.NewReader(conn))
if sr.pass != "" {
data := genRedisArray([]byte("AUTH"), []byte(sr.pass))
send(writer, "auth", data)
sr.waitRedis(reader, channel)
}
if sr.database != "" {
data := genRedisArray([]byte("SELECT"), []byte(sr.database))
send(writer, "select", data)
sr.waitRedis(reader, channel)
}
switch cmd.Command {
case "SET":
data := genRedisArray([]byte("SET"), []byte(cmd.Name), cmd.Data, []byte("EX"), []byte(fmt.Sprintf("%d", cmd.Duration)))
@@ -78,36 +111,39 @@ func askRedis(hostnamePort string, cmd RedisCmd, channel chan RedisCmd) {
for {
select {
case <-time.After(time.Second * 1):
channel <- RedisCmd{Error: fmt.Errorf(RedisTimeout)}
return
return redisCmd{Error: fmt.Errorf(RedisTimeout)}
default:
read, _ := reader.ReadLineBytes()
if string(read) != "$1" {
channel <- RedisCmd{Error: fmt.Errorf(RedisMiss)}
return
str := string(read)
if strings.Contains(str, "-NOAUTH") {
return redisCmd{Error: fmt.Errorf(RedisNoAuth)}
} else if str != "$1" {
return redisCmd{Error: fmt.Errorf(RedisMiss)}
}
read, _ = reader.ReadLineBytes()
channel <- RedisCmd{Data: read}
return
return redisCmd{Data: read}
}
}
}
return redisCmd{Error: fmt.Errorf(RedisIssue)}
}
// Init sets the redisHost used to connect to redis.
func (sr *SimpleRedis) Init(redisHost string) {
sr.redisHost = redisHost
func (sr *SimpleRedis) Init(host, pass, database string) {
sr.host = host
sr.pass = pass
sr.database = database
}
// Get fetches the value for key name in redis.
func (sr *SimpleRedis) Get(name string) ([]byte, error) {
redisCmd := RedisCmd{
cmd := redisCmd{
Command: "GET",
Name: name,
}
channel := make(chan RedisCmd)
go askRedis(sr.redisHost, redisCmd, channel)
resp := <-channel
channel := make(chan redisCmd)
resp := sr.askRedis(cmd, channel)
if resp.Error != nil {
return nil, resp.Error
}
@@ -116,22 +152,22 @@ func (sr *SimpleRedis) Get(name string) ([]byte, error) {
// 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{
cmd := redisCmd{
Command: "SET",
Name: name,
Data: data,
Duration: duration,
}
go askRedis(sr.redisHost, redisCmd, nil)
sr.askRedis(cmd, nil)
return nil
}
// Del removes the key name in redis.
func (sr *SimpleRedis) Del(name string) error {
redisCmd := RedisCmd{
cmd := redisCmd{
Command: "DEL",
Name: name,
}
go askRedis(sr.redisHost, redisCmd, nil)
sr.askRedis(cmd, nil)
return nil
}
+1 -1
View File
@@ -1,6 +1,6 @@
# github.com/leprosus/golang-ttl-map v1.1.7
## explicit; go 1.15
github.com/leprosus/golang-ttl-map
# github.com/maxlerebourg/simpleredis v1.0.3
# github.com/maxlerebourg/simpleredis v1.0.9
## explicit; go 1.19
github.com/maxlerebourg/simpleredis