Skip to content

Commit 45d5f38

Browse files
authored
✨ add remediation header when plugin made decision (#189)
* ✨ add remediation header when plugin made decision * 🍱 add documentation
1 parent f1de1c9 commit 45d5f38

File tree

5 files changed

+81
-63
lines changed

5 files changed

+81
-63
lines changed

README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -368,14 +368,18 @@ Only one instance of the plugin is *possible*.
368368
- string
369369
- default: []
370370
- List of client IPs to trust, they will bypass any check from the bouncer or cache (useful for LAN or VPN IP)
371-
- ForwardedHeadersTrustedIPs
372-
- []string
373-
- default: []
374-
- List of IPs of trusted Proxies that are in front of traefik (ex: Cloudflare)
371+
- RemediationHeadersCustomName
372+
- string
373+
- default: ""
374+
- Name of the header you want in response when request are cancelled (possible value of the header `ban` or `captcha`)
375375
- ForwardedHeadersCustomName
376376
- string
377377
- default: "X-Forwarded-For"
378378
- Name of the header where the real IP of the client should be retrieved
379+
- ForwardedHeadersTrustedIPs
380+
- []string
381+
- default: []
382+
- List of IPs of trusted Proxies that are in front of traefik (ex: Cloudflare)
379383
- RedisCacheEnabled
380384
- bool
381385
- default: false
@@ -508,6 +512,7 @@ http:
508512
clientTrustedIPs:
509513
- 192.168.1.0/24
510514
forwardedHeadersCustomName: X-Custom-Header
515+
remediationHeadersCustomName: cs-remediation
511516
redisCacheEnabled: false
512517
redisCacheHost: "redis:6379"
513518
redisCachePassword: password

bouncer.go

Lines changed: 53 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -59,31 +59,32 @@ type Bouncer struct {
5959
name string
6060
template *template.Template
6161

62-
enabled bool
63-
appsecEnabled bool
64-
appsecHost string
65-
appsecFailureBlock bool
66-
appsecUnreachableBlock bool
67-
crowdsecScheme string
68-
crowdsecHost string
69-
crowdsecKey string
70-
crowdsecMode string
71-
crowdsecMachineID string
72-
crowdsecPassword string
73-
crowdsecScenarios []string
74-
updateInterval int64
75-
updateMaxFailure int
76-
defaultDecisionTimeout int64
77-
customHeader string
78-
crowdsecStreamRoute string
79-
crowdsecHeader string
80-
banTemplateString string
81-
clientPoolStrategy *ip.PoolStrategy
82-
serverPoolStrategy *ip.PoolStrategy
83-
httpClient *http.Client
84-
cacheClient *cache.Client
85-
captchaClient *captcha.Client
86-
log *logger.Log
62+
enabled bool
63+
appsecEnabled bool
64+
appsecHost string
65+
appsecFailureBlock bool
66+
appsecUnreachableBlock bool
67+
crowdsecScheme string
68+
crowdsecHost string
69+
crowdsecKey string
70+
crowdsecMode string
71+
crowdsecMachineID string
72+
crowdsecPassword string
73+
crowdsecScenarios []string
74+
updateInterval int64
75+
updateMaxFailure int
76+
defaultDecisionTimeout int64
77+
remediationCustomHeader string
78+
forwardedCustomHeader string
79+
crowdsecStreamRoute string
80+
crowdsecHeader string
81+
banTemplateString string
82+
clientPoolStrategy *ip.PoolStrategy
83+
serverPoolStrategy *ip.PoolStrategy
84+
httpClient *http.Client
85+
cacheClient *cache.Client
86+
captchaClient *captcha.Client
87+
log *logger.Log
8788
}
8889

8990
// New creates the crowdsec bouncer plugin.
@@ -142,26 +143,27 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
142143
name: name,
143144
template: template.New("CrowdsecBouncer").Delims("[[", "]]"),
144145

145-
enabled: config.Enabled,
146-
crowdsecMode: config.CrowdsecMode,
147-
appsecEnabled: config.CrowdsecAppsecEnabled,
148-
appsecHost: config.CrowdsecAppsecHost,
149-
appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
150-
appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock,
151-
crowdsecScheme: config.CrowdsecLapiScheme,
152-
crowdsecHost: config.CrowdsecLapiHost,
153-
crowdsecKey: config.CrowdsecLapiKey,
154-
crowdsecMachineID: config.CrowdsecCapiMachineID,
155-
crowdsecPassword: config.CrowdsecCapiPassword,
156-
crowdsecScenarios: config.CrowdsecCapiScenarios,
157-
updateInterval: config.UpdateIntervalSeconds,
158-
updateMaxFailure: config.UpdateMaxFailure,
159-
customHeader: config.ForwardedHeadersCustomName,
160-
defaultDecisionTimeout: config.DefaultDecisionSeconds,
161-
banTemplateString: banTemplateString,
162-
crowdsecStreamRoute: crowdsecStreamRoute,
163-
crowdsecHeader: crowdsecHeader,
164-
log: log,
146+
enabled: config.Enabled,
147+
crowdsecMode: config.CrowdsecMode,
148+
appsecEnabled: config.CrowdsecAppsecEnabled,
149+
appsecHost: config.CrowdsecAppsecHost,
150+
appsecFailureBlock: config.CrowdsecAppsecFailureBlock,
151+
appsecUnreachableBlock: config.CrowdsecAppsecUnreachableBlock,
152+
crowdsecScheme: config.CrowdsecLapiScheme,
153+
crowdsecHost: config.CrowdsecLapiHost,
154+
crowdsecKey: config.CrowdsecLapiKey,
155+
crowdsecMachineID: config.CrowdsecCapiMachineID,
156+
crowdsecPassword: config.CrowdsecCapiPassword,
157+
crowdsecScenarios: config.CrowdsecCapiScenarios,
158+
updateInterval: config.UpdateIntervalSeconds,
159+
updateMaxFailure: config.UpdateMaxFailure,
160+
remediationCustomHeader: config.RemediationHeadersCustomName,
161+
forwardedCustomHeader: config.ForwardedHeadersCustomName,
162+
defaultDecisionTimeout: config.DefaultDecisionSeconds,
163+
banTemplateString: banTemplateString,
164+
crowdsecStreamRoute: crowdsecStreamRoute,
165+
crowdsecHeader: crowdsecHeader,
166+
log: log,
165167
serverPoolStrategy: &ip.PoolStrategy{
166168
Checker: serverChecker,
167169
},
@@ -202,6 +204,7 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
202204
config.CaptchaProvider,
203205
config.CaptchaSiteKey,
204206
config.CaptchaSecretKey,
207+
config.RemediationHeadersCustomName,
205208
config.CaptchaHTMLFilePath,
206209
config.CaptchaGracePeriodSeconds,
207210
)
@@ -236,8 +239,8 @@ func (bouncer *Bouncer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
236239
return
237240
}
238241

239-
// Here we check for the trusted IPs in the customHeader
240-
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.customHeader)
242+
// Here we check for the trusted IPs in the forwardedCustomHeader
243+
remoteIP, err := ip.GetRemoteIP(req, bouncer.serverPoolStrategy, bouncer.forwardedCustomHeader)
241244
if err != nil {
242245
bouncer.log.Error(fmt.Sprintf("ServeHTTP:getRemoteIp ip:%s %s", remoteIP, err.Error()))
243246
handleBanServeHTTP(bouncer, rw)
@@ -337,6 +340,9 @@ func handleBanServeHTTP(bouncer *Bouncer, rw http.ResponseWriter) {
337340
return
338341
}
339342
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
343+
if bouncer.remediationCustomHeader != "" {
344+
rw.Header().Set(bouncer.remediationCustomHeader, "ban")
345+
}
340346
rw.WriteHeader(http.StatusForbidden)
341347
fmt.Fprint(rw, bouncer.banTemplateString)
342348
}

bouncer_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func TestBouncer_ServeHTTP(t *testing.T) {
7575
crowdsecMode string
7676
updateInterval int64
7777
defaultDecisionTimeout int64
78-
customHeader string
78+
forwardedCustomHeader string
7979
clientPoolStrategy *ip.PoolStrategy
8080
serverPoolStrategy *ip.PoolStrategy
8181
httpClient *http.Client
@@ -105,7 +105,7 @@ func TestBouncer_ServeHTTP(t *testing.T) {
105105
crowdsecMode: tt.fields.crowdsecMode,
106106
updateInterval: tt.fields.updateInterval,
107107
defaultDecisionTimeout: tt.fields.defaultDecisionTimeout,
108-
customHeader: tt.fields.customHeader,
108+
forwardedCustomHeader: tt.fields.forwardedCustomHeader,
109109
clientPoolStrategy: tt.fields.clientPoolStrategy,
110110
serverPoolStrategy: tt.fields.serverPoolStrategy,
111111
httpClient: tt.fields.httpClient,

pkg/captcha/captcha.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,16 @@ import (
1616

1717
// Client Captcha client.
1818
type Client struct {
19-
Valid bool
20-
provider string
21-
siteKey string
22-
secretKey string
23-
gracePeriodSeconds int64
24-
captchaTemplate *template.Template
25-
cacheClient *cache.Client
26-
httpClient *http.Client
27-
log *logger.Log
19+
Valid bool
20+
provider string
21+
siteKey string
22+
secretKey string
23+
remediationCustomHeader string
24+
gracePeriodSeconds int64
25+
captchaTemplate *template.Template
26+
cacheClient *cache.Client
27+
httpClient *http.Client
28+
log *logger.Log
2829
}
2930

3031
type infoProvider struct {
@@ -55,14 +56,15 @@ var (
5556
)
5657

5758
// New Initialize captcha client.
58-
func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *http.Client, provider, siteKey, secretKey, captchaTemplatePath string, gracePeriodSeconds int64) error {
59+
func (c *Client) New(log *logger.Log, cacheClient *cache.Client, httpClient *http.Client, provider, siteKey, secretKey, remediationCustomHeader, captchaTemplatePath string, gracePeriodSeconds int64) error {
5960
c.Valid = provider != ""
6061
if !c.Valid {
6162
return nil
6263
}
6364
c.siteKey = siteKey
6465
c.secretKey = secretKey
6566
c.provider = provider
67+
c.remediationCustomHeader = remediationCustomHeader
6668
html, _ := configuration.GetHTMLTemplate(captchaTemplatePath)
6769
c.captchaTemplate = html
6870
c.gracePeriodSeconds = gracePeriodSeconds
@@ -87,6 +89,9 @@ func (c *Client) ServeHTTP(rw http.ResponseWriter, r *http.Request, remoteIP str
8789
return
8890
}
8991
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
92+
if c.remediationCustomHeader != "" {
93+
rw.Header().Set(c.remediationCustomHeader, "captcha")
94+
}
9095
rw.WriteHeader(http.StatusOK)
9196
err = c.captchaTemplate.Execute(rw, map[string]string{
9297
"SiteKey": c.siteKey,

pkg/configuration/configuration.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ type Config struct {
6262
UpdateMaxFailure int `json:"updateMaxFailure,omitempty"`
6363
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
6464
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
65+
RemediationHeadersCustomName string `json:"remediationHeadersCustomName,omitempty"`
6566
ForwardedHeadersCustomName string `json:"forwardedHeadersCustomName,omitempty"`
6667
ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"`
6768
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
@@ -113,6 +114,7 @@ func New() *Config {
113114
CaptchaGracePeriodSeconds: 1800,
114115
CaptchaHTMLFilePath: "/captcha.html",
115116
BanHTMLFilePath: "",
117+
RemediationHeadersCustomName: "",
116118
ForwardedHeadersCustomName: "X-Forwarded-For",
117119
ForwardedHeadersTrustedIPs: []string{},
118120
ClientTrustedIPs: []string{},

0 commit comments

Comments
 (0)