Compare commits

..
5 Commits
Author SHA1 Message Date
maxlerebourg de7e382fde Add env for RemediationStatusCode (#250)
*  Add env for defaultStatusCode

* 📝 doc

* change name of the parameter

* 🔧 Add config check

* fix lint
2025-07-01 21:59:12 +02:00
gptlang abae855d9e Set traefik logs to json so it can actually be parsed (#235) 2025-05-07 11:22:27 +02:00
Lluís D. 4bb6e830dc Fix query of the GET /decisions endpoint (#236)
- banned=true query parameter doesn't exist based on LAPI
  dcoumentation, remove it
2025-04-28 16:12:54 +02:00
mathieuHa 78869ecf77 🔧 Add support for logLevel in any case (#231) 2025-04-06 10:41:16 +02:00
maxlerebourg 7f99266f99 📝 doc: rename RedisUnreachableBlock to RedisCacheUnreachableBlock (#230) 2025-04-05 11:34:06 +02:00
6 changed files with 33 additions and 14 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ linters-settings:
locale: US locale: US
funlen: funlen:
lines: -1 lines: -1
statements: 50 statements: 60
godox: godox:
keywords: keywords:
- FIXME - FIXME
+7 -6
View File
@@ -419,7 +419,7 @@ _By careful when you upgrade to >1.4.x_
- string - string
- default: "" - default: ""
- Database selection for the Redis service - Database selection for the Redis service
- RedisUnreachableBlock - RedisCacheUnreachableBlock
- bool - bool
- default: true - default: true
- Block request when Redis is unreachable (if Redis is unreachable, 1-second delay is added to each request) - Block request when Redis is unreachable (if Redis is unreachable, 1-second delay is added to each request)
@@ -439,6 +439,10 @@ _By careful when you upgrade to >1.4.x_
- int64 - int64
- default: 60 - default: 60
- Used only in `live` mode, maximum decision duration - Used only in `live` mode, maximum decision duration
- RemediationStatusCode
- int
- default: 403
- HTTP status code for banned user (not captcha)
- CrowdsecCapiMachineId - CrowdsecCapiMachineId
- string - string
- Used only in `alone` mode, login for Crowdsec CAPI - Used only in `alone` mode, login for Crowdsec CAPI
@@ -518,6 +522,7 @@ http:
updateIntervalSeconds: 60 updateIntervalSeconds: 60
updateMaxFailure: 0 updateMaxFailure: 0
defaultDecisionSeconds: 60 defaultDecisionSeconds: 60
remediationStatusCode: 403
httpTimeoutSeconds: 10 httpTimeoutSeconds: 10
crowdsecMode: live crowdsecMode: live
crowdsecAppsecEnabled: false crowdsecAppsecEnabled: false
@@ -527,7 +532,6 @@ http:
crowdsecAppsecUnreachableBlock: true crowdsecAppsecUnreachableBlock: true
crowdsecAppsecBodyLimit: 10485760 crowdsecAppsecBodyLimit: 10485760
crowdsecLapiKey: privateKey-foo crowdsecLapiKey: privateKey-foo
crowdsecLapiKeyFile: /etc/traefik/cs-privateKey-foo
crowdsecLapiScheme: http crowdsecLapiScheme: http
crowdsecLapiHost: crowdsec:8080 crowdsecLapiHost: crowdsec:8080
crowdsecLapiPath: "/" crowdsecLapiPath: "/"
@@ -556,7 +560,6 @@ http:
... ...
Q0veeNzBQXg1f/JxfeA39IDIX1kiCf71tGlT Q0veeNzBQXg1f/JxfeA39IDIX1kiCf71tGlT
-----END CERTIFICATE----- -----END CERTIFICATE-----
crowdsecLapiTLSCertificateAuthorityFile: /etc/traefik/crowdsec-certs/ca.pem
crowdsecLapiTLSCertificateBouncer: |- crowdsecLapiTLSCertificateBouncer: |-
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
MIIEHjCCAwagAwIBAgIUOBTs1eqkaAUcPplztUr2xRapvNAwDQYJKoZIhvcNAQEL MIIEHjCCAwagAwIBAgIUOBTs1eqkaAUcPplztUr2xRapvNAwDQYJKoZIhvcNAQEL
@@ -564,14 +567,12 @@ http:
RaXAnYYUVRblS1jmePemh388hFxbmrpG2pITx8B5FMULqHoj11o2Rl0gSV6tHIHz RaXAnYYUVRblS1jmePemh388hFxbmrpG2pITx8B5FMULqHoj11o2Rl0gSV6tHIHz
N2U= N2U=
-----END CERTIFICATE----- -----END CERTIFICATE-----
crowdsecLapiTLSCertificateBouncerFile: /etc/traefik/crowdsec-certs/bouncer.pem
crowdsecLapiTLSCertificateBouncerKey: |- crowdsecLapiTLSCertificateBouncerKey: |-
-----BEGIN RSA PRIVATE KEY----- -----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAtYQnbJqifH+ZymePylDxGGLIuxzcAUU4/ajNj+qRAdI/Ux3d MIIEogIBAAKCAQEAtYQnbJqifH+ZymePylDxGGLIuxzcAUU4/ajNj+qRAdI/Ux3d
... ...
ic5cDRo6/VD3CS3MYzyBcibaGaV34nr0G/pI+KEqkYChzk/PZRA= ic5cDRo6/VD3CS3MYzyBcibaGaV34nr0G/pI+KEqkYChzk/PZRA=
-----END RSA PRIVATE KEY----- -----END RSA PRIVATE KEY-----
crowdsecLapiTLSCertificateBouncerKeyFile: /etc/traefik/crowdsec-certs/bouncer-key.pem
captchaProvider: hcaptcha captchaProvider: hcaptcha
captchaSiteKey: FIXME captchaSiteKey: FIXME
captchaSecretKey: FIXME captchaSecretKey: FIXME
@@ -582,7 +583,7 @@ http:
#### Fill variable with value of file #### Fill variable with value of file
`CrowdsecLapiTlsCertificateBouncerKey`, `CrowdsecLapiTlsCertificateBouncer`, `CrowdsecLapiTlsCertificateAuthority`, `CrowdsecCapiMachineId`, `CrowdsecCapiPassword`, `CrowdsecLapiKey`, `CaptchaSiteKey` and `CaptchaSecretKey` can be provided with the content as raw or through a file path that Traefik can read. `CrowdsecLapiTlsCertificateBouncerKey`, `CrowdsecLapiTlsCertificateBouncer`, `CrowdsecLapiTlsCertificateAuthority`, `CrowdsecCapiMachineId`, `CrowdsecCapiPassword`, `CrowdsecLapiKey`, `CaptchaSiteKey`, `CaptchaSecretKey` and `RedisCachePassword` can be provided with the content as raw or through a file path that Traefik can read.
The file variable will be used as preference if both content and file are provided for the same variable. The file variable will be used as preference if both content and file are provided for the same variable.
Format is: Format is:
+6 -3
View File
@@ -77,6 +77,7 @@ type Bouncer struct {
updateInterval int64 updateInterval int64
updateMaxFailure int updateMaxFailure int
defaultDecisionTimeout int64 defaultDecisionTimeout int64
remediationStatusCode int
remediationCustomHeader string remediationCustomHeader string
forwardedCustomHeader string forwardedCustomHeader string
crowdsecStreamRoute string crowdsecStreamRoute string
@@ -93,6 +94,7 @@ type Bouncer struct {
// New creates the crowdsec bouncer plugin. // New creates the crowdsec bouncer plugin.
func New(_ context.Context, next http.Handler, config *configuration.Config, name string) (http.Handler, error) { func New(_ context.Context, next http.Handler, config *configuration.Config, name string) (http.Handler, error) {
config.LogLevel = strings.ToUpper(config.LogLevel)
log := logger.New(config.LogLevel, config.LogFilePath) log := logger.New(config.LogLevel, config.LogFilePath)
err := configuration.ValidateParams(config) err := configuration.ValidateParams(config)
if err != nil { if err != nil {
@@ -169,6 +171,7 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
remediationCustomHeader: config.RemediationHeadersCustomName, remediationCustomHeader: config.RemediationHeadersCustomName,
forwardedCustomHeader: config.ForwardedHeadersCustomName, forwardedCustomHeader: config.ForwardedHeadersCustomName,
defaultDecisionTimeout: config.DefaultDecisionSeconds, defaultDecisionTimeout: config.DefaultDecisionSeconds,
remediationStatusCode: config.RemediationStatusCode,
redisUnreachableBlock: config.RedisCacheUnreachableBlock, redisUnreachableBlock: config.RedisCacheUnreachableBlock,
banTemplateString: banTemplateString, banTemplateString: banTemplateString,
crowdsecStreamRoute: crowdsecStreamRoute, crowdsecStreamRoute: crowdsecStreamRoute,
@@ -354,11 +357,11 @@ func handleBanServeHTTP(bouncer *Bouncer, rw http.ResponseWriter) {
rw.Header().Set(bouncer.remediationCustomHeader, "ban") rw.Header().Set(bouncer.remediationCustomHeader, "ban")
} }
if bouncer.banTemplateString == "" { if bouncer.banTemplateString == "" {
rw.WriteHeader(http.StatusForbidden) rw.WriteHeader(bouncer.remediationStatusCode)
return return
} }
rw.Header().Set("Content-Type", "text/html; charset=utf-8") rw.Header().Set("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusForbidden) rw.WriteHeader(bouncer.remediationStatusCode)
_, err := fmt.Fprint(rw, bouncer.banTemplateString) _, err := fmt.Fprint(rw, bouncer.banTemplateString)
if err != nil { if err != nil {
bouncer.log.Error("handleBanServeHTTP could not write template to ResponseWriter") bouncer.log.Error("handleBanServeHTTP could not write template to ResponseWriter")
@@ -427,7 +430,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
Scheme: bouncer.crowdsecScheme, Scheme: bouncer.crowdsecScheme,
Host: bouncer.crowdsecHost, Host: bouncer.crowdsecHost,
Path: bouncer.crowdsecPath + crowdsecLapiRoute, Path: bouncer.crowdsecPath + crowdsecLapiRoute,
RawQuery: fmt.Sprintf("ip=%v&banned=true", remoteIP), RawQuery: fmt.Sprintf("ip=%v", remoteIP),
} }
body, err := crowdsecQuery(bouncer, routeURL.String(), false) body, err := crowdsecQuery(bouncer, routeURL.String(), false)
if err != nil { if err != nil {
+1
View File
@@ -6,6 +6,7 @@ logs:
level: DEBUG level: DEBUG
access: access:
enabled: true enabled: true
format: json
fields: fields:
headers: headers:
defaultmode: keep defaultmode: keep
+7 -2
View File
@@ -68,6 +68,7 @@ type Config struct {
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"` UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
UpdateMaxFailure int `json:"updateMaxFailure,omitempty"` UpdateMaxFailure int `json:"updateMaxFailure,omitempty"`
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"` DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
RemediationStatusCode int `json:"remediationStatusCode,omitempty"`
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"` HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
RemediationHeadersCustomName string `json:"remediationHeadersCustomName,omitempty"` RemediationHeadersCustomName string `json:"remediationHeadersCustomName,omitempty"`
ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"` ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"`
@@ -119,6 +120,7 @@ func New() *Config {
UpdateIntervalSeconds: 60, UpdateIntervalSeconds: 60,
UpdateMaxFailure: 0, UpdateMaxFailure: 0,
DefaultDecisionSeconds: 60, DefaultDecisionSeconds: 60,
RemediationStatusCode: http.StatusForbidden,
HTTPTimeoutSeconds: 10, HTTPTimeoutSeconds: 10,
CaptchaProvider: "", CaptchaProvider: "",
CaptchaSiteKey: "", CaptchaSiteKey: "",
@@ -268,8 +270,8 @@ func ValidateParams(config *Config) error {
} }
// Check logging configuration // Check logging configuration
// to upper allow of anycase of log level
if !contains([]string{LogERROR, LogDEBUG, LogINFO}, config.LogLevel) { if !contains([]string{LogERROR, LogDEBUG, LogINFO}, strings.ToUpper(config.LogLevel)) {
return fmt.Errorf("LogLevel should be one of (%s,%s,%s)", LogDEBUG, LogINFO, LogERROR) return fmt.Errorf("LogLevel should be one of (%s,%s,%s)", LogDEBUG, LogINFO, LogERROR)
} }
if config.LogFilePath != "" { if config.LogFilePath != "" {
@@ -355,6 +357,9 @@ func validateParamsRequired(config *Config) error {
if config.CrowdsecAppsecBodyLimit < 0 { if config.CrowdsecAppsecBodyLimit < 0 {
return errors.New("CrowdsecAppsecBodyLimit: cannot be less than 0") return errors.New("CrowdsecAppsecBodyLimit: cannot be less than 0")
} }
if config.RemediationStatusCode < 100 || config.RemediationStatusCode >= 600 {
return errors.New("RemediationStatusCode: cannot be less than 100 and more than 600")
}
if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode, AppsecMode}, config.CrowdsecMode) { if !contains([]string{NoneMode, LiveMode, StreamMode, AloneMode, AppsecMode}, config.CrowdsecMode) {
return errors.New("CrowdsecMode: must be one of 'none', 'live', 'stream', 'alone' or 'appsec'") return errors.New("CrowdsecMode: must be one of 'none', 'live', 'stream', 'alone' or 'appsec'")
+11 -2
View File
@@ -85,8 +85,14 @@ func Test_ValidateParams(t *testing.T) {
cfg6 := getMinimalConfig() cfg6 := getMinimalConfig()
cfg6.CrowdsecLapiScheme = HTTPS cfg6.CrowdsecLapiScheme = HTTPS
cfg6.CrowdsecLapiTLSInsecureVerify = true cfg6.CrowdsecLapiTLSInsecureVerify = true
cfg7 := getMinimalConfig()
cfg7.CrowdsecLapiScheme = HTTPS
cfg8 := getMinimalConfig() cfg8 := getMinimalConfig()
cfg8.CrowdsecLapiScheme = HTTPS cfg8.LogLevel = LogINFO
cfg9 := getMinimalConfig()
cfg9.LogLevel = "info"
cfg10 := getMinimalConfig()
cfg10.LogLevel = "Warning"
type args struct { type args struct {
config *Config config *Config
} }
@@ -104,7 +110,10 @@ func Test_ValidateParams(t *testing.T) {
{name: "Not validate a bad clients ips", args: args{config: cfg5}, wantErr: true}, {name: "Not validate a bad clients ips", args: args{config: cfg5}, wantErr: true},
// HTTPS enabled // HTTPS enabled
{name: "Validate https config with insecure verify", args: args{config: cfg6}, wantErr: false}, {name: "Validate https config with insecure verify", args: args{config: cfg6}, wantErr: false},
{name: "Not validate https without cert authority", args: args{config: cfg8}, wantErr: true}, {name: "Not validate https without cert authority", args: args{config: cfg7}, wantErr: true},
{name: "Valid log level uppercase INFO", args: args{config: cfg8}, wantErr: false},
{name: "Valid log level lowercase info", args: args{config: cfg9}, wantErr: false},
{name: "Invalid log level Warning", args: args{config: cfg10}, wantErr: true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {