Fail closed on AppSec connection errors (by default), and add configurable timeout#110
Conversation
Connection errors (e.g. connection refused) were returned as errors, causing a 500 response to clients when the AppSec component is down. This is inconsistent with how HTTP 401/404/500 responses from AppSec are handled (all of which log and return nil to fail open). Apply the same fail-open behavior for connection-level errors, and add a test case covering this scenario. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous 10s client / 30s dial timeouts meant every request could block for up to 10 seconds waiting for the AppSec connection to fail when CrowdSec is down. Reduce the dial and TLS timeouts to 500ms (fast enough for any local Docker network) and cap the overall request at 2s. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The AppSec client timeout now defaults to 2s and is configurable via appsec_timeout in the Caddyfile. Dial and TLS handshake timeouts are set equal to the overall timeout, so all timeouts scale together. This is particularly relevant with fail-open behavior: when the AppSec component is unavailable, requests are delayed by up to appsec_timeout before failing open, so users should tune this to their network latency. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
👋 Hey @hslatman, no rush but would love a review whenever you get a chance, thanks! |
| func (c *CrowdSec) appSecTimeout() time.Duration { | ||
| if c.AppSecTimeout == 0 { | ||
| return 2 * time.Second | ||
| } | ||
| return time.Duration(c.AppSecTimeout) | ||
| } |
|
Btw, the change to have a shorter timeout by default certainly makes sense. I don't use AppSec myself, so I'm only testing it locally during development, and not in an actual production environment, so never hit that issue myself. |
Address review feedback: - Add enable_appsec_fail_open config knob (Caddyfile + JSON) to control whether AppSec errors allow requests through or block them - Default to fail-hard (blocking) for safety; users can opt into fail-open with enable_appsec_fail_open - Extract appsec timeout to named var in tests for clarity - Add test case for fail-hard on connection error Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Herman Slatman <hslatman@users.noreply.github.com>
|
I've released https://github.com/hslatman/caddy-crowdsec-bouncer/releases/tag/v0.11.0 with the changes from this PR. Thank you, @com6056! 😄 |
|
Hey, thanks for the new feature. I would like to use it but I could not find the documentation on how to use it ? Does it apply to all hosts in caddy or we can select the configuration per host ? Thanks for your help ! |
The new options in the Caddyfile are here: https://github.com/hslatman/caddy-crowdsec-bouncer/pull/110/changes#diff-71746e5ee17fe78a2ad76d2fcb3be5ab596738f40c9b0c907efce1b68481152aR89-R102. Currently these configurations are only available at the global Caddy level. |
Summary
When the AppSec component is unreachable (e.g. CrowdSec is stopped or restarting), the current behavior blocks or errors requests. This PR changes it to be configurable to fail open: if enabled, and the AppSec endpoint cannot be contacted, the request is logged and allowed through rather than being blocked.
This also adds a configurable
appsec_timeoutCaddyfile option (default: 2s) that controls how long the AppSec client waits before giving up. This is important for tuning fail-open latency — on a local/Docker network, operators may want to set a lower value (we tested with100ms) so that the overhead when AppSec is unavailable stays negligible.Changes
checkRequestnow distinguishes connection errors from AppSec deny decisions. On connection error, logs a warning and returnsnil(allow) rather than propagating the error.appsec_timeout: New Caddyfile option on thecrowdsecglobal block. Defaults to 2s. Sets the HTTP client timeout, dial timeout, and TLS handshake timeout for AppSec requests uniformly.Tested
Verified on a live setup with Caddy + CrowdSec in Docker, using
appsec_timeout 100ms:🤖 Generated with Claude Code