10 KiB
QA Final CrowdSec Validation Report
Date: December 15, 2025 QA Agent: QA_Security Test Environment: Fresh no-cache Docker build
VERDICT: ❌ FAIL
CrowdSec infrastructure is operational but traffic blocking is NOT working.
Test Results Summary
✅ PASS: Infrastructure Components
| Component | Status | Evidence |
|---|---|---|
| CrowdSec Process | ✅ RUNNING | PID 67, verified via logs |
| CrowdSec LAPI | ✅ HEALTHY | Listening on 127.0.0.1:8085 |
| Caddy App Config | ✅ POPULATED | apps.crowdsec is non-null |
| Bouncer Registration | ✅ REGISTERED | charon-caddy-bouncer active |
| Bouncer Last Pull | ✅ ACTIVE | 2025-12-15T18:01:21Z |
| Environment Variables | ✅ SET | All required vars configured |
❌ FAIL: Traffic Blocking
| Test | Expected | Actual | Result |
|---|---|---|---|
| Banned IP (172.16.0.99) | 403 Forbidden | 200 OK | ❌ FAIL |
| Normal Traffic | 200 OK | 200 OK | ✅ PASS |
| Decision in LAPI | Present | Present | ✅ PASS |
| Decision Streamed | Yes | Yes | ✅ PASS |
| Bouncer Blocking | Active | INACTIVE | ❌ FAIL |
Detailed Evidence
1. Database Enable Status
Method: Environment variables in docker-compose.override.yml
- CHARON_SECURITY_CROWDSEC_MODE=local
- CHARON_SECURITY_CROWDSEC_API_URL=http://localhost:8080
- CHARON_SECURITY_CROWDSEC_API_KEY=charonbouncerkey2024
- CERBERUS_SECURITY_CERBERUS_ENABLED=true
Status: ✅ Configured correctly
2. App-Level Config Verification
Command: docker exec charon curl -s http://localhost:2019/config/ | jq '.apps.crowdsec'
Output:
{
"api_key": "charonbouncerkey2024",
"api_url": "http://127.0.0.1:8085",
"enable_streaming": true,
"ticker_interval": "60s"
}
Status: ✅ Non-null and properly configured
3. Bouncer Registration
Command: docker exec charon cscli bouncers list
Output:
-----------------------------------------------------------------------------------------------------
Name IP Address Valid Last API pull Type Version Auth Type
-----------------------------------------------------------------------------------------------------
charon-caddy-bouncer 127.0.0.1 ✔️ 2025-12-15T18:01:21Z caddy-cs-bouncer v0.9.2 api-key
-----------------------------------------------------------------------------------------------------
Status: ✅ Registered and actively pulling
4. Decision Creation
Command: docker exec charon cscli decisions add --ip 172.16.0.99 --duration 15m --reason "FINAL QA TEST"
Output:
+----+--------+----------------+---------------+--------+---------+----+--------+------------+----------+
| ID | Source | Scope:Value | Reason | Action | Country | AS | Events | expiration | Alert ID |
+----+--------+----------------+---------------+--------+---------+----+--------+------------+----------+
| 1 | cscli | Ip:172.16.0.99 | FINAL QA TEST | ban | | | 1 | 14m55s | 1 |
+----+--------+----------------+---------------+--------+---------+----+--------+------------+----------+
Status: ✅ Decision created successfully
5. Decision Streaming Verification
Command: docker exec charon curl -s 'http://localhost:8085/v1/decisions/stream?startup=true' -H "X-Api-Key: charonbouncerkey2024"
Output:
{"deleted":null,"new":[{"duration":"13m58s","id":1,"origin":"cscli","scenario":"FINAL QA TEST","scope":"Ip","type":"ban","u...
Status: ✅ Decision is being streamed from LAPI
6. Traffic Blocking Test (CRITICAL FAILURE)
Test Command: curl -H "X-Forwarded-For: 172.16.0.99" http://localhost/ -v
Expected Result: HTTP/1.1 403 Forbidden with CrowdSec block message
Actual Result:
< HTTP/1.1 200 OK
< Accept-Ranges: bytes
< Alt-Svc: h3=":443"; ma=2592000
< Content-Length: 2367
< Content-Type: text/html; charset=utf-8
Status: ❌ FAIL - Request was NOT blocked
7. Bouncer Handler Verification
Command: docker exec charon curl -s http://localhost:2019/config/ | jq -r '.apps.http.servers | ... | select(.handler == "crowdsec")'
Output: Found crowdsec handler in multiple routes (5+ instances)
Status: ✅ Handler is registered in routes
8. Normal Traffic Test
Command: curl http://localhost/ -v
Result: HTTP/1.1 200 OK
Status: ✅ PASS - Normal traffic flows correctly
Root Cause Analysis
Primary Issue: Bouncer Not Transitioning from Startup Mode
Evidence:
- Bouncer continuously polls with
startup=trueparameter - Log entries show:
GET /v1/decisions/stream?additional_pull=false&community_pull=false&startup=true - This parameter should only be present during initial bouncer startup
- After initial pull, bouncer should switch to continuous streaming mode
Technical Details:
- Caddy CrowdSec bouncer initializes in "startup" mode
- Makes initial pull to get all existing decisions
- Should transition to streaming mode where it receives decision updates in real-time
- Actual behavior: Bouncer stays in startup mode indefinitely
- Because it's in startup mode, it may not be actively applying decisions to traffic
Secondary Issues Identified
-
Decision Application Lag
- Even though decisions are streamed, there's no evidence they're being applied to the in-memory decision store
- No blocking logs appear in Caddy access logs
- No "blocked by CrowdSec" entries in security logs
-
Potential Middleware Ordering
- CrowdSec handler is present in routes but may be positioned after other handlers
- Could be bypassed if reverse_proxy handler executes first
-
Client IP Detection
- Tested with
X-Forwarded-For: 172.16.0.99 - Bouncer may not be reading this header correctly
- No
trusted_proxiesconfiguration present in bouncer config
- Tested with
Configuration State
Caddy CrowdSec App Config
{
"api_key": "charonbouncerkey2024",
"api_url": "http://127.0.0.1:8085",
"enable_streaming": true,
"ticker_interval": "60s"
}
Missing Fields:
- ❌
trusted_proxies- Required for X-Forwarded-For support - ❌
captcha_provider- Optional but recommended - ❌
ban_template_path- Custom block page
Environment Variables
CHARON_SECURITY_CROWDSEC_MODE=local
CHARON_SECURITY_CROWDSEC_API_URL=http://localhost:8080 # ⚠️ Should be 8085
CHARON_SECURITY_CROWDSEC_API_KEY=charonbouncerkey2024
CERBERUS_SECURITY_CERBERUS_ENABLED=true
Issue: LAPI URL is set to 8080 (Charon backend) instead of 8085 (CrowdSec LAPI) Impact: Bouncer is connecting correctly because Caddy config uses 127.0.0.1:8085, but environment variable inconsistency could cause issues
Pre-Commit Checks
Status: ✅ ALL PASSED (Run at beginning of session)
Integration Test
Script: scripts/crowdsec_startup_test.sh
Last Run Status: ❌ FAIL (Exit code 1)
Note: Integration test was run in previous session; container restart invalidated results
ABSOLUTE REQUIREMENTS FOR PASS
| Requirement | Status |
|---|---|
✅ apps.crowdsec is non-null |
PASS |
✅ Bouncer registered in cscli bouncers list |
PASS |
| ❌ Test IP returns 403 Forbidden | FAIL |
| ✅ Normal traffic returns 200 OK | PASS |
| ❌ Security logs show crowdsec blocks | FAIL (Not tested - blocking doesn't work) |
| ✅ Pre-commit passes 100% | PASS |
Overall: 4/6 requirements met = FAIL
Recommendation: DO NOT DEPLOY
Critical Blockers
-
Traffic blocking is completely non-functional
- Despite all infrastructure being operational
- Decisions are created and streamed but not enforced
- Zero evidence of middleware intercepting requests
-
Bouncer stuck in startup mode
- Never transitions to active streaming
- May be a bug in caddy-cs-bouncer v0.9.2
- Requires investigation of bouncer implementation
Required Fixes
Immediate Actions
-
Add trusted_proxies configuration to Caddy CrowdSec app
{ "api_key": "charonbouncerkey2024", "api_url": "http://127.0.0.1:8085", "enable_streaming": true, "ticker_interval": "60s", "trusted_proxies": ["127.0.0.1/32", "172.20.0.0/16"] } -
Fix LAPI URL in environment
- Change
CHARON_SECURITY_CROWDSEC_API_URLfromhttp://localhost:8080tohttp://localhost:8085
- Change
-
Investigate bouncer startup mode persistence
- Check caddy-cs-bouncer source code for startup mode logic
- May need to restart Caddy after bouncer initialization
- Could be a timing issue with LAPI availability
-
Verify middleware ordering
- Ensure CrowdSec handler executes BEFORE reverse_proxy
- Check route handler chain in Caddy config
- Add explicit ordering if necessary
Verification Steps After Fix
- Add test decision
- Wait 60 seconds (one ticker interval)
- Test with curl from banned IP
- Verify 403 response
- Check Caddy access logs for "crowdsec" denial
- Verify security logs show block event
Next Steps
-
Backend Team: Investigate Caddy config generation in
internal/caddy/config.go- Add
trusted_proxiesfield to CrowdSec app config - Ensure middleware ordering is correct
- Add debug logging for bouncer decision application
- Add
-
DevOps Team: Consider alternative bouncer implementations
- Test with different caddy-cs-bouncer version
- Evaluate fallback to HTTP middleware bouncer
- Document bouncer version compatibility
-
QA Team: Create blocking verification test suite
- Automated test that validates actual blocking
- Part of integration test suite
- Must run before any security release
Evidence Files
final_block_test.txt- Contains full curl output showing 200 OK response- Container logs available via
docker logs charon - Caddy config available via
http://localhost:2019/config/
Summary
While the CrowdSec integration is architecturally sound and all components are operationally healthy, the critical functionality of blocking malicious traffic is completely broken. This is a show-stopper bug that makes the CrowdSec feature unusable in production.
The bouncer registers correctly, pulls decisions successfully, and integrates with Caddy's request pipeline, but fails to enforce any decisions. This represents a complete failure of the security feature's core purpose.
Status: ❌ FAIL - DO NOT DEPLOY
Signed: QA_Security Agent Date: 2025-12-15 Session: Final Validation After No-Cache Rebuild