Compare commits

...
8 Commits
Author SHA1 Message Date
mathieuHaandMax Lerebourg 0e9620bfe9 👷 chore(ci) bump CI and automate dep updates (#210)
* 👷 chore(ci) bump CI and automate dep updates

* 🚨 chore(go) fix golang lint

* 🍱 fix lint

---------

Co-authored-by: Max Lerebourg <maxlerebourg@gmail.com>
2025-01-29 08:07:04 +01:00
maxlerebourg 92f05b0ba5 [BREAKING-CHANGE] Add CrowdsecAppsecBodyLimit (#208)
*  Add CrowdsecAppsecBodyLimit

* 🍱 fix lint

* 🍱 fix lint

* 🍱 fix error on main
2025-01-24 21:04:45 +01:00
980a7dd05e Add AppSec Path Variable (#202)
* Added Appsec Path config Variable

*  Add path env var for lapi and appsec

* 🍱 Update README.md

---------

Co-authored-by: Tobias Heinze <tobias.heinze@telekom.de>
Co-authored-by: Max Lerebourg <maxlerebourg@gmail.com>
2025-01-24 20:12:30 +01:00
mathieuHaandmaxlerebourg 5c8a60118f 🐛 fix(user-agent) add version in ua to reduce warning logs from crowd… (#195)
* 🐛 fix(user-agent) add version in ua to reduce warning logs from crowdsec LAPI

* 🐛 fix(user-agent) add version in ua to reduce warning logs from crowdsec LAPI

* 🐛 fix(user-agent) remove whitespaces

* 🐛 fix(user-agent) Add Cap for Bouncer

---------

Co-authored-by: maxlerebourg <maxlerebourg@gmail.com>
2024-10-24 21:39:53 +02:00
mathieuHa 8fb0a016b6 Add Traefik Crowdsec Plugin Basic User Agent (#192)
*  Add Traefik Crowdsec Plugin Basic User Agent
2024-10-05 13:15:34 +02:00
maxlerebourg 45d5f38c4d add remediation header when plugin made decision (#189)
*  add remediation header when plugin made decision

* 🍱 add documentation
2024-09-25 19:30:27 +02:00
maxlerebourgandmax.lerebourg f1de1c924e 🔨 174 fix classname for Turnstile (#178)
Co-authored-by: max.lerebourg <max.lerebourg@monisnap.com>
2024-06-19 20:13:49 +02:00
mathieuHa 36e6043c32 📝 doc(cache) Specify that local cache is in memory and not in the filesystem (#177) 2024-06-09 16:20:55 +02:00
11 changed files with 183 additions and 109 deletions
+24
View File
@@ -0,0 +1,24 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
# Maintain dependencies for Go
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
# Maintain dependencies for build tools
- package-ecosystem: "gomod"
directory: "/tools"
schedule:
interval: "weekly"
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
+5 -5
View File
@@ -12,8 +12,8 @@ jobs:
name: Main Process name: Main Process
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
GO_VERSION: 1.22 GO_VERSION: 1.23
GOLANGCI_LINT_VERSION: v1.57.2 GOLANGCI_LINT_VERSION: v1.63.4
YAEGI_VERSION: v0.16.1 YAEGI_VERSION: v0.16.1
CGO_ENABLED: 0 CGO_ENABLED: 0
defaults: defaults:
@@ -24,20 +24,20 @@ jobs:
# https://github.com/marketplace/actions/setup-go-environment # https://github.com/marketplace/actions/setup-go-environment
- name: Set up Go ${{ env.GO_VERSION }} - name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v2 uses: actions/setup-go@v5
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
# https://github.com/marketplace/actions/checkout # https://github.com/marketplace/actions/checkout
- name: Check out code - name: Check out code
uses: actions/checkout@v2 uses: actions/checkout@v4
with: with:
path: go/src/github.com/${{ github.repository }} path: go/src/github.com/${{ github.repository }}
fetch-depth: 0 fetch-depth: 0
# https://github.com/marketplace/actions/cache # https://github.com/marketplace/actions/cache
- name: Cache Go modules - name: Cache Go modules
uses: actions/cache@v2 uses: actions/cache@v4
with: with:
path: ${{ github.workspace }}/go/pkg/mod path: ${{ github.workspace }}/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+2 -15
View File
@@ -50,20 +50,8 @@ linters-settings:
linters: linters:
enable-all: true enable-all: true
disable: disable:
- deadcode # deprecated
- exhaustivestruct # deprecated
- golint # deprecated
- ifshort # deprecated
- interfacer # deprecated
- maligned # deprecated
- nosnakecase # deprecated
- scopelint # deprecated
- scopelint # deprecated
- structcheck # deprecated
- varcheck # deprecated
- sqlclosecheck # not relevant (SQL) - sqlclosecheck # not relevant (SQL)
- rowserrcheck # not relevant (SQL) - rowserrcheck # not relevant (SQL)
- execinquery # not relevant (SQL)
- cyclop # duplicate of gocyclo - cyclop # duplicate of gocyclo
- bodyclose # Too many false positives: https://github.com/timakin/bodyclose/issues/30 - bodyclose # Too many false positives: https://github.com/timakin/bodyclose/issues/30
- dupl - dupl
@@ -74,17 +62,16 @@ linters:
- wsl - wsl
- exhaustive - exhaustive
- exhaustruct - exhaustruct
- goerr113 - err113
- wrapcheck - wrapcheck
- ifshort
- noctx - noctx
- lll - lll
- gomnd
- forbidigo - forbidigo
- varnamelen - varnamelen
- wastedassign # is disabled because of generics - wastedassign # is disabled because of generics
- gofumpt - gofumpt
- gci - gci
- mnd
issues: issues:
exclude-use-default: false exclude-use-default: false
+30 -7
View File
@@ -51,7 +51,7 @@ There are 5 operating modes (CrowdsecMode) for this plugin:
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.
The cache can be local to Traefik using the filesystem, or a separate Redis instance. The cache can be local to Traefik in memory or using a separate Redis instance.
Below are Mermaid diagrams detailling how each mode work: Below are Mermaid diagrams detailling how each mode work:
@@ -310,6 +310,9 @@ make run
*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**
*By careful when you upgrade to >1.4.x*
### Variables ### Variables
- Enabled - Enabled
- bool - bool
@@ -329,6 +332,10 @@ Only one instance of the plugin is *possible*.
- string - string
- default: "crowdsec:7422" - default: "crowdsec:7422"
- Crowdsec Appsec Server available on which host and port. The scheme will be handled by the CrowdsecLapiScheme var. - Crowdsec Appsec Server available on which host and port. The scheme will be handled by the CrowdsecLapiScheme var.
- CrowdsecAppsecPath
- string
- default: "/"
- Crowdsec Appsec Server available on this path. Will be appended to CrowdsecAppsecHost. Need to finish with "/".
- CrowdsecAppsecFailureBlock - CrowdsecAppsecFailureBlock
- bool - bool
- default: true - default: true
@@ -337,6 +344,10 @@ Only one instance of the plugin is *possible*.
- bool - bool
- default: true - default: true
- Block request when Crowdsec Appsec Server is unreachable. - Block request when Crowdsec Appsec Server is unreachable.
- CrowdsecAppsecBodyLimit
- int64
- default: 10485760 (= 10MB)
- Transmit only the first number of bytes to Crowdsec Appsec Server.
- CrowdsecLapiScheme - CrowdsecLapiScheme
- string - string
- default: `http`, expected values are: `http`, `https` - default: `http`, expected values are: `http`, `https`
@@ -344,6 +355,10 @@ Only one instance of the plugin is *possible*.
- string - string
- default: "crowdsec:8080" - default: "crowdsec:8080"
- Crowdsec LAPI available on which host and port. - Crowdsec LAPI available on which host and port.
- CrowdsecLapiPath
- string
- default: "/"
- Crowdsec LAPI Server available on this path. Will be appended to CrowdsecLapiHost. Need to finish with "/".
- CrowdsecLapiKey - CrowdsecLapiKey
- string - string
- default: "" - default: ""
@@ -368,18 +383,22 @@ Only one instance of the plugin is *possible*.
- string - string
- default: [] - default: []
- List of client IPs to trust, they will bypass any check from the bouncer or cache (useful for LAN or VPN IP) - List of client IPs to trust, they will bypass any check from the bouncer or cache (useful for LAN or VPN IP)
- ForwardedHeadersTrustedIPs - RemediationHeadersCustomName
- []string - string
- default: [] - default: ""
- List of IPs of trusted Proxies that are in front of traefik (ex: Cloudflare) - Name of the header you want in response when request are cancelled (possible value of the header `ban` or `captcha`)
- ForwardedHeadersCustomName - ForwardedHeadersCustomName
- string - string
- default: "X-Forwarded-For" - default: "X-Forwarded-For"
- Name of the header where the real IP of the client should be retrieved - Name of the header where the real IP of the client should be retrieved
- ForwardedHeadersTrustedIPs
- []string
- default: []
- List of IPs of trusted Proxies that are in front of traefik (ex: Cloudflare)
- RedisCacheEnabled - RedisCacheEnabled
- bool - bool
- default: false - default: false
- enable Redis cache instead of filesystem cache - enable Redis cache instead of in-memory cache
- RedisCacheHost - RedisCacheHost
- string - string
- default: "redis:6379" - default: "redis:6379"
@@ -489,12 +508,15 @@ http:
crowdsecMode: live crowdsecMode: live
crowdsecAppsecEnabled: false crowdsecAppsecEnabled: false
crowdsecAppsecHost: crowdsec:7422 crowdsecAppsecHost: crowdsec:7422
crowdsecAppsecPath: "/"
crowdsecAppsecFailureBlock: true crowdsecAppsecFailureBlock: true
crowdsecAppsecUnreachableBlock: true crowdsecAppsecUnreachableBlock: true
crowdsecAppsecBodyLimit: 10485760
crowdsecLapiKey: privateKey-foo crowdsecLapiKey: privateKey-foo
crowdsecLapiKeyFile: /etc/traefik/cs-privateKey-foo crowdsecLapiKeyFile: /etc/traefik/cs-privateKey-foo
crowdsecLapiHost: crowdsec:8080
crowdsecLapiScheme: http crowdsecLapiScheme: http
crowdsecLapiHost: crowdsec:8080
crowdsecLapiPath: "/"
crowdsecLapiTLSInsecureVerify: false crowdsecLapiTLSInsecureVerify: false
crowdsecCapiMachineId: login crowdsecCapiMachineId: login
crowdsecCapiPassword: password crowdsecCapiPassword: password
@@ -508,6 +530,7 @@ http:
clientTrustedIPs: clientTrustedIPs:
- 192.168.1.0/24 - 192.168.1.0/24
forwardedHeadersCustomName: X-Custom-Header forwardedHeadersCustomName: X-Custom-Header
remediationHeadersCustomName: cs-remediation
redisCacheEnabled: false redisCacheEnabled: false
redisCacheHost: "redis:6379" redisCacheHost: "redis:6379"
redisCachePassword: password redisCachePassword: password
+37 -14
View File
@@ -1,5 +1,5 @@
// Package crowdsec_bouncer_traefik_plugin implements a middleware that communicates with crowdsec. // Package crowdsec_bouncer_traefik_plugin implements a middleware that communicates with crowdsec.
// It can cache results to filesystem or redis, or even ask crowdsec for every requests. // It can cache results in memory or using redis, or even ask crowdsec for every requests.
package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
import ( import (
@@ -62,10 +62,13 @@ type Bouncer struct {
enabled bool enabled bool
appsecEnabled bool appsecEnabled bool
appsecHost string appsecHost string
appsecPath string
appsecFailureBlock bool appsecFailureBlock bool
appsecUnreachableBlock bool appsecUnreachableBlock bool
appsecBodyLimit int64
crowdsecScheme string crowdsecScheme string
crowdsecHost string crowdsecHost string
crowdsecPath string
crowdsecKey string crowdsecKey string
crowdsecMode string crowdsecMode string
crowdsecMachineID string crowdsecMachineID string
@@ -74,7 +77,8 @@ type Bouncer struct {
updateInterval int64 updateInterval int64
updateMaxFailure int updateMaxFailure int
defaultDecisionTimeout int64 defaultDecisionTimeout int64
customHeader string remediationCustomHeader string
forwardedCustomHeader string
crowdsecStreamRoute string crowdsecStreamRoute string
crowdsecHeader string crowdsecHeader string
banTemplateString string banTemplateString string
@@ -104,8 +108,10 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
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.CrowdsecLapiScheme = configuration.HTTPS
config.CrowdsecLapiHost = crowdsecCapiHost config.CrowdsecLapiHost = crowdsecCapiHost
config.CrowdsecLapiScheme = "https" config.CrowdsecLapiPath = "/"
config.CrowdsecAppsecEnabled = false
config.UpdateIntervalSeconds = 7200 // 2 hours config.UpdateIntervalSeconds = 7200 // 2 hours
crowdsecStreamRoute = crowdsecCapiStreamRoute crowdsecStreamRoute = crowdsecCapiStreamRoute
crowdsecHeader = crowdsecCapiHeader crowdsecHeader = crowdsecCapiHeader
@@ -120,7 +126,7 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
apiKey, errAPIKey := configuration.GetVariable(config, "CrowdsecLapiKey") apiKey, errAPIKey := configuration.GetVariable(config, "CrowdsecLapiKey")
if errAPIKey != nil && len(tlsConfig.Certificates) == 0 { if errAPIKey != nil && len(tlsConfig.Certificates) == 0 {
log.Error("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup " + errAPIKey.Error()) log.Error("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup " + errAPIKey.Error())
return nil, err return nil, errAPIKey
} }
config.CrowdsecLapiKey = apiKey config.CrowdsecLapiKey = apiKey
} }
@@ -146,17 +152,21 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
crowdsecMode: config.CrowdsecMode, crowdsecMode: config.CrowdsecMode,
appsecEnabled: config.CrowdsecAppsecEnabled, appsecEnabled: config.CrowdsecAppsecEnabled,
appsecHost: config.CrowdsecAppsecHost, appsecHost: config.CrowdsecAppsecHost,
appsecPath: config.CrowdsecAppsecPath,
appsecFailureBlock: config.CrowdsecAppsecFailureBlock, appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock, appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock,
appsecBodyLimit: config.CrowdsecAppsecBodyLimit,
crowdsecScheme: config.CrowdsecLapiScheme, crowdsecScheme: config.CrowdsecLapiScheme,
crowdsecHost: config.CrowdsecLapiHost, crowdsecHost: config.CrowdsecLapiHost,
crowdsecPath: config.CrowdsecLapiPath,
crowdsecKey: config.CrowdsecLapiKey, crowdsecKey: config.CrowdsecLapiKey,
crowdsecMachineID: config.CrowdsecCapiMachineID, crowdsecMachineID: config.CrowdsecCapiMachineID,
crowdsecPassword: config.CrowdsecCapiPassword, crowdsecPassword: config.CrowdsecCapiPassword,
crowdsecScenarios: config.CrowdsecCapiScenarios, crowdsecScenarios: config.CrowdsecCapiScenarios,
updateInterval: config.UpdateIntervalSeconds, updateInterval: config.UpdateIntervalSeconds,
updateMaxFailure: config.UpdateMaxFailure, updateMaxFailure: config.UpdateMaxFailure,
customHeader: config.ForwardedHeadersCustomName, remediationCustomHeader: config.RemediationHeadersCustomName,
forwardedCustomHeader: config.ForwardedHeadersCustomName,
defaultDecisionTimeout: config.DefaultDecisionSeconds, defaultDecisionTimeout: config.DefaultDecisionSeconds,
banTemplateString: banTemplateString, banTemplateString: banTemplateString,
crowdsecStreamRoute: crowdsecStreamRoute, crowdsecStreamRoute: crowdsecStreamRoute,
@@ -202,6 +212,7 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
config.CaptchaProvider, config.CaptchaProvider,
config.CaptchaSiteKey, config.CaptchaSiteKey,
config.CaptchaSecretKey, config.CaptchaSecretKey,
config.RemediationHeadersCustomName,
config.CaptchaHTMLFilePath, config.CaptchaHTMLFilePath,
config.CaptchaGracePeriodSeconds, config.CaptchaGracePeriodSeconds,
) )
@@ -236,8 +247,8 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
return return
} }
// Here we check for the trusted IPs in the customHeader // Here we check for the trusted IPs in the forwardedCustomHeader
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.customHeader) remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.forwardedCustomHeader)
if err != nil { if err != nil {
bouncer.log.Error(fmt.Sprintf("ServeHTTP:getRemoteIp ip:%s %s", remoteIP, err.Error())) bouncer.log.Error(fmt.Sprintf("ServeHTTP:getRemoteIp ip:%s %s", remoteIP, err.Error()))
handleBanServeHTTP(bouncer, rw) handleBanServeHTTP(bouncer, rw)
@@ -337,8 +348,14 @@ func handleBanServeHTTP(bouncer *Bouncer, rw http.ResponseWriter) {
return return
} }
rw.Header().Set("Content-Type", "text/html; charset=utf-8") rw.Header().Set("Content-Type", "text/html; charset=utf-8")
if bouncer.remediationCustomHeader != "" {
rw.Header().Set(bouncer.remediationCustomHeader, "ban")
}
rw.WriteHeader(http.StatusForbidden) rw.WriteHeader(http.StatusForbidden)
fmt.Fprint(rw, bouncer.banTemplateString) _, err := fmt.Fprint(rw, bouncer.banTemplateString)
if err != nil {
bouncer.log.Error("handleBanServeHTTP could not write template to ResponseWriter")
}
} }
func handleRemediationServeHTTP(bouncer *Bouncer, remoteIP, remediation string, rw http.ResponseWriter, req *http.Request) { func handleRemediationServeHTTP(bouncer *Bouncer, remoteIP, remediation string, rw http.ResponseWriter, req *http.Request) {
@@ -402,7 +419,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
routeURL := url.URL{ routeURL := url.URL{
Scheme: bouncer.crowdsecScheme, Scheme: bouncer.crowdsecScheme,
Host: bouncer.crowdsecHost, Host: bouncer.crowdsecHost,
Path: crowdsecLapiRoute, Path: bouncer.crowdsecPath + crowdsecLapiRoute,
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteIP), RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteIP),
} }
body, err := crowdsecQuery(bouncer, routeURL.String(), false) body, err := crowdsecQuery(bouncer, routeURL.String(), false)
@@ -498,7 +515,7 @@ func handleStreamCache(bouncer *Bouncer) error {
streamRouteURL := url.URL{ streamRouteURL := url.URL{
Scheme: bouncer.crowdsecScheme, Scheme: bouncer.crowdsecScheme,
Host: bouncer.crowdsecHost, Host: bouncer.crowdsecHost,
Path: bouncer.crowdsecStreamRoute, Path: bouncer.crowdsecPath + bouncer.crowdsecStreamRoute,
RawQuery: fmt.Sprintf("startup=%t", !isCrowdsecStreamHealthy || isStartup), RawQuery: fmt.Sprintf("startup=%t", !isCrowdsecStreamHealthy || isStartup),
} }
body, err := crowdsecQuery(bouncer, streamRouteURL.String(), false) body, err := crowdsecQuery(bouncer, streamRouteURL.String(), false)
@@ -546,6 +563,8 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, err
req, _ = http.NewRequest(http.MethodGet, stringURL, nil) req, _ = http.NewRequest(http.MethodGet, stringURL, nil)
} }
req.Header.Add(bouncer.crowdsecHeader, bouncer.crowdsecKey) req.Header.Add(bouncer.crowdsecHeader, bouncer.crowdsecKey)
req.Header.Add("User-Agent", "Crowdsec-Bouncer-Traefik-Plugin/1.X.X")
res, err := bouncer.httpClient.Do(req) res, err := bouncer.httpClient.Do(req)
if err != nil { if err != nil {
return nil, fmt.Errorf("crowdsecQuery:unreachable url:%s %w", stringURL, err) return nil, fmt.Errorf("crowdsecQuery:unreachable url:%s %w", stringURL, err)
@@ -576,15 +595,19 @@ func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
routeURL := url.URL{ routeURL := url.URL{
Scheme: bouncer.crowdsecScheme, Scheme: bouncer.crowdsecScheme,
Host: bouncer.appsecHost, Host: bouncer.appsecHost,
Path: "/", Path: bouncer.appsecPath,
} }
var req *http.Request var req *http.Request
if httpReq.Body != nil && httpReq.ContentLength > 0 { if bouncer.appsecBodyLimit > 0 && httpReq.Body != nil && httpReq.ContentLength > 0 {
bodyBytes, err := io.ReadAll(httpReq.Body) var bodyBuffer bytes.Buffer
limitedReader := io.LimitReader(httpReq.Body, bouncer.appsecBodyLimit)
teeReader := io.TeeReader(limitedReader, &bodyBuffer)
bodyBytes, err := io.ReadAll(teeReader)
if err != nil { if err != nil {
return fmt.Errorf("appsecQuery:GetBody %w", err) return fmt.Errorf("appsecQuery:GetBody %w", err)
} }
httpReq.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // Conserve body intact after reading it for other middlewares and service
httpReq.Body = io.NopCloser(io.MultiReader(&bodyBuffer, httpReq.Body))
req, _ = http.NewRequest(http.MethodPost, routeURL.String(), bytes.NewBuffer(bodyBytes)) req, _ = http.NewRequest(http.MethodPost, routeURL.String(), bytes.NewBuffer(bodyBytes))
} else { } else {
req, _ = http.NewRequest(http.MethodGet, routeURL.String(), nil) req, _ = http.NewRequest(http.MethodGet, routeURL.String(), nil)
+2 -2
View File
@@ -75,7 +75,7 @@ func TestBouncer_ServeHTTP(t *testing.T) {
crowdsecMode string crowdsecMode string
updateInterval int64 updateInterval int64
defaultDecisionTimeout int64 defaultDecisionTimeout int64
customHeader string forwardedCustomHeader string
clientPoolStrategy *ip.PoolStrategy clientPoolStrategy *ip.PoolStrategy
serverPoolStrategy *ip.PoolStrategy serverPoolStrategy *ip.PoolStrategy
httpClient *http.Client httpClient *http.Client
@@ -105,7 +105,7 @@ func TestBouncer_ServeHTTP(t *testing.T) {
crowdsecMode: tt.fields.crowdsecMode, crowdsecMode: tt.fields.crowdsecMode,
updateInterval: tt.fields.updateInterval, updateInterval: tt.fields.updateInterval,
defaultDecisionTimeout: tt.fields.defaultDecisionTimeout, defaultDecisionTimeout: tt.fields.defaultDecisionTimeout,
customHeader: tt.fields.customHeader, forwardedCustomHeader: tt.fields.forwardedCustomHeader,
clientPoolStrategy: tt.fields.clientPoolStrategy, clientPoolStrategy: tt.fields.clientPoolStrategy,
serverPoolStrategy: tt.fields.serverPoolStrategy, serverPoolStrategy: tt.fields.serverPoolStrategy,
httpClient: tt.fields.httpClient, httpClient: tt.fields.httpClient,
+3 -1
View File
@@ -3,7 +3,9 @@ You need to create a crowdsec API credentials for the CAPI.
You can follow the documentation here: https://docs.crowdsec.net/docs/central_api/intro You can follow the documentation here: https://docs.crowdsec.net/docs/central_api/intro
```bash ```bash
curl -X POST "https://api.crowdsec.net/v2/watchers" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"password\": \"PASSWORD\", \"machine_id\": \"LOGIN\"}" LOGIN=...
PASSWORD=...
curl -X POST "https://api.crowdsec.net/v2/watchers" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"password\": \"$PASSWORD\", \"machine_id\": \"$LOGIN\"}"
``` ```
These CAPI credentials must be set in your docker-compose.yml or in your config files These CAPI credentials must be set in your docker-compose.yml or in your config files
+1 -2
View File
@@ -35,8 +35,7 @@ services:
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true" - "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG" # - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=alone" - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecMode=alone"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiMachineId=FIXME" - "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiMachineId=FIXME"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiPassword=FIXME" - "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiPassword=FIXME"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/sshd,crowdsecurity/asterisk_bf,crowdsecurity/asterisk_user_enum,crowdsecurity/base-http-scenarios" - "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseccapiscenarios=crowdsecurity/sshd,crowdsecurity/asterisk_bf,crowdsecurity/asterisk_user_enum,crowdsecurity/base-http-scenarios"
+7 -2
View File
@@ -20,6 +20,7 @@ type Client struct {
provider string provider string
siteKey string siteKey string
secretKey string secretKey string
remediationCustomHeader string
gracePeriodSeconds int64 gracePeriodSeconds int64
captchaTemplate *template.Template captchaTemplate *template.Template
cacheClient *cache.Client cacheClient *cache.Client
@@ -48,14 +49,14 @@ var (
}, },
configuration.TurnstileProvider: { configuration.TurnstileProvider: {
js: "https://challenges.cloudflare.com/turnstile/v0/api.js", js: "https://challenges.cloudflare.com/turnstile/v0/api.js",
key: "cf-captcha", key: "cf-turnstile",
validate: "https://challenges.cloudflare.com/turnstile/v0/siteverify", validate: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
}, },
} }
) )
// New Initialize captcha client. // New Initialize captcha client.
func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *http.Client, provider, siteKey, secretKey, captchaTemplatePath string, gracePeriodSeconds int64) error { func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *http.Client, provider, siteKey, secretKey, remediationCustomHeader, captchaTemplatePath string, gracePeriodSeconds int64) error {
c.Valid = provider != "" c.Valid = provider != ""
if !c.Valid { if !c.Valid {
return nil return nil
@@ -63,6 +64,7 @@ func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *htt
c.siteKey = siteKey c.siteKey = siteKey
c.secretKey = secretKey c.secretKey = secretKey
c.provider = provider c.provider = provider
c.remediationCustomHeader = remediationCustomHeader
html, _ := configuration.GetHTMLTemplate(captchaTemplatePath) html, _ := configuration.GetHTMLTemplate(captchaTemplatePath)
c.captchaTemplate = html c.captchaTemplate = html
c.gracePeriodSeconds = gracePeriodSeconds c.gracePeriodSeconds = gracePeriodSeconds
@@ -87,6 +89,9 @@ func (c *Client) ServeHTTP(rw http.ResponseWriter, r *http.Request, remoteIP str
return return
} }
rw.Header().Set("Content-Type", "text/html; charset=utf-8") rw.Header().Set("Content-Type", "text/html; charset=utf-8")
if c.remediationCustomHeader != "" {
rw.Header().Set(c.remediationCustomHeader, "captcha")
}
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
err = c.captchaTemplate.Execute(rw, map[string]string{ err = c.captchaTemplate.Execute(rw, map[string]string{
"SiteKey": c.siteKey, "SiteKey": c.siteKey,
+17 -6
View File
@@ -40,10 +40,13 @@ type Config struct {
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"`
CrowdsecAppsecPath string `json:"crowdsecAppsecPath,omitempty"`
CrowdsecAppsecFailureBlock bool `json:"crowdsecAppsecFailureBlock,omitempty"` CrowdsecAppsecFailureBlock bool `json:"crowdsecAppsecFailureBlock,omitempty"`
CrowdsecAppsecUnreachableBlock bool `json:"crowdsecAppsecUnreachableBlock,omitempty"` CrowdsecAppsecUnreachableBlock bool `json:"crowdsecAppsecUnreachableBlock,omitempty"`
CrowdsecAppsecBodyLimit int64 `json:"crowdsecAppsecBodyLimit,omitempty"`
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"` CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"` CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
CrowdsecLapiPath string `json:"crowdsecLapiPath,omitempty"`
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"` CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
CrowdsecLapiKeyFile string `json:"crowdsecLapiKeyFile,omitempty"` CrowdsecLapiKeyFile string `json:"crowdsecLapiKeyFile,omitempty"`
CrowdsecLapiTLSInsecureVerify bool `json:"crowdsecLapiTlsInsecureVerify,omitempty"` CrowdsecLapiTLSInsecureVerify bool `json:"crowdsecLapiTlsInsecureVerify,omitempty"`
@@ -62,6 +65,7 @@ type Config struct {
UpdateMaxFailure int `json:"updateMaxFailure,omitempty"` UpdateMaxFailure int `json:"updateMaxFailure,omitempty"`
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"` DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"` HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
RemediationHeadersCustomName string `json:"remediationHeadersCustomName,omitempty"`
ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,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"`
@@ -97,10 +101,13 @@ func New() *Config {
CrowdsecMode: LiveMode, CrowdsecMode: LiveMode,
CrowdsecAppsecEnabled: false, CrowdsecAppsecEnabled: false,
CrowdsecAppsecHost: "crowdsec:7422", CrowdsecAppsecHost: "crowdsec:7422",
CrowdsecAppsecPath: "/",
CrowdsecAppsecFailureBlock: true, CrowdsecAppsecFailureBlock: true,
CrowdsecAppsecUnreachableBlock: true, CrowdsecAppsecUnreachableBlock: true,
CrowdsecAppsecBodyLimit: 10485760,
CrowdsecLapiScheme: HTTP, CrowdsecLapiScheme: HTTP,
CrowdsecLapiHost: "crowdsec:8080", CrowdsecLapiHost: "crowdsec:8080",
CrowdsecLapiPath: "/",
CrowdsecLapiKey: "", CrowdsecLapiKey: "",
CrowdsecLapiTLSInsecureVerify: false, CrowdsecLapiTLSInsecureVerify: false,
UpdateIntervalSeconds: 60, UpdateIntervalSeconds: 60,
@@ -113,6 +120,7 @@ func New() *Config {
CaptchaGracePeriodSeconds: 1800, CaptchaGracePeriodSeconds: 1800,
CaptchaHTMLFilePath: "/captcha.html", CaptchaHTMLFilePath: "/captcha.html",
BanHTMLFilePath: "", BanHTMLFilePath: "",
RemediationHeadersCustomName: "",
ForwardedHeadersCustomName: "X-Forwarded-For", ForwardedHeadersCustomName: "X-Forwarded-For",
ForwardedHeadersTrustedIPs: []string{}, ForwardedHeadersTrustedIPs: []string{},
ClientTrustedIPs: []string{}, ClientTrustedIPs: []string{},
@@ -215,11 +223,11 @@ func ValidateParams(config *Config) error {
} }
} }
if err := validateURL("CrowdsecLapi", config.CrowdsecLapiScheme, config.CrowdsecLapiHost); err != nil { if err := validateURL("CrowdsecLapi", config.CrowdsecLapiScheme, config.CrowdsecLapiHost, config.CrowdsecLapiPath); err != nil {
return err return err
} }
if err := validateURL("CrowdsecAppsec", config.CrowdsecLapiScheme, config.CrowdsecAppsecHost); err != nil { if err := validateURL("CrowdsecAppsec", config.CrowdsecLapiScheme, config.CrowdsecAppsecHost, config.CrowdsecAppsecPath); err != nil {
return err return err
} }
@@ -255,11 +263,11 @@ func ValidateParams(config *Config) error {
return nil return nil
} }
func validateURL(variable, scheme, host string) error { func validateURL(variable, scheme, host, path string) error {
// This only check that the format of the URL scheme://host is correct and do not make requests // This only check that the format of the URL scheme://host/path is correct and do not make requests
testURL := url.URL{Scheme: scheme, Host: host} testURL := url.URL{Scheme: scheme, Host: host, Path: path}
if _, err := http.NewRequest(http.MethodGet, testURL.String(), nil); err != nil { 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 fmt.Errorf("CrowdsecLapiScheme://%sHost: '%v://%v%v' must be a valid URL", variable, scheme, host, path)
} }
return nil return nil
} }
@@ -326,6 +334,9 @@ func validateParamsRequired(config *Config) error {
if config.UpdateMaxFailure < -1 { if config.UpdateMaxFailure < -1 {
return errors.New("UpdateMaxFailure: cannot be less than -1") return errors.New("UpdateMaxFailure: cannot be less than -1")
} }
if config.CrowdsecAppsecBodyLimit < 0 {
return errors.New("CrowdsecAppsecBodyLimit: cannot be less than 0")
}
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'")
+3 -3
View File
@@ -34,15 +34,15 @@ func New(logLevel string) *Log {
// Info log to Stdout. // Info log to Stdout.
func (l *Log) Info(str string) { func (l *Log) Info(str string) {
l.logInfo.Printf(str) l.logInfo.Printf("%s", str)
} }
// Debug log to Stdout. // Debug log to Stdout.
func (l *Log) Debug(str string) { func (l *Log) Debug(str string) {
l.logDebug.Printf(str) l.logDebug.Printf("%s", str)
} }
// Error log to Stderr. // Error log to Stderr.
func (l *Log) Error(str string) { func (l *Log) Error(str string) {
l.logError.Printf(str) l.logError.Printf("%s", str)
} }