mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-09-02 20:28:50 +02:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6059f23dc3 | ||
|
|
497d1a2928 | ||
|
|
c3e0c2d4c3 | ||
|
|
575d3a02e5 | ||
|
|
6c183d9231 | ||
|
|
bd71b58f19 | ||
|
|
b68c692ed1 | ||
|
|
fc3da2fc2d | ||
|
|
1a9bdc578f | ||
|
|
f2aea695fc | ||
|
|
2827fef273 | ||
|
|
07c8fae927 | ||
|
|
13c209be3f | ||
|
|
93340ffa55 |
@@ -20,69 +20,79 @@ 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_captcha:
|
||||
docker compose -f examples/captcha/docker-compose.captcha.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:
|
||||
|
||||
@@ -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,26 @@ 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/).
|
||||
|
||||
Remediation offered by [Crowdsec](https://docs.crowdsec.net/u/bouncers/intro) and supported by the plugin can be either `ban` or `captcha`.
|
||||
For the `ban` remediation the user will be blocked in Traefik (HTTP 403).
|
||||
For the `captcha` remediation, the user will be redirected to a page to complete a captcha challenge.
|
||||
|
||||
On successfull completion, he will be cleaned for a specified period of time before a new resolution challenge is expected if Crowdsec still has a decision to verify the user behavior. See the example captcha for more informations and configuration intructions.
|
||||
The following captcha providers are supported now:
|
||||
- [hcaptcha](https://www.hcaptcha.com/)
|
||||
- [recaptcha](https://www.google.com/recaptcha/about/)
|
||||
- [turnstile](https://www.cloudflare.com/fr-fr/products/turnstile/)
|
||||
|
||||
|
||||
There are 4 operating modes (CrowdsecMode) for this plugin:
|
||||
|
||||
| Mode | Description |
|
||||
@@ -24,12 +46,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 +67,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`
|
||||
@@ -66,7 +102,7 @@ make run
|
||||
- CrowdsecLapiKey
|
||||
- string
|
||||
- default: ""
|
||||
- Crowdsec LAPI key for the bouncer : **must be unique by service**.
|
||||
- Crowdsec LAPI key for the bouncer.
|
||||
- CrowdsecLapiTlsInsecureVerify
|
||||
- bool
|
||||
- default: false
|
||||
@@ -111,6 +147,10 @@ make run
|
||||
- string
|
||||
- default: ""
|
||||
- Database selection for the Redis service
|
||||
- HTTPTimeoutSeconds
|
||||
- int64
|
||||
- default: 10
|
||||
- Default timeout in seconds for contacting Crowdsec LAPI
|
||||
- UpdateIntervalSeconds
|
||||
- int64
|
||||
- default: 60
|
||||
@@ -128,12 +168,32 @@ make run
|
||||
- CrowdsecCapiScenarios
|
||||
- []string
|
||||
- Used only in `alone` mode, scenarios for Crowdsec CAPI
|
||||
- CaptchaProvider
|
||||
- string
|
||||
- Provider to validate the captcha, expected values are: `hcaptcha`, `recaptcha`, `turnstile`
|
||||
- CaptchaSiteKey
|
||||
- string
|
||||
- Site key for the captcha provider
|
||||
- CaptchaSecretKey
|
||||
- string
|
||||
- Site secret key for the captcha provider
|
||||
- CaptchaHTMLFilePath
|
||||
- string
|
||||
- default: /captcha.html
|
||||
- Path where the captcha template is stored
|
||||
- CaptchaGracePeriodSeconds
|
||||
- int64
|
||||
- default: 1800 (= 30 minutes)
|
||||
- Period after validation of a captcha before a new validation is required if Crowdsec decision is still valid
|
||||
|
||||
|
||||
### Configuration
|
||||
|
||||
For each plugin, the Traefik static configuration must define the module name (as is usual for Go packages).
|
||||
|
||||
The following declaration (given here in YAML) defines a plugin:
|
||||
> Note that you don't need to copy all thoses settings but only the ones you want to use.
|
||||
> See the examples for advanced usage.
|
||||
|
||||
```yaml
|
||||
# Static configuration
|
||||
@@ -172,7 +232,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
|
||||
@@ -216,6 +280,11 @@ http:
|
||||
ic5cDRo6/VD3CS3MYzyBcibaGaV34nr0G/pI+KEqkYChzk/PZRA=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
crowdsecLapiTLSCertificateBouncerKeyFile: /etc/traefik/crowdsec-certs/bouncer-key.pem
|
||||
captchaProvider: hcaptcha
|
||||
captchaSiteKey: FIXME
|
||||
captchaSecretKey: FIXME
|
||||
captchaGracePeriodSeconds: 1800
|
||||
captchaHTMLFilePath: /captcha.html
|
||||
```
|
||||
|
||||
#### Fill variable with value of file
|
||||
@@ -237,7 +306,7 @@ You can generate a crowdsec API key for the LAPI.
|
||||
You can follow the documentation here: [docs.crowdsec.net/docs/user_guides/lapi_mgmt](https://docs.crowdsec.net/docs/user_guides/lapi_mgmt)
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose-local.yml up -d crowdsec
|
||||
docker compose -f docker-compose-local.yml up -d crowdsec
|
||||
docker exec crowdsec cscli bouncers add crowdsecBouncer
|
||||
```
|
||||
|
||||
@@ -261,15 +330,15 @@ Note:
|
||||
|
||||
You can then run all the containers:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
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
|
||||
|
||||
@@ -277,31 +346,37 @@ 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)
|
||||
|
||||
```bash
|
||||
docker-compose up -d crowdsec
|
||||
docker compose up -d crowdsec
|
||||
docker exec crowdsec cscli decisions add --ip 10.0.0.10 -d 10m # this will be effective 10min
|
||||
docker exec crowdsec cscli decisions remove --ip 10.0.0.10
|
||||
docker exec crowdsec cscli decisions add --ip 10.0.0.10 -d 10m -t captcha # this will return a captcha challenge
|
||||
docker exec crowdsec cscli decisions remove --ip 10.0.0.10 -t captcha
|
||||
```
|
||||
|
||||
### 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 [exemples/standalone-mode/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/standalone-mode/README.md)
|
||||
#### 7. Using Traefik in standalone mode without Crowdsec [examples/standalone-mode/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/standalone-mode/README.md)
|
||||
|
||||
#### 8. Using Traefik with AppSec feature enabled [examples/appsec-enabled/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/appsec-enabled/README.md)
|
||||
|
||||
#### 9. Using Traefik with Captcha remediation feature enabled [examples/captcha/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/captcha/README.md)
|
||||
|
||||
### Local Mode
|
||||
|
||||
@@ -331,7 +406,7 @@ For local development, a `docker-compose.local.yml` is provided which reproduces
|
||||
This works once you have generated and filled your *LAPI-KEY* (crowdsecLapiKey), if not read above for informations.
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.local.yml up -d
|
||||
docker compose -f docker-compose.local.yml up -d
|
||||
```
|
||||
Equivalent to
|
||||
```bash
|
||||
|
||||
@@ -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
|
||||
|
||||
+209
-62
@@ -16,17 +16,24 @@ import (
|
||||
"time"
|
||||
|
||||
cache "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache"
|
||||
captcha "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/captcha"
|
||||
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
|
||||
ip "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/ip"
|
||||
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
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"
|
||||
crowdsecCapiHeader = "Authorization"
|
||||
crowdsecLapiRoute = "v1/decisions"
|
||||
crowdsecLapiStreamRoute = "v1/decisions/stream"
|
||||
crowdsecCapiLogin = "v2/watchers/login"
|
||||
crowdsecCapiHost = "api.crowdsec.net"
|
||||
crowdsecCapiHeader = "Authorization"
|
||||
crowdsecCapiLoginRoute = "v2/watchers/login"
|
||||
crowdsecCapiStreamRoute = "v2/decisions/stream"
|
||||
cacheTimeoutKey = "updated"
|
||||
)
|
||||
@@ -50,6 +57,9 @@ type Bouncer struct {
|
||||
template *template.Template
|
||||
|
||||
enabled bool
|
||||
appsecEnabled bool
|
||||
appsecHost string
|
||||
appsecFailureBlock bool
|
||||
crowdsecScheme string
|
||||
crowdsecHost string
|
||||
crowdsecKey string
|
||||
@@ -66,19 +76,21 @@ type Bouncer struct {
|
||||
serverPoolStrategy *ip.PoolStrategy
|
||||
httpClient *http.Client
|
||||
cacheClient *cache.Client
|
||||
captchaClient *captcha.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 +98,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 +126,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 +140,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,32 +153,55 @@ 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{},
|
||||
captchaClient: &captcha.Client{},
|
||||
}
|
||||
if config.CrowdsecMode == configuration.AppsecMode {
|
||||
return bouncer, nil
|
||||
}
|
||||
config.RedisCachePassword, _ = configuration.GetVariable(config, "RedisCachePassword")
|
||||
bouncer.cacheClient.New(
|
||||
log,
|
||||
config.RedisCacheEnabled,
|
||||
config.RedisCacheHost,
|
||||
config.RedisCachePassword,
|
||||
config.RedisCacheDatabase,
|
||||
)
|
||||
config.CaptchaSiteKey, _ = configuration.GetVariable(config, "CaptchaSiteKey")
|
||||
config.CaptchaSecretKey, _ = configuration.GetVariable(config, "CaptchaSecretKey")
|
||||
err = bouncer.captchaClient.New(
|
||||
log,
|
||||
bouncer.cacheClient,
|
||||
&http.Client{
|
||||
Transport: &http.Transport{MaxIdleConns: 10, IdleConnTimeout: 30 * time.Second},
|
||||
Timeout: time.Duration(config.HTTPTimeoutSeconds) * time.Second,
|
||||
},
|
||||
config.CaptchaProvider,
|
||||
config.CaptchaSiteKey,
|
||||
config.CaptchaSecretKey,
|
||||
config.CaptchaHTMLFilePath,
|
||||
config.CaptchaGracePeriodSeconds,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if (config.CrowdsecMode == configuration.StreamMode || config.CrowdsecMode == configuration.AloneMode) && ticker == nil {
|
||||
if config.CrowdsecMode == configuration.AloneMode {
|
||||
if err := getToken(bouncer); err != nil {
|
||||
logger.Error(fmt.Sprintf("New:getToken %s", err.Error()))
|
||||
bouncer.log.Error(fmt.Sprintf("New:getToken %s", err.Error()))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
handleStreamTicker(bouncer)
|
||||
isStartup = false
|
||||
ticker = startTicker(config, func() {
|
||||
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
|
||||
}
|
||||
@@ -179,40 +218,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)
|
||||
value, cacheErr := bouncer.cacheClient.Get(remoteIP)
|
||||
if cacheErr != nil {
|
||||
errString := cacheErr.Error()
|
||||
logger.Debug(fmt.Sprintf("ServeHTTP:getDecision ip:%s isBanned:false %s", remoteIP, errString))
|
||||
if errString != cache.CacheMiss {
|
||||
logger.Error(fmt.Sprintf("ServeHTTP:getDecision ip:%s %s", remoteIP, errString))
|
||||
cacheErrString := cacheErr.Error()
|
||||
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:Get ip:%s isBanned:false %s", remoteIP, cacheErrString))
|
||||
if cacheErrString != cache.CacheMiss {
|
||||
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s %s", remoteIP, cacheErrString))
|
||||
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)
|
||||
bouncer.log.Debug(fmt.Sprintf("ServeHTTP ip:%s cache:hit isBanned:%v", remoteIP, value))
|
||||
if value == cache.NoBannedValue {
|
||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
||||
} else {
|
||||
bouncer.next.ServeHTTP(rw, req)
|
||||
handleErrorServeHTTP(bouncer, remoteIP, value, rw, req)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -221,19 +265,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.Debug(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()))
|
||||
rw.WriteHeader(http.StatusForbidden)
|
||||
value, err := handleNoStreamCache(bouncer, remoteIP)
|
||||
if value == cache.NoBannedValue {
|
||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
||||
} else {
|
||||
logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:false", remoteIP))
|
||||
bouncer.next.ServeHTTP(rw, req)
|
||||
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:%v %s", remoteIP, value, err.Error()))
|
||||
handleErrorServeHTTP(bouncer, remoteIP, value, rw, req)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,20 +309,44 @@ type Login struct {
|
||||
Expire string `json:"expire"`
|
||||
}
|
||||
|
||||
func handleErrorServeHTTP(bouncer *Bouncer, remoteIP, remediation string, rw http.ResponseWriter, req *http.Request) {
|
||||
bouncer.log.Debug(fmt.Sprintf("handleErrorServeHTTP ip:%s remediation:%s", remoteIP, remediation))
|
||||
if bouncer.captchaClient.Valid && remediation == cache.CaptchaValue {
|
||||
if bouncer.captchaClient.Check(remoteIP) {
|
||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
||||
return
|
||||
}
|
||||
bouncer.captchaClient.ServeHTTP(rw, req, remoteIP)
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusForbidden)
|
||||
}
|
||||
|
||||
func handleNextServeHTTP(bouncer *Bouncer, remoteIP string, rw http.ResponseWriter, req *http.Request) {
|
||||
if bouncer.appsecEnabled {
|
||||
if err := appsecQuery(bouncer, remoteIP, req); err != nil {
|
||||
bouncer.log.Debug(fmt.Sprintf("handleNextServeHTTP ip:%s isWaf:true %s", remoteIP, err.Error()))
|
||||
rw.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
bouncer.next.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
func handleStreamTicker(bouncer *Bouncer) {
|
||||
if err := handleStreamCache(bouncer); err != nil {
|
||||
isCrowdsecStreamHealthy = false
|
||||
logger.Error(err.Error())
|
||||
bouncer.log.Error(err.Error())
|
||||
} else {
|
||||
isCrowdsecStreamHealthy = true
|
||||
}
|
||||
}
|
||||
|
||||
func startTicker(config *configuration.Config, work func()) chan bool {
|
||||
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:
|
||||
@@ -293,7 +360,7 @@ func startTicker(config *configuration.Config, work func()) chan bool {
|
||||
}
|
||||
|
||||
// We are now in none or live mode.
|
||||
func handleNoStreamCache(bouncer *Bouncer, remoteIP string) error {
|
||||
func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
|
||||
isLiveMode := bouncer.crowdsecMode == configuration.LiveMode
|
||||
routeURL := url.URL{
|
||||
Scheme: bouncer.crowdsecScheme,
|
||||
@@ -303,46 +370,62 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) error {
|
||||
}
|
||||
body, err := crowdsecQuery(bouncer, routeURL.String(), false)
|
||||
if err != nil {
|
||||
return err
|
||||
return cache.BannedValue, err
|
||||
}
|
||||
|
||||
if bytes.Equal(body, []byte("null")) {
|
||||
if isLiveMode {
|
||||
bouncer.cacheClient.SetDecision(remoteIP, false, bouncer.defaultDecisionTimeout)
|
||||
bouncer.cacheClient.Set(remoteIP, cache.NoBannedValue, bouncer.defaultDecisionTimeout)
|
||||
}
|
||||
return nil
|
||||
return cache.NoBannedValue, nil
|
||||
}
|
||||
|
||||
var decisions []Decision
|
||||
err = json.Unmarshal(body, &decisions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleNoStreamCache:parseBody %w", err)
|
||||
return cache.BannedValue, fmt.Errorf("handleNoStreamCache:parseBody %w", err)
|
||||
}
|
||||
if len(decisions) == 0 {
|
||||
if isLiveMode {
|
||||
bouncer.cacheClient.SetDecision(remoteIP, false, bouncer.defaultDecisionTimeout)
|
||||
bouncer.cacheClient.Set(remoteIP, cache.NoBannedValue, bouncer.defaultDecisionTimeout)
|
||||
}
|
||||
return nil
|
||||
return cache.NoBannedValue, nil
|
||||
}
|
||||
duration, err := time.ParseDuration(decisions[0].Duration)
|
||||
var decision Decision
|
||||
for _, d := range decisions {
|
||||
decision = d
|
||||
if decision.Type == "ban" {
|
||||
break
|
||||
}
|
||||
}
|
||||
duration, err := time.ParseDuration(decision.Duration)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handleNoStreamCache:parseDuration %w", err)
|
||||
return cache.BannedValue, fmt.Errorf("handleNoStreamCache:parseDuration %w", err)
|
||||
}
|
||||
var value string
|
||||
switch decision.Type {
|
||||
case "ban":
|
||||
value = cache.BannedValue
|
||||
case "captcha":
|
||||
value = cache.CaptchaValue
|
||||
default:
|
||||
bouncer.log.Debug(fmt.Sprintf("handleStreamCache:unknownType %s", decision.Type))
|
||||
}
|
||||
if isLiveMode {
|
||||
durationSecond := int64(duration.Seconds())
|
||||
if bouncer.defaultDecisionTimeout < durationSecond {
|
||||
durationSecond = bouncer.defaultDecisionTimeout
|
||||
}
|
||||
bouncer.cacheClient.SetDecision(remoteIP, true, durationSecond)
|
||||
bouncer.cacheClient.Set(remoteIP, value, durationSecond)
|
||||
}
|
||||
return fmt.Errorf("handleNoStreamCache:banned")
|
||||
return value, fmt.Errorf("handleNoStreamCache:banned")
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -356,7 +439,7 @@ 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)
|
||||
@@ -367,15 +450,15 @@ func handleStreamCache(bouncer *Bouncer) error {
|
||||
// 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)
|
||||
_, err := bouncer.cacheClient.Get(cacheTimeoutKey)
|
||||
if err == nil {
|
||||
logger.Debug("handleStreamCache:alreadyUpdated")
|
||||
bouncer.log.Debug("handleStreamCache:alreadyUpdated")
|
||||
return nil
|
||||
}
|
||||
if err.Error() != cache.CacheMiss {
|
||||
return err
|
||||
}
|
||||
bouncer.cacheClient.SetDecision(cacheTimeoutKey, false, bouncer.updateInterval-1)
|
||||
bouncer.cacheClient.Set(cacheTimeoutKey, cache.NoBannedValue, bouncer.updateInterval-1)
|
||||
streamRouteURL := url.URL{
|
||||
Scheme: bouncer.crowdsecScheme,
|
||||
Host: bouncer.crowdsecHost,
|
||||
@@ -394,13 +477,22 @@ func handleStreamCache(bouncer *Bouncer) error {
|
||||
for _, decision := range stream.New {
|
||||
duration, err := time.ParseDuration(decision.Duration)
|
||||
if err == nil {
|
||||
bouncer.cacheClient.SetDecision(decision.Value, true, int64(duration.Seconds()))
|
||||
var value string
|
||||
switch decision.Type {
|
||||
case "ban":
|
||||
value = cache.BannedValue
|
||||
case "captcha":
|
||||
value = cache.CaptchaValue
|
||||
default:
|
||||
bouncer.log.Debug(fmt.Sprintf("handleStreamCache:unknownType %s", decision.Type))
|
||||
}
|
||||
bouncer.cacheClient.Set(decision.Value, value, int64(duration.Seconds()))
|
||||
}
|
||||
}
|
||||
for _, decision := range stream.Deleted {
|
||||
bouncer.cacheClient.DeleteDecision(decision.Value)
|
||||
bouncer.cacheClient.Delete(decision.Value)
|
||||
}
|
||||
logger.Debug("handleStreamCache:updated")
|
||||
bouncer.log.Debug("handleStreamCache:updated")
|
||||
isCrowdsecStreamHealthy = true
|
||||
return nil
|
||||
}
|
||||
@@ -423,6 +515,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)
|
||||
@@ -432,11 +529,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 {
|
||||
@@ -444,3 +536,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
|
||||
}
|
||||
|
||||
+1
-1
@@ -130,7 +130,7 @@ func Test_handleNoStreamCache(t *testing.T) {
|
||||
}
|
||||
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 {
|
||||
if _, err := handleNoStreamCache(tt.args.bouncer, tt.args.remoteIP); (err != nil) != tt.wantErr {
|
||||
t.Errorf("handleNoStreamCache() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>CrowdSec Captcha</title>
|
||||
<meta content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<style>
|
||||
/*! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com*/
|
||||
*,
|
||||
:after,
|
||||
:before {
|
||||
border: 0 solid #e5e7eb;
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
:after,
|
||||
:before {
|
||||
--tw-content: ""
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
font-feature-settings: normal;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||
line-height: 1.5;
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: inherit;
|
||||
margin: 0
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
p,
|
||||
pre {
|
||||
margin: 0
|
||||
}
|
||||
|
||||
*,
|
||||
::backdrop,
|
||||
:after,
|
||||
:before {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: #3b82f680;
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia:
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex
|
||||
}
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.inline-flex {
|
||||
display: inline-flex
|
||||
}
|
||||
|
||||
.h-24 {
|
||||
height: 6rem
|
||||
}
|
||||
|
||||
.h-6 {
|
||||
height: 1.5rem
|
||||
}
|
||||
|
||||
.h-full {
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.h-screen {
|
||||
height: 100vh
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.w-24 {
|
||||
width: 6rem
|
||||
}
|
||||
|
||||
.w-6 {
|
||||
width: 1.5rem
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
.w-screen {
|
||||
width: 100vw
|
||||
}
|
||||
|
||||
.my-3 {
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.75rem
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between
|
||||
}
|
||||
|
||||
.space-y-1>:not([hidden])~:not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-bottom: calc(.25rem*var(--tw-space-y-reverse));
|
||||
margin-top: calc(.25rem*(1 - var(--tw-space-y-reverse)))
|
||||
}
|
||||
|
||||
.space-y-4>:not([hidden])~:not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-bottom: calc(1rem*var(--tw-space-y-reverse));
|
||||
margin-top: calc(1rem*(1 - var(--tw-space-y-reverse)))
|
||||
}
|
||||
|
||||
.rounded-xl {
|
||||
border-radius: .75rem
|
||||
}
|
||||
|
||||
.border-2 {
|
||||
border-width: 2px
|
||||
}
|
||||
|
||||
.border-black {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(0 0 0/var(--tw-border-opacity))
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 1rem
|
||||
}
|
||||
|
||||
.px-4 {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem
|
||||
}
|
||||
|
||||
.py-2 {
|
||||
padding-bottom: .5rem;
|
||||
padding-top: .5rem
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: .875rem;
|
||||
line-height: 1.25rem
|
||||
}
|
||||
|
||||
.text-xl {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700
|
||||
}
|
||||
|
||||
.text-white {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255/var(--tw-text-opacity))
|
||||
}
|
||||
|
||||
@media (min-width:640px) {
|
||||
.sm\:w-2\/3 {
|
||||
width: 66.666667%
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width:768px) {
|
||||
.md\:flex-row {
|
||||
flex-direction: row
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width:1024px) {
|
||||
.lg\:w-1\/2 {
|
||||
width: 50%
|
||||
}
|
||||
|
||||
.lg\:text-3xl {
|
||||
font-size: 1.875rem;
|
||||
line-height: 2.25rem
|
||||
}
|
||||
|
||||
.lg\:text-xl {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width:1280px) {
|
||||
.xl\:text-4xl {
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script src="{{ .FrontendJS }}" async defer></script>
|
||||
</head>
|
||||
|
||||
<body class="h-screen w-screen p-4">
|
||||
<div class="h-full w-full flex flex-col justify-center items-center">
|
||||
<div class="border-2 border-black rounded-xl p-4 text-center w-full sm:w-2/3 lg:w-1/2">
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<svg fill="black" class="h-24 w-24" aria-hidden="true" focusable="false" data-prefix="fas"
|
||||
data-icon="exclamation-triangle" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"
|
||||
class="warning">
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z">
|
||||
</path>
|
||||
</svg>
|
||||
<h1 class="text-2xl lg:text-3xl xl:text-4xl">CrowdSec Captcha</h1>
|
||||
</div>
|
||||
<form action="" method="POST" class="flex flex-col space-y-1" id="captcha-form">
|
||||
<div id="captcha" class="{{ .FrontendKey }}" data-sitekey="{{ .SiteKey }}" data-callback="captchaCallback">
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex justify-center flex-wrap">
|
||||
<p class="my-3">This security check has been powered by</p>
|
||||
<a href="https://crowdsec.net/" target="_blank" rel="noopener" class="inline-flex flex-col items-center">
|
||||
<svg fill="black" width="33.92" height="33.76" viewBox="0 0 254.4 253.2">
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path d="M0 52h84v201.2H0zm0 0" />
|
||||
</clipPath>
|
||||
<clipPath id="b">
|
||||
<path d="M170 52h84.4v201.2H170zm0 0" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<path
|
||||
d="M59.3 128.4c1.4 2.3 2.5 4.6 3.4 7-1-4.1-2.3-8.1-4.3-12-3.1-6-7.8-5.8-10.7 0-2 4-3.2 8-4.3 12.1 1-2.4 2-4.8 3.4-7.1 3.4-5.8 8.8-6 12.5 0M207.8 128.4a42.9 42.9 0 013.4 7c-1-4.1-2.3-8.1-4.3-12-3.2-6-7.8-5.8-10.7 0-2 4-3.3 8-4.3 12.1.9-2.4 2-4.8 3.4-7.1 3.4-5.8 8.8-6 12.5 0M134.6 92.9c2 3.5 3.6 7 4.8 10.7-1.3-5.4-3-10.6-5.6-15.7-4-7.5-9.7-7.2-13.3 0a75.4 75.4 0 00-5.6 16c1.2-3.8 2.7-7.4 4.7-11 4.1-7.2 10.6-7.5 15 0M43.8 136.8c.9 4.6 3.7 8.3 7.3 9.2 0 2.7 0 5.5.2 8.2.3 3.3.4 6.6 1 9.6.3 2.3 1 2.2 1.3 0 .5-3 .6-6.3 1-9.6l.2-8.2c3.5-1 6.4-4.6 7.2-9.2a17.8 17.8 0 01-9 2.4c-3.5 0-6.6-1-9.2-2.4M192.4 136.8c.8 4.6 3.7 8.3 7.2 9.2 0 2.7 0 5.5.3 8.2.3 3.3.4 6.6 1 9.6.3 2.3.9 2.2 1.2 0 .6-3 .7-6.3 1-9.6.2-2.7.3-5.5.2-8.2 3.6-1 6.4-4.6 7.3-9.2a17.8 17.8 0 01-9.1 2.4c-3.4 0-6.6-1-9.1-2.4M138.3 104.6c-3.1 1.9-7 3-11.3 3-4.3 0-8.2-1.1-11.3-3 1 5.8 4.5 10.3 9 11.5 0 3.4 0 6.8.3 10.2.4 4.1.5 8.2 1.2 12 .4 2.9 1.2 2.7 1.6 0 .7-3.8.8-7.9 1.2-12 .3-3.4.3-6.8.3-10.2 4.5-1.2 8-5.7 9-11.5" />
|
||||
<path
|
||||
d="M51 146c0 2.7.1 5.5.3 8.2.3 3.3.4 6.6 1 9.6.3 2.3 1 2.2 1.3 0 .5-3 .6-6.3 1-9.6l.2-8.2c3.5-1 6.4-4.6 7.2-9.2a17.8 17.8 0 01-9 2.4c-3.5 0-6.6-1-9.2-2.4.9 4.6 3.7 8.3 7.3 9.2M143.9 105c-1.9-.4-3.5-1.2-4.9-2.3 1.4 5.6 2.5 11.3 4 17 1.2 5 2 10 2.4 15 .6 7.8-4.5 14.5-10.9 14.5h-15c-6.4 0-11.5-6.7-11-14.5.5-5 1.3-10 2.6-15 1.3-5.3 2.3-10.5 3.6-15.7-2.2 1.2-4.8 1.9-7.7 2-4.7.1-9.4-.3-14-1-4-.4-6.7-3-8-6.7-1.3-3.4-2-7-3.3-10.4-.5-1.5-1.6-2.8-2.4-4.2-.4-.6-.8-1.2-.9-1.8v-7.8a77 77 0 0124.5-3c6.1 0 12 1 17.8 3.2 4.7 1.7 9.7 1.8 14.4 0 9-3.4 18.2-3.8 27.5-3 4.9.5 9.8 1.6 14.8 2.4v8.2c0 .6-.3 1.5-.7 1.7-2 .9-2.2 2.7-2.7 4.5-.9 3.2-1.8 6.4-2.9 9.5a11 11 0 01-8.8 7.7 40.6 40.6 0 01-18.4-.2m29.4 80.6c-3.2-26.8-6.4-50-8.9-60.7a14.3 14.3 0 0014.1-14h.4a9 9 0 005.6-16.5 14.3 14.3 0 00-3.7-27.2 9 9 0 00-6.9-14.6c2.4-1.1 4.5-3 5.8-5 3.4-5.3 4-29-8-44.4-5-6.3-9.8-2.5-10 1.8-1 13.2-1.1 23-4.5 34.3a9 9 0 00-16-4.1 14.3 14.3 0 00-28.4 0 9 9 0 00-16 4.1c-3.4-11.2-3.5-21.1-4.4-34.3-.3-4.3-5.2-8-10-1.8-12 15.3-11.5 39-8.1 44.4 1.3 2 3.4 3.9 5.8 5a9 9 0 00-7 14.6 14.3 14.3 0 00-3.6 27.2A9 9 0 0075 111h.5a14.5 14.5 0 0014.3 14c-4 17.2-10 66.3-15 111.3l-1.3 13.4a1656.4 1656.4 0 01106.6 0l-1.4-12.7-5.4-51.3" />
|
||||
<g clip-path="url(#a)">
|
||||
<path
|
||||
d="M83.5 136.6l-2.3.7c-5 1-9.8 1-14.8-.2-1.4-.3-2.7-1-3.8-1.9l3.1 13.7c1 4 1.7 8 2 12 .5 6.3-3.6 11.6-8.7 11.6H46.9c-5.1 0-9.2-5.3-8.7-11.6.3-4 1-8 2-12 1-4.2 1.8-8.5 2.9-12.6-1.8 1-3.9 1.5-6.3 1.6a71 71 0 01-11.1-.7 7.7 7.7 0 01-6.5-5.5c-1-2.7-1.6-5.6-2.6-8.3-.4-1.2-1.3-2.3-2-3.4-.2-.4-.6-1-.6-1.4v-6.3c6.4-2 13-2.6 19.6-2.5 4.9.1 9.6 1 14.2 2.6 3.9 1.4 7.9 1.5 11.7 0 1.8-.7 3.6-1.2 5.5-1.6a13 13 0 01-1.6-15.5A18.3 18.3 0 0159 73.1a11.5 11.5 0 00-17.4 8.1 7.2 7.2 0 00-12.9 3.3c-2.7-9-2.8-17-3.6-27.5-.2-3.4-4-6.5-8-1.4C7.5 67.8 7.9 86.9 10.6 91c1.1 1.7 2.8 3.1 4.7 4a7.2 7.2 0 00-5.6 11.7 11.5 11.5 0 00-2.9 21.9 7.2 7.2 0 004.5 13.2h.3c0 .6 0 1.1.2 1.7.9 5.4 5.6 9.5 11.3 9.5A1177.2 1177.2 0 0010 253.2c18.1-1.5 38.1-2.6 59.5-3.4.4-4.6.8-9.3 1.4-14 1.2-11.6 3.3-30.5 5.7-49.7 2.2-18 4.7-36.3 7-49.5" />
|
||||
</g>
|
||||
<g clip-path="url(#b)">
|
||||
<path
|
||||
d="M254.4 118.2c0-5.8-4.2-10.5-9.7-11.4a7.2 7.2 0 00-5.6-11.7c2-.9 3.6-2.3 4.7-4 2.7-4.2 3.1-23.3-6.5-35.5-4-5.1-7.8-2-8 1.4-.8 10.5-.9 18.5-3.6 27.5a7.2 7.2 0 00-12.8-3.3 11.5 11.5 0 00-17.8-7.9 18.4 18.4 0 01-4.5 22 13 13 0 01-1.3 15.2c2.4.5 4.8 1 7.1 2 3.8 1.3 7.8 1.4 11.6 0 7.2-2.8 14.6-3 22-2.4 4 .4 7.9 1.2 12 1.9l-.1 6.6c0 .5-.2 1.2-.5 1.3-1.7.7-1.8 2.2-2.2 3.7l-2.3 7.6a8.8 8.8 0 01-7 6.1c-5 1-10 1-14.9-.2-1.5-.3-2.8-1-3.9-1.9 1.2 4.5 2 9.1 3.2 13.7 1 4 1.6 8 2 12 .4 6.3-3.6 11.6-8.8 11.6h-12c-5.2 0-9.3-5.3-8.8-11.6.4-4 1-8 2-12 1-4.2 1.9-8.5 3-12.6-1.8 1-4 1.5-6.3 1.6-3.7 0-7.5-.3-11.2-.7a7.7 7.7 0 01-3.7-1.5c3.1 18.4 7.1 51.2 12.5 100.9l.6 5.3.8 7.9c21.4.7 41.5 1.9 59.7 3.4L243 243l-4.4-41.2a606 606 0 00-7-48.7 11.5 11.5 0 0011.2-11.2h.4a7.2 7.2 0 004.4-13.2c4-1.8 6.8-5.8 6.8-10.5" />
|
||||
</g>
|
||||
<path
|
||||
d="M180 249.6h.4a6946 6946 0 00-7.1-63.9l5.4 51.3 1.4 12.6M164.4 125c2.5 10.7 5.7 33.9 8.9 60.7a570.9 570.9 0 00-8.9-60.7M74.8 236.3l-1.4 13.4 1.4-13.4" />
|
||||
</svg>
|
||||
<span>CrowdSec</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function captchaCallback() {
|
||||
setTimeout(() => document.querySelector('#captcha-form').submit(), 500);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+14
-16
@@ -1,8 +1,6 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.9.10"
|
||||
image: "traefik:v2.11.0"
|
||||
container_name: "traefik"
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -20,7 +18,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 +29,10 @@ 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.routers.router-foo.middlewares=crowdsec@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
|
||||
@@ -44,22 +40,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.routers.router-bar.middlewares=crowdsec@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.plugin.bouncer.enabled=true"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsecenabled=true"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5="
|
||||
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:v1.4.6
|
||||
image: crowdsecurity/crowdsec:1.6.0
|
||||
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 +64,7 @@ services:
|
||||
- crowdsec-config-local:/etc/crowdsec/
|
||||
labels:
|
||||
- "traefik.enable=false"
|
||||
|
||||
volumes:
|
||||
logs-local:
|
||||
crowdsec-db-local:
|
||||
|
||||
+12
-19
@@ -1,8 +1,6 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.9.10"
|
||||
image: "traefik:v2.11.0"
|
||||
container_name: "traefik"
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -14,7 +12,7 @@ services:
|
||||
- "--entrypoints.web.address=:80"
|
||||
|
||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
- "--experimental.plugins.bouncer.version=v1.1.11"
|
||||
- "--experimental.plugins.bouncer.version=v1.1.15"
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
- "logs:/var/log/traefik"
|
||||
@@ -33,15 +31,9 @@ services:
|
||||
# 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"
|
||||
- "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
|
||||
# Definition of the service
|
||||
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
|
||||
# Definition of the middleware
|
||||
- "traefik.http.middlewares.crowdsec-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
|
||||
@@ -52,26 +44,27 @@ services:
|
||||
# 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"
|
||||
- "traefik.http.routers.router-bar.middlewares=crowdsec@docker"
|
||||
# Definition of the service
|
||||
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
|
||||
# Definitin of the middleware
|
||||
- "traefik.http.middlewares.crowdsec-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.plugin.bouncer.enabled=true"
|
||||
# crowdseclapikey is the key to authenticate to crowdsec
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-1="
|
||||
# enable AppSec real time check
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsecenabled=true"
|
||||
# forwardedheaderstrustedips should be the IP of the proxy that is in front of traefik (if any)
|
||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
|
||||
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:v1.4.6
|
||||
image: crowdsecurity/crowdsec:v1.6.0
|
||||
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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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@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
|
||||
+16
-23
@@ -2,7 +2,7 @@ version: "3.8"
|
||||
|
||||
services:
|
||||
cloudflare:
|
||||
image: "traefik:v2.9.10"
|
||||
image: "traefik:v2.10.4"
|
||||
container_name: "cloudflare"
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -14,14 +14,14 @@ services:
|
||||
- "--providers.file.filename=/cloud.yaml"
|
||||
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.10"
|
||||
image: "traefik:v2.10.4"
|
||||
container_name: "traefik"
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -35,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.11"
|
||||
- "--experimental.plugins.bouncer.version=v1.1.13"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- logs-traefik:/var/log/traefik
|
||||
@@ -54,16 +54,15 @@ services:
|
||||
# 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"
|
||||
- "traefik.http.routers.router-foo.middlewares=crowdsec@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"
|
||||
|
||||
whoami-bar:
|
||||
image: traefik/whoami
|
||||
@@ -74,27 +73,21 @@ services:
|
||||
# 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"
|
||||
- "traefik.http.routers.router-bar.middlewares=crowdsec@docker"
|
||||
# Definition of the service
|
||||
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
|
||||
# Definitin of the middleware
|
||||
- "traefik.http.middlewares.crowdsec-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"
|
||||
# Definition of the middleware
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||
|
||||
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:v1.4.6
|
||||
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
|
||||
|
||||
+1
-1
@@ -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.10/traefik_v2.9.10_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/
|
||||
@@ -0,0 +1,161 @@
|
||||
# Example
|
||||
## Enabling catpcha response from crowdsec
|
||||
|
||||
Crowdsec support 3 remediations solutions `ban`, `captcha`, and `throttle`.
|
||||
This plugins support the `ban` and `captcha` remediation.
|
||||
|
||||
### Traefik configuration
|
||||
|
||||
The minimal configuration is defined below.
|
||||
For now 3 captcha providers are supported:
|
||||
- [hcaptcha](https://www.hcaptcha.com/)
|
||||
- [recaptcha](https://www.google.com/recaptcha/about/)
|
||||
- [turnstile](https://www.cloudflare.com/fr-fr/products/turnstile/)
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
# Choose captcha provider
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaProvider=hcaptcha"
|
||||
# Define captcha site key
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaSiteKey=FIXME"
|
||||
# Define captcha secret key
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaSecretKey=FIXME"
|
||||
# Define captcha grade period seconds
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaGracePeriodSeconds=1800"
|
||||
# Define captcha HTML file path
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaHTMLFilePath=/captcha.html"
|
||||
```
|
||||
|
||||
The captcha HTML file must be present in the Traefik container (bind mounted or added during a custom build).
|
||||
It is not directly accessible from Traefik even when importing the plugin, so [download](https://raw.githubusercontent.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/master/captcha.html) it locally to expose it to Traefik.
|
||||
|
||||
```yaml
|
||||
...
|
||||
traefik:
|
||||
image: "traefik:v2.11.0"
|
||||
volumes:
|
||||
- './captcha.html:/captcha.html'
|
||||
...
|
||||
```
|
||||
### Crowdsec configuration
|
||||
|
||||
Crowdsec by default will take the ban action on suspicious activity detected in logs.
|
||||
To instruct Crowdsec to use captcha remediation, change the `/etc/crowdsec/profiles.yaml`.
|
||||
|
||||
2 modes are supported:
|
||||
- Always return a captcha decision
|
||||
- Return a captcha decision the first X times and then a ban decision.
|
||||
|
||||
The second mode could be used to prevent repeated malicious activity.
|
||||
More information is available on configuring Crowdsec in the [official documentation](https://docs.crowdsec.net/docs/next/profiles/captcha_profile/).
|
||||
|
||||
```yaml
|
||||
...
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:v1.6.0
|
||||
volumes:
|
||||
# For captcha and ban mixed decision
|
||||
- './profiles.yaml:/etc/crowdsec/profiles.yaml:ro'
|
||||
# For captcha only remediation
|
||||
# - './profiles_captcha_only.yaml:/etc/crowdsec/profiles.yaml:ro'
|
||||
...
|
||||
```
|
||||
## Exemple navigation
|
||||
We can try to query normally the whoami server:
|
||||
```bash
|
||||
curl http://localhost:8000/foo
|
||||
```
|
||||
|
||||
We can try to ban ourself
|
||||
|
||||
```bash
|
||||
docker exec crowdsec cscli decisions add --ip 10.0.0.20 -d 4h --type captcha
|
||||
```
|
||||
|
||||

|
||||
|
||||
We will see in the browser the captcha validation page:
|
||||
|
||||

|
||||
|
||||
To play the demo environment run:
|
||||
```bash
|
||||
make run_captcha
|
||||
```
|
||||
|
||||
> Note, if we are banned with a "ban" decision from crowdsec a captcha will not be asked and you will have to wait for the decision to expire or remove it manually.
|
||||
|
||||
```bash
|
||||
docker exec crowdsec cscli decisions add --ip 10.0.0.10 -d 10m --type ban
|
||||
```
|
||||
|
||||
## Captcha Workflow
|
||||
|
||||
> Context: The user has no decision attached to his IP
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant TraefikPlugin
|
||||
User->>TraefikPlugin: Can I access that webpage
|
||||
create participant PluginCache
|
||||
TraefikPlugin-->>PluginCache: Does the user IP has a crowdsec decision ?
|
||||
Destroy PluginCache
|
||||
PluginCache-->>TraefikPlugin: Nothing, all good!
|
||||
Destroy TraefikPlugin
|
||||
TraefikPlugin->>Webserver: Forwarding this HTTP Request from User
|
||||
Webserver->>User: HTTP Response
|
||||
```
|
||||
|
||||
> Context: The user has a captcha decision attached to his IP
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant TraefikPlugin
|
||||
User->>TraefikPlugin: Can I access that webpage
|
||||
create participant PluginCache
|
||||
TraefikPlugin-->>PluginCache: Does the User IP has a Crowdsec Decision ?
|
||||
PluginCache-->>TraefikPlugin: Yes a Catpcha Decision
|
||||
TraefikPlugin->>User: Please complete this captcha
|
||||
User->>TraefikPlugin: Fine, done!
|
||||
create participant ProviderCaptcha
|
||||
TraefikPlugin-->>ProviderCaptcha: Is the validation OK ?
|
||||
Destroy ProviderCaptcha
|
||||
ProviderCaptcha-->>TraefikPlugin: Yes
|
||||
TraefikPlugin-->>PluginCache: Set the User IP Clean for captchaGracePeriodSeconds
|
||||
Destroy PluginCache
|
||||
PluginCache-->>TraefikPlugin: Done
|
||||
Destroy TraefikPlugin
|
||||
TraefikPlugin->>Webserver: Forwarding this HTTP Request from User
|
||||
Webserver->>User: HTTP Response
|
||||
```
|
||||
|
||||
> Context: The user has a ban decision attached to his IP
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant TraefikPlugin
|
||||
User->>TraefikPlugin: Can I access that webpage
|
||||
create participant PluginCache
|
||||
TraefikPlugin-->>PluginCache: Does the User IP has a Crowdsec Decision ?
|
||||
Destroy PluginCache
|
||||
PluginCache-->>TraefikPlugin: Yes a ban Decision
|
||||
TraefikPlugin->>User: No, HTTP 403
|
||||
```
|
||||
|
||||
## Captcha Configuration:
|
||||
|
||||
- Recaptcha
|
||||
|
||||
Choose v2 (challenge) and configure the domain to protect:
|
||||

|
||||
|
||||
- Turnstile
|
||||
|
||||
TODO
|
||||
|
||||
- Hcatpcha
|
||||
|
||||
TODO
|
||||
@@ -0,0 +1,338 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>CrowdSec Captcha</title>
|
||||
<meta content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<style>
|
||||
/*! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com*/
|
||||
*,
|
||||
:after,
|
||||
:before {
|
||||
border: 0 solid #e5e7eb;
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
:after,
|
||||
:before {
|
||||
--tw-content: ""
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
font-feature-settings: normal;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||
line-height: 1.5;
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: inherit;
|
||||
margin: 0
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
p,
|
||||
pre {
|
||||
margin: 0
|
||||
}
|
||||
|
||||
*,
|
||||
::backdrop,
|
||||
:after,
|
||||
:before {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: #3b82f680;
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia:
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex
|
||||
}
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.inline-flex {
|
||||
display: inline-flex
|
||||
}
|
||||
|
||||
.h-24 {
|
||||
height: 6rem
|
||||
}
|
||||
|
||||
.h-6 {
|
||||
height: 1.5rem
|
||||
}
|
||||
|
||||
.h-full {
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.h-screen {
|
||||
height: 100vh
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.w-24 {
|
||||
width: 6rem
|
||||
}
|
||||
|
||||
.w-6 {
|
||||
width: 1.5rem
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
.w-screen {
|
||||
width: 100vw
|
||||
}
|
||||
|
||||
.my-3 {
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.75rem
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between
|
||||
}
|
||||
|
||||
.space-y-1>:not([hidden])~:not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-bottom: calc(.25rem*var(--tw-space-y-reverse));
|
||||
margin-top: calc(.25rem*(1 - var(--tw-space-y-reverse)))
|
||||
}
|
||||
|
||||
.space-y-4>:not([hidden])~:not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-bottom: calc(1rem*var(--tw-space-y-reverse));
|
||||
margin-top: calc(1rem*(1 - var(--tw-space-y-reverse)))
|
||||
}
|
||||
|
||||
.rounded-xl {
|
||||
border-radius: .75rem
|
||||
}
|
||||
|
||||
.border-2 {
|
||||
border-width: 2px
|
||||
}
|
||||
|
||||
.border-black {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(0 0 0/var(--tw-border-opacity))
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 1rem
|
||||
}
|
||||
|
||||
.px-4 {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem
|
||||
}
|
||||
|
||||
.py-2 {
|
||||
padding-bottom: .5rem;
|
||||
padding-top: .5rem
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: .875rem;
|
||||
line-height: 1.25rem
|
||||
}
|
||||
|
||||
.text-xl {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700
|
||||
}
|
||||
|
||||
.text-white {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255/var(--tw-text-opacity))
|
||||
}
|
||||
|
||||
@media (min-width:640px) {
|
||||
.sm\:w-2\/3 {
|
||||
width: 66.666667%
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width:768px) {
|
||||
.md\:flex-row {
|
||||
flex-direction: row
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width:1024px) {
|
||||
.lg\:w-1\/2 {
|
||||
width: 50%
|
||||
}
|
||||
|
||||
.lg\:text-3xl {
|
||||
font-size: 1.875rem;
|
||||
line-height: 2.25rem
|
||||
}
|
||||
|
||||
.lg\:text-xl {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width:1280px) {
|
||||
.xl\:text-4xl {
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script src="{{ .FrontendJS }}" async defer></script>
|
||||
</head>
|
||||
|
||||
<body class="h-screen w-screen p-4">
|
||||
<div class="h-full w-full flex flex-col justify-center items-center">
|
||||
<div class="border-2 border-black rounded-xl p-4 text-center w-full sm:w-2/3 lg:w-1/2">
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<svg fill="black" class="h-24 w-24" aria-hidden="true" focusable="false" data-prefix="fas"
|
||||
data-icon="exclamation-triangle" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"
|
||||
class="warning">
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z">
|
||||
</path>
|
||||
</svg>
|
||||
<h1 class="text-2xl lg:text-3xl xl:text-4xl">CrowdSec Captcha</h1>
|
||||
</div>
|
||||
<form action="" method="POST" class="flex flex-col space-y-1" id="captcha-form">
|
||||
<div id="captcha" class="{{ .FrontendKey }}" data-sitekey="{{ .SiteKey }}" data-callback="captchaCallback">
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex justify-center flex-wrap">
|
||||
<p class="my-3">This security check has been powered by</p>
|
||||
<a href="https://crowdsec.net/" target="_blank" rel="noopener" class="inline-flex flex-col items-center">
|
||||
<svg fill="black" width="33.92" height="33.76" viewBox="0 0 254.4 253.2">
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path d="M0 52h84v201.2H0zm0 0" />
|
||||
</clipPath>
|
||||
<clipPath id="b">
|
||||
<path d="M170 52h84.4v201.2H170zm0 0" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<path
|
||||
d="M59.3 128.4c1.4 2.3 2.5 4.6 3.4 7-1-4.1-2.3-8.1-4.3-12-3.1-6-7.8-5.8-10.7 0-2 4-3.2 8-4.3 12.1 1-2.4 2-4.8 3.4-7.1 3.4-5.8 8.8-6 12.5 0M207.8 128.4a42.9 42.9 0 013.4 7c-1-4.1-2.3-8.1-4.3-12-3.2-6-7.8-5.8-10.7 0-2 4-3.3 8-4.3 12.1.9-2.4 2-4.8 3.4-7.1 3.4-5.8 8.8-6 12.5 0M134.6 92.9c2 3.5 3.6 7 4.8 10.7-1.3-5.4-3-10.6-5.6-15.7-4-7.5-9.7-7.2-13.3 0a75.4 75.4 0 00-5.6 16c1.2-3.8 2.7-7.4 4.7-11 4.1-7.2 10.6-7.5 15 0M43.8 136.8c.9 4.6 3.7 8.3 7.3 9.2 0 2.7 0 5.5.2 8.2.3 3.3.4 6.6 1 9.6.3 2.3 1 2.2 1.3 0 .5-3 .6-6.3 1-9.6l.2-8.2c3.5-1 6.4-4.6 7.2-9.2a17.8 17.8 0 01-9 2.4c-3.5 0-6.6-1-9.2-2.4M192.4 136.8c.8 4.6 3.7 8.3 7.2 9.2 0 2.7 0 5.5.3 8.2.3 3.3.4 6.6 1 9.6.3 2.3.9 2.2 1.2 0 .6-3 .7-6.3 1-9.6.2-2.7.3-5.5.2-8.2 3.6-1 6.4-4.6 7.3-9.2a17.8 17.8 0 01-9.1 2.4c-3.4 0-6.6-1-9.1-2.4M138.3 104.6c-3.1 1.9-7 3-11.3 3-4.3 0-8.2-1.1-11.3-3 1 5.8 4.5 10.3 9 11.5 0 3.4 0 6.8.3 10.2.4 4.1.5 8.2 1.2 12 .4 2.9 1.2 2.7 1.6 0 .7-3.8.8-7.9 1.2-12 .3-3.4.3-6.8.3-10.2 4.5-1.2 8-5.7 9-11.5" />
|
||||
<path
|
||||
d="M51 146c0 2.7.1 5.5.3 8.2.3 3.3.4 6.6 1 9.6.3 2.3 1 2.2 1.3 0 .5-3 .6-6.3 1-9.6l.2-8.2c3.5-1 6.4-4.6 7.2-9.2a17.8 17.8 0 01-9 2.4c-3.5 0-6.6-1-9.2-2.4.9 4.6 3.7 8.3 7.3 9.2M143.9 105c-1.9-.4-3.5-1.2-4.9-2.3 1.4 5.6 2.5 11.3 4 17 1.2 5 2 10 2.4 15 .6 7.8-4.5 14.5-10.9 14.5h-15c-6.4 0-11.5-6.7-11-14.5.5-5 1.3-10 2.6-15 1.3-5.3 2.3-10.5 3.6-15.7-2.2 1.2-4.8 1.9-7.7 2-4.7.1-9.4-.3-14-1-4-.4-6.7-3-8-6.7-1.3-3.4-2-7-3.3-10.4-.5-1.5-1.6-2.8-2.4-4.2-.4-.6-.8-1.2-.9-1.8v-7.8a77 77 0 0124.5-3c6.1 0 12 1 17.8 3.2 4.7 1.7 9.7 1.8 14.4 0 9-3.4 18.2-3.8 27.5-3 4.9.5 9.8 1.6 14.8 2.4v8.2c0 .6-.3 1.5-.7 1.7-2 .9-2.2 2.7-2.7 4.5-.9 3.2-1.8 6.4-2.9 9.5a11 11 0 01-8.8 7.7 40.6 40.6 0 01-18.4-.2m29.4 80.6c-3.2-26.8-6.4-50-8.9-60.7a14.3 14.3 0 0014.1-14h.4a9 9 0 005.6-16.5 14.3 14.3 0 00-3.7-27.2 9 9 0 00-6.9-14.6c2.4-1.1 4.5-3 5.8-5 3.4-5.3 4-29-8-44.4-5-6.3-9.8-2.5-10 1.8-1 13.2-1.1 23-4.5 34.3a9 9 0 00-16-4.1 14.3 14.3 0 00-28.4 0 9 9 0 00-16 4.1c-3.4-11.2-3.5-21.1-4.4-34.3-.3-4.3-5.2-8-10-1.8-12 15.3-11.5 39-8.1 44.4 1.3 2 3.4 3.9 5.8 5a9 9 0 00-7 14.6 14.3 14.3 0 00-3.6 27.2A9 9 0 0075 111h.5a14.5 14.5 0 0014.3 14c-4 17.2-10 66.3-15 111.3l-1.3 13.4a1656.4 1656.4 0 01106.6 0l-1.4-12.7-5.4-51.3" />
|
||||
<g clip-path="url(#a)">
|
||||
<path
|
||||
d="M83.5 136.6l-2.3.7c-5 1-9.8 1-14.8-.2-1.4-.3-2.7-1-3.8-1.9l3.1 13.7c1 4 1.7 8 2 12 .5 6.3-3.6 11.6-8.7 11.6H46.9c-5.1 0-9.2-5.3-8.7-11.6.3-4 1-8 2-12 1-4.2 1.8-8.5 2.9-12.6-1.8 1-3.9 1.5-6.3 1.6a71 71 0 01-11.1-.7 7.7 7.7 0 01-6.5-5.5c-1-2.7-1.6-5.6-2.6-8.3-.4-1.2-1.3-2.3-2-3.4-.2-.4-.6-1-.6-1.4v-6.3c6.4-2 13-2.6 19.6-2.5 4.9.1 9.6 1 14.2 2.6 3.9 1.4 7.9 1.5 11.7 0 1.8-.7 3.6-1.2 5.5-1.6a13 13 0 01-1.6-15.5A18.3 18.3 0 0159 73.1a11.5 11.5 0 00-17.4 8.1 7.2 7.2 0 00-12.9 3.3c-2.7-9-2.8-17-3.6-27.5-.2-3.4-4-6.5-8-1.4C7.5 67.8 7.9 86.9 10.6 91c1.1 1.7 2.8 3.1 4.7 4a7.2 7.2 0 00-5.6 11.7 11.5 11.5 0 00-2.9 21.9 7.2 7.2 0 004.5 13.2h.3c0 .6 0 1.1.2 1.7.9 5.4 5.6 9.5 11.3 9.5A1177.2 1177.2 0 0010 253.2c18.1-1.5 38.1-2.6 59.5-3.4.4-4.6.8-9.3 1.4-14 1.2-11.6 3.3-30.5 5.7-49.7 2.2-18 4.7-36.3 7-49.5" />
|
||||
</g>
|
||||
<g clip-path="url(#b)">
|
||||
<path
|
||||
d="M254.4 118.2c0-5.8-4.2-10.5-9.7-11.4a7.2 7.2 0 00-5.6-11.7c2-.9 3.6-2.3 4.7-4 2.7-4.2 3.1-23.3-6.5-35.5-4-5.1-7.8-2-8 1.4-.8 10.5-.9 18.5-3.6 27.5a7.2 7.2 0 00-12.8-3.3 11.5 11.5 0 00-17.8-7.9 18.4 18.4 0 01-4.5 22 13 13 0 01-1.3 15.2c2.4.5 4.8 1 7.1 2 3.8 1.3 7.8 1.4 11.6 0 7.2-2.8 14.6-3 22-2.4 4 .4 7.9 1.2 12 1.9l-.1 6.6c0 .5-.2 1.2-.5 1.3-1.7.7-1.8 2.2-2.2 3.7l-2.3 7.6a8.8 8.8 0 01-7 6.1c-5 1-10 1-14.9-.2-1.5-.3-2.8-1-3.9-1.9 1.2 4.5 2 9.1 3.2 13.7 1 4 1.6 8 2 12 .4 6.3-3.6 11.6-8.8 11.6h-12c-5.2 0-9.3-5.3-8.8-11.6.4-4 1-8 2-12 1-4.2 1.9-8.5 3-12.6-1.8 1-4 1.5-6.3 1.6-3.7 0-7.5-.3-11.2-.7a7.7 7.7 0 01-3.7-1.5c3.1 18.4 7.1 51.2 12.5 100.9l.6 5.3.8 7.9c21.4.7 41.5 1.9 59.7 3.4L243 243l-4.4-41.2a606 606 0 00-7-48.7 11.5 11.5 0 0011.2-11.2h.4a7.2 7.2 0 004.4-13.2c4-1.8 6.8-5.8 6.8-10.5" />
|
||||
</g>
|
||||
<path
|
||||
d="M180 249.6h.4a6946 6946 0 00-7.1-63.9l5.4 51.3 1.4 12.6M164.4 125c2.5 10.7 5.7 33.9 8.9 60.7a570.9 570.9 0 00-8.9-60.7M74.8 236.3l-1.4 13.4 1.4-13.4" />
|
||||
</svg>
|
||||
<span>CrowdSec</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function captchaCallback() {
|
||||
setTimeout(() => document.querySelector('#captcha-form').submit(), 500);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,80 @@
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.11.0"
|
||||
container_name: "traefik"
|
||||
restart: unless-stopped
|
||||
command:
|
||||
# - "--log.level=DEBUG"
|
||||
- "--accesslog"
|
||||
- "--accesslog.filepath=/var/log/traefik/access.log"
|
||||
- "--api.insecure=true"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.web.address=:80"
|
||||
|
||||
# - "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
# - "--experimental.plugins.bouncer.version=v1.2.0"
|
||||
- "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- logs-captcha-enabled:/var/log/traefik
|
||||
- './captcha.html:/captcha.html'
|
||||
- ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||
ports:
|
||||
- 8000:80
|
||||
- 8080:8080
|
||||
depends_on:
|
||||
- crowdsec
|
||||
|
||||
whoami1:
|
||||
image: traefik/whoami
|
||||
container_name: "simple-service-foo"
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
# Definition of the router
|
||||
- "traefik.http.routers.router-foo.rule=PathPrefix(`/foo`)"
|
||||
- "traefik.http.routers.router-foo.entrypoints=web"
|
||||
- "traefik.http.routers.router-foo.middlewares=crowdsec@docker"
|
||||
# Definition of the service
|
||||
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
|
||||
# Definition of the middleware
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
|
||||
|
||||
# Choose captcha provider
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaProvider=hcaptcha"
|
||||
# Define captcha site key
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaSiteKey=FIXME"
|
||||
# Define captcha secret key
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaSecretKey=FIXME"
|
||||
# Define captcha grade period seconds
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaGracePeriodSeconds=20"
|
||||
# Define captcha HTML file path
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.captchaHTMLFilePath=/captcha.html"
|
||||
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:v1.6.0
|
||||
container_name: "crowdsec"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
COLLECTIONS: crowdsecurity/traefik
|
||||
CUSTOM_HOSTNAME: crowdsec
|
||||
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
|
||||
volumes:
|
||||
# For captcha and ban mixed decision
|
||||
- './profiles.yaml:/etc/crowdsec/profiles.yaml:ro'
|
||||
# For captcha only remediation
|
||||
# - './profiles_captcha_only.yaml:/etc/crowdsec/profiles.yaml:ro'
|
||||
- './acquis.yaml:/etc/crowdsec/acquis.yaml:ro'
|
||||
- logs-captcha-enabled:/var/log/traefik:ro
|
||||
- crowdsec-db-captcha-enabled:/var/lib/crowdsec/data/
|
||||
- crowdsec-config-captcha-enabled:/etc/crowdsec/
|
||||
labels:
|
||||
- "traefik.enable=false"
|
||||
|
||||
volumes:
|
||||
logs-captcha-enabled:
|
||||
crowdsec-db-captcha-enabled:
|
||||
crowdsec-config-captcha-enabled:
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
@@ -0,0 +1,17 @@
|
||||
name: captcha_remediation
|
||||
filters:
|
||||
- Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.GetScenario() contains "http" && GetDecisionsSinceCount(Alert.GetValue(), "24h") <= 3
|
||||
## Same as above but only 3 captcha decision per 24 hours before ban
|
||||
decisions:
|
||||
- type: captcha
|
||||
duration: 4h
|
||||
on_success: break
|
||||
---
|
||||
name: default_ip_remediation
|
||||
filters:
|
||||
- Alert.Remediation == true && Alert.GetScope() == "Ip"
|
||||
decisions:
|
||||
- type: ban
|
||||
duration: 4h
|
||||
#duration_expr: "Sprintf('%dh', (GetDecisionsCount(Alert.GetValue()) + 1) * 4)"
|
||||
on_success: break
|
||||
@@ -0,0 +1,17 @@
|
||||
name: captcha_remediation
|
||||
filters:
|
||||
- Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.GetScenario() contains "http"
|
||||
## Any scenario with http in its name will trigger a captcha challenge
|
||||
decisions:
|
||||
- type: captcha
|
||||
duration: 4h
|
||||
on_success: break
|
||||
---
|
||||
name: default_ip_remediation
|
||||
filters:
|
||||
- Alert.Remediation == true && Alert.GetScope() == "Ip"
|
||||
decisions:
|
||||
- type: ban
|
||||
duration: 4h
|
||||
#duration_expr: "Sprintf('%dh', (GetDecisionsCount(Alert.GetValue()) + 1) * 4)"
|
||||
on_success: break
|
||||
@@ -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:
|
||||
@@ -1,5 +1,5 @@
|
||||
image:
|
||||
tag: v1.4.6
|
||||
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"
|
||||
@@ -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@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@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:
|
||||
+10
-10
@@ -2,7 +2,7 @@ version: "3.8"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.9.10"
|
||||
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.11"
|
||||
- "--experimental.plugins.bouncer.version=v1.1.13"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
ports:
|
||||
@@ -30,16 +30,16 @@ services:
|
||||
- "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.routers.router-foo.middlewares=crowdsec@docker"
|
||||
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
|
||||
|
||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.enabled=true"
|
||||
# - "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.loglevel=DEBUG"
|
||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdsecmode=alone"
|
||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.CrowdsecCapiMachineId=logincacacalfkrjebfreifgzfblezgyfoerxsqxsqxsqxsr"
|
||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.CrowdsecCapiPassword=Password2"
|
||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/sshd,crowdsecurity/asterisk_bf,crowdsecurity/asterisk_user_enum,crowdsecurity/base-http-scenarios"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
|
||||
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=alone"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiMachineId=logincacacalfkrjebfreifgzfblezgyfoerxsqxsqxsqxsr"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiPassword=Password2"
|
||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/sshd,crowdsecurity/asterisk_bf,crowdsecurity/asterisk_user_enum,crowdsecurity/base-http-scenarios"
|
||||
|
||||
volumes:
|
||||
logs-local:
|
||||
@@ -1,4 +1,4 @@
|
||||
# Exemple
|
||||
# Example
|
||||
## Using https communication and tls authentication with Crowdsec
|
||||
|
||||
##### Summary
|
||||
+28
-26
@@ -2,7 +2,7 @@ version: "3.8"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.9.10"
|
||||
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.11"
|
||||
- "--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@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"
|
||||
@@ -54,17 +56,17 @@ services:
|
||||
- "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.routers.router-bar.middlewares=crowdsec@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.6
|
||||
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
|
||||
+13
-16
@@ -2,7 +2,7 @@ version: "3.8"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.9.10"
|
||||
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.11"
|
||||
- "--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.6
|
||||
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
|
||||
@@ -1,19 +0,0 @@
|
||||
image:
|
||||
tag: v2.9.10
|
||||
|
||||
logs:
|
||||
general:
|
||||
level: DEBUG
|
||||
access:
|
||||
enabled: true
|
||||
fields:
|
||||
headers:
|
||||
defaultmode: keep
|
||||
|
||||
experimental:
|
||||
plugins:
|
||||
enabled: true
|
||||
|
||||
additionalArguments:
|
||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
- "--experimental.plugins.bouncer.version=v1.1.11"
|
||||
@@ -1,116 +0,0 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:v2.9.10"
|
||||
container_name: "traefik"
|
||||
restart: unless-stopped
|
||||
command:
|
||||
# - "--log.level=DEBUG"
|
||||
- "--accesslog"
|
||||
- "--accesslog.filepath=/var/log/traefik/access.log"
|
||||
- "--api.insecure=true"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.web.address=:80"
|
||||
|
||||
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
- "--experimental.plugins.bouncer.version=v1.1.11"
|
||||
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- logs-redis:/var/log/traefik
|
||||
# - ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
|
||||
ports:
|
||||
- 80:80
|
||||
- 8080:8080
|
||||
depends_on:
|
||||
- crowdsec
|
||||
- redis-insecure
|
||||
- redis-secure
|
||||
|
||||
whoami-redis-insecure:
|
||||
image: traefik/whoami
|
||||
container_name: "simple-service-foo"
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
# Definition of the router
|
||||
- "traefik.http.routers.router-foo.rule=Path(`/redis-insecure`)"
|
||||
- "traefik.http.routers.router-foo.entrypoints=web"
|
||||
- "traefik.http.routers.router-foo.middlewares=crowdsec-foo@docker"
|
||||
# Definition of the service
|
||||
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
|
||||
# Definition of the middleware
|
||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.enabled=true"
|
||||
# crowdseclapikey must be uniq to the middleware attached to the service
|
||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
|
||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.rediscacheenabled=true"
|
||||
# Contact redis-unsecure without a password
|
||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.rediscachehost=redis-insecure:6379"
|
||||
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.loglevel=DEBUG"
|
||||
|
||||
whoami-redis-secure:
|
||||
image: traefik/whoami
|
||||
container_name: "simple-service-bar"
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
# Definition of the router
|
||||
- "traefik.http.routers.router-bar.rule=Path(`/redis-secure`)"
|
||||
- "traefik.http.routers.router-bar.entrypoints=web"
|
||||
- "traefik.http.routers.router-bar.middlewares=crowdsec-bar@docker"
|
||||
# Definition of the service
|
||||
- "traefik.http.services.service-bar.loadbalancer.server.port=80"
|
||||
# Definition of the middleware
|
||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.enabled=true"
|
||||
# crowdseclapikey must be uniq to the middleware attached to the service
|
||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapikey=44c36dac5c4140af9f06f397508e82c7"
|
||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.rediscacheenabled=true"
|
||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.rediscachepassword=FIXME"
|
||||
# Contact redis-secure with password
|
||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.rediscachehost=redis-secure:6379"
|
||||
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.loglevel=DEBUG"
|
||||
|
||||
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:v1.4.6
|
||||
container_name: "crowdsec"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
COLLECTIONS: crowdsecurity/traefik
|
||||
CUSTOM_HOSTNAME: crowdsec
|
||||
BOUNCER_KEY_TRAEFIK_DEV_1: 40796d93c2958f9e58345514e67740e5
|
||||
BOUNCER_KEY_TRAEFIK_DEV_2: 44c36dac5c4140af9f06f397508e82c7
|
||||
volumes:
|
||||
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
||||
- logs-redis:/var/log/traefik:ro
|
||||
- crowdsec-db-redis:/var/lib/crowdsec/data/
|
||||
- crowdsec-config-redis:/etc/crowdsec/
|
||||
labels:
|
||||
- "traefik.enable=false"
|
||||
|
||||
redis-secure:
|
||||
image: "redis:7.0.9-alpine"
|
||||
container_name: "redis-secure"
|
||||
hostname: redis-secure
|
||||
restart: unless-stopped
|
||||
command: "redis-server --save 60 1 --loglevel debug --requirepass FIXME"
|
||||
volumes:
|
||||
- redis-secure-data:/data
|
||||
|
||||
redis-insecure:
|
||||
image: "redis:7.0.9-alpine"
|
||||
container_name: "redis-insecure"
|
||||
hostname: redis-unsecure
|
||||
restart: unless-stopped
|
||||
command: "redis-server --save 60 1 --loglevel debug"
|
||||
volumes:
|
||||
- redis-unsecure-data:/data
|
||||
|
||||
volumes:
|
||||
logs-redis:
|
||||
crowdsec-db-redis:
|
||||
crowdsec-config-redis:
|
||||
redis-unsecure-data:
|
||||
redis-secure-data:
|
||||
@@ -4,5 +4,5 @@ go 1.19
|
||||
|
||||
require (
|
||||
github.com/leprosus/golang-ttl-map v1.1.7
|
||||
github.com/maxlerebourg/simpleredis v1.0.7
|
||||
github.com/maxlerebourg/simpleredis v1.0.9
|
||||
)
|
||||
|
||||
@@ -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.7 h1:d53p3GOIgQtxxWuqsWMGTJ0dZjP8UhiDX8L1q2QZ/vY=
|
||||
github.com/maxlerebourg/simpleredis v1.0.7/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=
|
||||
|
||||
Vendored
+57
-54
@@ -12,12 +12,17 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
cacheBannedValue = "t"
|
||||
cacheNoBannedValue = "f"
|
||||
)
|
||||
|
||||
// BannedValue Banned string.
|
||||
BannedValue = "t"
|
||||
// NoBannedValue No banned string.
|
||||
NoBannedValue = "f"
|
||||
// CaptchaValue Need captcha string.
|
||||
CaptchaValue = "c"
|
||||
// CaptchaDoneValue Captcha done string.
|
||||
CaptchaDoneValue = "d"
|
||||
// CacheMiss error string when cache is miss.
|
||||
const CacheMiss = "cache:miss"
|
||||
CacheMiss = "cache:miss"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
@@ -27,92 +32,90 @@ var (
|
||||
|
||||
type localCache struct{}
|
||||
|
||||
func (localCache) getDecision(clientIP string) (bool, error) {
|
||||
banned, isCached := cache.Get(clientIP)
|
||||
bannedString, isValid := banned.(string)
|
||||
if isCached && isValid && len(bannedString) > 0 {
|
||||
return bannedString == cacheBannedValue, nil
|
||||
func (localCache) get(key string) (string, error) {
|
||||
value, isCached := cache.Get(key)
|
||||
valueString, isValid := value.(string)
|
||||
if isCached && isValid && len(valueString) > 0 {
|
||||
return valueString, nil
|
||||
}
|
||||
return false, fmt.Errorf(CacheMiss)
|
||||
return "", fmt.Errorf(CacheMiss)
|
||||
}
|
||||
|
||||
func (localCache) setDecision(clientIP string, value string, duration int64) {
|
||||
cache.Set(clientIP, value, duration)
|
||||
func (localCache) set(key, value string, duration int64) {
|
||||
cache.Set(key, value, duration)
|
||||
}
|
||||
|
||||
func (localCache) deleteDecision(clientIP string) {
|
||||
cache.Del(clientIP)
|
||||
func (localCache) delete(key string) {
|
||||
cache.Del(key)
|
||||
}
|
||||
|
||||
type redisCache struct{}
|
||||
type redisCache struct {
|
||||
log *logger.Log
|
||||
}
|
||||
|
||||
func (redisCache) getDecision(clientIP string) (bool, error) {
|
||||
banned, err := redis.Get(clientIP)
|
||||
bannedString := string(banned)
|
||||
if err == nil && len(bannedString) > 0 {
|
||||
return bannedString == cacheBannedValue, nil
|
||||
func (redisCache) get(key string) (string, error) {
|
||||
value, err := redis.Get(key)
|
||||
valueString := string(value)
|
||||
if err == nil && len(valueString) > 0 {
|
||||
return valueString, nil
|
||||
}
|
||||
if err.Error() == simpleredis.RedisMiss {
|
||||
return false, fmt.Errorf(CacheMiss)
|
||||
return "", fmt.Errorf(CacheMiss)
|
||||
}
|
||||
return false, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
func (redisCache) setDecision(clientIP string, value string, duration int64) {
|
||||
if err := redis.Set(clientIP, []byte(value), duration); err != nil {
|
||||
logger.Error(fmt.Sprintf("cache:setDecisionRedisCache %s", err.Error()))
|
||||
func (rc redisCache) set(key, value string, duration int64) {
|
||||
if err := redis.Set(key, []byte(value), duration); err != nil {
|
||||
rc.log.Error(fmt.Sprintf("cache:setDecisionRedisCache %s", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
func (redisCache) deleteDecision(clientIP string) {
|
||||
if err := redis.Del(clientIP); err != nil {
|
||||
logger.Error(fmt.Sprintf("cache:deleteDecisionRedisCache %s", err.Error()))
|
||||
func (rc redisCache) delete(key string) {
|
||||
if err := redis.Del(key); err != nil {
|
||||
rc.log.Error(fmt.Sprintf("cache:deleteDecisionRedisCache %s", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
type cacheInterface interface {
|
||||
setDecision(clientIP string, value string, duration int64)
|
||||
getDecision(clientIP string) (bool, error)
|
||||
deleteDecision(clientIP string)
|
||||
set(key, value string, duration int64)
|
||||
get(key string) (string, error)
|
||||
delete(key string)
|
||||
}
|
||||
|
||||
// Client Cache client.
|
||||
type Client struct {
|
||||
cache cacheInterface
|
||||
log *logger.Log
|
||||
}
|
||||
|
||||
// New Initialize cache client.
|
||||
func (client *Client) New(isRedis bool, host, pass, database string) {
|
||||
func (c *Client) New(log *logger.Log, isRedis bool, host, pass, database string) {
|
||||
c.log = log
|
||||
if isRedis {
|
||||
redis.Init(host, pass, database)
|
||||
client.cache = &redisCache{}
|
||||
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)
|
||||
// Delete delete decision in cache.
|
||||
func (c *Client) Delete(key string) {
|
||||
c.log.Debug(fmt.Sprintf("cache:Delete key:%v", key))
|
||||
c.cache.delete(key)
|
||||
}
|
||||
|
||||
// GetDecision check in the cache if the IP has the banned / not banned value.
|
||||
// Get check in the cache if the IP has the banned / not banned value.
|
||||
// Otherwise return with an error to add the IP in cache if we are on.
|
||||
func (client *Client) GetDecision(clientIP string) (bool, error) {
|
||||
logger.Debug(fmt.Sprintf("cache:GetDecision ip:%v", clientIP))
|
||||
return client.cache.getDecision(clientIP)
|
||||
func (c *Client) Get(key string) (string, error) {
|
||||
c.log.Debug(fmt.Sprintf("cache:Get key:%v", key))
|
||||
return c.cache.get(key)
|
||||
}
|
||||
|
||||
// SetDecision update the cache with the IP as key and the value banned / not banned.
|
||||
func (client *Client) SetDecision(clientIP string, isBanned bool, duration int64) {
|
||||
logger.Debug(fmt.Sprintf("cache:SetDecision ip:%v isBanned:%v duration:%vs", clientIP, isBanned, duration))
|
||||
var value string
|
||||
if isBanned {
|
||||
value = cacheBannedValue
|
||||
} else {
|
||||
value = cacheNoBannedValue
|
||||
}
|
||||
client.cache.setDecision(clientIP, value, duration)
|
||||
// Set update the cache with the IP as key and the value banned / not banned.
|
||||
func (c *Client) Set(key string, value string, duration int64) {
|
||||
c.log.Debug(fmt.Sprintf("cache:Set key:%v value:%v duration:%vs", key, value, duration))
|
||||
c.cache.set(key, value, duration)
|
||||
}
|
||||
|
||||
Vendored
+55
-33
@@ -4,99 +4,121 @@ package cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
||||
)
|
||||
|
||||
func Test_GetDecision(t *testing.T) {
|
||||
func Test_Get(t *testing.T) {
|
||||
IPInCache := "10.0.0.10"
|
||||
IPNotInCache := "10.0.0.20"
|
||||
client := &Client{cache: &localCache{}}
|
||||
client.SetDecision(IPInCache, true, 10)
|
||||
client := &Client{cache: &localCache{}, log: logger.New("INFO")}
|
||||
client.Set(IPInCache, BannedValue, 10)
|
||||
type args struct {
|
||||
clientIP string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
want string
|
||||
wantErr bool
|
||||
valueErr string
|
||||
}{
|
||||
{name: "Fetch Known valid IP", args: args{clientIP: IPInCache}, want: true, wantErr: false, valueErr: ""},
|
||||
{name: "Fetch Unknown valid IP", args: args{clientIP: IPNotInCache}, want: false, wantErr: true, valueErr: "cache:miss"},
|
||||
{name: "Fetch invalid value", args: args{clientIP: "test"}, want: false, wantErr: true, valueErr: "cache:miss"},
|
||||
{name: "Fetch empty value", args: args{clientIP: ""}, want: false, wantErr: true, valueErr: "cache:miss"},
|
||||
{name: "Fetch Known valid IP", args: args{clientIP: IPInCache}, want: BannedValue, wantErr: false, valueErr: ""},
|
||||
{name: "Fetch Unknown valid IP", args: args{clientIP: IPNotInCache}, want: "", wantErr: true, valueErr: CacheMiss},
|
||||
{name: "Fetch invalid value", args: args{clientIP: "test"}, want: "", wantErr: true, valueErr: CacheMiss},
|
||||
{name: "Fetch empty value", args: args{clientIP: ""}, want: "", wantErr: true, valueErr: CacheMiss},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := client.GetDecision(tt.args.clientIP)
|
||||
got, err := client.Get(tt.args.clientIP)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetDecision() error = %v, wantErr %v", err, tt.wantErr)
|
||||
t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("GetDecision() = %v, want %v", got, tt.want)
|
||||
t.Errorf("Get() = %v, want %v", got, tt.want)
|
||||
return
|
||||
}
|
||||
if tt.valueErr != "" && tt.valueErr != err.Error() {
|
||||
t.Errorf("GetDecision() err = %v, want %v", err.Error(), tt.valueErr)
|
||||
t.Errorf("Get() err = %v, want %v", err.Error(), tt.valueErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SetDecision(t *testing.T) {
|
||||
client := &Client{cache: &localCache{}}
|
||||
func Test_Set(t *testing.T) {
|
||||
client := &Client{cache: &localCache{}, log: logger.New("INFO")}
|
||||
IPInCache := "10.0.0.11"
|
||||
type args struct {
|
||||
clientIP string
|
||||
value bool
|
||||
value string
|
||||
duration int64
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
want string
|
||||
wantErr bool
|
||||
valueErr string
|
||||
}{
|
||||
{name: "Set valid IP in local cache for 0 sec", args: args{clientIP: IPInCache, value: true, duration: 0}, want: false},
|
||||
{name: "Set valid IP in local cache for 10 sec", args: args{clientIP: IPInCache, value: true, duration: 10}, want: true},
|
||||
{name: "Set valid IP in local cache for 10 sec", args: args{clientIP: IPInCache, value: false, duration: 10}, want: false},
|
||||
{name: "Set valid IP in local cache for 0 sec", args: args{clientIP: IPInCache, value: BannedValue, duration: 0}, want: "", wantErr: true, valueErr: CacheMiss},
|
||||
{name: "Set valid IP in local cache for 10 sec", args: args{clientIP: IPInCache, value: BannedValue, duration: 10}, want: BannedValue, wantErr: false, valueErr: ""},
|
||||
{name: "Set valid IP in local cache for 10 sec", args: args{clientIP: IPInCache, value: NoBannedValue, duration: 10}, want: NoBannedValue, wantErr: false, valueErr: ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client.SetDecision(tt.args.clientIP, tt.args.value, tt.args.duration)
|
||||
got, _ := client.GetDecision(tt.args.clientIP)
|
||||
if got != tt.want {
|
||||
t.Errorf("SetDecision() = %v, want %v", got, tt.want)
|
||||
client.Set(tt.args.clientIP, tt.args.value, tt.args.duration)
|
||||
got, err := client.Get(tt.args.clientIP)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Set() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("Set() = %v, want %v", got, tt.want)
|
||||
return
|
||||
}
|
||||
if tt.valueErr != "" && tt.valueErr != err.Error() {
|
||||
t.Errorf("Set() err = %v, want %v", err.Error(), tt.valueErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_DeleteDecision(t *testing.T) {
|
||||
func Test_Delete(t *testing.T) {
|
||||
IPInCache := "10.0.0.12"
|
||||
IPNotInCache := "10.0.0.22"
|
||||
client := &Client{cache: &localCache{}}
|
||||
client.SetDecision(IPInCache, true, 10)
|
||||
client := &Client{cache: &localCache{}, log: logger.New("INFO")}
|
||||
client.Set(IPInCache, BannedValue, 10)
|
||||
type args struct {
|
||||
clientIP string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
want string
|
||||
wantErr bool
|
||||
valueErr string
|
||||
}{
|
||||
{name: "Delete Known valid IP", args: args{clientIP: IPInCache}, want: false},
|
||||
{name: "Delete Unknown valid IP", args: args{clientIP: IPNotInCache}, want: false},
|
||||
{name: "Delete Known valid IP", args: args{clientIP: IPInCache}, want: "", wantErr: true, valueErr: CacheMiss},
|
||||
{name: "Delete Unknown valid IP", args: args{clientIP: IPNotInCache}, want: "", wantErr: true, valueErr: CacheMiss},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client.DeleteDecision(tt.args.clientIP)
|
||||
got, _ := client.GetDecision(tt.args.clientIP)
|
||||
if got != tt.want {
|
||||
t.Errorf("DeleteDecision() = %v, want %v", got, tt.want)
|
||||
client.Delete(tt.args.clientIP)
|
||||
got, err := client.Get(tt.args.clientIP)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Delete() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("Delete() = %v, want %v", got, tt.want)
|
||||
return
|
||||
}
|
||||
if tt.valueErr != "" && tt.valueErr != err.Error() {
|
||||
t.Errorf("Delete() err = %v, want %v", err.Error(), tt.valueErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
// Package captcha implements utility for captcha management.
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
cache "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/cache"
|
||||
configuration "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/configuration"
|
||||
logger "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/pkg/logger"
|
||||
)
|
||||
|
||||
// Client Captcha client.
|
||||
type Client struct {
|
||||
Valid bool
|
||||
provider string
|
||||
siteKey string
|
||||
secretKey string
|
||||
gracePeriodSeconds int64
|
||||
htmlPage *template.Template
|
||||
cacheClient *cache.Client
|
||||
httpClient *http.Client
|
||||
log *logger.Log
|
||||
}
|
||||
|
||||
type infoProvider struct {
|
||||
js string
|
||||
key string
|
||||
validate string
|
||||
}
|
||||
|
||||
var (
|
||||
//nolint:gochecknoglobals
|
||||
captcha = map[string]infoProvider{
|
||||
configuration.HcaptchaProvider: {
|
||||
js: "https://hcaptcha.com/1/api.js",
|
||||
key: "h-captcha",
|
||||
validate: "https://api.hcaptcha.com/siteverify",
|
||||
},
|
||||
configuration.RecaptchaProvider: {
|
||||
js: "https://www.google.com/recaptcha/api.js",
|
||||
key: "g-recaptcha",
|
||||
validate: "https://www.google.com/recaptcha/api/siteverify",
|
||||
},
|
||||
configuration.TurnstileProvider: {
|
||||
js: "https://challenges.cloudflare.com/turnstile/v0/api.js",
|
||||
key: "cf-captcha",
|
||||
validate: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func compileTemplate(path string) (*template.Template, error) {
|
||||
var err error
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("no captcha template provided")
|
||||
}
|
||||
//nolint:gosec
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
html := string(b)
|
||||
compiledTemplate, err := template.New("captcha").Parse(html)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("impossible to compile captcha template: %w", err)
|
||||
}
|
||||
return compiledTemplate, nil
|
||||
}
|
||||
|
||||
// New Initialize captcha client.
|
||||
func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *http.Client, provider, siteKey, secretKey, htmlPagePath string, gracePeriodSeconds int64) error {
|
||||
c.Valid = provider != ""
|
||||
if !c.Valid {
|
||||
return nil
|
||||
}
|
||||
c.siteKey = siteKey
|
||||
c.secretKey = secretKey
|
||||
c.provider = provider
|
||||
html, err := compileTemplate(htmlPagePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.htmlPage = html
|
||||
c.gracePeriodSeconds = gracePeriodSeconds
|
||||
c.log = log
|
||||
c.httpClient = httpClient
|
||||
c.cacheClient = cacheClient
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeHTTP Handle captcha html page or validation.
|
||||
func (c *Client) ServeHTTP(rw http.ResponseWriter, r *http.Request, remoteIP string) {
|
||||
valid, err := c.Validate(r)
|
||||
if err != nil {
|
||||
c.log.Debug(fmt.Sprintf("captcha:ServeHTTP:validate %s", err.Error()))
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if valid {
|
||||
c.log.Debug("captcha:ServeHTTP captcha:valid")
|
||||
c.cacheClient.Set(fmt.Sprintf("%s_captcha", remoteIP), cache.CaptchaDoneValue, c.gracePeriodSeconds)
|
||||
http.Redirect(rw, r, r.URL.String(), http.StatusFound)
|
||||
return
|
||||
}
|
||||
err = c.htmlPage.Execute(rw, map[string]string{
|
||||
"SiteKey": c.siteKey,
|
||||
"FrontendJS": captcha[c.provider].js,
|
||||
"FrontendKey": captcha[c.provider].key,
|
||||
})
|
||||
if err != nil {
|
||||
c.log.Info("captcha:ServeHTTP Can't serve HTML")
|
||||
}
|
||||
}
|
||||
|
||||
// Check Verify if the captcha is already done.
|
||||
func (c *Client) Check(remoteIP string) bool {
|
||||
value, _ := c.cacheClient.Get(fmt.Sprintf("%s_captcha", remoteIP))
|
||||
passed := value == cache.CaptchaDoneValue
|
||||
c.log.Debug(fmt.Sprintf("captcha:Check ip:%s pass:%v", remoteIP, passed))
|
||||
return passed
|
||||
}
|
||||
|
||||
type responseProvider struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
// Validate Verify the captcha from provider API.
|
||||
func (c *Client) Validate(r *http.Request) (bool, error) {
|
||||
if r.Method != http.MethodPost {
|
||||
c.log.Debug(fmt.Sprintf("captcha:Validate invalid method: %s", r.Method))
|
||||
return false, nil
|
||||
}
|
||||
var response = r.FormValue(fmt.Sprintf("%s-response", captcha[c.provider].key))
|
||||
if response == "" {
|
||||
c.log.Debug("captcha:Validate no captcha response found in request")
|
||||
return false, nil
|
||||
}
|
||||
var body = url.Values{}
|
||||
body.Add("secret", c.secretKey)
|
||||
body.Add("response", response)
|
||||
res, err := c.httpClient.PostForm(captcha[c.provider].validate, body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer func() {
|
||||
if err = res.Body.Close(); err != nil {
|
||||
c.log.Error(fmt.Sprintf("captcha:Validate %s", err.Error()))
|
||||
}
|
||||
}()
|
||||
if !strings.Contains(res.Header.Get("content-type"), "application/json") {
|
||||
c.log.Debug("captcha:Validate responseType:noJson")
|
||||
return false, nil
|
||||
}
|
||||
var captchaResponse responseProvider
|
||||
err = json.NewDecoder(res.Body).Decode(&captchaResponse)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
c.log.Debug(fmt.Sprintf("captcha:Validate success:%v", captchaResponse.Success))
|
||||
return captchaResponse.Success, nil
|
||||
}
|
||||
@@ -23,8 +23,12 @@ const (
|
||||
StreamMode = "stream"
|
||||
LiveMode = "live"
|
||||
NoneMode = "none"
|
||||
AppsecMode = "appsec"
|
||||
HTTPS = "https"
|
||||
HTTP = "http"
|
||||
HcaptchaProvider = "hcaptcha"
|
||||
RecaptchaProvider = "recaptcha"
|
||||
TurnstileProvider = "turnstile"
|
||||
)
|
||||
|
||||
// Config the plugin configuration.
|
||||
@@ -32,6 +36,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,7 +57,8 @@ 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"`
|
||||
@@ -58,6 +66,13 @@ type Config struct {
|
||||
RedisCachePassword string `json:"redisCachePassword,omitempty"`
|
||||
RedisCachePasswordFile string `json:"redisCachePasswordFile,omitempty"`
|
||||
RedisCacheDatabase string `json:"redisCacheDatabase,omitempty"`
|
||||
CaptchaHTMLFilePath string `json:"captchaHtmlFilePath,omitempty"`
|
||||
CaptchaProvider string `json:"captchaProvider,omitempty"`
|
||||
CaptchaSiteKey string `json:"captchaSiteKey,omitempty"`
|
||||
CaptchaSiteKeyFile string `json:"captchaSiteKeyFile,omitempty"`
|
||||
CaptchaSecretKey string `json:"captchaSecretKey,omitempty"`
|
||||
CaptchaSecretKeyFile string `json:"captchaSecretKeyFile,omitempty"`
|
||||
CaptchaGracePeriodSeconds int64 `json:"captchaGracePeriodSeconds,omitempty"`
|
||||
}
|
||||
|
||||
func contains(source []string, target string) bool {
|
||||
@@ -75,12 +90,21 @@ 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,
|
||||
CaptchaProvider: "",
|
||||
CaptchaSiteKey: "",
|
||||
CaptchaSecretKey: "",
|
||||
CaptchaHTMLFilePath: "/captcha.html",
|
||||
CaptchaGracePeriodSeconds: 1800,
|
||||
ForwardedHeadersCustomName: "X-Forwarded-For",
|
||||
ForwardedHeadersTrustedIPs: []string{},
|
||||
ClientTrustedIPs: []string{},
|
||||
@@ -147,13 +171,21 @@ 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 config.CaptchaProvider != "" {
|
||||
if _, err := GetVariable(config, "CaptchaSiteKey"); 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 := GetVariable(config, "CaptchaSecretKey"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateURL("CrowdsecLapi", config.CrowdsecLapiScheme, config.CrowdsecLapiHost); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateURL("CrowdsecAppsec", config.CrowdsecLapiScheme, config.CrowdsecAppsecHost); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lapiKey, err := GetVariable(config, "CrowdsecLapiKey")
|
||||
@@ -188,12 +220,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())
|
||||
}
|
||||
@@ -218,11 +259,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
|
||||
}
|
||||
@@ -241,32 +280,39 @@ func validateParamsRequired(config *Config) error {
|
||||
requiredInt := map[string]int64{
|
||||
"UpdateIntervalSeconds": config.UpdateIntervalSeconds,
|
||||
"DefaultDecisionSeconds": config.DefaultDecisionSeconds,
|
||||
"HTTPTimeoutSeconds": config.HTTPTimeoutSeconds,
|
||||
"CaptchaGracePeriodSeconds": config.CaptchaGracePeriodSeconds,
|
||||
}
|
||||
for key, val := range requiredInt {
|
||||
if val < 1 {
|
||||
return fmt.Errorf("%v: cannot be less than 1", key)
|
||||
}
|
||||
}
|
||||
if !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'")
|
||||
}
|
||||
if !contains([]string{"", HcaptchaProvider, RecaptchaProvider, TurnstileProvider}, config.CaptchaProvider) {
|
||||
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'hcaptcha', 'recaptcha' or 'turnstile'")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -274,13 +320,14 @@ 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")
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
certBouncer, err := GetVariable(config, "CrowdsecLapiTLSCertificateBouncer")
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
+12
-15
@@ -18,6 +18,7 @@ const (
|
||||
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.
|
||||
@@ -70,12 +71,11 @@ func (sr *SimpleRedis) waitRedis(reader *textproto.Reader, channel chan redisCmd
|
||||
}
|
||||
}
|
||||
|
||||
func (sr *SimpleRedis) askRedis(cmd redisCmd, channel chan redisCmd) {
|
||||
func (sr *SimpleRedis) askRedis(cmd redisCmd, channel chan redisCmd) redisCmd {
|
||||
dialer := net.Dialer{Timeout: 2 * time.Second}
|
||||
conn, err := dialer.Dial("tcp", sr.host)
|
||||
if err != nil {
|
||||
channel <- redisCmd{Error: fmt.Errorf(RedisUnreachable)}
|
||||
return
|
||||
return redisCmd{Error: fmt.Errorf(RedisUnreachable)}
|
||||
}
|
||||
defer func() {
|
||||
if err := conn.Close(); err != nil {
|
||||
@@ -111,24 +111,22 @@ func (sr *SimpleRedis) askRedis(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()
|
||||
str := string(read)
|
||||
if strings.Contains(str, "-NOAUTH") {
|
||||
channel <- redisCmd{Error: fmt.Errorf(RedisNoAuth)}
|
||||
return
|
||||
return redisCmd{Error: fmt.Errorf(RedisNoAuth)}
|
||||
} else if str != "$1" {
|
||||
channel <- redisCmd{Error: fmt.Errorf(RedisMiss)}
|
||||
return
|
||||
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.
|
||||
@@ -145,8 +143,7 @@ func (sr *SimpleRedis) Get(name string) ([]byte, error) {
|
||||
Name: name,
|
||||
}
|
||||
channel := make(chan redisCmd)
|
||||
go sr.askRedis(cmd, channel)
|
||||
resp := <-channel
|
||||
resp := sr.askRedis(cmd, channel)
|
||||
if resp.Error != nil {
|
||||
return nil, resp.Error
|
||||
}
|
||||
@@ -161,7 +158,7 @@ func (sr *SimpleRedis) Set(name string, data []byte, duration int64) error {
|
||||
Data: data,
|
||||
Duration: duration,
|
||||
}
|
||||
go sr.askRedis(cmd, nil)
|
||||
sr.askRedis(cmd, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -171,6 +168,6 @@ func (sr *SimpleRedis) Del(name string) error {
|
||||
Command: "DEL",
|
||||
Name: name,
|
||||
}
|
||||
go sr.askRedis(cmd, nil)
|
||||
sr.askRedis(cmd, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -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.7
|
||||
# github.com/maxlerebourg/simpleredis v1.0.9
|
||||
## explicit; go 1.19
|
||||
github.com/maxlerebourg/simpleredis
|
||||
|
||||
Reference in New Issue
Block a user