diff --git a/Makefile b/Makefile index bbfe8e2..3425071 100644 --- a/Makefile +++ b/Makefile @@ -20,66 +20,73 @@ 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 examples/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 examples/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 examples/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 examples/binary-vm/ && sudo vagrant up run_tlsauth: - 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 + docker compose -f examples/tls-auth/docker-compose.tls-auth.yml down && docker compose -f examples/tls-auth/docker-compose.tls-auth.yml up -d && docker compose -f examples/tls-auth/docker-compose.tls-auth.yml restart && docker compose -f examples/tls-auth/docker-compose.tls-auth.yml logs -f + +run_appsec: + docker compose -f examples/appsec-enabled/docker-compose.appsec-enabled.yml up -d run: - docker-compose -f docker-compose.yml up -d --remove-orphans + docker compose -f docker-compose.yml up -d --remove-orphans restart_dev: - docker-compose -f docker-compose.dev.yml restart + docker compose -f docker-compose.dev.yml restart restart_local: - docker-compose -f docker-compose.local.yml restart + docker compose -f docker-compose.local.yml restart restart: - docker-compose -f docker-compose.yml restart + docker compose -f docker-compose.yml restart restart_behindproxy: - docker-compose -f examples/behind-proxy/docker-compose.cloudflare.yml restart + docker compose -f examples/behind-proxy/docker-compose.cloudflare.yml restart restart_cacheredis: - docker-compose -f examples/redis-cache/docker-compose.redis.yml restart + docker compose -f examples/redis-cache/docker-compose.redis.yml restart restart_trustedips: - docker-compose -f examples/trusted-ips/docker-compose.trusted.yml restart + docker compose -f examples/trusted-ips/docker-compose.trusted.yml restart restart_tlsauth: - docker-compose -f examples/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 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 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 examples/binary-vm/ && sudo vagrant destroy -f diff --git a/README.md b/README.md index edd648e..0dbd30e 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ # Crowdsec Bouncer Traefik plugin +> New! This plugin now supports [AppSec](https://doc.crowdsec.net/docs/next/appsec/intro/) feature including virtual patching and capabilities support for your legacy ModSecurity rules. + This plugin aims to implement a Crowdsec Bouncer in a Traefik plugin. > [CrowdSec](https://www.crowdsec.net/) is an open-source and collaborative IPS (Intrusion Prevention System) and a security suite. @@ -17,6 +19,16 @@ The Crowdsec utility will provide the community blocklist which contains highly When used with Crowdsec it will leverage the local API which will analyze Traefik logs and take decisions on the requests made by users/bots. Malicious actors will be banned based on patterns used against your website. +Appsec feature is supported from plugin version 1.2.0 and Crowdsec 1.6.0. + +The AppSec Component offers: + +- Low-effort virtual patching capabilities. +- Support for your legacy ModSecurity rules. +- Combining classic WAF benefits with advanced CrowdSec features for otherwise difficult advanced behavior detection. +More information on appsec in the [Crowdsec Documentation](https://doc.crowdsec.net/docs/next/appsec/intro/). + + There are 4 operating modes (CrowdsecMode) for this plugin: | Mode | Description | @@ -50,13 +62,25 @@ Only one instance of the plugin is *possible*. - 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` +- 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` @@ -179,6 +203,9 @@ http: 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 @@ -309,6 +336,10 @@ docker exec crowdsec cscli decisions remove --ip 10.0.0.10 #### 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) + + ### Local Mode Traefik also offers a developer mode that can be used for temporary testing of plugins not hosted on GitHub. diff --git a/acquis.yaml b/acquis.yaml index 5d52554..67469ba 100644 --- a/acquis.yaml +++ b/acquis.yaml @@ -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 diff --git a/bouncer.go b/bouncer.go index a6b7d6b..f38f1ed 100644 --- a/bouncer.go +++ b/bouncer.go @@ -22,13 +22,19 @@ import ( ) const ( - crowdsecLapiHeader = "X-Api-Key" - crowdsecCapiHeader = "Authorization" - crowdsecLapiRoute = "v1/decisions" - crowdsecLapiStreamRoute = "v1/decisions/stream" - crowdsecCapiLogin = "v2/watchers/login" - crowdsecCapiStreamRoute = "v2/decisions/stream" - cacheTimeoutKey = "updated" + crowdsecAppsecIPHeader = "X-Crowdsec-Appsec-Ip" + crowdsecAppsecURIHeader = "X-Crowdsec-Appsec-Uri" + crowdsecAppsecHostHeader = "X-Crowdsec-Appsec-Host" + crowdsecAppsecVerbHeader = "X-Crowdsec-Appsec-Verb" + crowdsecAppsecHeader = "X-Crowdsec-Appsec-Api-Key" + crowdsecLapiHeader = "X-Api-Key" + crowdsecLapiRoute = "v1/decisions" + crowdsecLapiStreamRoute = "v1/decisions/stream" + crowdsecCapiHost = "api.crowdsec.net" + crowdsecCapiHeader = "Authorization" + crowdsecCapiLoginRoute = "v2/watchers/login" + crowdsecCapiStreamRoute = "v2/decisions/stream" + cacheTimeoutKey = "updated" ) //nolint:gochecknoglobals @@ -50,6 +56,9 @@ type Bouncer struct { template *template.Template enabled bool + appsecEnabled bool + appsecHost string + appsecFailureBlock bool crowdsecScheme string crowdsecHost string crowdsecKey string @@ -86,9 +95,9 @@ 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 { @@ -114,6 +123,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, @@ -212,7 +224,7 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { if isBanned { rw.WriteHeader(http.StatusForbidden) } else { - bouncer.next.ServeHTTP(rw, req) + handleNextServeHTTP(bouncer, remoteIP, rw, req) } return } @@ -221,7 +233,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. 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)) rw.WriteHeader(http.StatusForbidden) @@ -232,8 +244,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())) rw.WriteHeader(http.StatusForbidden) } else { - logger.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:false", remoteIP)) - bouncer.next.ServeHTTP(rw, req) + handleNextServeHTTP(bouncer, remoteIP, rw, req) } } } @@ -266,6 +277,18 @@ type Login struct { 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) { if err := handleStreamCache(bouncer); err != nil { isCrowdsecStreamHealthy = false @@ -342,7 +365,7 @@ func getToken(bouncer *Bouncer) error { loginURL := url.URL{ Scheme: bouncer.crowdsecScheme, Host: bouncer.crowdsecHost, - Path: crowdsecCapiLogin, + Path: crowdsecCapiLoginRoute, } body, err := crowdsecQuery(bouncer, loginURL.String(), true) if err != nil { @@ -423,6 +446,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 { + logger.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 +460,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 +467,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 { + 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 +} diff --git a/docker-compose.local.yml b/docker-compose.local.yml index b45777d..0b0f375 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -20,7 +20,7 @@ services: - logs-local:/var/log/traefik - ./:/plugins-local/src/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin ports: - - 80:80 + - 8000:80 - 8080:8080 depends_on: - crowdsec @@ -31,7 +31,7 @@ services: restart: unless-stopped labels: - "traefik.enable=true" - - "traefik.http.routers.router-foo.rule=Path(`/foo`)" + - "traefik.http.routers.router-foo.rule=PathPrefix(`/foo`)" - "traefik.http.routers.router-foo.entrypoints=web" - "traefik.http.routers.router-foo.middlewares=crowdsec-foo@docker" - "traefik.http.services.service-foo.loadbalancer.server.port=80" @@ -44,19 +44,21 @@ services: restart: unless-stopped labels: - "traefik.enable=true" - - "traefik.http.routers.router-bar.rule=Path(`/bar`)" + - "traefik.http.routers.router-bar.rule=PathPrefix(`/bar`)" - "traefik.http.routers.router-bar.entrypoints=web" - "traefik.http.routers.router-bar.middlewares=crowdsec-bar@docker" - "traefik.http.services.service-bar.loadbalancer.server.port=80" - "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.enabled=true" + - "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.loglevel=DEBUG" + - "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecappsecenabled=true" - "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5=" crowdsec: - image: crowdsecurity/crowdsec:v1.5.3 + image: crowdsecurity/crowdsec:dev container_name: "crowdsec" restart: unless-stopped environment: - COLLECTIONS: crowdsecurity/traefik + COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching CUSTOM_HOSTNAME: crowdsec BOUNCER_KEY_TRAEFIK: 40796d93c2958f9e58345514e67740e5= volumes: @@ -66,6 +68,7 @@ services: - crowdsec-config-local:/etc/crowdsec/ labels: - "traefik.enable=false" + volumes: logs-local: crowdsec-db-local: diff --git a/docker-compose.yml b/docker-compose.yml index 931784a..2c383dc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,6 +40,7 @@ services: - "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" @@ -59,6 +60,8 @@ services: - "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-1=" + # enable AppSec real time check + - "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.crowdsecappsecenabled=true" # forwardedheaderstrustedips should be the IP of the proxy that is in front of traefik (if any) - "traefik.http.middlewares.crowdsec-bar.plugin.bouncer.forwardedheaderstrustedips=172.21.0.5" diff --git a/examples/appsec-enabled/README.md b/examples/appsec-enabled/README.md new file mode 100644 index 0000000..89248e2 --- /dev/null +++ b/examples/appsec-enabled/README.md @@ -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 +``` diff --git a/examples/appsec-enabled/acquis.yaml b/examples/appsec-enabled/acquis.yaml new file mode 100644 index 0000000..c488192 --- /dev/null +++ b/examples/appsec-enabled/acquis.yaml @@ -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 diff --git a/examples/appsec-enabled/docker-compose.appsec-enabled.yml b/examples/appsec-enabled/docker-compose.appsec-enabled.yml new file mode 100644 index 0000000..4fa9e7f --- /dev/null +++ b/examples/appsec-enabled/docker-compose.appsec-enabled.yml @@ -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: diff --git a/pkg/configuration/configuration.go b/pkg/configuration/configuration.go index 8f293d8..5a81ac3 100644 --- a/pkg/configuration/configuration.go +++ b/pkg/configuration/configuration.go @@ -32,6 +32,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"` @@ -51,7 +54,7 @@ type Config struct { UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"` DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"` HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"` - ForwardedHeadersCustomName string `json:"forwardedheaderscustomheader,omitempty"` + ForwardedHeadersCustomName string `json:"forwardedHeadersCustomHeader,omitempty"` ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"` ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"` RedisCacheEnabled bool `json:"redisCacheEnabled,omitempty"` @@ -76,6 +79,9 @@ func New() *Config { Enabled: false, LogLevel: "INFO", CrowdsecMode: LiveMode, + CrowdsecAppsecEnabled: false, + CrowdsecAppsecHost: "crowdsec:7422", + CrowdsecAppsecFailureBlock: true, CrowdsecLapiScheme: HTTP, CrowdsecLapiHost: "crowdsec:8080", CrowdsecLapiKey: "", @@ -149,13 +155,12 @@ func ValidateParams(config *Config) error { return nil } - // This only check that the format of the URL scheme:// is correct and do not make requests - testURL := url.URL{ - Scheme: config.CrowdsecLapiScheme, - Host: config.CrowdsecLapiHost, + if err := validateURL("CrowdsecLapi", config.CrowdsecLapiScheme, config.CrowdsecLapiHost); err != nil { + return err } - if _, err := http.NewRequest(http.MethodGet, testURL.String(), nil); err != nil { - return fmt.Errorf("CrowdsecLapiScheme://CrowdsecLapiHost: '%v://%v' must be an URL", config.CrowdsecLapiScheme, config.CrowdsecLapiHost) + + if err := validateURL("CrowdsecAppsec", config.CrowdsecLapiScheme, config.CrowdsecAppsecHost); err != nil { + return err } lapiKey, err := GetVariable(config, "CrowdsecLapiKey") @@ -190,6 +195,15 @@ 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