mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-07-21 11:38:59 +02:00
✨️ Do not block middleware startup due to stream intialization (#281)
* 🍱 not block on stream startup * 🍱 fix * 🍱 fix lint * 🍱 fix lint * 🍱 fix lint * 🍱 update readme * 📚 clarify StreamStartupBlock warning in README Make the fail-open implication of StreamStartupBlock=false explicit: banned IPs are allowed through until the first stream sync completes. * 🐛 clear isCrowdsecStreamStartup on alreadyUpdated path When cacheTimeoutKey is already set (another instance/process has populated the stream cache within the update window), the early return in handleStreamCache used to leave isCrowdsecStreamStartup at true, causing this instance to keep sending startup=true on subsequent ticks. Flip the flag in the early-return path so startup is correctly tracked across multi-instance deployments sharing a cache. --------- Co-authored-by: mhx <mathieu@hanotaux.fr>
This commit is contained in:
@@ -465,6 +465,12 @@ make run
|
|||||||
- int64
|
- int64
|
||||||
- default: 0
|
- default: 0
|
||||||
- Used only in `stream` and `alone` mode, the maximum number of time we can not reach Crowdsec before blocking traffic (set -1 to never block)
|
- Used only in `stream` and `alone` mode, the maximum number of time we can not reach Crowdsec before blocking traffic (set -1 to never block)
|
||||||
|
- StreamStartupBlock
|
||||||
|
- bool
|
||||||
|
- default: true
|
||||||
|
- Used only in `stream` and `alone` mode, controls whether the initial stream update runs synchronously or asynchronously during plugin initialization
|
||||||
|
- When `true`, plugin initialization waits for Crowdsec to be ready before serving traffic.
|
||||||
|
- **Warning**: When `false`, all requests bypass remediation until the first stream sync completes — banned IPs will be allowed through during this window. Only disable when startup availability is more important than blocking at startup.
|
||||||
- DefaultDecisionSeconds
|
- DefaultDecisionSeconds
|
||||||
- int64
|
- int64
|
||||||
- default: 60
|
- default: 60
|
||||||
@@ -598,6 +604,7 @@ http:
|
|||||||
LogFilePath: ""
|
LogFilePath: ""
|
||||||
updateIntervalSeconds: 60
|
updateIntervalSeconds: 60
|
||||||
updateMaxFailure: 0
|
updateMaxFailure: 0
|
||||||
|
streamStartupBlock: true
|
||||||
defaultDecisionSeconds: 60
|
defaultDecisionSeconds: 60
|
||||||
remediationStatusCode: 403
|
remediationStatusCode: 403
|
||||||
httpTimeoutSeconds: 10
|
httpTimeoutSeconds: 10
|
||||||
|
|||||||
+10
-5
@@ -64,7 +64,7 @@ const (
|
|||||||
|
|
||||||
//nolint:gochecknoglobals
|
//nolint:gochecknoglobals
|
||||||
var (
|
var (
|
||||||
isStartup = true
|
isCrowdsecStreamStartup = true
|
||||||
isCrowdsecStreamHealthy = true
|
isCrowdsecStreamHealthy = true
|
||||||
updateFailure int64
|
updateFailure int64
|
||||||
streamTicker chan bool
|
streamTicker chan bool
|
||||||
@@ -123,7 +123,7 @@ type Bouncer struct {
|
|||||||
|
|
||||||
// New creates the crowdsec bouncer plugin.
|
// New creates the crowdsec bouncer plugin.
|
||||||
//
|
//
|
||||||
//nolint:gocyclo
|
//nolint:nestif,gocyclo,gocognit
|
||||||
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)
|
config.LogLevel = strings.ToUpper(config.LogLevel)
|
||||||
log := logger.NewWithFormat(config.LogLevel, config.LogFilePath, config.LogFormat)
|
log := logger.NewWithFormat(config.LogLevel, config.LogFilePath, config.LogFormat)
|
||||||
@@ -291,8 +291,11 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if config.StreamStartupBlock {
|
||||||
handleStreamTicker(bouncer)
|
handleStreamTicker(bouncer)
|
||||||
isStartup = false
|
} else {
|
||||||
|
go handleStreamTicker(bouncer)
|
||||||
|
}
|
||||||
streamTicker = startTicker("stream", config.UpdateIntervalSeconds, log, func() {
|
streamTicker = startTicker("stream", config.UpdateIntervalSeconds, log, func() {
|
||||||
handleStreamTicker(bouncer)
|
handleStreamTicker(bouncer)
|
||||||
})
|
})
|
||||||
@@ -301,7 +304,7 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
|
|||||||
// Start metrics ticker if not already running
|
// Start metrics ticker if not already running
|
||||||
if metricsTicker == nil && config.MetricsUpdateIntervalSeconds > 0 {
|
if metricsTicker == nil && config.MetricsUpdateIntervalSeconds > 0 {
|
||||||
lastMetricsPush = time.Now() // Initialize lastMetricsPush when starting the metrics ticker
|
lastMetricsPush = time.Now() // Initialize lastMetricsPush when starting the metrics ticker
|
||||||
handleMetricsTicker(bouncer)
|
go handleMetricsTicker(bouncer)
|
||||||
metricsTicker = startTicker("metrics", config.MetricsUpdateIntervalSeconds, log, func() {
|
metricsTicker = startTicker("metrics", config.MetricsUpdateIntervalSeconds, log, func() {
|
||||||
handleMetricsTicker(bouncer)
|
handleMetricsTicker(bouncer)
|
||||||
})
|
})
|
||||||
@@ -624,6 +627,7 @@ func handleStreamCache(bouncer *Bouncer) error {
|
|||||||
_, err := bouncer.cacheClient.Get(cacheTimeoutKey)
|
_, err := bouncer.cacheClient.Get(cacheTimeoutKey)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
bouncer.log.Debug("handleStreamCache:alreadyUpdated")
|
bouncer.log.Debug("handleStreamCache:alreadyUpdated")
|
||||||
|
isCrowdsecStreamStartup = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err.Error() != cache.CacheMiss {
|
if err.Error() != cache.CacheMiss {
|
||||||
@@ -634,7 +638,7 @@ func handleStreamCache(bouncer *Bouncer) error {
|
|||||||
Scheme: bouncer.crowdsecScheme,
|
Scheme: bouncer.crowdsecScheme,
|
||||||
Host: bouncer.crowdsecHost,
|
Host: bouncer.crowdsecHost,
|
||||||
Path: bouncer.crowdsecPath + bouncer.crowdsecStreamRoute,
|
Path: bouncer.crowdsecPath + bouncer.crowdsecStreamRoute,
|
||||||
RawQuery: fmt.Sprintf("startup=%t", !isCrowdsecStreamHealthy || isStartup),
|
RawQuery: fmt.Sprintf("startup=%t", !isCrowdsecStreamHealthy || isCrowdsecStreamStartup),
|
||||||
}
|
}
|
||||||
body, err := crowdsecQuery(bouncer, streamRouteURL.String(), nil)
|
body, err := crowdsecQuery(bouncer, streamRouteURL.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -664,6 +668,7 @@ func handleStreamCache(bouncer *Bouncer) error {
|
|||||||
bouncer.cacheClient.Delete(decision.Value)
|
bouncer.cacheClient.Delete(decision.Value)
|
||||||
}
|
}
|
||||||
bouncer.log.Debug("handleStreamCache:updated")
|
bouncer.log.Debug("handleStreamCache:updated")
|
||||||
|
isCrowdsecStreamStartup = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ type Config struct {
|
|||||||
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
||||||
MetricsUpdateIntervalSeconds int64 `json:"metricsUpdateIntervalSeconds,omitempty"`
|
MetricsUpdateIntervalSeconds int64 `json:"metricsUpdateIntervalSeconds,omitempty"`
|
||||||
UpdateMaxFailure int64 `json:"updateMaxFailure,omitempty"`
|
UpdateMaxFailure int64 `json:"updateMaxFailure,omitempty"`
|
||||||
|
StreamStartupBlock bool `json:"streamStartupBlock,omitempty"`
|
||||||
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
||||||
RemediationStatusCode int `json:"remediationStatusCode,omitempty"`
|
RemediationStatusCode int `json:"remediationStatusCode,omitempty"`
|
||||||
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
|
HTTPTimeoutSeconds int64 `json:"httpTimeoutSeconds,omitempty"`
|
||||||
@@ -146,6 +147,7 @@ func New() *Config {
|
|||||||
UpdateIntervalSeconds: 60,
|
UpdateIntervalSeconds: 60,
|
||||||
MetricsUpdateIntervalSeconds: 600,
|
MetricsUpdateIntervalSeconds: 600,
|
||||||
UpdateMaxFailure: 0,
|
UpdateMaxFailure: 0,
|
||||||
|
StreamStartupBlock: true,
|
||||||
DefaultDecisionSeconds: 60,
|
DefaultDecisionSeconds: 60,
|
||||||
RemediationStatusCode: http.StatusForbidden,
|
RemediationStatusCode: http.StatusForbidden,
|
||||||
HTTPTimeoutSeconds: 10,
|
HTTPTimeoutSeconds: 10,
|
||||||
|
|||||||
Reference in New Issue
Block a user