diff --git a/README.md b/README.md index 75c913e..270e51b 100644 --- a/README.md +++ b/README.md @@ -368,14 +368,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 @@ -508,6 +512,7 @@ http: clientTrustedIPs: - 192.168.1.0/24 forwardedHeadersCustomName: X-Custom-Header + remediationHeadersCustomName: cs-remediation redisCacheEnabled: false redisCacheHost: "redis:6379" redisCachePassword: password diff --git a/bouncer.go b/bouncer.go index 8e06864..9d3fc07 100644 --- a/bouncer.go +++ b/bouncer.go @@ -59,31 +59,32 @@ type Bouncer struct { name string template *template.Template - enabled bool - appsecEnabled bool - appsecHost string - appsecFailureBlock bool - appsecUnreachableBlock bool - crowdsecScheme string - crowdsecHost string - crowdsecKey string - crowdsecMode string - crowdsecMachineID string - crowdsecPassword string - crowdsecScenarios []string - updateInterval int64 - updateMaxFailure int - defaultDecisionTimeout int64 - customHeader string - crowdsecStreamRoute string - crowdsecHeader string - banTemplateString string - clientPoolStrategy *ip.PoolStrategy - serverPoolStrategy *ip.PoolStrategy - httpClient *http.Client - cacheClient *cache.Client - captchaClient *captcha.Client - log *logger.Log + enabled bool + appsecEnabled bool + appsecHost string + appsecFailureBlock bool + appsecUnreachableBlock bool + crowdsecScheme string + crowdsecHost string + crowdsecKey string + crowdsecMode string + crowdsecMachineID string + crowdsecPassword string + crowdsecScenarios []string + updateInterval int64 + updateMaxFailure int + defaultDecisionTimeout int64 + remediationCustomHeader string + forwardedCustomHeader string + crowdsecStreamRoute string + crowdsecHeader string + banTemplateString string + clientPoolStrategy *ip.PoolStrategy + serverPoolStrategy *ip.PoolStrategy + httpClient *http.Client + cacheClient *cache.Client + captchaClient *captcha.Client + log *logger.Log } // New creates the crowdsec bouncer plugin. @@ -142,26 +143,27 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam name: name, template: template.New("CrowdsecBouncer").Delims("[[", "]]"), - enabled: config.Enabled, - crowdsecMode: config.CrowdsecMode, - appsecEnabled: config.CrowdsecAppsecEnabled, - appsecHost: config.CrowdsecAppsecHost, - appsecFailureBlock: config.CrowdsecAppsecFailureBlock, - appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock, - crowdsecScheme: config.CrowdsecLapiScheme, - crowdsecHost: config.CrowdsecLapiHost, - crowdsecKey: config.CrowdsecLapiKey, - crowdsecMachineID: config.CrowdsecCapiMachineID, - crowdsecPassword: config.CrowdsecCapiPassword, - crowdsecScenarios: config.CrowdsecCapiScenarios, - updateInterval: config.UpdateIntervalSeconds, - updateMaxFailure: config.UpdateMaxFailure, - customHeader: config.ForwardedHeadersCustomName, - defaultDecisionTimeout: config.DefaultDecisionSeconds, - banTemplateString: banTemplateString, - crowdsecStreamRoute: crowdsecStreamRoute, - crowdsecHeader: crowdsecHeader, - log: log, + enabled: config.Enabled, + crowdsecMode: config.CrowdsecMode, + appsecEnabled: config.CrowdsecAppsecEnabled, + appsecHost: config.CrowdsecAppsecHost, + appsecFailureBlock: config.CrowdsecAppsecFailureBlock, + appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock, + crowdsecScheme: config.CrowdsecLapiScheme, + crowdsecHost: config.CrowdsecLapiHost, + crowdsecKey: config.CrowdsecLapiKey, + crowdsecMachineID: config.CrowdsecCapiMachineID, + crowdsecPassword: config.CrowdsecCapiPassword, + crowdsecScenarios: config.CrowdsecCapiScenarios, + updateInterval: config.UpdateIntervalSeconds, + updateMaxFailure: config.UpdateMaxFailure, + remediationCustomHeader: config.RemediationHeadersCustomName, + forwardedCustomHeader: config.ForwardedHeadersCustomName, + defaultDecisionTimeout: config.DefaultDecisionSeconds, + banTemplateString: banTemplateString, + crowdsecStreamRoute: crowdsecStreamRoute, + crowdsecHeader: crowdsecHeader, + log: log, serverPoolStrategy: &ip.PoolStrategy{ Checker: serverChecker, }, @@ -202,6 +204,7 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam config.CaptchaProvider, config.CaptchaSiteKey, config.CaptchaSecretKey, + config.RemediationHeadersCustomName, config.CaptchaHTMLFilePath, config.CaptchaGracePeriodSeconds, ) @@ -236,8 +239,8 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 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) @@ -337,6 +340,9 @@ 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) } diff --git a/bouncer_test.go b/bouncer_test.go index d9dd023..270a5f5 100644 --- a/bouncer_test.go +++ b/bouncer_test.go @@ -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, diff --git a/pkg/captcha/captcha.go b/pkg/captcha/captcha.go index ab5f837..d763cfa 100644 --- a/pkg/captcha/captcha.go +++ b/pkg/captcha/captcha.go @@ -16,15 +16,16 @@ import ( // Client Captcha client. type Client struct { - Valid bool - provider string - siteKey string - secretKey string - gracePeriodSeconds int64 - captchaTemplate *template.Template - cacheClient *cache.Client - httpClient *http.Client - log *logger.Log + Valid bool + provider string + siteKey string + secretKey string + remediationCustomHeader string + gracePeriodSeconds int64 + captchaTemplate *template.Template + cacheClient *cache.Client + httpClient *http.Client + log *logger.Log } type infoProvider struct { @@ -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, diff --git a/pkg/configuration/configuration.go b/pkg/configuration/configuration.go index bd136b9..ba7432b 100644 --- a/pkg/configuration/configuration.go +++ b/pkg/configuration/configuration.go @@ -62,6 +62,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"` @@ -113,6 +114,7 @@ func New() *Config { CaptchaGracePeriodSeconds: 1800, CaptchaHTMLFilePath: "/captcha.html", BanHTMLFilePath: "", + RemediationHeadersCustomName: "", ForwardedHeadersCustomName: "X-Forwarded-For", ForwardedHeadersTrustedIPs: []string{}, ClientTrustedIPs: []string{},