Compare commits

...
7 Commits
Author SHA1 Message Date
maxlerebourg 6c183d9231 add a new mode to enable only appsec checking (#128)
*  add a new mode to enable only appsec checking

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

* 🐛 lint

* 🐛 fix lint

* 🐛 fix lint

* 🐛 fix lint

* fix: comments

* 🐛 lint and doc

* 🐛 fix comment and lint

* 📝 Start documentation for appsec with exemple

* 📝 Fix readme typos and update example

* 🚨 Fix Lint

---------

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

* Exemples -> Examples

* Uppercase

* Uppercase v2

* Uppercase v3

* Uppercases

* Another one

* Add whoami because .gitignore is ignoring conf folder
2023-09-24 13:31:29 +02:00
mathieuHa f2aea695fc 🐛 Update regex to support = in lapikey update some exemples with 1.5.3 crowdsec (#115) 2023-09-19 20:57:29 +02:00
Joseph Kavanagh 2827fef273 🐛 env values need to be string (#112)
cannot unmarshal bool into Go struct field EnvVar.spec.template.spec.containers.env.value of type string
2023-09-05 13:26:50 +02:00
66 changed files with 362 additions and 97 deletions
+32 -25
View File
@@ -20,69 +20,76 @@ clean:
rm -rf ./vendor rm -rf ./vendor
run_dev: 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: 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: 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: 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: 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: run_binaryvm:
cd exemples/binary-vm/ && sudo vagrant up cd examples/binary-vm/ && sudo vagrant up
run_tlsauth: 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: run:
docker-compose -f docker-compose.yml up -d --remove-orphans docker compose -f docker-compose.yml up -d --remove-orphans
restart_dev: restart_dev:
docker-compose -f docker-compose.dev.yml restart docker compose -f docker-compose.dev.yml restart
restart_local: restart_local:
docker-compose -f docker-compose.local.yml restart docker compose -f docker-compose.local.yml restart
restart: restart:
docker-compose -f docker-compose.yml restart docker compose -f docker-compose.yml restart
restart_behindproxy: 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: 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: 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: 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: show_logs:
docker-compose -f docker-compose.yml restart docker compose -f docker-compose.yml restart
show_local_logs: show_local_logs:
docker-compose -f docker-compose.local.yml logs -f docker compose -f docker-compose.local.yml logs -f
show_dev_logs: show_dev_logs:
docker-compose -f docker-compose.dev.yml logs -f docker compose -f docker-compose.dev.yml logs -f
clean_all_docker: clean_all_docker:
docker-compose -f exemples/behind-proxy/docker-compose.cloudflare.yml down --remove-orphans docker compose -f examples/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 examples/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 examples/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 examples/tls-auth/docker-compose.tls-auth.yml down --remove-orphans
docker-compose -f docker-compose.local.yml down --remove-orphans docker compose -f examples/appsec-enabled/docker-compose.appsec-enabled.yml down --remove-orphans
docker-compose -f docker-compose.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: clean_vagrant:
cd exemples/binary-vm/ && sudo vagrant destroy -f cd examples/binary-vm/ && sudo vagrant destroy -f
show_metrics: show_metrics:
+46 -14
View File
@@ -6,6 +6,8 @@
# Crowdsec Bouncer Traefik plugin # 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. 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. > [CrowdSec](https://www.crowdsec.net/) is an open-source and collaborative IPS (Intrusion Prevention System) and a security suite.
@@ -17,6 +19,16 @@ The Crowdsec utility will provide the community blocklist which contains highly
When used with Crowdsec it will leverage the local API which will analyze Traefik logs and take decisions on the requests made by users/bots. Malicious actors will be banned based on patterns used against your website. When used with Crowdsec it will leverage the local API which will analyze Traefik logs and take decisions on the requests made by users/bots. Malicious actors will be banned based on patterns used against your website.
Appsec feature is supported from plugin version 1.2.0 and Crowdsec 1.6.0.
The AppSec Component offers:
- Low-effort virtual patching capabilities.
- Support for your legacy ModSecurity rules.
- Combining classic WAF benefits with advanced CrowdSec features for otherwise difficult advanced behavior detection.
More information on appsec in the [Crowdsec Documentation](https://doc.crowdsec.net/docs/next/appsec/intro/).
There are 4 operating modes (CrowdsecMode) for this plugin: There are 4 operating modes (CrowdsecMode) for this plugin:
| Mode | Description | | Mode | Description |
@@ -24,7 +36,8 @@ 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 | | 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. |
| 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 `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.
@@ -50,13 +63,25 @@ Only one instance of the plugin is *possible*.
- 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` - default: `INFO`, expected values are: `INFO`, `DEBUG`
- CrowdsecMode - CrowdsecMode
- string - 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 - CrowdsecLapiScheme
- string - string
- default: `http`, expected values are: `http`, `https` - default: `http`, expected values are: `http`, `https`
@@ -179,6 +204,9 @@ http:
defaultDecisionSeconds: 60 defaultDecisionSeconds: 60
httpTimeoutSeconds: 10 httpTimeoutSeconds: 10
crowdsecMode: live crowdsecMode: live
crowdsecAppsecEnabled: false
crowdsecAppsecHost: crowdsec:7422
crowdsecAppsecFailureBlock: true
crowdsecLapiKey: privateKey-foo crowdsecLapiKey: privateKey-foo
crowdsecLapiKeyFile: /etc/traefik/cs-privateKey-foo crowdsecLapiKeyFile: /etc/traefik/cs-privateKey-foo
crowdsecLapiHost: crowdsec:8080 crowdsecLapiHost: crowdsec:8080
@@ -272,10 +300,10 @@ docker-compose up -d
#### Use certificates to authenticate with CrowdSec #### 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. 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 #### Use HTTPS to communicate with the LAPI
@@ -283,7 +311,7 @@ To communicate with the LAPI in HTTPS you need to either accept any certificates
Set the `crowdsecLapiScheme` to https. Set the `crowdsecLapiScheme` to https.
Crowdsec must be listening in HTTPS for this to work. 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) #### Manually add an IP to the blocklist (for testing purposes)
@@ -293,21 +321,25 @@ docker exec crowdsec cscli decisions add --ip 10.0.0.10 -d 10m # this will be ef
docker exec crowdsec cscli decisions remove --ip 10.0.0.10 docker exec crowdsec cscli decisions remove --ip 10.0.0.10
``` ```
### Exemples ### Examples
#### 1. Behind another proxy service (ex: clouflare) [exemples/behind-proxy/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/behind-proxy/README.md) #### 1. Behind another proxy service (ex: clouflare) [examples/behind-proxy/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/behind-proxy/README.md)
#### 2. With Redis as an external shared cache [exemples/redis-cache/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/redis-cache/README.md) #### 2. With Redis as an external shared cache [examples/redis-cache/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/redis-cache/README.md)
#### 3. Using Trusted IP (ex: LAN OR VPN) that won't get filtered by crowdsec [exemples/trusted-ips/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/trusted-ips/README.md) #### 3. Using Trusted IP (ex: LAN OR VPN) that won't get filtered by crowdsec [examples/trusted-ips/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/trusted-ips/README.md)
#### 4. Using Crowdsec and Traefik installed as binary in a single VM [exemples/binary-vm/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/binary-vm/README.md) #### 4. Using Crowdsec and Traefik installed as binary in a single VM [examples/binary-vm/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/binary-vm/README.md)
#### 5. Using https communication and tls authentication with Crowdsec [exemples/tls-auth/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/tls-auth/README.md) #### 5. Using https communication and tls authentication with Crowdsec [examples/tls-auth/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/tls-auth/README.md)
#### 6. Using Crowdsec and Traefik in Kubernetes [exemples/kubernetes/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/kubernetes/README.md) #### 6. Using Crowdsec and Traefik in Kubernetes [examples/kubernetes/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/kubernetes/README.md)
#### 7. Using Traefik in standalone mode without Crowdsec [examples/standalone-mode/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/standalone-mode/README.md)
#### 8. Using Traefik with AppSec feature enabled [examples/appsec-enabled/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/examples/appsec-enabled/README.md)
#### 7. Using Traefik in standalone mode without Crowdsec [exemples/standalone-mode/README.md](https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/blob/main/exemples/standalone-mode/README.md)
### Local Mode ### Local Mode
+9
View File
@@ -1,4 +1,13 @@
---
filenames: filenames:
- /var/log/traefik/access.log - /var/log/traefik/access.log
labels: labels:
type: traefik type: traefik
---
listen_addr: 0.0.0.0:7422
appsec_config: crowdsecurity/virtual-patching
name: myAppSecComponent
source: appsec
labels:
type: appsec
+105 -19
View File
@@ -22,13 +22,19 @@ import (
) )
const ( const (
crowdsecLapiHeader = "X-Api-Key" crowdsecAppsecIPHeader = "X-Crowdsec-Appsec-Ip"
crowdsecCapiHeader = "Authorization" crowdsecAppsecURIHeader = "X-Crowdsec-Appsec-Uri"
crowdsecLapiRoute = "v1/decisions" crowdsecAppsecHostHeader = "X-Crowdsec-Appsec-Host"
crowdsecLapiStreamRoute = "v1/decisions/stream" crowdsecAppsecVerbHeader = "X-Crowdsec-Appsec-Verb"
crowdsecCapiLogin = "v2/watchers/login" crowdsecAppsecHeader = "X-Crowdsec-Appsec-Api-Key"
crowdsecCapiStreamRoute = "v2/decisions/stream" crowdsecLapiHeader = "X-Api-Key"
cacheTimeoutKey = "updated" crowdsecLapiRoute = "v1/decisions"
crowdsecLapiStreamRoute = "v1/decisions/stream"
crowdsecCapiHost = "api.crowdsec.net"
crowdsecCapiHeader = "Authorization"
crowdsecCapiLoginRoute = "v2/watchers/login"
crowdsecCapiStreamRoute = "v2/decisions/stream"
cacheTimeoutKey = "updated"
) )
//nolint:gochecknoglobals //nolint:gochecknoglobals
@@ -50,6 +56,9 @@ type Bouncer struct {
template *template.Template template *template.Template
enabled bool enabled bool
appsecEnabled bool
appsecHost string
appsecFailureBlock bool
crowdsecScheme string crowdsecScheme string
crowdsecHost string crowdsecHost string
crowdsecKey string crowdsecKey string
@@ -86,9 +95,9 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
if config.CrowdsecMode == configuration.AloneMode { if config.CrowdsecMode == configuration.AloneMode {
config.CrowdsecCapiMachineID, _ = configuration.GetVariable(config, "CrowdsecCapiMachineID") config.CrowdsecCapiMachineID, _ = configuration.GetVariable(config, "CrowdsecCapiMachineID")
config.CrowdsecCapiPassword, _ = configuration.GetVariable(config, "CrowdsecCapiPassword") config.CrowdsecCapiPassword, _ = configuration.GetVariable(config, "CrowdsecCapiPassword")
config.CrowdsecLapiHost = "api.crowdsec.net" config.CrowdsecLapiHost = crowdsecCapiHost
config.CrowdsecLapiScheme = "https" config.CrowdsecLapiScheme = "https"
config.UpdateIntervalSeconds = 7200 config.UpdateIntervalSeconds = 7200 // 2 hours
crowdsecStreamRoute = crowdsecCapiStreamRoute crowdsecStreamRoute = crowdsecCapiStreamRoute
crowdsecHeader = crowdsecCapiHeader crowdsecHeader = crowdsecCapiHeader
} else { } else {
@@ -114,6 +123,9 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
enabled: config.Enabled, enabled: config.Enabled,
crowdsecMode: config.CrowdsecMode, crowdsecMode: config.CrowdsecMode,
appsecEnabled: config.CrowdsecAppsecEnabled,
appsecHost: config.CrowdsecAppsecHost,
appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
crowdsecScheme: config.CrowdsecLapiScheme, crowdsecScheme: config.CrowdsecLapiScheme,
crowdsecHost: config.CrowdsecLapiHost, crowdsecHost: config.CrowdsecLapiHost,
crowdsecKey: config.CrowdsecLapiKey, crowdsecKey: config.CrowdsecLapiKey,
@@ -141,6 +153,9 @@ func New(ctx context.Context, next http.Handler, config *configuration.Config, n
}, },
cacheClient: &cache.Client{}, cacheClient: &cache.Client{},
} }
if config.CrowdsecMode == configuration.AppsecMode {
return bouncer, nil
}
config.RedisCachePassword, _ = configuration.GetVariable(config, "RedisCachePassword") config.RedisCachePassword, _ = configuration.GetVariable(config, "RedisCachePassword")
bouncer.cacheClient.New( bouncer.cacheClient.New(
config.RedisCacheEnabled, config.RedisCacheEnabled,
@@ -196,6 +211,11 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
return return
} }
if bouncer.crowdsecMode == configuration.AppsecMode {
handleNextServeHTTP(bouncer, remoteIP, rw, req)
return
}
// TODO This should be simplified // TODO This should be simplified
if bouncer.crowdsecMode != configuration.NoneMode { if bouncer.crowdsecMode != configuration.NoneMode {
isBanned, cacheErr := bouncer.cacheClient.GetDecision(remoteIP) isBanned, cacheErr := bouncer.cacheClient.GetDecision(remoteIP)
@@ -212,7 +232,7 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if isBanned { if isBanned {
rw.WriteHeader(http.StatusForbidden) rw.WriteHeader(http.StatusForbidden)
} else { } else {
bouncer.next.ServeHTTP(rw, req) handleNextServeHTTP(bouncer, remoteIP, rw, req)
} }
return return
} }
@@ -221,7 +241,7 @@ 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. // 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 bouncer.crowdsecMode == configuration.StreamMode || bouncer.crowdsecMode == configuration.AloneMode {
if isCrowdsecStreamHealthy { if isCrowdsecStreamHealthy {
bouncer.next.ServeHTTP(rw, req) handleNextServeHTTP(bouncer, remoteIP, rw, req)
} else { } else {
logger.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s", remoteIP)) logger.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s", remoteIP))
rw.WriteHeader(http.StatusForbidden) rw.WriteHeader(http.StatusForbidden)
@@ -232,8 +252,7 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:true %s", remoteIP, err.Error())) logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:true %s", remoteIP, err.Error()))
rw.WriteHeader(http.StatusForbidden) rw.WriteHeader(http.StatusForbidden)
} else { } else {
logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:false", remoteIP)) handleNextServeHTTP(bouncer, remoteIP, rw, req)
bouncer.next.ServeHTTP(rw, req)
} }
} }
} }
@@ -266,6 +285,18 @@ type Login struct {
Expire string `json:"expire"` Expire string `json:"expire"`
} }
func handleNextServeHTTP(bouncer *Bouncer, remoteIP string, rw http.ResponseWriter, req *http.Request) {
if bouncer.appsecEnabled {
err := appsecQuery(bouncer, remoteIP, req)
if err != nil {
logger.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) { func handleStreamTicker(bouncer *Bouncer) {
if err := handleStreamCache(bouncer); err != nil { if err := handleStreamCache(bouncer); err != nil {
isCrowdsecStreamHealthy = false isCrowdsecStreamHealthy = false
@@ -342,7 +373,7 @@ func getToken(bouncer *Bouncer) error {
loginURL := url.URL{ loginURL := url.URL{
Scheme: bouncer.crowdsecScheme, Scheme: bouncer.crowdsecScheme,
Host: bouncer.crowdsecHost, Host: bouncer.crowdsecHost,
Path: crowdsecCapiLogin, Path: crowdsecCapiLoginRoute,
} }
body, err := crowdsecQuery(bouncer, loginURL.String(), true) body, err := crowdsecQuery(bouncer, loginURL.String(), true)
if err != nil { if err != nil {
@@ -423,6 +454,11 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, err
if err != nil { if err != nil {
return nil, fmt.Errorf("crowdsecQuery url:%s %w", stringURL, err) return nil, fmt.Errorf("crowdsecQuery url:%s %w", stringURL, err)
} }
defer func() {
if err = res.Body.Close(); err != nil {
logger.Error(fmt.Sprintf("crowdsecQuery:closeBody %s", err.Error()))
}
}()
if res.StatusCode == http.StatusUnauthorized && bouncer.crowdsecMode == configuration.AloneMode { if res.StatusCode == http.StatusUnauthorized && bouncer.crowdsecMode == configuration.AloneMode {
if errToken := getToken(bouncer); errToken != nil { if errToken := getToken(bouncer); errToken != nil {
return nil, fmt.Errorf("crowdsecQuery:renewToken url:%s %w", stringURL, errToken) return nil, fmt.Errorf("crowdsecQuery:renewToken url:%s %w", stringURL, errToken)
@@ -432,11 +468,6 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, err
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("crowdsecQuery url:%s, statusCode:%d", stringURL, res.StatusCode) 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) body, err := io.ReadAll(res.Body)
if err != nil { if err != nil {
@@ -444,3 +475,58 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, err
} }
return body, nil 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 {
logger.Error(fmt.Sprintf("appsecQuery:closeBody %s", err.Error()))
}
}()
if res.StatusCode == http.StatusInternalServerError {
logger.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
}
+12 -10
View File
@@ -2,7 +2,7 @@ version: "3.8"
services: services:
traefik: traefik:
image: "traefik:v2.9.10" image: "traefik:v2.10.4"
container_name: "traefik" container_name: "traefik"
restart: unless-stopped restart: unless-stopped
command: command:
@@ -20,7 +20,7 @@ services:
- logs-local:/var/log/traefik - logs-local:/var/log/traefik
- ./:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin - ./:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports: ports:
- 80:80 - 8000:80
- 8080:8080 - 8080:8080
depends_on: depends_on:
- crowdsec - crowdsec
@@ -31,12 +31,12 @@ services:
restart: unless-stopped restart: unless-stopped
labels: labels:
- "traefik.enable=true" - "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.entrypoints=web"
- "traefik.http.routers.router-foo.middlewares=crowdsec-foo@docker" - "traefik.http.routers.router-foo.middlewares=crowdsec-foo@docker"
- "traefik.http.services.service-foo.loadbalancer.server.port=80" - "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.enabled=true"
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5" - "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5="
whoami2: whoami2:
image: traefik/whoami image: traefik/whoami
@@ -44,22 +44,23 @@ services:
restart: unless-stopped restart: unless-stopped
labels: labels:
- "traefik.enable=true" - "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.entrypoints=web"
- "traefik.http.routers.router-bar.middlewares=crowdsec-bar@docker" - "traefik.http.routers.router-bar.middlewares=crowdsec-bar@docker"
- "traefik.http.services.service-bar.loadbalancer.server.port=80" - "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.enabled=true"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapikey=44c36dac5c4140af9f06f397508e82c7" - "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecappsecenabled=true"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5="
crowdsec: crowdsec:
image: crowdsecurity/crowdsec:v1.4.6 image: crowdsecurity/crowdsec:dev
container_name: "crowdsec" container_name: "crowdsec"
restart: unless-stopped restart: unless-stopped
environment: environment:
COLLECTIONS: crowdsecurity/traefik COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching
CUSTOM_HOSTNAME: crowdsec CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK_1: 40796d93c2958f9e58345514e67740e5 BOUNCER_KEY_TRAEFIK: 40796d93c2958f9e58345514e67740e5=
BOUNCER_KEY_TRAEFIK_2: 44c36dac5c4140af9f06f397508e82c7
volumes: volumes:
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro - ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
- logs-local:/var/log/traefik:ro - logs-local:/var/log/traefik:ro
@@ -67,6 +68,7 @@ services:
- crowdsec-config-local:/etc/crowdsec/ - crowdsec-config-local:/etc/crowdsec/
labels: labels:
- "traefik.enable=false" - "traefik.enable=false"
volumes: volumes:
logs-local: logs-local:
crowdsec-db-local: crowdsec-db-local:
+9 -7
View File
@@ -2,7 +2,7 @@ version: "3.8"
services: services:
traefik: traefik:
image: "traefik:v2.9.10" image: "traefik:v2.10.4"
container_name: "traefik" container_name: "traefik"
restart: unless-stopped restart: unless-stopped
command: command:
@@ -14,7 +14,7 @@ services:
- "--entrypoints.web.address=:80" - "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" - "--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: volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro" - "/var/run/docker.sock:/var/run/docker.sock:ro"
- "logs:/var/log/traefik" - "logs:/var/log/traefik"
@@ -39,7 +39,8 @@ services:
# Definition of the middleware # Definition of the middleware
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.enabled=true" - "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.enabled=true"
# crowdseclapikey must be unique to the middleware attached to the service # crowdseclapikey must be unique to the middleware attached to the service
- "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-1" - "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-1="
# forwardedheaderstrustedips should be the IP of the proxy that is in front of traefik (if any) # 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" - "traefik.http.middlewares.crowdsec-foo.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
@@ -58,20 +59,21 @@ services:
# Definitin of the middleware # Definitin of the middleware
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.enabled=true" - "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.enabled=true"
# crowdseclapikey must be unique to the middleware attached to the service # crowdseclapikey must be unique to the middleware attached to the service
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-2" - "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapikey=FIXME-LAPI-KEY-1="
# enable AppSec real time check
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecappsecenabled=true"
# forwardedheaderstrustedips should be the IP of the proxy that is in front of traefik (if any) # 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-bar.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5"
crowdsec: crowdsec:
image: crowdsecurity/crowdsec:v1.4.6 image: crowdsecurity/crowdsec:v1.5.3
container_name: "crowdsec" container_name: "crowdsec"
restart: unless-stopped restart: unless-stopped
environment: environment:
COLLECTIONS: crowdsecurity/traefik COLLECTIONS: crowdsecurity/traefik
CUSTOM_HOSTNAME: crowdsec CUSTOM_HOSTNAME: crowdsec
# We need to register one api key per service we will use # We need to register one api key per service we will use
BOUNCER_KEY_TRAEFIK_1: FIXME-LAPI-KEY-1 BOUNCER_KEY_TRAEFIK: FIXME-LAPI-KEY-1=
BOUNCER_KEY_TRAEFIK_2: FIXME-LAPI-KEY-2
volumes: volumes:
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro - ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
- logs:/var/log/traefik:ro - logs:/var/log/traefik:ro
+28
View File
@@ -0,0 +1,28 @@
# Example
## Enabling AppSec WAF feature from crowdsec
You mostly need to configure Crowdsec for this to work by enabling virtual patching and configuring some custom rules.
In the example we use a whoami container protected by crowdsec with virtual patching enabled.
The Traefik instance just needs to know where appsec engine is located
```yaml
labels:
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecappsecenabled=true"
- "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecappsechost=crowdsec:7422"
```
We can try to query normally the whoami server:
```bash
curl http://localhost:8000/foo
```
And then we verify that a malicious request will be blocked:
```bash
curl http://localhost:8000/foo/rpc2
```
You should get a 403 on http://localhost:8000/foo/rpc2
To play the demo environment run:
```bash
make run_appsec
```
+12
View File
@@ -0,0 +1,12 @@
filenames:
- /var/log/traefik/access.log
labels:
type: traefik
---
listen_addr: 0.0.0.0:7422
appsec_config: crowdsecurity/virtual-patching
name: myAppSecComponent
source: appsec
labels:
type: appsec
@@ -0,0 +1,72 @@
version: "3.8"
services:
traefik:
image: "traefik:v2.10.7"
container_name: "traefik"
restart: unless-stopped
command:
# - "--log.level=DEBUG"
- "--accesslog"
- "--accesslog.filepath=/var/log/traefik/access.log"
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.bouncer.version=v1.2.0"
# - "--experimental.localplugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- logs-appsec-enabled:/var/log/traefik
- ./../../:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
ports:
- 8000:80
- 8080:8080
depends_on:
- crowdsec
whoami1:
image: traefik/whoami
container_name: "simple-service-foo"
restart: unless-stopped
labels:
- "traefik.enable=true"
# Definition of the router
- "traefik.http.routers.router-foo.rule=PathPrefix(`/foo`)"
- "traefik.http.routers.router-foo.entrypoints=web"
- "traefik.http.routers.router-foo.middlewares=crowdsec-foo@docker"
# Definition of the service
- "traefik.http.services.service-foo.loadbalancer.server.port=80"
# Definition of the middleware
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
# Enable AppSec
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsecenabled=true"
# Define AppSec host and port informations
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecappsechost=crowdsec:7422"
crowdsec:
image: crowdsecurity/crowdsec:v1.6.0
container_name: "crowdsec"
restart: unless-stopped
environment:
COLLECTIONS: crowdsecurity/traefik
CUSTOM_HOSTNAME: crowdsec
BOUNCER_KEY_TRAEFIK_DEV: 40796d93c2958f9e58345514e67740e5
volumes:
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
- logs-appsec-enabled:/var/log/traefik:ro
- crowdsec-db-appsec-enabled:/var/lib/crowdsec/data/
- crowdsec-config-appsec-enabled:/etc/crowdsec/
labels:
- "traefik.enable=false"
volumes:
logs-appsec-enabled:
crowdsec-db-appsec-enabled:
crowdsec-config-appsec-enabled:
@@ -1,4 +1,4 @@
# Exemple # Example
## Behind another proxy service (ex: clouflare) ## Behind another proxy service (ex: clouflare)
You need to configure your Traefik to trust Forwarded headers by your front proxy You need to configure your Traefik to trust Forwarded headers by your front proxy
@@ -14,7 +14,7 @@ services:
- "--providers.file.filename=/cloud.yaml" - "--providers.file.filename=/cloud.yaml"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /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 - logs-cloudflare:/var/log/traefik
ports: ports:
- 80:80 - 80:80
@@ -1,4 +1,4 @@
# Exemple # Example
### Install vagrant ### Install vagrant
@@ -1,4 +1,4 @@
### Kubernetes Exemple ### Kubernetes Example
#### Official docs #### Official docs
@@ -88,4 +88,4 @@ kubectl -n crowdsec exec -it $(kubectl get pods -n crowdsec --selector "k8s-app=
```bash ```bash
kubectl -n traefik exec -it $(kubectl get pods -n traefik --selector "app.kubernetes.io/name=traefik" --output=name) sh kubectl -n traefik exec -it $(kubectl get pods -n traefik --selector "app.kubernetes.io/name=traefik" --output=name) sh
``` ```
@@ -22,4 +22,4 @@ lapi:
value: "k8s linux test" value: "k8s linux test"
# If it's a test, we don't want to share signals with CrowdSec so disable the Online API. # If it's a test, we don't want to share signals with CrowdSec so disable the Online API.
- name: DISABLE_ONLINE_API - name: DISABLE_ONLINE_API
value: true value: "true"
@@ -1,4 +1,4 @@
# Exemple # Example
## With Redis as an external shared cache ## With Redis as an external shared cache
The plugin must be configured to connect to a redis instance The plugin must be configured to connect to a redis instance
@@ -26,7 +26,7 @@ services:
- 8080:8080 - 8080:8080
depends_on: depends_on:
- crowdsec - crowdsec
- redis-insecure # - redis-insecure
- redis-secure - redis-secure
# Either use secure, or insecure but do not mix both # Either use secure, or insecure but do not mix both
@@ -1,4 +1,4 @@
# Exemple # Example
## Using https communication and tls authentication with Crowdsec ## Using https communication and tls authentication with Crowdsec
##### Summary ##### Summary
@@ -1,4 +1,4 @@
# Exemple # Example
## Using Trusted IP (ex: LAN OR VPN) that won't get filtered by crowdsec ## 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 You need to configure your Traefik to trust Forwarded headers by your front proxy
@@ -36,4 +36,4 @@ You should get a 200 on http://localhost/foo even if you are on the ban cache
To play the demo environment run: To play the demo environment run:
```bash ```bash
make run_trustedips make run_trustedips
``` ```
+25 -10
View File
@@ -23,6 +23,7 @@ const (
StreamMode = "stream" StreamMode = "stream"
LiveMode = "live" LiveMode = "live"
NoneMode = "none" NoneMode = "none"
AppsecMode = "appsec"
HTTPS = "https" HTTPS = "https"
HTTP = "http" HTTP = "http"
) )
@@ -32,6 +33,9 @@ type Config struct {
Enabled bool `json:"enabled,omitempty"` Enabled bool `json:"enabled,omitempty"`
LogLevel string `json:"logLevel,omitempty"` LogLevel string `json:"logLevel,omitempty"`
CrowdsecMode string `json:"crowdsecMode,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"` CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"` CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"` CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
@@ -51,7 +55,7 @@ type Config struct {
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"` UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"` DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"` HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
ForwardedHeadersCustomName string `json:"forwardedheaderscustomheader,omitempty"` ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"`
ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"` ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"`
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"` ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
RedisCacheEnabled bool `json:"redisCacheEnabled,omitempty"` RedisCacheEnabled bool `json:"redisCacheEnabled,omitempty"`
@@ -76,6 +80,9 @@ func New() *Config {
Enabled: false, Enabled: false,
LogLevel: "INFO", LogLevel: "INFO",
CrowdsecMode: LiveMode, CrowdsecMode: LiveMode,
CrowdsecAppsecEnabled: false,
CrowdsecAppsecHost: "crowdsec:7422",
CrowdsecAppsecFailureBlock: true,
CrowdsecLapiScheme: HTTP, CrowdsecLapiScheme: HTTP,
CrowdsecLapiHost: "crowdsec:8080", CrowdsecLapiHost: "crowdsec:8080",
CrowdsecLapiKey: "", CrowdsecLapiKey: "",
@@ -149,13 +156,12 @@ func ValidateParams(config *Config) error {
return nil return nil
} }
// This only check that the format of the URL scheme:// is correct and do not make requests if err := validateURL("CrowdsecLapi", config.CrowdsecLapiScheme, config.CrowdsecLapiHost); err != nil {
testURL := url.URL{ return err
Scheme: config.CrowdsecLapiScheme,
Host: config.CrowdsecLapiHost,
} }
if _, err := http.NewRequest(http.MethodGet, testURL.String(), nil); err != nil {
return fmt.Errorf("CrowdsecLapiScheme://CrowdsecLapiHost: '%v://%v' must be an URL", config.CrowdsecLapiScheme, config.CrowdsecLapiHost) if err := validateURL("CrowdsecAppsec", config.CrowdsecLapiScheme, config.CrowdsecAppsecHost); err != nil {
return err
} }
lapiKey, err := GetVariable(config, "CrowdsecLapiKey") lapiKey, err := GetVariable(config, "CrowdsecLapiKey")
@@ -190,12 +196,21 @@ func ValidateParams(config *Config) error {
return nil 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 // validHeaderFieldByte reports whether b is a valid byte in a header
// field name. RFC 7230 says: // field name. RFC 7230 says:
// valid ! # $ % & ' * + - . ^ _ ` | ~ DIGIT ALPHA // valid ! # $ % & ' * + - . ^ _ ` | ~ DIGIT ALPHA
// See https://httpwg.github.io/specs/rfc7230.html#rule.token.separators // See https://httpwg.github.io/specs/rfc7230.html#rule.token.separators
func validateParamsAPIKey(lapiKey string) error { func validateParamsAPIKey(lapiKey string) error {
reg := regexp.MustCompile("^[a-zA-Z0-9 !#$%&'*+-.^_`|~]*$") reg := regexp.MustCompile("^[a-zA-Z0-9 !#$%&'*+-.^_`|~=/]*$")
if !reg.Match([]byte(lapiKey)) { if !reg.Match([]byte(lapiKey)) {
return fmt.Errorf("CrowdsecLapiKey doesn't valid this regexp: '/%s/'", reg.String()) return fmt.Errorf("CrowdsecLapiKey doesn't valid this regexp: '/%s/'", reg.String())
} }
@@ -250,8 +265,8 @@ func validateParamsRequired(config *Config) error {
return fmt.Errorf("%v: cannot be less than 1", key) return fmt.Errorf("%v: cannot be less than 1", key)
} }
} }
if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode}, config.CrowdsecMode) { if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode, AppsecMode}, config.CrowdsecMode) {
return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live', 'stream' or 'alone'") return fmt.Errorf("CrowdsecMode: must be one of 'none', 'live', 'stream', 'alone' or 'appsec'")
} }
if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) { if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) {
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'http' or 'https'") return fmt.Errorf("CrowdsecLapiScheme: must be one of 'http' or 'https'")
+1 -1
View File
@@ -28,4 +28,4 @@ if err != nil {
## Author ## Author
Max Lerebourg @ [Primadviz.com](https://primadviz.com) Max Lerebourg @ [Primadviz.com](https://primadviz.com)
Mathieu Hanotaux Mathieu Hanotaux