11 KiB
CrowdSec App-Level Configuration Implementation Report
Date: December 15, 2025 Agent: Backend_Dev Status: ✅ COMPLETE
Executive Summary
Successfully implemented app-level CrowdSec configuration for Caddy, moving from inline handler configuration to the proper apps.crowdsec section as required by the caddy-crowdsec-bouncer plugin.
Key Changes:
- ✅ Added
CrowdSecAppstruct tobackend/internal/caddy/types.go - ✅ Populated
config.Apps.CrowdSecinGenerateConfigwhen enabled - ✅ Simplified handler to minimal
{"handler": "crowdsec"} - ✅ Updated all tests to reflect new structure
- ✅ All tests pass
Implementation Details
1. App-Level Configuration Struct
File: backend/internal/caddy/types.go
Added new CrowdSecApp struct:
// CrowdSecApp configures the CrowdSec app module.
// Reference: https://github.com/hslatman/caddy-crowdsec-bouncer
type CrowdSecApp struct {
APIUrl string `json:"api_url"`
APIKey string `json:"api_key"`
TickerInterval string `json:"ticker_interval,omitempty"`
EnableStreaming *bool `json:"enable_streaming,omitempty"`
}
Updated Apps struct to include CrowdSec:
type Apps struct {
HTTP *HTTPApp `json:"http,omitempty"`
TLS *TLSApp `json:"tls,omitempty"`
CrowdSec *CrowdSecApp `json:"crowdsec,omitempty"`
}
2. Config Population
File: backend/internal/caddy/config.go in GenerateConfig function
When CrowdSec is enabled, populate the app-level configuration:
// Configure CrowdSec app if enabled
if crowdsecEnabled {
apiURL := "http://127.0.0.1:8085"
if secCfg != nil && secCfg.CrowdSecAPIURL != "" {
apiURL = secCfg.CrowdSecAPIURL
}
apiKey := getCrowdSecAPIKey()
enableStreaming := true
config.Apps.CrowdSec = &CrowdSecApp{
APIUrl: apiURL,
APIKey: apiKey,
TickerInterval: "60s",
EnableStreaming: &enableStreaming,
}
}
3. Simplified Handler
File: backend/internal/caddy/config.go in buildCrowdSecHandler function
Handler is now minimal - all configuration is at app-level:
func buildCrowdSecHandler(_ *models.ProxyHost, _ *models.SecurityConfig, crowdsecEnabled bool) (Handler, error) {
if !crowdsecEnabled {
return nil, nil
}
// Return minimal handler - all config is at app-level
return Handler{"handler": "crowdsec"}, nil
}
4. Test Updates
Files Updated:
backend/internal/caddy/config_crowdsec_test.go- All handler tests updated to expect minimal structurebackend/internal/caddy/config_generate_additional_test.go- Config generation test updated to check app-level config
Key Test Changes:
- Handlers no longer have inline
lapi_url,api_keyfields - Tests verify
config.Apps.CrowdSecis populated correctly - Tests verify handler is minimal
{"handler": "crowdsec"}
Configuration Structure
Before (Inline Handler Config) ❌
{
"apps": {
"http": {
"servers": {
"srv0": {
"routes": [{
"handle": [{
"handler": "crowdsec",
"lapi_url": "http://127.0.0.1:8085",
"api_key": "xxx",
"enable_streaming": true,
"ticker_interval": "60s"
}]
}]
}
}
}
}
}
Problem: Plugin rejected inline config with "json: unknown field" errors.
After (App-Level Config) ✅
{
"apps": {
"crowdsec": {
"api_url": "http://127.0.0.1:8085",
"api_key": "xxx",
"ticker_interval": "60s",
"enable_streaming": true
},
"http": {
"servers": {
"srv0": {
"routes": [{
"handle": [{
"handler": "crowdsec"
}]
}]
}
}
}
}
}
Solution: Configuration at app-level, handler references module only.
Verification
Unit Tests
All CrowdSec-related tests pass:
cd backend && go test ./internal/caddy/... -run "CrowdSec" -v
Results:
- ✅
TestBuildCrowdSecHandler_Disabled - ✅
TestBuildCrowdSecHandler_EnabledWithoutConfig - ✅
TestBuildCrowdSecHandler_EnabledWithEmptyAPIURL - ✅
TestBuildCrowdSecHandler_EnabledWithCustomAPIURL - ✅
TestBuildCrowdSecHandler_JSONFormat - ✅
TestBuildCrowdSecHandler_WithHost - ✅
TestGenerateConfig_WithCrowdSec - ✅
TestGenerateConfig_CrowdSecDisabled - ✅
TestGenerateConfig_CrowdSecHandlerFromSecCfg
Build Verification
Backend compiles successfully:
cd backend && go build ./...
Docker image builds successfully:
docker build -t charon:local .
Runtime Verification Steps
To verify in a running container:
1. Enable CrowdSec
Via Security dashboard UI:
- Navigate to http://localhost:8080/security
- Toggle "CrowdSec" ON
- Click "Save"
2. Check App-Level Config
docker exec charon curl -s http://localhost:2019/config/ | jq '.apps.crowdsec'
Expected Output:
{
"api_url": "http://127.0.0.1:8085",
"api_key": "<generated-key>",
"ticker_interval": "60s",
"enable_streaming": true
}
3. Check Handler is Minimal
docker exec charon curl -s http://localhost:2019/config/ | \
jq '.apps.http.servers[].routes[].handle[] | select(.handler == "crowdsec")'
Expected Output:
{
"handler": "crowdsec"
}
4. Verify Bouncer Registration
docker exec charon cscli bouncers list
Expected: Bouncer registered with name containing "caddy"
5. Test Blocking
Add test ban:
docker exec charon cscli decisions add --ip 10.255.255.250 --duration 5m --reason "app-level test"
Test request:
curl -H "X-Forwarded-For: 10.255.255.250" http://localhost/ -v
Expected: 403 Forbidden with X-Crowdsec-Decision header
Cleanup:
docker exec charon cscli decisions delete --ip 10.255.255.250
6. Check Security Logs
Navigate to http://localhost:8080/security/logs
Expected: Blocked entry with:
source: "crowdsec"blocked: trueX-Crowdsec-Decision: "ban"
Configuration Details
API URL
Default: http://127.0.0.1:8085
Can be overridden via SecurityConfig.CrowdSecAPIURL in database.
API Key
Read from environment variables in order:
CROWDSEC_API_KEYCROWDSEC_BOUNCER_API_KEYCERBERUS_SECURITY_CROWDSEC_API_KEYCHARON_SECURITY_CROWDSEC_API_KEYCPM_SECURITY_CROWDSEC_API_KEY
Generated automatically during CrowdSec startup via register_bouncer.sh.
Ticker Interval
Default: 60s
How often to poll for decisions when streaming is disabled.
Enable Streaming
Default: true
Maintains persistent connection to LAPI for real-time decision updates (no polling delay).
Architecture Benefits
1. Proper Plugin Integration
App-level configuration is the correct way to configure Caddy plugins that need global state. The bouncer plugin can now:
- Maintain a single LAPI connection across all routes
- Share decision cache across all virtual hosts
- Properly initialize streaming mode
2. Performance
Single LAPI connection instead of per-route connections:
- Reduced memory footprint
- Lower LAPI load
- Faster startup time
3. Maintainability
Clear separation of concerns:
- App config: Global CrowdSec settings
- Handler config: Which routes use CrowdSec (minimal reference)
4. Consistency
Matches other Caddy apps (HTTP, TLS) structure:
{
"apps": {
"http": { /* HTTP app config */ },
"tls": { /* TLS app config */ },
"crowdsec": { /* CrowdSec app config */ }
}
}
Troubleshooting
App Config Not Appearing
Cause: CrowdSec not enabled in SecurityConfig
Solution:
# Check current mode
docker exec charon curl http://localhost:8080/api/v1/admin/security/config
# Enable via UI or update database
Bouncer Not Registering
Possible Causes:
- LAPI not running:
docker exec charon ps aux | grep crowdsec - API key missing:
docker exec charon env | grep CROWDSEC - Network issue:
docker exec charon curl http://127.0.0.1:8085/health
Debug:
# Check Caddy logs
docker logs charon 2>&1 | grep -i "crowdsec"
# Check LAPI logs
docker exec charon tail -f /app/data/crowdsec/log/crowdsec.log
Handler Still Has Inline Config
Cause: Using old Docker image
Solution:
# Rebuild
docker build -t charon:local .
# Restart
docker-compose -f docker-compose.override.yml restart
Files Changed
| File | Lines Changed | Description |
|---|---|---|
| backend/internal/caddy/types.go | +14 | Added CrowdSecApp struct and field to Apps |
| backend/internal/caddy/config.go | +15, -23 | App-level config population, simplified handler |
| backend/internal/caddy/config_crowdsec_test.go | +~80, -~40 | Updated all handler tests |
| backend/internal/caddy/config_generate_additional_test.go | +~20, -~10 | Updated config generation test |
| scripts/verify_crowdsec_app_config.sh | +90 | New verification script |
Related Documentation
- Current Spec: CrowdSec Configuration Research
- CrowdSec Bouncer Field Investigation
- Security Implementation Plan
- Caddy CrowdSec Bouncer Plugin
Success Criteria
| Criterion | Status |
|---|---|
apps.crowdsec populated in Caddy config |
✅ Verified in tests |
Handler is minimal {"handler": "crowdsec"} |
✅ Verified in tests |
Bouncer registered in cscli bouncers list |
⏳ Requires runtime verification |
| Test ban results in 403 Forbidden | ⏳ Requires runtime verification |
Security logs show source="crowdsec", blocked=true |
⏳ Requires runtime verification |
Note: Runtime verification requires CrowdSec to be enabled in SecurityConfig. Use the verification steps above to complete end-to-end testing.
Next Steps
-
Runtime Verification:
- Enable CrowdSec via Security dashboard
- Run verification steps above
- Document results in follow-up report
-
Integration Test Update:
- Update
scripts/crowdsec_startup_test.shto verify app-level config - Add check for
apps.crowdsecpresence - Add check for minimal handler structure
- Update
-
Documentation Update:
- Update Security Docs with app-level config details
- Add troubleshooting section for bouncer registration
Implementation Status: ✅ COMPLETE Runtime Verification: ⏳ PENDING (requires CrowdSec enabled in SecurityConfig) Estimated Blocking Time: 2-5 minutes after CrowdSec enabled (bouncer registration + first decision sync)