Compare commits

..
8 Commits
Author SHA1 Message Date
maxlerebourg 4708d76854 Add variable to not block if redis is unreachable (#214)
*  Add variable to not block if redis is unreachable

* 🚨 fix lint

* 📝 Update README.md
2025-02-10 20:10:29 +01:00
maxlerebourg c34d7f4617 🍱 Fix lint (#211) 2025-01-29 08:14:15 +01:00
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
12 changed files with 202 additions and 108 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
runs-on: ubuntu-latest
env:
GO_VERSION: 1.22
GOLANGCI_LINT_VERSION: v1.57.2
GO_VERSION: 1.23
GOLANGCI_LINT_VERSION: v1.63.4
YAEGI_VERSION: v0.16.1
CGO_ENABLED: 0
defaults:
@@ -24,20 +24,20 @@ jobs:
# https://github.com/marketplace/actions/setup-go-environment
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v2
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
# https://github.com/marketplace/actions/checkout
- name: Check out code
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
path: go/src/github.com/${{ github.repository }}
fetch-depth: 0
# https://github.com/marketplace/actions/cache
- name: Cache Go modules
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+2 -15
View File
@@ -50,20 +50,8 @@ linters-settings:
linters:
enable-all: true
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)
- rowserrcheck # not relevant (SQL)
- execinquery # not relevant (SQL)
- cyclop # duplicate of gocyclo
- bodyclose # Too many false positives: https://github.com/timakin/bodyclose/issues/30
- dupl
@@ -74,17 +62,16 @@ linters:
- wsl
- exhaustive
- exhaustruct
- goerr113
- err113
- wrapcheck
- ifshort
- noctx
- lll
- gomnd
- forbidigo
- varnamelen
- wastedassign # is disabled because of generics
- gofumpt
- gci
- mnd
issues:
exclude-use-default: false
+33 -5
View File
@@ -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*
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
- Enabled
- bool
@@ -329,6 +332,10 @@ Only one instance of the plugin is *possible*.
- string
- default: "crowdsec:7422"
- 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
- bool
- default: true
@@ -337,6 +344,10 @@ Only one instance of the plugin is *possible*.
- bool
- default: true
- 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
- string
- default: `http`, expected values are: `http`, `https`
@@ -344,6 +355,10 @@ Only one instance of the plugin is *possible*.
- string
- default: "crowdsec:8080"
- 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
- string
- default: ""
@@ -368,14 +383,18 @@ Only one instance of the plugin is *possible*.
- string
- default: []
- List of client IPs to trust, they will bypass any check from the bouncer or cache (useful for LAN or VPN IP)
- ForwardedHeadersTrustedIPs
- []string
- default: []
- List of IPs of trusted Proxies that are in front of traefik (ex: Cloudflare)
- RemediationHeadersCustomName
- string
- default: ""
- Name of the header you want in response when request are cancelled (possible value of the header `ban` or `captcha`)
- ForwardedHeadersCustomName
- string
- default: "X-Forwarded-For"
- 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
- bool
- default: false
@@ -392,6 +411,10 @@ Only one instance of the plugin is *possible*.
- string
- default: ""
- Database selection for the Redis service
- RedisUnreachableBlock
- bool
- default: true
- Block request when Redis is unreachable (if Redis is unreachable, 1-second delay is added to each request)
- HTTPTimeoutSeconds
- int64
- default: 10
@@ -489,12 +512,15 @@ http:
crowdsecMode: live
crowdsecAppsecEnabled: false
crowdsecAppsecHost: crowdsec:7422
crowdsecAppsecPath: "/"
crowdsecAppsecFailureBlock: true
crowdsecAppsecUnreachableBlock: true
crowdsecAppsecBodyLimit: 10485760
crowdsecLapiKey: privateKey-foo
crowdsecLapiKeyFile: /etc/traefik/cs-privateKey-foo
crowdsecLapiHost: crowdsec:8080
crowdsecLapiScheme: http
crowdsecLapiHost: crowdsec:8080
crowdsecLapiPath: "/"
crowdsecLapiTLSInsecureVerify: false
crowdsecCapiMachineId: login
crowdsecCapiPassword: password
@@ -508,10 +534,12 @@ http:
clientTrustedIPs:
- 192.168.1.0/24
forwardedHeadersCustomName: X-Custom-Header
remediationHeadersCustomName: cs-remediation
redisCacheEnabled: false
redisCacheHost: "redis:6379"
redisCachePassword: password
redisCacheDatabase: "5"
redisCacheUnreachableBlock: true
crowdsecLapiTLSCertificateAuthority: |-
-----BEGIN CERTIFICATE-----
MIIEBzCCAu+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
+44 -14
View File
@@ -62,10 +62,13 @@ type Bouncer struct {
enabled bool
appsecEnabled bool
appsecHost string
appsecPath string
appsecFailureBlock bool
appsecUnreachableBlock bool
appsecBodyLimit int64
crowdsecScheme string
crowdsecHost string
crowdsecPath string
crowdsecKey string
crowdsecMode string
crowdsecMachineID string
@@ -74,9 +77,11 @@ type Bouncer struct {
updateInterval int64
updateMaxFailure int
defaultDecisionTimeout int64
customHeader string
remediationCustomHeader string
forwardedCustomHeader string
crowdsecStreamRoute string
crowdsecHeader string
redisUnreachableBlock bool
banTemplateString string
clientPoolStrategy *ip.PoolStrategy
serverPoolStrategy *ip.PoolStrategy
@@ -104,8 +109,10 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
if config.CrowdsecMode == configuration.AloneMode {
config.CrowdsecCapiMachineID, _ = configuration.GetVariable(config, "CrowdsecCapiMachineID")
config.CrowdsecCapiPassword, _ = configuration.GetVariable(config, "CrowdsecCapiPassword")
config.CrowdsecLapiScheme = configuration.HTTPS
config.CrowdsecLapiHost = crowdsecCapiHost
config.CrowdsecLapiScheme = "https"
config.CrowdsecLapiPath = "/"
config.CrowdsecAppsecEnabled = false
config.UpdateIntervalSeconds = 7200 // 2 hours
crowdsecStreamRoute = crowdsecCapiStreamRoute
crowdsecHeader = crowdsecCapiHeader
@@ -120,7 +127,7 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
apiKey, errAPIKey := configuration.GetVariable(config, "CrowdsecLapiKey")
if errAPIKey != nil && len(tlsConfig.Certificates) == 0 {
log.Error("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup " + errAPIKey.Error())
return nil, err
return nil, errAPIKey
}
config.CrowdsecLapiKey = apiKey
}
@@ -146,18 +153,23 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
crowdsecMode: config.CrowdsecMode,
appsecEnabled: config.CrowdsecAppsecEnabled,
appsecHost: config.CrowdsecAppsecHost,
appsecPath: config.CrowdsecAppsecPath,
appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock,
appsecBodyLimit: config.CrowdsecAppsecBodyLimit,
crowdsecScheme: config.CrowdsecLapiScheme,
crowdsecHost: config.CrowdsecLapiHost,
crowdsecPath: config.CrowdsecLapiPath,
crowdsecKey: config.CrowdsecLapiKey,
crowdsecMachineID: config.CrowdsecCapiMachineID,
crowdsecPassword: config.CrowdsecCapiPassword,
crowdsecScenarios: config.CrowdsecCapiScenarios,
updateInterval: config.UpdateIntervalSeconds,
updateMaxFailure: config.UpdateMaxFailure,
customHeader: config.ForwardedHeadersCustomName,
remediationCustomHeader: config.RemediationHeadersCustomName,
forwardedCustomHeader: config.ForwardedHeadersCustomName,
defaultDecisionTimeout: config.DefaultDecisionSeconds,
redisUnreachableBlock: config.RedisCacheUnreachableBlock,
banTemplateString: banTemplateString,
crowdsecStreamRoute: crowdsecStreamRoute,
crowdsecHeader: crowdsecHeader,
@@ -202,6 +214,7 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
config.CaptchaProvider,
config.CaptchaSiteKey,
config.CaptchaSecretKey,
config.RemediationHeadersCustomName,
config.CaptchaHTMLFilePath,
config.CaptchaGracePeriodSeconds,
)
@@ -229,15 +242,15 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
// ServeHTTP principal function of plugin.
//
//nolint:nestif
//nolint:nestif,gocyclo
func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if !bouncer.enabled {
bouncer.next.ServeHTTP(rw, req)
return
}
// Here we check for the trusted IPs in the customHeader
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.customHeader)
// Here we check for the trusted IPs in the forwardedCustomHeader
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.forwardedCustomHeader)
if err != nil {
bouncer.log.Error(fmt.Sprintf("ServeHTTP:getRemoteIp ip:%s %s", remoteIP, err.Error()))
handleBanServeHTTP(bouncer, rw)
@@ -267,6 +280,11 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if cacheErr != nil {
cacheErrString := cacheErr.Error()
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:Get ip:%s isBanned:false %s", remoteIP, cacheErrString))
if !bouncer.redisUnreachableBlock && cacheErrString == cache.CacheUnreachable {
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s redisUnreachable=true", remoteIP))
handleNextServeHTTP(bouncer, remoteIP, rw, req)
return
}
if cacheErrString != cache.CacheMiss {
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s %s", remoteIP, cacheErrString))
handleBanServeHTTP(bouncer, rw)
@@ -337,8 +355,14 @@ func handleBanServeHTTP(bouncer *Bouncer, rw http.ResponseWriter) {
return
}
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
if bouncer.remediationCustomHeader != "" {
rw.Header().Set(bouncer.remediationCustomHeader, "ban")
}
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) {
@@ -402,7 +426,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
routeURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Host: bouncer.crowdsecHost,
Path: crowdsecLapiRoute,
Path: bouncer.crowdsecPath + crowdsecLapiRoute,
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteIP),
}
body, err := crowdsecQuery(bouncer, routeURL.String(), false)
@@ -498,7 +522,7 @@ func handleStreamCache(bouncer *Bouncer) error {
streamRouteURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Host: bouncer.crowdsecHost,
Path: bouncer.crowdsecStreamRoute,
Path: bouncer.crowdsecPath + bouncer.crowdsecStreamRoute,
RawQuery: fmt.Sprintf("startup=%t", !isCrowdsecStreamHealthy || isStartup),
}
body, err := crowdsecQuery(bouncer, streamRouteURL.String(), false)
@@ -546,6 +570,8 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string, isPost bool) ([]byte, err
req, _ = http.NewRequest(http.MethodGet, stringURL, nil)
}
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)
if err != nil {
return nil, fmt.Errorf("crowdsecQuery:unreachable url:%s %w", stringURL, err)
@@ -576,15 +602,19 @@ func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
routeURL := url.URL{
Scheme: bouncer.crowdsecScheme,
Host: bouncer.appsecHost,
Path: "/",
Path: bouncer.appsecPath,
}
var req *http.Request
if httpReq.Body != nil && httpReq.ContentLength > 0 {
bodyBytes, err := io.ReadAll(httpReq.Body)
if bouncer.appsecBodyLimit > 0 && httpReq.Body != nil && httpReq.ContentLength > 0 {
var bodyBuffer bytes.Buffer
limitedReader := io.LimitReader(httpReq.Body, bouncer.appsecBodyLimit)
teeReader := io.TeeReader(limitedReader, &bodyBuffer)
bodyBytes, err := io.ReadAll(teeReader)
if err != nil {
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))
} else {
req, _ = http.NewRequest(http.MethodGet, routeURL.String(), nil)
+2 -2
View File
@@ -75,7 +75,7 @@ func TestBouncer_ServeHTTP(t *testing.T) {
crowdsecMode string
updateInterval int64
defaultDecisionTimeout int64
customHeader string
forwardedCustomHeader string
clientPoolStrategy *ip.PoolStrategy
serverPoolStrategy *ip.PoolStrategy
httpClient *http.Client
@@ -105,7 +105,7 @@ func TestBouncer_ServeHTTP(t *testing.T) {
crowdsecMode: tt.fields.crowdsecMode,
updateInterval: tt.fields.updateInterval,
defaultDecisionTimeout: tt.fields.defaultDecisionTimeout,
customHeader: tt.fields.customHeader,
forwardedCustomHeader: tt.fields.forwardedCustomHeader,
clientPoolStrategy: tt.fields.clientPoolStrategy,
serverPoolStrategy: tt.fields.serverPoolStrategy,
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
```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
+1 -2
View File
@@ -35,8 +35,7 @@ services:
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
# - "traefik.http.middlewares.crowdsec.plugin.bouncer.loglevel=DEBUG"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecmode=alone"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=40796d93c2958f9e58345514e67740e5"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecMode=alone"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.CrowdsecCapiMachineId=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"
+7 -1
View File
@@ -23,6 +23,8 @@ const (
CaptchaDoneValue = "d"
// CacheMiss error string when cache is miss.
CacheMiss = "cache:miss"
// CacheUnreachable error string when cache is unreachable.
CacheUnreachable = "cache:unreachable"
)
//nolint:gochecknoglobals
@@ -60,9 +62,13 @@ func (redisCache) get(key string) (string, error) {
if err == nil && len(valueString) > 0 {
return valueString, nil
}
if err.Error() == simpleredis.RedisMiss {
errRedisMessage := err.Error()
if errRedisMessage == simpleredis.RedisMiss {
return "", errors.New(CacheMiss)
}
if errRedisMessage == simpleredis.RedisUnreachable {
return "", errors.New(CacheUnreachable)
}
return "", err
}
+7 -2
View File
@@ -20,6 +20,7 @@ type Client struct {
provider string
siteKey string
secretKey string
remediationCustomHeader string
gracePeriodSeconds int64
captchaTemplate *template.Template
cacheClient *cache.Client
@@ -55,7 +56,7 @@ var (
)
// 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 != ""
if !c.Valid {
return nil
@@ -63,6 +64,7 @@ func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *htt
c.siteKey = siteKey
c.secretKey = secretKey
c.provider = provider
c.remediationCustomHeader = remediationCustomHeader
html, _ := configuration.GetHTMLTemplate(captchaTemplatePath)
c.captchaTemplate = html
c.gracePeriodSeconds = gracePeriodSeconds
@@ -87,6 +89,9 @@ func (c *Client) ServeHTTP(rw http.ResponseWriter, r *http.Request, remoteIP str
return
}
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
if c.remediationCustomHeader != "" {
rw.Header().Set(c.remediationCustomHeader, "captcha")
}
rw.WriteHeader(http.StatusOK)
err = c.captchaTemplate.Execute(rw, map[string]string{
"SiteKey": c.siteKey,
@@ -133,7 +138,7 @@ func (c *Client) Validate(r *http.Request) (bool, error) {
c.log.Error("captcha:Validate " + err.Error())
}
}()
if !strings.Contains(res.Header.Get("content-type"), "application/json") {
if !strings.Contains(res.Header.Get("Content-Type"), "application/json") {
c.log.Debug("captcha:Validate responseType:noJson")
return false, nil
}
+19 -6
View File
@@ -40,10 +40,13 @@ type Config struct {
CrowdsecMode string `json:"crowdsecMode,omitempty"`
CrowdsecAppsecEnabled bool `json:"crowdsecAppsecEnabled,omitempty"`
CrowdsecAppsecHost string `json:"crowdsecAppsecHost,omitempty"`
CrowdsecAppsecPath string `json:"crowdsecAppsecPath,omitempty"`
CrowdsecAppsecFailureBlock bool `json:"crowdsecAppsecFailureBlock,omitempty"`
CrowdsecAppsecUnreachableBlock bool `json:"crowdsecAppsecUnreachableBlock,omitempty"`
CrowdsecAppsecBodyLimit int64 `json:"crowdsecAppsecBodyLimit,omitempty"`
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
CrowdsecLapiPath string `json:"crowdsecLapiPath,omitempty"`
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
CrowdsecLapiKeyFile string `json:"crowdsecLapiKeyFile,omitempty"`
CrowdsecLapiTLSInsecureVerify bool `json:"crowdsecLapiTlsInsecureVerify,omitempty"`
@@ -62,6 +65,7 @@ type Config struct {
UpdateMaxFailure int `json:"updateMaxFailure,omitempty"`
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
RemediationHeadersCustomName string `json:"remediationHeadersCustomName,omitempty"`
ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"`
ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"`
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
@@ -70,6 +74,7 @@ type Config struct {
RedisCachePassword string `json:"redisCachePassword,omitempty"`
RedisCachePasswordFile string `json:"redisCachePasswordFile,omitempty"`
RedisCacheDatabase string `json:"redisCacheDatabase,omitempty"`
RedisCacheUnreachableBlock bool `json:"redisCacheUnreachableBlock,omitempty"`
BanHTMLFilePath string `json:"banHtmlFilePath,omitempty"`
CaptchaHTMLFilePath string `json:"captchaHtmlFilePath,omitempty"`
CaptchaProvider string `json:"captchaProvider,omitempty"`
@@ -97,10 +102,13 @@ func New() *Config {
CrowdsecMode: LiveMode,
CrowdsecAppsecEnabled: false,
CrowdsecAppsecHost: "crowdsec:7422",
CrowdsecAppsecPath: "/",
CrowdsecAppsecFailureBlock: true,
CrowdsecAppsecUnreachableBlock: true,
CrowdsecAppsecBodyLimit: 10485760,
CrowdsecLapiScheme: HTTP,
CrowdsecLapiHost: "crowdsec:8080",
CrowdsecLapiPath: "/",
CrowdsecLapiKey: "",
CrowdsecLapiTLSInsecureVerify: false,
UpdateIntervalSeconds: 60,
@@ -113,6 +121,7 @@ func New() *Config {
CaptchaGracePeriodSeconds: 1800,
CaptchaHTMLFilePath: "/captcha.html",
BanHTMLFilePath: "",
RemediationHeadersCustomName: "",
ForwardedHeadersCustomName: "X-Forwarded-For",
ForwardedHeadersTrustedIPs: []string{},
ClientTrustedIPs: []string{},
@@ -120,6 +129,7 @@ func New() *Config {
RedisCacheHost: "redis:6379",
RedisCachePassword: "",
RedisCacheDatabase: "",
RedisCacheUnreachableBlock: true,
}
}
@@ -215,11 +225,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
}
if err := validateURL("CrowdsecAppsec", config.CrowdsecLapiScheme, config.CrowdsecAppsecHost); err != nil {
if err := validateURL("CrowdsecAppsec", config.CrowdsecLapiScheme, config.CrowdsecAppsecHost, config.CrowdsecAppsecPath); err != nil {
return err
}
@@ -255,11 +265,11 @@ 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}
func validateURL(variable, scheme, host, path string) error {
// 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, Path: path}
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
}
@@ -326,6 +336,9 @@ func validateParamsRequired(config *Config) error {
if config.UpdateMaxFailure < -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) {
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.
func (l *Log) Info(str string) {
l.logInfo.Printf(str)
l.logInfo.Printf("%s", str)
}
// Debug log to Stdout.
func (l *Log) Debug(str string) {
l.logDebug.Printf(str)
l.logDebug.Printf("%s", str)
}
// Error log to Stderr.
func (l *Log) Error(str string) {
l.logError.Printf(str)
l.logError.Printf("%s", str)
}