mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-09-02 20:28:50 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de7e382fde | ||
|
|
abae855d9e | ||
|
|
4bb6e830dc | ||
|
|
78869ecf77 | ||
|
|
7f99266f99 | ||
|
|
5418d35feb | ||
|
|
a184ae6db9 | ||
|
|
e4c84409e7 |
+2
-2
@@ -15,7 +15,7 @@ linters-settings:
|
|||||||
locale: US
|
locale: US
|
||||||
funlen:
|
funlen:
|
||||||
lines: -1
|
lines: -1
|
||||||
statements: 50
|
statements: 60
|
||||||
godox:
|
godox:
|
||||||
keywords:
|
keywords:
|
||||||
- FIXME
|
- FIXME
|
||||||
@@ -72,7 +72,7 @@ linters:
|
|||||||
- gofumpt
|
- gofumpt
|
||||||
- gci
|
- gci
|
||||||
- mnd
|
- mnd
|
||||||
|
- exportloopref
|
||||||
issues:
|
issues:
|
||||||
exclude-use-default: false
|
exclude-use-default: false
|
||||||
max-same-issues: 0
|
max-same-issues: 0
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ The AppSec Component offers:
|
|||||||
- Low-effort virtual patching capabilities.
|
- Low-effort virtual patching capabilities.
|
||||||
- Support for your legacy ModSecurity rules.
|
- Support for your legacy ModSecurity rules.
|
||||||
- Combining classic WAF benefits with advanced CrowdSec features for otherwise difficult advanced behavior detection.
|
- 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/).
|
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`.
|
Remediation offered by [Crowdsec](https://docs.crowdsec.net/u/bouncers/intro) and supported by the plugin can be either `ban` or `captcha`.
|
||||||
@@ -34,15 +35,15 @@ For the `captcha` remediation, the user will be redirected to a page to complete
|
|||||||
|
|
||||||
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.
|
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:
|
The following captcha providers are supported now:
|
||||||
- [hcaptcha](https://www.hcaptcha.com/)
|
|
||||||
- [recaptcha](https://www.google.com/recaptcha/about/)
|
|
||||||
- [turnstile](https://www.cloudflare.com/products/turnstile/)
|
|
||||||
|
|
||||||
|
- [hcaptcha](https://www.hcaptcha.com/)
|
||||||
|
- [recaptcha](https://www.google.com/recaptcha/about/)
|
||||||
|
- [turnstile](https://www.cloudflare.com/products/turnstile/)
|
||||||
|
|
||||||
There are 5 operating modes (CrowdsecMode) for this plugin:
|
There are 5 operating modes (CrowdsecMode) for this plugin:
|
||||||
|
|
||||||
| Mode | Description |
|
| Mode | Description |
|
||||||
|------|------|
|
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| none | If the client IP is on ban list, it will get a http code 403 response. Otherwise, request will continue as usual. All request call the Crowdsec LAPI |
|
| 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. |
|
| 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. |
|
| 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. |
|
||||||
@@ -300,6 +301,7 @@ sequenceDiagram
|
|||||||
To get started, use the `docker-compose.yml` file.
|
To get started, use the `docker-compose.yml` file.
|
||||||
|
|
||||||
You can run it with:
|
You can run it with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make run
|
make run
|
||||||
```
|
```
|
||||||
@@ -307,20 +309,26 @@ make run
|
|||||||
### Note
|
### Note
|
||||||
|
|
||||||
**/!\ Cache is shared by all services**
|
**/!\ 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*
|
_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*.
|
Only one instance of the plugin is _possible_.
|
||||||
|
|
||||||
**/!\ Appsec maximum body limit is defaulted to 10MB**
|
**/!\ Appsec maximum body limit is defaulted to 10MB**
|
||||||
*By careful when you upgrade to >1.4.x*
|
_By careful when you upgrade to >1.4.x_
|
||||||
|
|
||||||
### Variables
|
### Variables
|
||||||
|
|
||||||
- Enabled
|
- Enabled
|
||||||
- bool
|
- bool
|
||||||
- default: false
|
- default: false
|
||||||
- Enable the plugin
|
- Enable the plugin
|
||||||
- LogLevel
|
- LogLevel
|
||||||
- string
|
- string
|
||||||
- default: `INFO`, expected values are: `INFO`, `DEBUG`, `ERROR`, log are written to `stdout` / `stderr`
|
- default: `INFO`, expected values are: `INFO`, `DEBUG`, `ERROR`
|
||||||
|
- Log are written to `stdout` / `stderr` of file if LogFilePath is provided
|
||||||
|
- LogFilePath
|
||||||
|
- string
|
||||||
|
- default: ""
|
||||||
|
- File Path to write logs, must be writable by Traefik, Log rotation may require a restart of traefik
|
||||||
- CrowdsecMode
|
- CrowdsecMode
|
||||||
- string
|
- string
|
||||||
- default: `live`, expected values are: `none`, `live`, `stream`, `alone`, `appsec`
|
- default: `live`, expected values are: `none`, `live`, `stream`, `alone`, `appsec`
|
||||||
@@ -411,7 +419,7 @@ Only one instance of the plugin is *possible*.
|
|||||||
- string
|
- string
|
||||||
- default: ""
|
- default: ""
|
||||||
- Database selection for the Redis service
|
- Database selection for the Redis service
|
||||||
- RedisUnreachableBlock
|
- RedisCacheUnreachableBlock
|
||||||
- bool
|
- bool
|
||||||
- default: true
|
- default: true
|
||||||
- Block request when Redis is unreachable (if Redis is unreachable, 1-second delay is added to each request)
|
- Block request when Redis is unreachable (if Redis is unreachable, 1-second delay is added to each request)
|
||||||
@@ -431,6 +439,10 @@ Only one instance of the plugin is *possible*.
|
|||||||
- int64
|
- int64
|
||||||
- default: 60
|
- default: 60
|
||||||
- Used only in `live` mode, maximum decision duration
|
- Used only in `live` mode, maximum decision duration
|
||||||
|
- RemediationStatusCode
|
||||||
|
- int
|
||||||
|
- default: 403
|
||||||
|
- HTTP status code for banned user (not captcha)
|
||||||
- CrowdsecCapiMachineId
|
- CrowdsecCapiMachineId
|
||||||
- string
|
- string
|
||||||
- Used only in `alone` mode, login for Crowdsec CAPI
|
- Used only in `alone` mode, login for Crowdsec CAPI
|
||||||
@@ -467,6 +479,7 @@ Only one instance of the plugin is *possible*.
|
|||||||
For each plugin, the Traefik static configuration must define the module name (as is usual for Go packages).
|
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:
|
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.
|
> 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.
|
> See the examples for advanced usage.
|
||||||
|
|
||||||
@@ -505,9 +518,11 @@ http:
|
|||||||
bouncer:
|
bouncer:
|
||||||
enabled: false
|
enabled: false
|
||||||
logLevel: DEBUG
|
logLevel: DEBUG
|
||||||
|
LogFilePath: ""
|
||||||
updateIntervalSeconds: 60
|
updateIntervalSeconds: 60
|
||||||
updateMaxFailure: 0
|
updateMaxFailure: 0
|
||||||
defaultDecisionSeconds: 60
|
defaultDecisionSeconds: 60
|
||||||
|
remediationStatusCode: 403
|
||||||
httpTimeoutSeconds: 10
|
httpTimeoutSeconds: 10
|
||||||
crowdsecMode: live
|
crowdsecMode: live
|
||||||
crowdsecAppsecEnabled: false
|
crowdsecAppsecEnabled: false
|
||||||
@@ -517,7 +532,6 @@ http:
|
|||||||
crowdsecAppsecUnreachableBlock: true
|
crowdsecAppsecUnreachableBlock: true
|
||||||
crowdsecAppsecBodyLimit: 10485760
|
crowdsecAppsecBodyLimit: 10485760
|
||||||
crowdsecLapiKey: privateKey-foo
|
crowdsecLapiKey: privateKey-foo
|
||||||
crowdsecLapiKeyFile: /etc/traefik/cs-privateKey-foo
|
|
||||||
crowdsecLapiScheme: http
|
crowdsecLapiScheme: http
|
||||||
crowdsecLapiHost: crowdsec:8080
|
crowdsecLapiHost: crowdsec:8080
|
||||||
crowdsecLapiPath: "/"
|
crowdsecLapiPath: "/"
|
||||||
@@ -546,7 +560,6 @@ http:
|
|||||||
...
|
...
|
||||||
Q0veeNzBQXg1f/JxfeA39IDIX1kiCf71tGlT
|
Q0veeNzBQXg1f/JxfeA39IDIX1kiCf71tGlT
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
crowdsecLapiTLSCertificateAuthorityFile: /etc/traefik/crowdsec-certs/ca.pem
|
|
||||||
crowdsecLapiTLSCertificateBouncer: |-
|
crowdsecLapiTLSCertificateBouncer: |-
|
||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
MIIEHjCCAwagAwIBAgIUOBTs1eqkaAUcPplztUr2xRapvNAwDQYJKoZIhvcNAQEL
|
MIIEHjCCAwagAwIBAgIUOBTs1eqkaAUcPplztUr2xRapvNAwDQYJKoZIhvcNAQEL
|
||||||
@@ -554,14 +567,12 @@ http:
|
|||||||
RaXAnYYUVRblS1jmePemh388hFxbmrpG2pITx8B5FMULqHoj11o2Rl0gSV6tHIHz
|
RaXAnYYUVRblS1jmePemh388hFxbmrpG2pITx8B5FMULqHoj11o2Rl0gSV6tHIHz
|
||||||
N2U=
|
N2U=
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
crowdsecLapiTLSCertificateBouncerFile: /etc/traefik/crowdsec-certs/bouncer.pem
|
|
||||||
crowdsecLapiTLSCertificateBouncerKey: |-
|
crowdsecLapiTLSCertificateBouncerKey: |-
|
||||||
-----BEGIN RSA PRIVATE KEY-----
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
MIIEogIBAAKCAQEAtYQnbJqifH+ZymePylDxGGLIuxzcAUU4/ajNj+qRAdI/Ux3d
|
MIIEogIBAAKCAQEAtYQnbJqifH+ZymePylDxGGLIuxzcAUU4/ajNj+qRAdI/Ux3d
|
||||||
...
|
...
|
||||||
ic5cDRo6/VD3CS3MYzyBcibaGaV34nr0G/pI+KEqkYChzk/PZRA=
|
ic5cDRo6/VD3CS3MYzyBcibaGaV34nr0G/pI+KEqkYChzk/PZRA=
|
||||||
-----END RSA PRIVATE KEY-----
|
-----END RSA PRIVATE KEY-----
|
||||||
crowdsecLapiTLSCertificateBouncerKeyFile: /etc/traefik/crowdsec-certs/bouncer-key.pem
|
|
||||||
captchaProvider: hcaptcha
|
captchaProvider: hcaptcha
|
||||||
captchaSiteKey: FIXME
|
captchaSiteKey: FIXME
|
||||||
captchaSecretKey: FIXME
|
captchaSecretKey: FIXME
|
||||||
@@ -572,10 +583,11 @@ http:
|
|||||||
|
|
||||||
#### Fill variable with value of file
|
#### Fill variable with value of file
|
||||||
|
|
||||||
`CrowdsecLapiTlsCertificateBouncerKey`, `CrowdsecLapiTlsCertificateBouncer`, `CrowdsecLapiTlsCertificateAuthority`, `CrowdsecCapiMachineId`, `CrowdsecCapiPassword`, `CrowdsecLapiKey`, `CaptchaSiteKey` and `CaptchaSecretKey` can be provided with the content as raw or through a file path that Traefik can read.
|
`CrowdsecLapiTlsCertificateBouncerKey`, `CrowdsecLapiTlsCertificateBouncer`, `CrowdsecLapiTlsCertificateAuthority`, `CrowdsecCapiMachineId`, `CrowdsecCapiPassword`, `CrowdsecLapiKey`, `CaptchaSiteKey`, `CaptchaSecretKey` and `RedisCachePassword` can be provided with the content as raw or through a file path that Traefik can read.
|
||||||
The file variable will be used as preference if both content and file are provided for the same variable.
|
The file variable will be used as preference if both content and file are provided for the same variable.
|
||||||
|
|
||||||
Format is:
|
Format is:
|
||||||
|
|
||||||
- Content: VariableName: XXX
|
- Content: VariableName: XXX
|
||||||
- File : VariableNameFile: /path
|
- File : VariableNameFile: /path
|
||||||
|
|
||||||
@@ -585,6 +597,7 @@ You can authenticate to the LAPI either with LAPIKEY or by using client certific
|
|||||||
Please see below for more details on each option.
|
Please see below for more details on each option.
|
||||||
|
|
||||||
#### Generate LAPI KEY
|
#### Generate LAPI KEY
|
||||||
|
|
||||||
You can generate a crowdsec API key for the LAPI.
|
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)
|
You can follow the documentation here: [docs.crowdsec.net/docs/user_guides/lapi_mgmt](https://docs.crowdsec.net/docs/user_guides/lapi_mgmt)
|
||||||
|
|
||||||
@@ -594,24 +607,26 @@ docker exec crowdsec cscli bouncers add crowdsecBouncer
|
|||||||
```
|
```
|
||||||
|
|
||||||
This LAPI key must be set where is noted FIXME-LAPI-KEY in the docker-compose.yml
|
This LAPI key must be set where is noted FIXME-LAPI-KEY in the docker-compose.yml
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
...
|
..
|
||||||
whoami:
|
whoami:
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY"
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapischeme=http"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapischeme=http"
|
||||||
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapihost=crowdsec:8080"
|
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapihost=crowdsec:8080"
|
||||||
...
|
..
|
||||||
crowdsec:
|
crowdsec:
|
||||||
environment:
|
environment:
|
||||||
BOUNCER_KEY_TRAEFIK: FIXME-LAPI-KEY
|
BOUNCER_KEY_TRAEFIK: FIXME-LAPI-KEY
|
||||||
...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
|
|
||||||
> Crowdsec does not require a specific format for la LAPI-key, you may use something like FIXME-LAPI-KEY but that is not recommanded for obvious reasons
|
> Crowdsec does not require a specific format for la LAPI-key, you may use something like FIXME-LAPI-KEY but that is not recommanded for obvious reasons
|
||||||
|
|
||||||
You can then run all the containers:
|
You can then run all the containers:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
@@ -688,12 +703,14 @@ The source code of the plugin should be organized as follows:
|
|||||||
```
|
```
|
||||||
|
|
||||||
For local development, a `docker-compose.local.yml` is provided which reproduces the directory layout needed by Traefik.
|
For local development, a `docker-compose.local.yml` is provided which reproduces the directory layout needed by Traefik.
|
||||||
This works once you have generated and filled your *LAPI-KEY* (crowdsecLapiKey), if not read above for informations.
|
This works once you have generated and filled your _LAPI-KEY_ (crowdsecLapiKey), if not read above for informations.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.local.yml up -d
|
docker compose -f docker-compose.local.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Equivalent to
|
Equivalent to
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make run_local
|
make run_local
|
||||||
```
|
```
|
||||||
|
|||||||
+11
-8
@@ -77,6 +77,7 @@ type Bouncer struct {
|
|||||||
updateInterval int64
|
updateInterval int64
|
||||||
updateMaxFailure int
|
updateMaxFailure int
|
||||||
defaultDecisionTimeout int64
|
defaultDecisionTimeout int64
|
||||||
|
remediationStatusCode int
|
||||||
remediationCustomHeader string
|
remediationCustomHeader string
|
||||||
forwardedCustomHeader string
|
forwardedCustomHeader string
|
||||||
crowdsecStreamRoute string
|
crowdsecStreamRoute string
|
||||||
@@ -93,7 +94,8 @@ type Bouncer struct {
|
|||||||
|
|
||||||
// New creates the crowdsec bouncer plugin.
|
// New creates the crowdsec bouncer plugin.
|
||||||
func New(_ context.Context, next http.Handler, config *configuration.Config, name string) (http.Handler, error) {
|
func New(_ context.Context, next http.Handler, config *configuration.Config, name string) (http.Handler, error) {
|
||||||
log := logger.New(config.LogLevel)
|
config.LogLevel = strings.ToUpper(config.LogLevel)
|
||||||
|
log := logger.New(config.LogLevel, config.LogFilePath)
|
||||||
err := configuration.ValidateParams(config)
|
err := configuration.ValidateParams(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("New:validateParams " + err.Error())
|
log.Error("New:validateParams " + err.Error())
|
||||||
@@ -169,6 +171,7 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
|
|||||||
remediationCustomHeader: config.RemediationHeadersCustomName,
|
remediationCustomHeader: config.RemediationHeadersCustomName,
|
||||||
forwardedCustomHeader: config.ForwardedHeadersCustomName,
|
forwardedCustomHeader: config.ForwardedHeadersCustomName,
|
||||||
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
||||||
|
remediationStatusCode: config.RemediationStatusCode,
|
||||||
redisUnreachableBlock: config.RedisCacheUnreachableBlock,
|
redisUnreachableBlock: config.RedisCacheUnreachableBlock,
|
||||||
banTemplateString: banTemplateString,
|
banTemplateString: banTemplateString,
|
||||||
crowdsecStreamRoute: crowdsecStreamRoute,
|
crowdsecStreamRoute: crowdsecStreamRoute,
|
||||||
@@ -350,15 +353,15 @@ type Login struct {
|
|||||||
|
|
||||||
// To append Headers we need to call rw.WriteHeader after set any header.
|
// To append Headers we need to call rw.WriteHeader after set any header.
|
||||||
func handleBanServeHTTP(bouncer *Bouncer, rw http.ResponseWriter) {
|
func handleBanServeHTTP(bouncer *Bouncer, rw http.ResponseWriter) {
|
||||||
if bouncer.banTemplateString == "" {
|
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
if bouncer.remediationCustomHeader != "" {
|
if bouncer.remediationCustomHeader != "" {
|
||||||
rw.Header().Set(bouncer.remediationCustomHeader, "ban")
|
rw.Header().Set(bouncer.remediationCustomHeader, "ban")
|
||||||
}
|
}
|
||||||
rw.WriteHeader(http.StatusForbidden)
|
if bouncer.banTemplateString == "" {
|
||||||
|
rw.WriteHeader(bouncer.remediationStatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
rw.WriteHeader(bouncer.remediationStatusCode)
|
||||||
_, err := fmt.Fprint(rw, bouncer.banTemplateString)
|
_, err := fmt.Fprint(rw, bouncer.banTemplateString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bouncer.log.Error("handleBanServeHTTP could not write template to ResponseWriter")
|
bouncer.log.Error("handleBanServeHTTP could not write template to ResponseWriter")
|
||||||
@@ -427,7 +430,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
|
|||||||
Scheme: bouncer.crowdsecScheme,
|
Scheme: bouncer.crowdsecScheme,
|
||||||
Host: bouncer.crowdsecHost,
|
Host: bouncer.crowdsecHost,
|
||||||
Path: bouncer.crowdsecPath + crowdsecLapiRoute,
|
Path: bouncer.crowdsecPath + crowdsecLapiRoute,
|
||||||
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteIP),
|
RawQuery: fmt.Sprintf("ip=%v", remoteIP),
|
||||||
}
|
}
|
||||||
body, err := crowdsecQuery(bouncer, routeURL.String(), false)
|
body, err := crowdsecQuery(bouncer, routeURL.String(), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+1
-1
@@ -293,7 +293,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<h1 class="text-2xl lg:text-3xl xl:text-4xl">CrowdSec Captcha</h1>
|
<h1 class="text-2xl lg:text-3xl xl:text-4xl">CrowdSec Captcha</h1>
|
||||||
</div>
|
</div>
|
||||||
<form action="" method="POST" class="flex flex-col space-y-1" id="captcha-form">
|
<form action="" method="POST" class="flex flex-col items-center space-y-1" id="captcha-form">
|
||||||
<div id="captcha" class="{{ .FrontendKey }}" data-sitekey="{{ .SiteKey }}" data-callback="captchaCallback">
|
<div id="captcha" class="{{ .FrontendKey }}" data-sitekey="{{ .SiteKey }}" data-callback="captchaCallback">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ logs:
|
|||||||
level: DEBUG
|
level: DEBUG
|
||||||
access:
|
access:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
format: json
|
||||||
fields:
|
fields:
|
||||||
headers:
|
headers:
|
||||||
defaultmode: keep
|
defaultmode: keep
|
||||||
|
|||||||
Vendored
+3
-3
@@ -11,7 +11,7 @@ import (
|
|||||||
func Test_Get(t *testing.T) {
|
func Test_Get(t *testing.T) {
|
||||||
IPInCache := "10.0.0.10"
|
IPInCache := "10.0.0.10"
|
||||||
IPNotInCache := "10.0.0.20"
|
IPNotInCache := "10.0.0.20"
|
||||||
client := &Client{cache: &localCache{}, log: logger.New("INFO")}
|
client := &Client{cache: &localCache{}, log: logger.New("INFO", "")}
|
||||||
client.Set(IPInCache, BannedValue, 10)
|
client.Set(IPInCache, BannedValue, 10)
|
||||||
type args struct {
|
type args struct {
|
||||||
clientIP string
|
clientIP string
|
||||||
@@ -47,7 +47,7 @@ func Test_Get(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_Set(t *testing.T) {
|
func Test_Set(t *testing.T) {
|
||||||
client := &Client{cache: &localCache{}, log: logger.New("INFO")}
|
client := &Client{cache: &localCache{}, log: logger.New("INFO", "")}
|
||||||
IPInCache := "10.0.0.11"
|
IPInCache := "10.0.0.11"
|
||||||
type args struct {
|
type args struct {
|
||||||
clientIP string
|
clientIP string
|
||||||
@@ -88,7 +88,7 @@ func Test_Set(t *testing.T) {
|
|||||||
func Test_Delete(t *testing.T) {
|
func Test_Delete(t *testing.T) {
|
||||||
IPInCache := "10.0.0.12"
|
IPInCache := "10.0.0.12"
|
||||||
IPNotInCache := "10.0.0.22"
|
IPNotInCache := "10.0.0.22"
|
||||||
client := &Client{cache: &localCache{}, log: logger.New("INFO")}
|
client := &Client{cache: &localCache{}, log: logger.New("INFO", "")}
|
||||||
client.Set(IPInCache, BannedValue, 10)
|
client.Set(IPInCache, BannedValue, 10)
|
||||||
type args struct {
|
type args struct {
|
||||||
clientIP string
|
clientIP string
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ const (
|
|||||||
AppsecMode = "appsec"
|
AppsecMode = "appsec"
|
||||||
HTTPS = "https"
|
HTTPS = "https"
|
||||||
HTTP = "http"
|
HTTP = "http"
|
||||||
|
LogDEBUG = "DEBUG"
|
||||||
|
LogINFO = "INFO"
|
||||||
|
LogERROR = "ERROR"
|
||||||
HcaptchaProvider = "hcaptcha"
|
HcaptchaProvider = "hcaptcha"
|
||||||
RecaptchaProvider = "recaptcha"
|
RecaptchaProvider = "recaptcha"
|
||||||
TurnstileProvider = "turnstile"
|
TurnstileProvider = "turnstile"
|
||||||
@@ -37,6 +40,7 @@ const (
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
LogLevel string `json:"logLevel,omitempty"`
|
LogLevel string `json:"logLevel,omitempty"`
|
||||||
|
LogFilePath string `json:"logFilePath,omitempty"`
|
||||||
CrowdsecMode string `json:"crowdsecMode,omitempty"`
|
CrowdsecMode string `json:"crowdsecMode,omitempty"`
|
||||||
CrowdsecAppsecEnabled bool `json:"crowdsecAppsecEnabled,omitempty"`
|
CrowdsecAppsecEnabled bool `json:"crowdsecAppsecEnabled,omitempty"`
|
||||||
CrowdsecAppsecHost string `json:"crowdsecAppsecHost,omitempty"`
|
CrowdsecAppsecHost string `json:"crowdsecAppsecHost,omitempty"`
|
||||||
@@ -64,6 +68,7 @@ type Config struct {
|
|||||||
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
||||||
UpdateMaxFailure int `json:"updateMaxFailure,omitempty"`
|
UpdateMaxFailure int `json:"updateMaxFailure,omitempty"`
|
||||||
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
||||||
|
RemediationStatusCode int `json:"remediationStatusCode,omitempty"`
|
||||||
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
|
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
|
||||||
RemediationHeadersCustomName string `json:"remediationHeadersCustomName,omitempty"`
|
RemediationHeadersCustomName string `json:"remediationHeadersCustomName,omitempty"`
|
||||||
ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"`
|
ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"`
|
||||||
@@ -98,7 +103,8 @@ func contains(source []string, target string) bool {
|
|||||||
func New() *Config {
|
func New() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
LogLevel: "INFO",
|
LogLevel: LogINFO,
|
||||||
|
LogFilePath: "",
|
||||||
CrowdsecMode: LiveMode,
|
CrowdsecMode: LiveMode,
|
||||||
CrowdsecAppsecEnabled: false,
|
CrowdsecAppsecEnabled: false,
|
||||||
CrowdsecAppsecHost: "crowdsec:7422",
|
CrowdsecAppsecHost: "crowdsec:7422",
|
||||||
@@ -114,6 +120,7 @@ func New() *Config {
|
|||||||
UpdateIntervalSeconds: 60,
|
UpdateIntervalSeconds: 60,
|
||||||
UpdateMaxFailure: 0,
|
UpdateMaxFailure: 0,
|
||||||
DefaultDecisionSeconds: 60,
|
DefaultDecisionSeconds: 60,
|
||||||
|
RemediationStatusCode: http.StatusForbidden,
|
||||||
HTTPTimeoutSeconds: 10,
|
HTTPTimeoutSeconds: 10,
|
||||||
CaptchaProvider: "",
|
CaptchaProvider: "",
|
||||||
CaptchaSiteKey: "",
|
CaptchaSiteKey: "",
|
||||||
@@ -262,6 +269,17 @@ func ValidateParams(config *Config) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check logging configuration
|
||||||
|
// to upper allow of anycase of log level
|
||||||
|
if !contains([]string{LogERROR, LogDEBUG, LogINFO}, strings.ToUpper(config.LogLevel)) {
|
||||||
|
return fmt.Errorf("LogLevel should be one of (%s,%s,%s)", LogDEBUG, LogINFO, LogERROR)
|
||||||
|
}
|
||||||
|
if config.LogFilePath != "" {
|
||||||
|
_, err = os.OpenFile(filepath.Clean(config.LogFilePath), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("LogFilePath is not writable %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +322,7 @@ func validateParamsTLS(config *Config) error {
|
|||||||
|
|
||||||
func validateParamsIPs(listIP []string, key string) error {
|
func validateParamsIPs(listIP []string, key string) error {
|
||||||
if len(listIP) > 0 {
|
if len(listIP) > 0 {
|
||||||
if _, err := ip.NewChecker(logger.New("INFO"), listIP); err != nil {
|
if _, err := ip.NewChecker(logger.New(LogINFO, ""), listIP); err != nil {
|
||||||
return fmt.Errorf("%s must be a list of IP/CIDR :%w", key, err)
|
return fmt.Errorf("%s must be a list of IP/CIDR :%w", key, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,6 +357,9 @@ func validateParamsRequired(config *Config) error {
|
|||||||
if config.CrowdsecAppsecBodyLimit < 0 {
|
if config.CrowdsecAppsecBodyLimit < 0 {
|
||||||
return errors.New("CrowdsecAppsecBodyLimit: cannot be less than 0")
|
return errors.New("CrowdsecAppsecBodyLimit: cannot be less than 0")
|
||||||
}
|
}
|
||||||
|
if config.RemediationStatusCode < 100 || config.RemediationStatusCode >= 600 {
|
||||||
|
return errors.New("RemediationStatusCode: cannot be less than 100 and more than 600")
|
||||||
|
}
|
||||||
|
|
||||||
if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode, AppsecMode}, config.CrowdsecMode) {
|
if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode, AppsecMode}, config.CrowdsecMode) {
|
||||||
return errors.New("CrowdsecMode: must be one of 'none', 'live', 'stream', 'alone' or 'appsec'")
|
return errors.New("CrowdsecMode: must be one of 'none', 'live', 'stream', 'alone' or 'appsec'")
|
||||||
|
|||||||
@@ -85,8 +85,14 @@ func Test_ValidateParams(t *testing.T) {
|
|||||||
cfg6 := getMinimalConfig()
|
cfg6 := getMinimalConfig()
|
||||||
cfg6.CrowdsecLapiScheme = HTTPS
|
cfg6.CrowdsecLapiScheme = HTTPS
|
||||||
cfg6.CrowdsecLapiTLSInsecureVerify = true
|
cfg6.CrowdsecLapiTLSInsecureVerify = true
|
||||||
|
cfg7 := getMinimalConfig()
|
||||||
|
cfg7.CrowdsecLapiScheme = HTTPS
|
||||||
cfg8 := getMinimalConfig()
|
cfg8 := getMinimalConfig()
|
||||||
cfg8.CrowdsecLapiScheme = HTTPS
|
cfg8.LogLevel = LogINFO
|
||||||
|
cfg9 := getMinimalConfig()
|
||||||
|
cfg9.LogLevel = "info"
|
||||||
|
cfg10 := getMinimalConfig()
|
||||||
|
cfg10.LogLevel = "Warning"
|
||||||
type args struct {
|
type args struct {
|
||||||
config *Config
|
config *Config
|
||||||
}
|
}
|
||||||
@@ -104,7 +110,10 @@ func Test_ValidateParams(t *testing.T) {
|
|||||||
{name: "Not validate a bad clients ips", args: args{config: cfg5}, wantErr: true},
|
{name: "Not validate a bad clients ips", args: args{config: cfg5}, wantErr: true},
|
||||||
// HTTPS enabled
|
// HTTPS enabled
|
||||||
{name: "Validate https config with insecure verify", args: args{config: cfg6}, wantErr: false},
|
{name: "Validate https config with insecure verify", args: args{config: cfg6}, wantErr: false},
|
||||||
{name: "Not validate https without cert authority", args: args{config: cfg8}, wantErr: true},
|
{name: "Not validate https without cert authority", args: args{config: cfg7}, wantErr: true},
|
||||||
|
{name: "Valid log level uppercase INFO", args: args{config: cfg8}, wantErr: false},
|
||||||
|
{name: "Valid log level lowercase info", args: args{config: cfg9}, wantErr: false},
|
||||||
|
{name: "Invalid log level Warning", args: args{config: cfg10}, wantErr: true},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@@ -233,7 +242,7 @@ func Test_GetTLSConfigCrowdsec(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := GetTLSConfigCrowdsec(tt.args.config, logger.New("INFO"))
|
got, err := GetTLSConfigCrowdsec(tt.args.config, logger.New("INFO", ""))
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("getTLSConfigCrowdsec() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("getTLSConfigCrowdsec() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
|
|||||||
+18
-1
@@ -3,9 +3,11 @@
|
|||||||
package logger
|
package logger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Log Logger struct.
|
// Log Logger struct.
|
||||||
@@ -16,15 +18,30 @@ type Log struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New Set Default log level to info in case log level to defined.
|
// New Set Default log level to info in case log level to defined.
|
||||||
func New(logLevel string) *Log {
|
func New(logLevel string, logFilePath string) *Log {
|
||||||
logError := log.New(io.Discard, "ERROR: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime)
|
logError := log.New(io.Discard, "ERROR: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime)
|
||||||
logInfo := log.New(io.Discard, "INFO: 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)
|
logDebug := log.New(io.Discard, "DEBUG: CrowdsecBouncerTraefikPlugin: ", log.Ldate|log.Ltime)
|
||||||
|
|
||||||
logError.SetOutput(os.Stderr)
|
logError.SetOutput(os.Stderr)
|
||||||
logInfo.SetOutput(os.Stdout)
|
logInfo.SetOutput(os.Stdout)
|
||||||
|
// we initialize logger to STDOUT/STDERR first so if the file logger cannot be initialized we can inform the user
|
||||||
if logLevel == "DEBUG" {
|
if logLevel == "DEBUG" {
|
||||||
logDebug.SetOutput(os.Stdout)
|
logDebug.SetOutput(os.Stdout)
|
||||||
}
|
}
|
||||||
|
if logFilePath != "" {
|
||||||
|
logFile, err := os.OpenFile(filepath.Clean(logFilePath), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||||
|
if err != nil {
|
||||||
|
_ = fmt.Errorf("LogFilePath is not writable %w", err)
|
||||||
|
} else {
|
||||||
|
logInfo.SetOutput(logFile)
|
||||||
|
logError.SetOutput(logFile)
|
||||||
|
if logLevel == "DEBUG" {
|
||||||
|
logDebug.SetOutput(logFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &Log{
|
return &Log{
|
||||||
logError: logError,
|
logError: logError,
|
||||||
logInfo: logInfo,
|
logInfo: logInfo,
|
||||||
|
|||||||
Reference in New Issue
Block a user