mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-09-02 20:28:50 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9d83f2097 | ||
|
|
e20ccc5d0c | ||
|
|
50beb4294f | ||
|
|
4ab4f3f183 | ||
|
|
2aac531ea7 | ||
|
|
e8e60c958f | ||
|
|
a2d3708bc3 | ||
|
|
65a2f79fb3 | ||
|
|
a2ecc95dc9 |
@@ -24,13 +24,13 @@ 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@v5
|
uses: actions/setup-go@v6
|
||||||
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@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
path: go/src/github.com/${{ github.repository }}
|
path: go/src/github.com/${{ github.repository }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|||||||
@@ -497,6 +497,10 @@ make run
|
|||||||
- string
|
- string
|
||||||
- default: ""
|
- default: ""
|
||||||
- Path where the ban html file is stored (default empty ""=disabled)
|
- Path where the ban html file is stored (default empty ""=disabled)
|
||||||
|
- TraceHeadersCustomName
|
||||||
|
- string
|
||||||
|
- default: ""
|
||||||
|
- Request Header name whose value to inject in ban HTML response (default empty ""=disabled)
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
@@ -550,6 +554,7 @@ http:
|
|||||||
httpTimeoutSeconds: 10
|
httpTimeoutSeconds: 10
|
||||||
crowdsecMode: live
|
crowdsecMode: live
|
||||||
crowdsecAppsecEnabled: false
|
crowdsecAppsecEnabled: false
|
||||||
|
crowdsecAppsecScheme: ""
|
||||||
crowdsecAppsecHost: crowdsec:7422
|
crowdsecAppsecHost: crowdsec:7422
|
||||||
crowdsecAppsecPath: "/"
|
crowdsecAppsecPath: "/"
|
||||||
crowdsecAppsecFailureBlock: true
|
crowdsecAppsecFailureBlock: true
|
||||||
@@ -603,6 +608,7 @@ http:
|
|||||||
captchaGracePeriodSeconds: 1800
|
captchaGracePeriodSeconds: 1800
|
||||||
captchaHTMLFilePath: /captcha.html
|
captchaHTMLFilePath: /captcha.html
|
||||||
banHTMLFilePath: /ban.html
|
banHTMLFilePath: /ban.html
|
||||||
|
traceHeadersCustomName: X-Request-ID
|
||||||
metricsUpdateIntervalSeconds: 600
|
metricsUpdateIntervalSeconds: 600
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+80
-42
@@ -9,6 +9,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
htmltemplate "html/template"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -84,8 +85,10 @@ type Bouncer struct {
|
|||||||
|
|
||||||
enabled bool
|
enabled bool
|
||||||
appsecEnabled bool
|
appsecEnabled bool
|
||||||
|
appsecScheme string
|
||||||
appsecHost string
|
appsecHost string
|
||||||
appsecPath string
|
appsecPath string
|
||||||
|
appsecKey string
|
||||||
appsecFailureBlock bool
|
appsecFailureBlock bool
|
||||||
appsecUnreachableBlock bool
|
appsecUnreachableBlock bool
|
||||||
appsecBodyLimit int64
|
appsecBodyLimit int64
|
||||||
@@ -106,10 +109,12 @@ type Bouncer struct {
|
|||||||
crowdsecStreamRoute string
|
crowdsecStreamRoute string
|
||||||
crowdsecHeader string
|
crowdsecHeader string
|
||||||
redisUnreachableBlock bool
|
redisUnreachableBlock bool
|
||||||
banTemplateString string
|
banTemplate *htmltemplate.Template
|
||||||
|
traceCustomHeader string
|
||||||
clientPoolStrategy *ip.PoolStrategy
|
clientPoolStrategy *ip.PoolStrategy
|
||||||
serverPoolStrategy *ip.PoolStrategy
|
serverPoolStrategy *ip.PoolStrategy
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
|
httpAppsecClient *http.Client
|
||||||
cacheClient *cache.Client
|
cacheClient *cache.Client
|
||||||
captchaClient *captcha.Client
|
captchaClient *captcha.Client
|
||||||
log *logger.Log
|
log *logger.Log
|
||||||
@@ -129,6 +134,17 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
|
|||||||
|
|
||||||
serverChecker, _ := ip.NewChecker(log, config.ForwardedHeadersTrustedIPs)
|
serverChecker, _ := ip.NewChecker(log, config.ForwardedHeadersTrustedIPs)
|
||||||
clientChecker, _ := ip.NewChecker(log, config.ClientTrustedIPs)
|
clientChecker, _ := ip.NewChecker(log, config.ClientTrustedIPs)
|
||||||
|
tlsAppsecConfig, err := configuration.GetTLSConfigCrowdsec(config, log, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("New:getTLSConfigCrowdsec fail to get tlsAppsecConfig " + err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
apiAppsecKey, errAppsecKey := configuration.GetVariable(config, "CrowdsecAppsecKey")
|
||||||
|
if errAppsecKey != nil && len(tlsAppsecConfig.Certificates) == 0 {
|
||||||
|
log.Error("New:crowdsecLapiKey fail to get CrowdsecAppsecKey and no client certificate setup " + errAppsecKey.Error())
|
||||||
|
return nil, errAppsecKey
|
||||||
|
}
|
||||||
|
config.CrowdsecAppsecKey = apiAppsecKey
|
||||||
|
|
||||||
var tlsConfig *tls.Config
|
var tlsConfig *tls.Config
|
||||||
crowdsecStreamRoute := ""
|
crowdsecStreamRoute := ""
|
||||||
@@ -139,36 +155,29 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
|
|||||||
config.CrowdsecLapiScheme = configuration.HTTPS
|
config.CrowdsecLapiScheme = configuration.HTTPS
|
||||||
config.CrowdsecLapiHost = crowdsecCapiHost
|
config.CrowdsecLapiHost = crowdsecCapiHost
|
||||||
config.CrowdsecLapiPath = "/"
|
config.CrowdsecLapiPath = "/"
|
||||||
config.CrowdsecAppsecEnabled = false
|
config.CrowdsecAppsecEnabled = config.CrowdsecAppsecEnabled && config.CrowdsecAppsecScheme != ""
|
||||||
config.UpdateIntervalSeconds = 7200 // 2 hours
|
config.UpdateIntervalSeconds = 7200 // 2 hours
|
||||||
crowdsecStreamRoute = crowdsecCapiStreamRoute
|
crowdsecStreamRoute = crowdsecCapiStreamRoute
|
||||||
crowdsecHeader = crowdsecCapiHeader
|
crowdsecHeader = crowdsecCapiHeader
|
||||||
} else {
|
} else {
|
||||||
crowdsecStreamRoute = crowdsecLapiStreamRoute
|
crowdsecStreamRoute = crowdsecLapiStreamRoute
|
||||||
crowdsecHeader = crowdsecLapiHeader
|
crowdsecHeader = crowdsecLapiHeader
|
||||||
tlsConfig, err = configuration.GetTLSConfigCrowdsec(config, log)
|
tlsConfig, err = configuration.GetTLSConfigCrowdsec(config, log, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("New:getTLSConfigCrowdsec fail to get tlsConfig " + err.Error())
|
log.Error("New:getTLSConfigCrowdsec fail to get tlsConfig " + err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
apiKey, errAPIKey := configuration.GetVariable(config, "CrowdsecLapiKey")
|
apiKey, errKey := configuration.GetVariable(config, "CrowdsecLapiKey")
|
||||||
if errAPIKey != nil && len(tlsConfig.Certificates) == 0 {
|
if errKey != 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 " + errKey.Error())
|
||||||
return nil, errAPIKey
|
return nil, errKey
|
||||||
}
|
}
|
||||||
config.CrowdsecLapiKey = apiKey
|
config.CrowdsecLapiKey = apiKey
|
||||||
}
|
}
|
||||||
|
|
||||||
var banTemplateString string
|
var banTemplate *htmltemplate.Template
|
||||||
if config.BanHTMLFilePath != "" {
|
if config.BanHTMLFilePath != "" {
|
||||||
var buf bytes.Buffer
|
banTemplate, _ = configuration.GetHTMLTemplate(config.BanHTMLFilePath)
|
||||||
banTemplate, _ := configuration.GetHTMLTemplate(config.BanHTMLFilePath)
|
|
||||||
err = banTemplate.Execute(&buf, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("New:banTemplate is bad formatted " + err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
banTemplateString = buf.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bouncer := &Bouncer{
|
bouncer := &Bouncer{
|
||||||
@@ -179,8 +188,10 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
|
|||||||
enabled: config.Enabled,
|
enabled: config.Enabled,
|
||||||
crowdsecMode: config.CrowdsecMode,
|
crowdsecMode: config.CrowdsecMode,
|
||||||
appsecEnabled: config.CrowdsecAppsecEnabled,
|
appsecEnabled: config.CrowdsecAppsecEnabled,
|
||||||
|
appsecScheme: config.CrowdsecAppsecScheme,
|
||||||
appsecHost: config.CrowdsecAppsecHost,
|
appsecHost: config.CrowdsecAppsecHost,
|
||||||
appsecPath: config.CrowdsecAppsecPath,
|
appsecPath: config.CrowdsecAppsecPath,
|
||||||
|
appsecKey: config.CrowdsecAppsecKey,
|
||||||
appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
|
appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
|
||||||
appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock,
|
appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock,
|
||||||
appsecBodyLimit: config.CrowdsecAppsecBodyLimit,
|
appsecBodyLimit: config.CrowdsecAppsecBodyLimit,
|
||||||
@@ -198,7 +209,8 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
|
|||||||
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
||||||
remediationStatusCode: config.RemediationStatusCode,
|
remediationStatusCode: config.RemediationStatusCode,
|
||||||
redisUnreachableBlock: config.RedisCacheUnreachableBlock,
|
redisUnreachableBlock: config.RedisCacheUnreachableBlock,
|
||||||
banTemplateString: banTemplateString,
|
banTemplate: banTemplate,
|
||||||
|
traceCustomHeader: config.TraceHeadersCustomName,
|
||||||
crowdsecStreamRoute: crowdsecStreamRoute,
|
crowdsecStreamRoute: crowdsecStreamRoute,
|
||||||
crowdsecHeader: crowdsecHeader,
|
crowdsecHeader: crowdsecHeader,
|
||||||
log: log,
|
log: log,
|
||||||
@@ -216,6 +228,14 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
|
|||||||
},
|
},
|
||||||
Timeout: time.Duration(config.HTTPTimeoutSeconds) * time.Second,
|
Timeout: time.Duration(config.HTTPTimeoutSeconds) * time.Second,
|
||||||
},
|
},
|
||||||
|
httpAppsecClient: &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
MaxIdleConns: 10,
|
||||||
|
IdleConnTimeout: 30 * time.Second,
|
||||||
|
TLSClientConfig: tlsAppsecConfig,
|
||||||
|
},
|
||||||
|
Timeout: time.Duration(config.HTTPTimeoutSeconds) * time.Second,
|
||||||
|
},
|
||||||
cacheClient: &cache.Client{},
|
cacheClient: &cache.Client{},
|
||||||
captchaClient: &captcha.Client{},
|
captchaClient: &captcha.Client{},
|
||||||
}
|
}
|
||||||
@@ -296,13 +316,13 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.forwardedCustomHeader)
|
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)
|
bouncer.handleBanServeHTTP(rw, req, remoteIP, configuration.ReasonTECH)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isTrusted, err := bouncer.clientPoolStrategy.Checker.Contains(remoteIP)
|
isTrusted, err := bouncer.clientPoolStrategy.Checker.Contains(remoteIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bouncer.log.Error(fmt.Sprintf("ServeHTTP:checkerContains ip:%s %s", remoteIP, err.Error()))
|
bouncer.log.Error(fmt.Sprintf("ServeHTTP:checkerContains ip:%s %s", remoteIP, err.Error()))
|
||||||
handleBanServeHTTP(bouncer, rw)
|
bouncer.handleBanServeHTTP(rw, req, remoteIP, configuration.ReasonTECH)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// if our IP is in the trusted list we bypass the next checks
|
// if our IP is in the trusted list we bypass the next checks
|
||||||
@@ -313,7 +333,7 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if bouncer.crowdsecMode == configuration.AppsecMode {
|
if bouncer.crowdsecMode == configuration.AppsecMode {
|
||||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
bouncer.handleNextServeHTTP(rw, req, remoteIP)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,20 +345,20 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:Get ip:%s isBanned:false %s", remoteIP, cacheErrString))
|
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:Get ip:%s isBanned:false %s", remoteIP, cacheErrString))
|
||||||
if !bouncer.redisUnreachableBlock && cacheErrString == cache.CacheUnreachable {
|
if !bouncer.redisUnreachableBlock && cacheErrString == cache.CacheUnreachable {
|
||||||
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s redisUnreachable=true", remoteIP))
|
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s redisUnreachable=true", remoteIP))
|
||||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
bouncer.handleNextServeHTTP(rw, req, remoteIP)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cacheErrString != cache.CacheMiss {
|
if cacheErrString != cache.CacheMiss {
|
||||||
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s %s", remoteIP, cacheErrString))
|
bouncer.log.Error(fmt.Sprintf("ServeHTTP:Get ip:%s %s", remoteIP, cacheErrString))
|
||||||
handleBanServeHTTP(bouncer, rw)
|
bouncer.handleBanServeHTTP(rw, req, remoteIP, configuration.ReasonTECH)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bouncer.log.Debug(fmt.Sprintf("ServeHTTP ip:%s cache:hit isBanned:%v", remoteIP, value))
|
bouncer.log.Debug(fmt.Sprintf("ServeHTTP ip:%s cache:hit isBanned:%v", remoteIP, value))
|
||||||
if value == cache.NoBannedValue {
|
if value == cache.NoBannedValue {
|
||||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
bouncer.handleNextServeHTTP(rw, req, remoteIP)
|
||||||
} else {
|
} else {
|
||||||
handleRemediationServeHTTP(bouncer, remoteIP, value, rw, req)
|
bouncer.handleRemediationServeHTTP(rw, req, remoteIP, value)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -347,18 +367,18 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
// Right here if we cannot join the stream we forbid the request to go on.
|
// Right here if we cannot join the stream we forbid the request to go on.
|
||||||
if bouncer.crowdsecMode == configuration.StreamMode || bouncer.crowdsecMode == configuration.AloneMode {
|
if bouncer.crowdsecMode == configuration.StreamMode || bouncer.crowdsecMode == configuration.AloneMode {
|
||||||
if isCrowdsecStreamHealthy {
|
if isCrowdsecStreamHealthy {
|
||||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
bouncer.handleNextServeHTTP(rw, req, remoteIP)
|
||||||
} else {
|
} else {
|
||||||
bouncer.log.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s updateFailure:%d", remoteIP, updateFailure))
|
bouncer.log.Debug(fmt.Sprintf("ServeHTTP isCrowdsecStreamHealthy:false ip:%s updateFailure:%d", remoteIP, updateFailure))
|
||||||
handleBanServeHTTP(bouncer, rw)
|
bouncer.handleBanServeHTTP(rw, req, remoteIP, configuration.ReasonTECH)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value, err := handleNoStreamCache(bouncer, remoteIP)
|
value, err := handleNoStreamCache(bouncer, remoteIP)
|
||||||
if value == cache.NoBannedValue {
|
if value == cache.NoBannedValue {
|
||||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
bouncer.handleNextServeHTTP(rw, req, remoteIP)
|
||||||
} else {
|
} else {
|
||||||
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:%v %s", remoteIP, value, err.Error()))
|
bouncer.log.Debug(fmt.Sprintf("ServeHTTP:handleNoStreamCache ip:%s isBanned:%v %s", remoteIP, value, err.Error()))
|
||||||
handleRemediationServeHTTP(bouncer, remoteIP, value, rw, req)
|
bouncer.handleRemediationServeHTTP(rw, req, remoteIP, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,43 +412,61 @@ type Login struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// To append Headers we need to call rw.WriteHeader after set any header.
|
// To append Headers we need to call rw.WriteHeader after set any header.
|
||||||
func handleBanServeHTTP(bouncer *Bouncer, rw http.ResponseWriter) {
|
func (bouncer *Bouncer) handleBanServeHTTP(rw http.ResponseWriter, req *http.Request, remoteIP, reason string) {
|
||||||
atomic.AddInt64(&blockedRequests, 1)
|
atomic.AddInt64(&blockedRequests, 1)
|
||||||
|
|
||||||
if bouncer.remediationCustomHeader != "" {
|
if bouncer.remediationCustomHeader != "" {
|
||||||
rw.Header().Set(bouncer.remediationCustomHeader, "ban")
|
rw.Header().Set(bouncer.remediationCustomHeader, "ban")
|
||||||
}
|
}
|
||||||
if bouncer.banTemplateString == "" {
|
if bouncer.banTemplate == nil {
|
||||||
rw.WriteHeader(bouncer.remediationStatusCode)
|
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(bouncer.remediationStatusCode)
|
rw.WriteHeader(bouncer.remediationStatusCode)
|
||||||
_, err := fmt.Fprint(rw, bouncer.banTemplateString)
|
|
||||||
|
if req.Method == http.MethodHead {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
templateData := map[string]string{
|
||||||
|
"RemediationReason": reason,
|
||||||
|
"ClientIP": remoteIP,
|
||||||
|
}
|
||||||
|
|
||||||
|
if bouncer.traceCustomHeader != "" {
|
||||||
|
headerVal := req.Header.Get(bouncer.traceCustomHeader)
|
||||||
|
|
||||||
|
if headerVal != "" {
|
||||||
|
templateData["TraceID"] = headerVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := bouncer.banTemplate.Execute(rw, templateData)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bouncer.log.Error("handleBanServeHTTP could not write template to ResponseWriter")
|
bouncer.log.Error("handleBanServeHTTP banTemplateServe " + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRemediationServeHTTP(bouncer *Bouncer, remoteIP, remediation string, rw http.ResponseWriter, req *http.Request) {
|
func (bouncer *Bouncer) handleRemediationServeHTTP(rw http.ResponseWriter, req *http.Request, remoteIP, remediation string) {
|
||||||
bouncer.log.Debug(fmt.Sprintf("handleRemediationServeHTTP ip:%s remediation:%s", remoteIP, remediation))
|
bouncer.log.Debug(fmt.Sprintf("handleRemediationServeHTTP ip:%s remediation:%s", remoteIP, remediation))
|
||||||
if bouncer.captchaClient.Valid && remediation == cache.CaptchaValue {
|
if bouncer.captchaClient.Valid && remediation == cache.CaptchaValue && req.Method != http.MethodHead {
|
||||||
if bouncer.captchaClient.Check(remoteIP) {
|
if bouncer.captchaClient.Check(remoteIP) {
|
||||||
handleNextServeHTTP(bouncer, remoteIP, rw, req)
|
bouncer.handleNextServeHTTP(rw, req, remoteIP)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
atomic.AddInt64(&blockedRequests, 1) // If we serve a captcha that should count as a dropped request.
|
atomic.AddInt64(&blockedRequests, 1) // If we serve a captcha that should count as a dropped request.
|
||||||
bouncer.captchaClient.ServeHTTP(rw, req, remoteIP)
|
bouncer.captchaClient.ServeHTTP(rw, req, remoteIP)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleBanServeHTTP(bouncer, rw)
|
bouncer.handleBanServeHTTP(rw, req, remoteIP, configuration.ReasonLAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleNextServeHTTP(bouncer *Bouncer, remoteIP string, rw http.ResponseWriter, req *http.Request) {
|
func (bouncer *Bouncer) handleNextServeHTTP(rw http.ResponseWriter, req *http.Request, remoteIP string) {
|
||||||
if bouncer.appsecEnabled {
|
if bouncer.appsecEnabled {
|
||||||
if err := appsecQuery(bouncer, remoteIP, req); err != nil {
|
if err := appsecQuery(bouncer, remoteIP, req); err != nil {
|
||||||
bouncer.log.Debug(fmt.Sprintf("handleNextServeHTTP ip:%s isWaf:true %s", remoteIP, err.Error()))
|
bouncer.log.Debug(fmt.Sprintf("handleNextServeHTTP ip:%s isWaf:true %s", remoteIP, err.Error()))
|
||||||
handleBanServeHTTP(bouncer, rw)
|
bouncer.handleBanServeHTTP(rw, req, remoteIP, configuration.ReasonAPPSEC)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -524,7 +562,7 @@ func handleNoStreamCache(bouncer *Bouncer, remoteIP string) (string, error) {
|
|||||||
default:
|
default:
|
||||||
bouncer.log.Debug("handleStreamCache:unknownType " + decision.Type)
|
bouncer.log.Debug("handleStreamCache:unknownType " + decision.Type)
|
||||||
}
|
}
|
||||||
if isLiveMode {
|
if isLiveMode && bouncer.defaultDecisionTimeout > 0 {
|
||||||
durationSecond := int64(duration.Seconds())
|
durationSecond := int64(duration.Seconds())
|
||||||
if bouncer.defaultDecisionTimeout < durationSecond {
|
if bouncer.defaultDecisionTimeout < durationSecond {
|
||||||
durationSecond = bouncer.defaultDecisionTimeout
|
durationSecond = bouncer.defaultDecisionTimeout
|
||||||
@@ -658,7 +696,7 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string, data []byte) ([]byte, err
|
|||||||
|
|
||||||
func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
|
func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
|
||||||
routeURL := url.URL{
|
routeURL := url.URL{
|
||||||
Scheme: bouncer.crowdsecScheme,
|
Scheme: bouncer.appsecScheme,
|
||||||
Host: bouncer.appsecHost,
|
Host: bouncer.appsecHost,
|
||||||
Path: bouncer.appsecPath,
|
Path: bouncer.appsecPath,
|
||||||
}
|
}
|
||||||
@@ -683,14 +721,14 @@ func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error {
|
|||||||
req.Header.Add(key, value)
|
req.Header.Add(key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
req.Header.Set(crowdsecAppsecHeader, bouncer.crowdsecKey)
|
req.Header.Set(crowdsecAppsecHeader, bouncer.appsecKey)
|
||||||
req.Header.Set(crowdsecAppsecIPHeader, ip)
|
req.Header.Set(crowdsecAppsecIPHeader, ip)
|
||||||
req.Header.Set(crowdsecAppsecVerbHeader, httpReq.Method)
|
req.Header.Set(crowdsecAppsecVerbHeader, httpReq.Method)
|
||||||
req.Header.Set(crowdsecAppsecHostHeader, httpReq.Host)
|
req.Header.Set(crowdsecAppsecHostHeader, httpReq.Host)
|
||||||
req.Header.Set(crowdsecAppsecURIHeader, httpReq.URL.String())
|
req.Header.Set(crowdsecAppsecURIHeader, httpReq.URL.String())
|
||||||
req.Header.Set(crowdsecAppsecUserAgent, httpReq.Header.Get("User-Agent"))
|
req.Header.Set(crowdsecAppsecUserAgent, httpReq.Header.Get("User-Agent"))
|
||||||
|
|
||||||
res, err := bouncer.httpClient.Do(req)
|
res, err := bouncer.httpAppsecClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
bouncer.log.Error("appsecQuery:unreachable")
|
bouncer.log.Error("appsecQuery:unreachable")
|
||||||
if bouncer.appsecUnreachableBlock {
|
if bouncer.appsecUnreachableBlock {
|
||||||
|
|||||||
+146
@@ -2,6 +2,7 @@ package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
htmltemplate "html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -186,3 +187,148 @@ func Test_crowdsecQuery(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandleBanServeHTTPWithDifferentMethods(t *testing.T) {
|
||||||
|
html := "<html>You are banned</html>"
|
||||||
|
banTemplate, _ := htmltemplate.New("html").Parse(html)
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
banTemplate *htmltemplate.Template
|
||||||
|
expectBodyContent bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "GET request should have body with template",
|
||||||
|
method: http.MethodGet,
|
||||||
|
banTemplate: banTemplate,
|
||||||
|
expectBodyContent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HEAD request should NOT have body even with template",
|
||||||
|
method: http.MethodHead,
|
||||||
|
banTemplate: banTemplate,
|
||||||
|
expectBodyContent: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "POST request should have body with template",
|
||||||
|
method: http.MethodPost,
|
||||||
|
banTemplate: banTemplate,
|
||||||
|
expectBodyContent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PUT request should have body with template",
|
||||||
|
method: http.MethodPut,
|
||||||
|
banTemplate: banTemplate,
|
||||||
|
expectBodyContent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DELETE request should have body with template",
|
||||||
|
method: http.MethodDelete,
|
||||||
|
banTemplate: banTemplate,
|
||||||
|
expectBodyContent: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
bouncer := &Bouncer{
|
||||||
|
remediationStatusCode: http.StatusForbidden,
|
||||||
|
remediationCustomHeader: "X-Test-Remediation",
|
||||||
|
banTemplate: tt.banTemplate,
|
||||||
|
}
|
||||||
|
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
req := &http.Request{Method: tt.method}
|
||||||
|
bouncer.handleBanServeHTTP(rw, req, "0.0.0.0", "TEST")
|
||||||
|
|
||||||
|
// Check status code
|
||||||
|
if rw.Code != http.StatusForbidden {
|
||||||
|
t.Errorf("Expected status code 403, got %d", rw.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check custom header
|
||||||
|
headerValue := rw.Header().Get("X-Test-Remediation")
|
||||||
|
if headerValue != "ban" {
|
||||||
|
t.Errorf("Expected header X-Test-Remediation to be 'ban', got %s", headerValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check body content
|
||||||
|
body := rw.Body.String()
|
||||||
|
hasBodyContent := len(body) > 0
|
||||||
|
|
||||||
|
if hasBodyContent != tt.expectBodyContent {
|
||||||
|
t.Errorf("Method %s: expected body content: %v, got body content: %v (body: %q)",
|
||||||
|
tt.method, tt.expectBodyContent, hasBodyContent, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we expect body content, verify it matches template
|
||||||
|
if tt.expectBodyContent && body != html {
|
||||||
|
t.Errorf("Expected body %q, got %q", html, body)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCaptchaMethodBasedLogic(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
remediation string
|
||||||
|
expectBanFallback bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "GET with captcha remediation should allow captcha",
|
||||||
|
method: http.MethodGet,
|
||||||
|
remediation: cache.CaptchaValue,
|
||||||
|
expectBanFallback: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HEAD with captcha remediation should fallback to ban",
|
||||||
|
method: http.MethodHead,
|
||||||
|
remediation: cache.CaptchaValue,
|
||||||
|
expectBanFallback: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "POST with captcha remediation should allow captcha",
|
||||||
|
method: http.MethodPost,
|
||||||
|
remediation: cache.CaptchaValue,
|
||||||
|
expectBanFallback: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PUT with captcha remediation should allow captcha",
|
||||||
|
method: http.MethodPut,
|
||||||
|
remediation: cache.CaptchaValue,
|
||||||
|
expectBanFallback: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DELETE with captcha remediation should allow captcha",
|
||||||
|
method: http.MethodDelete,
|
||||||
|
remediation: cache.CaptchaValue,
|
||||||
|
expectBanFallback: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PATCH with captcha remediation should allow captcha",
|
||||||
|
method: http.MethodPatch,
|
||||||
|
remediation: cache.CaptchaValue,
|
||||||
|
expectBanFallback: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OPTIONS with captcha remediation should allow captcha",
|
||||||
|
method: http.MethodOptions,
|
||||||
|
remediation: cache.CaptchaValue,
|
||||||
|
expectBanFallback: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Test the core logic: captcha is served for all methods except HEAD
|
||||||
|
shouldUseCaptcha := tt.remediation == cache.CaptchaValue && tt.method != http.MethodHead
|
||||||
|
|
||||||
|
if shouldUseCaptcha == tt.expectBanFallback {
|
||||||
|
t.Errorf("Method %s with %s remediation: expected ban fallback %v, but logic would use captcha %v",
|
||||||
|
tt.method, tt.remediation, tt.expectBanFallback, shouldUseCaptcha)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ To instruct Crowdsec to use captcha remediation, change the `/etc/crowdsec/profi
|
|||||||
- Return a captcha decision the first X times and then a ban decision.
|
- Return a captcha decision the first X times and then a ban decision.
|
||||||
|
|
||||||
The second mode could be used to prevent repeated malicious activity.
|
The second mode could be used to prevent repeated malicious activity.
|
||||||
More information is available on configuring Crowdsec in the [official documentation](https://docs.crowdsec.net/docs/next/profiles/captcha_profile/).
|
More information is available on configuring Crowdsec in the [official documentation](https://docs.crowdsec.net/docs/next/local_api/profiles/captcha_profile/).
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
...
|
...
|
||||||
@@ -158,4 +158,4 @@ TODO
|
|||||||
|
|
||||||
- Hcatpcha
|
- Hcatpcha
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|||||||
@@ -45,3 +45,15 @@ To play the demo environment run:
|
|||||||
```bash
|
```bash
|
||||||
make run_custom_ban_page
|
make run_custom_ban_page
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Another thing to note
|
||||||
|
In the html of the ban page, you can use:
|
||||||
|
- {{ .ClientIP }} to display the IP used to ban the request.
|
||||||
|
- {{ .RemediationReason }} that convert on runtime into why the ban page is served. It's an enum with "APPSEC", "LAPI", "TECHNICAL_ISSUE" and it is useful to help user understand why the request is blocked.
|
||||||
|
- {{ .CustomHeader }} value of the specified Request Header (for example X-Request-ID)
|
||||||
|
```
|
||||||
|
<script>var remediation = "{{ .RemediationReason }}"</script>
|
||||||
|
<script>var clientIp = "{{ .ClientIP }}"</script>
|
||||||
|
<script>var traceID = "{{ .TraceID }}"</script>
|
||||||
|
```
|
||||||
|
With the above tweak and some other js, you can customize your ban page on runtime.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Read the example captcha before this, to better understand what is done here.
|
|||||||
### Traefik configuration
|
### Traefik configuration
|
||||||
|
|
||||||
The minimal configuration is defined below to implement custom captcha.
|
The minimal configuration is defined below to implement custom captcha.
|
||||||
This documentation use https://github.com/a-ve/wicketpeeker, a self-hosted captcha provider that have a similar API than big providers.
|
This documentation use https://github.com/a-ve/wicketkeeper, a self-hosted captcha provider that have a similar API than big providers.
|
||||||
|
|
||||||
Minimal API requirement:
|
Minimal API requirement:
|
||||||
|
|
||||||
@@ -41,7 +41,6 @@ wicketkeeper:
|
|||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
environment:
|
environment:
|
||||||
- ROOT_URL=http://localhost:8080
|
|
||||||
- LISTEN_PORT=8080
|
- LISTEN_PORT=8080
|
||||||
- REDIS_ADDR=redis:6379
|
- REDIS_ADDR=redis:6379
|
||||||
- DIFFICULTY=4
|
- DIFFICULTY=4
|
||||||
@@ -55,6 +54,10 @@ redis:
|
|||||||
image: redis/redis-stack-server:latest
|
image: redis/redis-stack-server:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div id="captcha" class="{{ .FrontendKey }}" data-sitekey="{{ .SiteKey }}" data-callback="captchaCallback" data-challenge-url="http://captcha.localhost:8000/v0/challenge">
|
||||||
|
```
|
||||||
|
|
||||||
## Exemple navigation
|
## Exemple navigation
|
||||||
|
|
||||||
We can try to query normally the whoami server:
|
We can try to query normally the whoami server:
|
||||||
|
|||||||
@@ -294,7 +294,7 @@
|
|||||||
<h1 class="text-2xl lg:text-3xl xl:text-4xl">CrowdSec Captcha</h1>
|
<h1 class="text-2xl lg:text-3xl xl:text-4xl">CrowdSec Captcha</h1>
|
||||||
</div>
|
</div>
|
||||||
<form action="" method="POST" class="flex flex-col items-center space-y-1" id="captcha-form">
|
<form action="" method="POST" class="flex flex-col items-center space-y-1" id="captcha-form">
|
||||||
<div id="captcha" class="{{ .FrontendKey }}" data-sitekey="{{ .SiteKey }}" data-callback="captchaCallback">
|
<div id="captcha" class="{{ .FrontendKey }}" data-sitekey="{{ .SiteKey }}" data-callback="captchaCallback" data-challenge-url="http://captcha.localhost:8000/v0/challenge">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="flex justify-center flex-wrap">
|
<div class="flex justify-center flex-wrap">
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ services:
|
|||||||
image: ghcr.io/a-ve/wicketkeeper:latest
|
image: ghcr.io/a-ve/wicketkeeper:latest
|
||||||
container_name: "wicketkeeper"
|
container_name: "wicketkeeper"
|
||||||
environment:
|
environment:
|
||||||
- ROOT_URL=http://captcha.localhost:8000
|
|
||||||
- LISTEN_PORT=8080
|
- LISTEN_PORT=8080
|
||||||
- REDIS_ADDR=redis:6379
|
- REDIS_ADDR=redis:6379
|
||||||
- DIFFICULTY=4
|
- DIFFICULTY=4
|
||||||
@@ -94,10 +93,10 @@ services:
|
|||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
# Definition of the router
|
# Definition of the router
|
||||||
- "traefik.http.routers.router-wicketpeeker.rule=Host(`captcha.localhost`)"
|
- "traefik.http.routers.router-wicketkeeper.rule=Host(`captcha.localhost`)"
|
||||||
- "traefik.http.routers.router-wicketpeeker.entrypoints=web"
|
- "traefik.http.routers.router-wicketkeeper.entrypoints=web"
|
||||||
# Definition of the service
|
# Definition of the service
|
||||||
- "traefik.http.services.service-whitekeeper.loadbalancer.server.port=8080"
|
- "traefik.http.services.service-wicketkeeper.loadbalancer.server.port=8080"
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
|
|
||||||
|
|||||||
@@ -89,4 +89,8 @@ make run_tlsauth
|
|||||||
```
|
```
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
> Traefik need to be restart if certificates are regenerated after his launch
|
> Traefik need to be restarted if certificates are regenerated after his launch
|
||||||
|
|
||||||
|
## Separate LAPI and Appsec HTTP/S config
|
||||||
|
To separate TLS config for LAPI and Appsec, you can use all the TLS LAPI variable beginning with `CrowdsecLapi...` into `CrowdsecAppsec...`.
|
||||||
|
Don't forget to set `CrowdsecAppsecScheme: HTTP` or `HTTPS` to trigger the separate setup.
|
||||||
|
|||||||
+138
-110
@@ -31,6 +31,9 @@ const (
|
|||||||
LogDEBUG = "DEBUG"
|
LogDEBUG = "DEBUG"
|
||||||
LogINFO = "INFO"
|
LogINFO = "INFO"
|
||||||
LogERROR = "ERROR"
|
LogERROR = "ERROR"
|
||||||
|
ReasonTECH = "TECHNICAL_ISSUE"
|
||||||
|
ReasonLAPI = "LAPI"
|
||||||
|
ReasonAPPSEC = "APPSEC"
|
||||||
HcaptchaProvider = "hcaptcha"
|
HcaptchaProvider = "hcaptcha"
|
||||||
RecaptchaProvider = "recaptcha"
|
RecaptchaProvider = "recaptcha"
|
||||||
TurnstileProvider = "turnstile"
|
TurnstileProvider = "turnstile"
|
||||||
@@ -39,61 +42,71 @@ const (
|
|||||||
|
|
||||||
// Config the plugin configuration.
|
// Config the plugin configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
LogLevel string `json:"logLevel,omitempty"`
|
LogLevel string `json:"logLevel,omitempty"`
|
||||||
LogFilePath string `json:"logFilePath,omitempty"`
|
LogFilePath string `json:"logFilePath,omitempty"`
|
||||||
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"`
|
CrowdsecAppsecScheme string `json:"crowdsecAppsecScheme,omitempty"`
|
||||||
CrowdsecAppsecPath string `json:"crowdsecAppsecPath,omitempty"`
|
CrowdsecAppsecHost string `json:"crowdsecAppsecHost,omitempty"`
|
||||||
CrowdsecAppsecFailureBlock bool `json:"crowdsecAppsecFailureBlock,omitempty"`
|
CrowdsecAppsecPath string `json:"crowdsecAppsecPath,omitempty"`
|
||||||
CrowdsecAppsecUnreachableBlock bool `json:"crowdsecAppsecUnreachableBlock,omitempty"`
|
CrowdsecAppsecKey string `json:"crowdsecAppsecKey,omitempty"`
|
||||||
CrowdsecAppsecBodyLimit int64 `json:"crowdsecAppsecBodyLimit,omitempty"`
|
CrowdsecAppsecKeyFile string `json:"crowdsecAppsecKeyFile,omitempty"`
|
||||||
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
|
CrowdsecAppsecTLSInsecureVerify bool `json:"crowdsecAppsecTlsInsecureVerify,omitempty"`
|
||||||
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
|
CrowdsecAppsecTLSCertificateAuthority string `json:"crowdsecAppsecTlsCertificateAuthority,omitempty"`
|
||||||
CrowdsecLapiPath string `json:"crowdsecLapiPath,omitempty"`
|
CrowdsecAppsecTLSCertificateAuthorityFile string `json:"crowdsecAppsecTlsCertificateAuthorityFile,omitempty"`
|
||||||
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
|
CrowdsecAppsecTLSCertificateBouncer string `json:"crowdsecAppsecTlsCertificateBouncer,omitempty"`
|
||||||
CrowdsecLapiKeyFile string `json:"crowdsecLapiKeyFile,omitempty"`
|
CrowdsecAppsecTLSCertificateBouncerFile string `json:"crowdsecAppsecTlsCertificateBouncerFile,omitempty"`
|
||||||
CrowdsecLapiTLSInsecureVerify bool `json:"crowdsecLapiTlsInsecureVerify,omitempty"`
|
CrowdsecAppsecTLSCertificateBouncerKey string `json:"crowdsecAppsecTlsCertificateBouncerKey,omitempty"`
|
||||||
CrowdsecLapiTLSCertificateAuthority string `json:"crowdsecLapiTlsCertificateAuthority,omitempty"`
|
CrowdsecAppsecFailureBlock bool `json:"crowdsecAppsecFailureBlock,omitempty"`
|
||||||
CrowdsecLapiTLSCertificateAuthorityFile string `json:"crowdsecLapiTlsCertificateAuthorityFile,omitempty"`
|
CrowdsecAppsecUnreachableBlock bool `json:"crowdsecAppsecUnreachableBlock,omitempty"`
|
||||||
CrowdsecLapiTLSCertificateBouncer string `json:"crowdsecLapiTlsCertificateBouncer,omitempty"`
|
CrowdsecAppsecBodyLimit int64 `json:"crowdsecAppsecBodyLimit,omitempty"`
|
||||||
CrowdsecLapiTLSCertificateBouncerFile string `json:"crowdsecLapiTlsCertificateBouncerFile,omitempty"`
|
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
|
||||||
CrowdsecLapiTLSCertificateBouncerKey string `json:"crowdsecLapiTlsCertificateBouncerKey,omitempty"`
|
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
|
||||||
CrowdsecLapiTLSCertificateBouncerKeyFile string `json:"crowdsecLapiTlsCertificateBouncerKeyFile,omitempty"`
|
CrowdsecLapiPath string `json:"crowdsecLapiPath,omitempty"`
|
||||||
CrowdsecCapiMachineID string `json:"crowdsecCapiMachineId,omitempty"`
|
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
|
||||||
CrowdsecCapiMachineIDFile string `json:"crowdsecCapiMachineIdFile,omitempty"`
|
CrowdsecLapiKeyFile string `json:"crowdsecLapiKeyFile,omitempty"`
|
||||||
CrowdsecCapiPassword string `json:"crowdsecCapiPassword,omitempty"`
|
CrowdsecLapiTLSInsecureVerify bool `json:"crowdsecLapiTlsInsecureVerify,omitempty"`
|
||||||
CrowdsecCapiPasswordFile string `json:"crowdsecCapiPasswordFile,omitempty"`
|
CrowdsecLapiTLSCertificateAuthority string `json:"crowdsecLapiTlsCertificateAuthority,omitempty"`
|
||||||
CrowdsecCapiScenarios []string `json:"crowdsecCapiScenarios,omitempty"`
|
CrowdsecLapiTLSCertificateAuthorityFile string `json:"crowdsecLapiTlsCertificateAuthorityFile,omitempty"`
|
||||||
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
CrowdsecLapiTLSCertificateBouncer string `json:"crowdsecLapiTlsCertificateBouncer,omitempty"`
|
||||||
MetricsUpdateIntervalSeconds int64 `json:"metricsUpdateIntervalSeconds,omitempty"`
|
CrowdsecLapiTLSCertificateBouncerFile string `json:"crowdsecLapiTlsCertificateBouncerFile,omitempty"`
|
||||||
UpdateMaxFailure int64 `json:"updateMaxFailure,omitempty"`
|
CrowdsecLapiTLSCertificateBouncerKey string `json:"crowdsecLapiTlsCertificateBouncerKey,omitempty"`
|
||||||
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
CrowdsecLapiTLSCertificateBouncerKeyFile string `json:"crowdsecLapiTlsCertificateBouncerKeyFile,omitempty"`
|
||||||
RemediationStatusCode int `json:"remediationStatusCode,omitempty"`
|
CrowdsecCapiMachineID string `json:"crowdsecCapiMachineId,omitempty"`
|
||||||
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
|
CrowdsecCapiMachineIDFile string `json:"crowdsecCapiMachineIdFile,omitempty"`
|
||||||
RemediationHeadersCustomName string `json:"remediationHeadersCustomName,omitempty"`
|
CrowdsecCapiPassword string `json:"crowdsecCapiPassword,omitempty"`
|
||||||
ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"`
|
CrowdsecCapiPasswordFile string `json:"crowdsecCapiPasswordFile,omitempty"`
|
||||||
ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"`
|
CrowdsecCapiScenarios []string `json:"crowdsecCapiScenarios,omitempty"`
|
||||||
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
|
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
||||||
RedisCacheEnabled bool `json:"redisCacheEnabled,omitempty"`
|
MetricsUpdateIntervalSeconds int64 `json:"metricsUpdateIntervalSeconds,omitempty"`
|
||||||
RedisCacheHost string `json:"redisCacheHost,omitempty"`
|
UpdateMaxFailure int64 `json:"updateMaxFailure,omitempty"`
|
||||||
RedisCachePassword string `json:"redisCachePassword,omitempty"`
|
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
||||||
RedisCachePasswordFile string `json:"redisCachePasswordFile,omitempty"`
|
RemediationStatusCode int `json:"remediationStatusCode,omitempty"`
|
||||||
RedisCacheDatabase string `json:"redisCacheDatabase,omitempty"`
|
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
|
||||||
RedisCacheUnreachableBlock bool `json:"redisCacheUnreachableBlock,omitempty"`
|
TraceHeadersCustomName string `json:"traceHeadersCustomName,omitempty"`
|
||||||
BanHTMLFilePath string `json:"banHtmlFilePath,omitempty"`
|
RemediationHeadersCustomName string `json:"remediationHeadersCustomName,omitempty"`
|
||||||
CaptchaHTMLFilePath string `json:"captchaHtmlFilePath,omitempty"`
|
ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"`
|
||||||
CaptchaProvider string `json:"captchaProvider,omitempty"`
|
ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"`
|
||||||
CaptchaCustomJsURL string `json:"captchaCustomJsUrl,omitempty"`
|
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
|
||||||
CaptchaCustomValidateURL string `json:"captchaCustomValidateUrl,omitempty"`
|
RedisCacheEnabled bool `json:"redisCacheEnabled,omitempty"`
|
||||||
CaptchaCustomKey string `json:"captchaCustomKey,omitempty"`
|
RedisCacheHost string `json:"redisCacheHost,omitempty"`
|
||||||
CaptchaCustomResponse string `json:"captchaCustomResponse,omitempty"`
|
RedisCachePassword string `json:"redisCachePassword,omitempty"`
|
||||||
CaptchaSiteKey string `json:"captchaSiteKey,omitempty"`
|
RedisCachePasswordFile string `json:"redisCachePasswordFile,omitempty"`
|
||||||
CaptchaSiteKeyFile string `json:"captchaSiteKeyFile,omitempty"`
|
RedisCacheDatabase string `json:"redisCacheDatabase,omitempty"`
|
||||||
CaptchaSecretKey string `json:"captchaSecretKey,omitempty"`
|
RedisCacheUnreachableBlock bool `json:"redisCacheUnreachableBlock,omitempty"`
|
||||||
CaptchaSecretKeyFile string `json:"captchaSecretKeyFile,omitempty"`
|
BanHTMLFilePath string `json:"banHtmlFilePath,omitempty"`
|
||||||
CaptchaGracePeriodSeconds int64 `json:"captchaGracePeriodSeconds,omitempty"`
|
CaptchaHTMLFilePath string `json:"captchaHtmlFilePath,omitempty"`
|
||||||
|
CaptchaProvider string `json:"captchaProvider,omitempty"`
|
||||||
|
CaptchaCustomJsURL string `json:"captchaCustomJsUrl,omitempty"`
|
||||||
|
CaptchaCustomValidateURL string `json:"captchaCustomValidateUrl,omitempty"`
|
||||||
|
CaptchaCustomKey string `json:"captchaCustomKey,omitempty"`
|
||||||
|
CaptchaCustomResponse string `json:"captchaCustomResponse,omitempty"`
|
||||||
|
CaptchaSiteKey string `json:"captchaSiteKey,omitempty"`
|
||||||
|
CaptchaSiteKeyFile string `json:"captchaSiteKeyFile,omitempty"`
|
||||||
|
CaptchaSecretKey string `json:"captchaSecretKey,omitempty"`
|
||||||
|
CaptchaSecretKeyFile string `json:"captchaSecretKeyFile,omitempty"`
|
||||||
|
CaptchaGracePeriodSeconds int64 `json:"captchaGracePeriodSeconds,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func contains(source []string, target string) bool {
|
func contains(source []string, target string) bool {
|
||||||
@@ -108,46 +121,50 @@ func contains(source []string, target string) bool {
|
|||||||
// New creates the default plugin configuration.
|
// New creates the default plugin configuration.
|
||||||
func New() *Config {
|
func New() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
LogLevel: LogINFO,
|
LogLevel: LogINFO,
|
||||||
LogFilePath: "",
|
LogFilePath: "",
|
||||||
CrowdsecMode: LiveMode,
|
CrowdsecMode: LiveMode,
|
||||||
CrowdsecAppsecEnabled: false,
|
CrowdsecAppsecEnabled: false,
|
||||||
CrowdsecAppsecHost: "crowdsec:7422",
|
CrowdsecAppsecFailureBlock: true,
|
||||||
CrowdsecAppsecPath: "/",
|
CrowdsecAppsecUnreachableBlock: true,
|
||||||
CrowdsecAppsecFailureBlock: true,
|
CrowdsecAppsecBodyLimit: 10485760,
|
||||||
CrowdsecAppsecUnreachableBlock: true,
|
CrowdsecAppsecScheme: "",
|
||||||
CrowdsecAppsecBodyLimit: 10485760,
|
CrowdsecAppsecHost: "crowdsec:7422",
|
||||||
CrowdsecLapiScheme: HTTP,
|
CrowdsecAppsecPath: "/",
|
||||||
CrowdsecLapiHost: "crowdsec:8080",
|
CrowdsecAppsecKey: "",
|
||||||
CrowdsecLapiPath: "/",
|
CrowdsecAppsecTLSInsecureVerify: false,
|
||||||
CrowdsecLapiKey: "",
|
CrowdsecLapiScheme: HTTP,
|
||||||
CrowdsecLapiTLSInsecureVerify: false,
|
CrowdsecLapiHost: "crowdsec:8080",
|
||||||
UpdateIntervalSeconds: 60,
|
CrowdsecLapiPath: "/",
|
||||||
MetricsUpdateIntervalSeconds: 600,
|
CrowdsecLapiKey: "",
|
||||||
UpdateMaxFailure: 0,
|
CrowdsecLapiTLSInsecureVerify: false,
|
||||||
DefaultDecisionSeconds: 60,
|
UpdateIntervalSeconds: 60,
|
||||||
RemediationStatusCode: http.StatusForbidden,
|
MetricsUpdateIntervalSeconds: 600,
|
||||||
HTTPTimeoutSeconds: 10,
|
UpdateMaxFailure: 0,
|
||||||
CaptchaProvider: "",
|
DefaultDecisionSeconds: 60,
|
||||||
CaptchaCustomJsURL: "",
|
RemediationStatusCode: http.StatusForbidden,
|
||||||
CaptchaCustomValidateURL: "",
|
HTTPTimeoutSeconds: 10,
|
||||||
CaptchaCustomKey: "",
|
CaptchaProvider: "",
|
||||||
CaptchaCustomResponse: "",
|
CaptchaCustomJsURL: "",
|
||||||
CaptchaSiteKey: "",
|
CaptchaCustomValidateURL: "",
|
||||||
CaptchaSecretKey: "",
|
CaptchaCustomKey: "",
|
||||||
CaptchaGracePeriodSeconds: 1800,
|
CaptchaCustomResponse: "",
|
||||||
CaptchaHTMLFilePath: "/captcha.html",
|
CaptchaSiteKey: "",
|
||||||
BanHTMLFilePath: "",
|
CaptchaSecretKey: "",
|
||||||
RemediationHeadersCustomName: "",
|
CaptchaGracePeriodSeconds: 1800,
|
||||||
ForwardedHeadersCustomName: "X-Forwarded-For",
|
CaptchaHTMLFilePath: "/captcha.html",
|
||||||
ForwardedHeadersTrustedIPs: []string{},
|
BanHTMLFilePath: "",
|
||||||
ClientTrustedIPs: []string{},
|
TraceHeadersCustomName: "",
|
||||||
RedisCacheEnabled: false,
|
RemediationHeadersCustomName: "",
|
||||||
RedisCacheHost: "redis:6379",
|
ForwardedHeadersCustomName: "X-Forwarded-For",
|
||||||
RedisCachePassword: "",
|
ForwardedHeadersTrustedIPs: []string{},
|
||||||
RedisCacheDatabase: "",
|
ClientTrustedIPs: []string{},
|
||||||
RedisCacheUnreachableBlock: true,
|
RedisCacheEnabled: false,
|
||||||
|
RedisCacheHost: "redis:6379",
|
||||||
|
RedisCachePassword: "",
|
||||||
|
RedisCacheDatabase: "",
|
||||||
|
RedisCacheUnreachableBlock: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,26 +426,27 @@ func validateParamsRequired(config *Config) error {
|
|||||||
if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) {
|
if !contains([]string{HTTP, HTTPS}, config.CrowdsecLapiScheme) {
|
||||||
return errors.New("CrowdsecLapiScheme: must be one of 'http' or 'https'")
|
return errors.New("CrowdsecLapiScheme: must be one of 'http' or 'https'")
|
||||||
}
|
}
|
||||||
|
if !contains([]string{HTTP, HTTPS, ""}, config.CrowdsecAppsecScheme) {
|
||||||
|
return errors.New("CrowdsecAppsecScheme: must be one of 'http' or 'https'")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTLSConfigCrowdsec get TLS config from Config.
|
func getTLSConfig(config *Config, log *logger.Log, prefix, scheme string, insecureVerify bool) (*tls.Config, error) {
|
||||||
//
|
|
||||||
//nolint:nestif
|
|
||||||
func GetTLSConfigCrowdsec(config *Config, log *logger.Log) (*tls.Config, error) {
|
|
||||||
tlsConfig := new(tls.Config)
|
tlsConfig := new(tls.Config)
|
||||||
tlsConfig.RootCAs = x509.NewCertPool()
|
tlsConfig.RootCAs = x509.NewCertPool()
|
||||||
//nolint:gocritic
|
if scheme != HTTPS {
|
||||||
if config.CrowdsecLapiScheme != HTTPS {
|
log.Debug("getTLSConfigCrowdsec:" + prefix + "Scheme https:no")
|
||||||
log.Debug("getTLSConfigCrowdsec:CrowdsecLapiScheme https:no")
|
|
||||||
return tlsConfig, nil
|
return tlsConfig, nil
|
||||||
} else if config.CrowdsecLapiTLSInsecureVerify {
|
}
|
||||||
|
//nolint:nestif
|
||||||
|
if insecureVerify {
|
||||||
tlsConfig.InsecureSkipVerify = true
|
tlsConfig.InsecureSkipVerify = true
|
||||||
log.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSInsecureVerify tlsInsecure:true")
|
log.Debug("getTLSConfigCrowdsec:" + prefix + "TLSInsecureVerify tlsInsecure:true")
|
||||||
// If we return here and still want to use client auth this won't work
|
// If we return here and still want to use client auth this won't work
|
||||||
// return tlsConfig, nil
|
// return tlsConfig, nil
|
||||||
} else {
|
} else {
|
||||||
certAuthority, err := GetVariable(config, "CrowdsecLapiTLSCertificateAuthority")
|
certAuthority, err := GetVariable(config, prefix+"TLSCertificateAuthority")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -436,17 +454,16 @@ func GetTLSConfigCrowdsec(config *Config, log *logger.Log) (*tls.Config, error)
|
|||||||
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuthority)) {
|
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuthority)) {
|
||||||
// here we return because if CrowdsecLapiTLSInsecureVerify is false
|
// here we return because if CrowdsecLapiTLSInsecureVerify is false
|
||||||
// and CA not load, we can't communicate with https
|
// and CA not load, we can't communicate with https
|
||||||
return nil, errors.New("getTLSConfigCrowdsec:cannot load CA and verify cert is enabled")
|
return nil, errors.New("getTLSConfigCrowdsec:" + prefix + "cannot load CA and verify cert is enabled")
|
||||||
}
|
}
|
||||||
log.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSCertificateAuthority CA added successfully")
|
log.Debug("getTLSConfigCrowdsec:" + prefix + "TLSCertificateAuthority CA added successfully")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
certBouncer, err := GetVariable(config, prefix+"TLSCertificateBouncer")
|
||||||
certBouncer, err := GetVariable(config, "CrowdsecLapiTLSCertificateBouncer")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
certBouncerKey, err := GetVariable(config, "CrowdsecLapiTLSCertificateBouncerKey")
|
certBouncerKey, err := GetVariable(config, prefix+"TLSCertificateBouncerKey")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -461,3 +478,14 @@ func GetTLSConfigCrowdsec(config *Config, log *logger.Log) (*tls.Config, error)
|
|||||||
|
|
||||||
return tlsConfig, nil
|
return tlsConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTLSConfigCrowdsec get TLS config from Config.
|
||||||
|
func GetTLSConfigCrowdsec(config *Config, log *logger.Log, isAppsec bool) (*tls.Config, error) {
|
||||||
|
var prefix string
|
||||||
|
if isAppsec && config.CrowdsecAppsecScheme != "" {
|
||||||
|
prefix = "CrowdsecAppsec"
|
||||||
|
return getTLSConfig(config, log, prefix, config.CrowdsecAppsecScheme, config.CrowdsecAppsecTLSInsecureVerify)
|
||||||
|
}
|
||||||
|
prefix = "CrowdsecLapi"
|
||||||
|
return getTLSConfig(config, log, prefix, config.CrowdsecLapiScheme, config.CrowdsecLapiTLSInsecureVerify)
|
||||||
|
}
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ func Test_GetTLSConfigCrowdsec(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := GetTLSConfigCrowdsec(tt.args.config, logger.New("INFO", ""))
|
got, err := GetTLSConfigCrowdsec(tt.args.config, logger.New("INFO", ""), false)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("getTLSConfigCrowdsec() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("getTLSConfigCrowdsec() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user