Compare commits

..
3 Commits
Author SHA1 Message Date
maxlerebourg 45d5f38c4d add remediation header when plugin made decision (#189)
*  add remediation header when plugin made decision

* 🍱 add documentation
2024-09-25 19:30:27 +02:00
maxlerebourgandmax.lerebourg f1de1c924e 🔨 174 fix classname for Turnstile (#178)
Co-authored-by: max.lerebourg <max.lerebourg@monisnap.com>
2024-06-19 20:13:49 +02:00
mathieuHa 36e6043c32 📝 doc(cache) Specify that local cache is in memory and not in the filesystem (#177) 2024-06-09 16:20:55 +02:00
5 changed files with 85 additions and 67 deletions
+11 -6
View File
@@ -51,7 +51,7 @@ There are 5 operating modes (CrowdsecMode) for this plugin:
The `streaming mode` is recommended for performance, decisions are updated every 60 sec by default and that's the only communication between Traefik and Crowdsec. Every request that happens hits the cache for quick decisions. The `streaming mode` is recommended for performance, decisions are updated every 60 sec by default and that's the only communication between Traefik and Crowdsec. Every request that happens hits the cache for quick decisions.
The cache can be local to Traefik using the filesystem, or a separate Redis instance. The cache can be local to Traefik in memory or using a separate Redis instance.
Below are Mermaid diagrams detailling how each mode work: Below are Mermaid diagrams detailling how each mode work:
@@ -368,18 +368,22 @@ Only one instance of the plugin is *possible*.
- string - string
- default: [] - default: []
- List of client IPs to trust, they will bypass any check from the bouncer or cache (useful for LAN or VPN IP) - List of client IPs to trust, they will bypass any check from the bouncer or cache (useful for LAN or VPN IP)
- ForwardedHeadersTrustedIPs - RemediationHeadersCustomName
- []string - string
- default: [] - default: ""
- List of IPs of trusted Proxies that are in front of traefik (ex: Cloudflare) - Name of the header you want in response when request are cancelled (possible value of the header `ban` or `captcha`)
- ForwardedHeadersCustomName - ForwardedHeadersCustomName
- string - string
- default: "X-Forwarded-For" - default: "X-Forwarded-For"
- Name of the header where the real IP of the client should be retrieved - Name of the header where the real IP of the client should be retrieved
- ForwardedHeadersTrustedIPs
- []string
- default: []
- List of IPs of trusted Proxies that are in front of traefik (ex: Cloudflare)
- RedisCacheEnabled - RedisCacheEnabled
- bool - bool
- default: false - default: false
- enable Redis cache instead of filesystem cache - enable Redis cache instead of in-memory cache
- RedisCacheHost - RedisCacheHost
- string - string
- default: "redis:6379" - default: "redis:6379"
@@ -508,6 +512,7 @@ http:
clientTrustedIPs: clientTrustedIPs:
- 192.168.1.0/24 - 192.168.1.0/24
forwardedHeadersCustomName: X-Custom-Header forwardedHeadersCustomName: X-Custom-Header
remediationHeadersCustomName: cs-remediation
redisCacheEnabled: false redisCacheEnabled: false
redisCacheHost: "redis:6379" redisCacheHost: "redis:6379"
redisCachePassword: password redisCachePassword: password
+54 -48
View File
@@ -1,5 +1,5 @@
// Package crowdsec_bouncer_traefik_plugin implements a middleware that communicates with crowdsec. // Package crowdsec_bouncer_traefik_plugin implements a middleware that communicates with crowdsec.
// It can cache results to filesystem or redis, or even ask crowdsec for every requests. // It can cache results in memory or using redis, or even ask crowdsec for every requests.
package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
import ( import (
@@ -59,31 +59,32 @@ type Bouncer struct {
name string name string
template *template.Template template *template.Template
enabled bool enabled bool
appsecEnabled bool appsecEnabled bool
appsecHost string appsecHost string
appsecFailureBlock bool appsecFailureBlock bool
appsecUnreachableBlock bool appsecUnreachableBlock bool
crowdsecScheme string crowdsecScheme string
crowdsecHost string crowdsecHost string
crowdsecKey string crowdsecKey string
crowdsecMode string crowdsecMode string
crowdsecMachineID string crowdsecMachineID string
crowdsecPassword string crowdsecPassword string
crowdsecScenarios []string crowdsecScenarios []string
updateInterval int64 updateInterval int64
updateMaxFailure int updateMaxFailure int
defaultDecisionTimeout int64 defaultDecisionTimeout int64
customHeader string remediationCustomHeader string
crowdsecStreamRoute string forwardedCustomHeader string
crowdsecHeader string crowdsecStreamRoute string
banTemplateString string crowdsecHeader string
clientPoolStrategy *ip.PoolStrategy banTemplateString string
serverPoolStrategy *ip.PoolStrategy clientPoolStrategy *ip.PoolStrategy
httpClient *http.Client serverPoolStrategy *ip.PoolStrategy
cacheClient *cache.Client httpClient *http.Client
captchaClient *captcha.Client cacheClient *cache.Client
log *logger.Log captchaClient *captcha.Client
log *logger.Log
} }
// New creates the crowdsec bouncer plugin. // New creates the crowdsec bouncer plugin.
@@ -142,26 +143,27 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
name: name, name: name,
template: template.New("CrowdsecBouncer").Delims("[[", "]]"), template: template.New("CrowdsecBouncer").Delims("[[", "]]"),
enabled: config.Enabled, enabled: config.Enabled,
crowdsecMode: config.CrowdsecMode, crowdsecMode: config.CrowdsecMode,
appsecEnabled: config.CrowdsecAppsecEnabled, appsecEnabled: config.CrowdsecAppsecEnabled,
appsecHost: config.CrowdsecAppsecHost, appsecHost: config.CrowdsecAppsecHost,
appsecFailureBlock: config.CrowdsecAppsecFailureBlock, appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock, appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock,
crowdsecScheme: config.CrowdsecLapiScheme, crowdsecScheme: config.CrowdsecLapiScheme,
crowdsecHost: config.CrowdsecLapiHost, crowdsecHost: config.CrowdsecLapiHost,
crowdsecKey: config.CrowdsecLapiKey, crowdsecKey: config.CrowdsecLapiKey,
crowdsecMachineID: config.CrowdsecCapiMachineID, crowdsecMachineID: config.CrowdsecCapiMachineID,
crowdsecPassword: config.CrowdsecCapiPassword, crowdsecPassword: config.CrowdsecCapiPassword,
crowdsecScenarios: config.CrowdsecCapiScenarios, crowdsecScenarios: config.CrowdsecCapiScenarios,
updateInterval: config.UpdateIntervalSeconds, updateInterval: config.UpdateIntervalSeconds,
updateMaxFailure: config.UpdateMaxFailure, updateMaxFailure: config.UpdateMaxFailure,
customHeader: config.ForwardedHeadersCustomName, remediationCustomHeader: config.RemediationHeadersCustomName,
defaultDecisionTimeout: config.DefaultDecisionSeconds, forwardedCustomHeader: config.ForwardedHeadersCustomName,
banTemplateString: banTemplateString, defaultDecisionTimeout: config.DefaultDecisionSeconds,
crowdsecStreamRoute: crowdsecStreamRoute, banTemplateString: banTemplateString,
crowdsecHeader: crowdsecHeader, crowdsecStreamRoute: crowdsecStreamRoute,
log: log, crowdsecHeader: crowdsecHeader,
log: log,
serverPoolStrategy: &ip.PoolStrategy{ serverPoolStrategy: &ip.PoolStrategy{
Checker: serverChecker, Checker: serverChecker,
}, },
@@ -202,6 +204,7 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
config.CaptchaProvider, config.CaptchaProvider,
config.CaptchaSiteKey, config.CaptchaSiteKey,
config.CaptchaSecretKey, config.CaptchaSecretKey,
config.RemediationHeadersCustomName,
config.CaptchaHTMLFilePath, config.CaptchaHTMLFilePath,
config.CaptchaGracePeriodSeconds, config.CaptchaGracePeriodSeconds,
) )
@@ -236,8 +239,8 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
return return
} }
// Here we check for the trusted IPs in the customHeader // Here we check for the trusted IPs in the forwardedCustomHeader
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.customHeader) remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.forwardedCustomHeader)
if err != nil { if err != nil {
bouncer.log.Error(fmt.Sprintf("ServeHTTP:getRemoteIp ip:%s %s", remoteIP, err.Error())) bouncer.log.Error(fmt.Sprintf("ServeHTTP:getRemoteIp ip:%s %s", remoteIP, err.Error()))
handleBanServeHTTP(bouncer, rw) handleBanServeHTTP(bouncer, rw)
@@ -337,6 +340,9 @@ func handleBanServeHTTP(bouncer *Bouncer, rw http.ResponseWriter) {
return return
} }
rw.Header().Set("Content-Type", "text/html; charset=utf-8") rw.Header().Set("Content-Type", "text/html; charset=utf-8")
if bouncer.remediationCustomHeader != "" {
rw.Header().Set(bouncer.remediationCustomHeader, "ban")
}
rw.WriteHeader(http.StatusForbidden) rw.WriteHeader(http.StatusForbidden)
fmt.Fprint(rw, bouncer.banTemplateString) fmt.Fprint(rw, bouncer.banTemplateString)
} }
+2 -2
View File
@@ -75,7 +75,7 @@ func TestBouncer_ServeHTTP(t *testing.T) {
crowdsecMode string crowdsecMode string
updateInterval int64 updateInterval int64
defaultDecisionTimeout int64 defaultDecisionTimeout int64
customHeader string forwardedCustomHeader string
clientPoolStrategy *ip.PoolStrategy clientPoolStrategy *ip.PoolStrategy
serverPoolStrategy *ip.PoolStrategy serverPoolStrategy *ip.PoolStrategy
httpClient *http.Client httpClient *http.Client
@@ -105,7 +105,7 @@ func TestBouncer_ServeHTTP(t *testing.T) {
crowdsecMode: tt.fields.crowdsecMode, crowdsecMode: tt.fields.crowdsecMode,
updateInterval: tt.fields.updateInterval, updateInterval: tt.fields.updateInterval,
defaultDecisionTimeout: tt.fields.defaultDecisionTimeout, defaultDecisionTimeout: tt.fields.defaultDecisionTimeout,
customHeader: tt.fields.customHeader, forwardedCustomHeader: tt.fields.forwardedCustomHeader,
clientPoolStrategy: tt.fields.clientPoolStrategy, clientPoolStrategy: tt.fields.clientPoolStrategy,
serverPoolStrategy: tt.fields.serverPoolStrategy, serverPoolStrategy: tt.fields.serverPoolStrategy,
httpClient: tt.fields.httpClient, httpClient: tt.fields.httpClient,
+16 -11
View File
@@ -16,15 +16,16 @@ import (
// Client Captcha client. // Client Captcha client.
type Client struct { type Client struct {
Valid bool Valid bool
provider string provider string
siteKey string siteKey string
secretKey string secretKey string
gracePeriodSeconds int64 remediationCustomHeader string
captchaTemplate *template.Template gracePeriodSeconds int64
cacheClient *cache.Client captchaTemplate *template.Template
httpClient *http.Client cacheClient *cache.Client
log *logger.Log httpClient *http.Client
log *logger.Log
} }
type infoProvider struct { type infoProvider struct {
@@ -48,14 +49,14 @@ var (
}, },
configuration.TurnstileProvider: { configuration.TurnstileProvider: {
js: "https://challenges.cloudflare.com/turnstile/v0/api.js", js: "https://challenges.cloudflare.com/turnstile/v0/api.js",
key: "cf-captcha", key: "cf-turnstile",
validate: "https://challenges.cloudflare.com/turnstile/v0/siteverify", validate: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
}, },
} }
) )
// New Initialize captcha client. // New Initialize captcha client.
func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *http.Client, provider, siteKey, secretKey, captchaTemplatePath string, gracePeriodSeconds int64) error { func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *http.Client, provider, siteKey, secretKey, remediationCustomHeader, captchaTemplatePath string, gracePeriodSeconds int64) error {
c.Valid = provider != "" c.Valid = provider != ""
if !c.Valid { if !c.Valid {
return nil return nil
@@ -63,6 +64,7 @@ func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *htt
c.siteKey = siteKey c.siteKey = siteKey
c.secretKey = secretKey c.secretKey = secretKey
c.provider = provider c.provider = provider
c.remediationCustomHeader = remediationCustomHeader
html, _ := configuration.GetHTMLTemplate(captchaTemplatePath) html, _ := configuration.GetHTMLTemplate(captchaTemplatePath)
c.captchaTemplate = html c.captchaTemplate = html
c.gracePeriodSeconds = gracePeriodSeconds c.gracePeriodSeconds = gracePeriodSeconds
@@ -87,6 +89,9 @@ func (c *Client) ServeHTTP(rw http.ResponseWriter, r *http.Request, remoteIP str
return return
} }
rw.Header().Set("Content-Type", "text/html; charset=utf-8") rw.Header().Set("Content-Type", "text/html; charset=utf-8")
if c.remediationCustomHeader != "" {
rw.Header().Set(c.remediationCustomHeader, "captcha")
}
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
err = c.captchaTemplate.Execute(rw, map[string]string{ err = c.captchaTemplate.Execute(rw, map[string]string{
"SiteKey": c.siteKey, "SiteKey": c.siteKey,
+2
View File
@@ -62,6 +62,7 @@ type Config struct {
UpdateMaxFailure int `json:"updateMaxFailure,omitempty"` UpdateMaxFailure int `json:"updateMaxFailure,omitempty"`
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"` DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"` HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
RemediationHeadersCustomName string `json:"remediationHeadersCustomName,omitempty"`
ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"` ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"`
ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"` ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"`
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"` ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
@@ -113,6 +114,7 @@ func New() *Config {
CaptchaGracePeriodSeconds: 1800, CaptchaGracePeriodSeconds: 1800,
CaptchaHTMLFilePath: "/captcha.html", CaptchaHTMLFilePath: "/captcha.html",
BanHTMLFilePath: "", BanHTMLFilePath: "",
RemediationHeadersCustomName: "",
ForwardedHeadersCustomName: "X-Forwarded-For", ForwardedHeadersCustomName: "X-Forwarded-For",
ForwardedHeadersTrustedIPs: []string{}, ForwardedHeadersTrustedIPs: []string{},
ClientTrustedIPs: []string{}, ClientTrustedIPs: []string{},