Files
Charon/docs/plans/CORAZA_WAF_FIX_PLAN.md
GitHub Actions 2adf094f1c feat: Implement comprehensive tests and fixes for Coraza WAF integration
- Add unit tests for WAF ruleset selection priority and handler validation in config_waf_test.go.
- Enhance manager.go to sanitize ruleset names, preventing path traversal vulnerabilities.
- Introduce debug logging for WAF configuration state in manager.go to aid troubleshooting.
- Create integration tests to verify WAF handler presence and ruleset sanitization in manager_additional_test.go.
- Update coraza_integration.sh to include verification steps for WAF configuration and improved error handling.
- Document the Coraza WAF integration fix plan, detailing root cause analysis and implementation tasks.
2025-12-04 04:04:37 +00:00

12 KiB

📋 Plan: Coraza WAF Integration Fix

Status: Ready for Implementation Created: 2024-12-04 CI Failure Reference: Run #19912145599


🧐 UX & Context Analysis

Current State

The Coraza WAF integration is architecturally correct - the plugin is properly compiled into Caddy via xcaddy, and the handler generation pipeline exists. However, the CI integration test consistently fails because the generated Caddy configuration has bugs that prevent the WAF from properly evaluating requests.

Desired User Flow

  1. User creates a WAF ruleset via Security → WAF Config page
  2. User enables WAF mode (block or monitor) in Security settings
  3. WAF automatically applies to all proxy hosts
  4. Malicious requests are blocked (block mode) or logged (monitor mode)
  5. User sees WAF activity in logs and metrics

Integration Test Expectation

POST http://integration.local/post
Body: <script>alert(1)</script>
Expected: HTTP 403 (blocked by Coraza)
Actual: HTTP 200 (request passed through)

🤝 Handoff Contract (The Truth)

Caddy JSON API - WAF Handler Format

The coraza-caddy plugin registers as http.handlers.waf. The JSON structure must be:

{
  "handler": "waf",
  "directives": "SecRuleEngine On\nSecRequestBodyAccess On\nInclude /app/data/caddy/coraza/rulesets/integration-xss-a1b2c3d4.conf"
}

Key Fields:

Field Type Required Description
handler string Must be "waf" (maps to http.handlers.waf)
directives string ModSecurity directive string including Include statements
load_owasp_crs bool If true, loads embedded OWASP CRS (not used in our integration)

Ruleset File Format

Files in /app/data/caddy/coraza/rulesets/{name}-{hash}.conf:

SecRuleEngine On
SecRequestBodyAccess On

SecRule REQUEST_BODY "<script>" "id:12345,phase:2,deny,status:403,msg:'XSS blocked'"

Critical Directives:

  • SecRuleEngine On → Blocking mode (returns 403)
  • SecRuleEngine DetectionOnly → Monitor mode (logs but passes through)
  • SecRequestBodyAccess On → Required to inspect POST bodies

🔍 Root Cause Analysis

Bug 1: Ruleset Selection Priority (CRITICAL)

File: backend/internal/caddy/config.go#L743-L748

// CURRENT (buggy):
if r.Name == "owasp-crs" || (host != nil && r.Name == host.Application) ||
   (hostRulesetName != "" && r.Name == hostRulesetName) ||
   (secCfg != nil && r.Name == secCfg.WAFRulesSource) {

Problem: owasp-crs is checked FIRST, so if any ruleset named "owasp-crs" exists in the database, it will always be selected even when the user specifies a different ruleset via waf_rules_source.

Fix: Reorder conditions to prioritize user-specified names:

// FIXED:
if (secCfg != nil && secCfg.WAFRulesSource != "" && r.Name == secCfg.WAFRulesSource) ||
   (hostRulesetName != "" && r.Name == hostRulesetName) ||
   (host != nil && r.Name == host.Application) ||
   r.Name == "owasp-crs" {

Bug 2: WAF Handler Returned Without Directives

File: backend/internal/caddy/config.go#L754-L770

// CURRENT (buggy):
h := Handler{"handler": "waf"}
if selected != nil {
    // set directives...
} else if secCfg != nil && secCfg.WAFRulesSource != "" {
    // set directives...
}
// BUG: Returns handler even if no directives were set!
return h, nil

Problem: If no matching ruleset is found, the handler is returned without any rules, creating a no-op WAF that blocks nothing.

Fix: Return nil if no directives could be set:

// FIXED:
h := Handler{"handler": "waf"}
directivesSet := false

if selected != nil {
    if rulesetPaths != nil {
        if p, ok := rulesetPaths[selected.Name]; ok && p != "" {
            h["directives"] = fmt.Sprintf("Include %s", p)
            directivesSet = true
        }
    }
} else if secCfg != nil && secCfg.WAFRulesSource != "" {
    if rulesetPaths != nil {
        if p, ok := rulesetPaths[secCfg.WAFRulesSource]; ok && p != "" {
            h["directives"] = fmt.Sprintf("Include %s", p)
            directivesSet = true
        }
    }
}

if !directivesSet {
    logger.Log().Warn("WAF enabled but no ruleset directives could be set")
    return nil, nil  // Don't create a useless handler
}

return h, nil

Bug 3: Missing Debug Logging for Generated Config

File: backend/internal/caddy/manager.go

Problem: When the integration test fails, there's no easy way to see what Caddy config was actually generated and sent.

Fix: Add structured debug logging:

// In ApplyConfig, after generating handlers:
logger.Log().WithFields(map[string]interface{}{
    "waf_enabled":    wafEnabled,
    "ruleset_count":  len(rulesets),
    "ruleset_paths":  rulesetPaths,
}).Debug("WAF configuration state")

Bug 4: Integration Test Timing Issues

File: scripts/coraza_integration.sh

Problem: The test creates a proxy host, then a ruleset, then security config. Each triggers ApplyConfig. The final config might not include the WAF handler if timing is off.

Fix: Add explicit config reload and verification:

# After setting up config, force a reload and verify
echo "Forcing Caddy config reload..."
curl -s http://localhost:8080/api/v1/caddy/reload || true
sleep 3

# Verify WAF handler is present in Caddy config
echo "Verifying WAF handler in Caddy config..."
CADDY_CONFIG=$(curl -s http://localhost:2019/config)
if echo "$CADDY_CONFIG" | grep -q '"handler":"waf"'; then
    echo "✓ WAF handler found in Caddy config"
else
    echo "✗ WAF handler NOT found in Caddy config"
    echo "Caddy config dump:"
    echo "$CADDY_CONFIG" | head -100
    exit 1
fi

🏗️ Phase 1: Backend Implementation (Go)

Task 1.1: Fix Ruleset Selection Priority

File: backend/internal/caddy/config.go Function: buildWAFHandler Estimate: 30 minutes

// Replace lines 743-748 with:
for i, r := range rulesets {
    // Priority order:
    // 1. Exact match to secCfg.WAFRulesSource (user's global choice)
    // 2. Exact match to hostRulesetName (per-host advanced_config)
    // 3. Match to host.Application (app-specific defaults)
    // 4. Fallback to owasp-crs
    if (secCfg != nil && secCfg.WAFRulesSource != "" && r.Name == secCfg.WAFRulesSource) {
        selected = &rulesets[i]
        break
    }
    if hostRulesetName != "" && r.Name == hostRulesetName {
        selected = &rulesets[i]
        break
    }
    if host != nil && r.Name == host.Application {
        selected = &rulesets[i]
        break
    }
    if r.Name == "owasp-crs" && selected == nil {
        selected = &rulesets[i]
        // Don't break - keep looking for better matches
    }
}

Task 1.2: Add Validation for WAF Handler

File: backend/internal/caddy/config.go Function: buildWAFHandler Estimate: 30 minutes

Ensure the handler is only returned if it has valid directives. Log a warning otherwise.

Task 1.3: Add Debug Logging

File: backend/internal/caddy/manager.go Function: ApplyConfig Estimate: 20 minutes

Add structured logging to capture WAF state during config generation.

Task 1.4: Update Unit Tests

Files:

  • backend/internal/caddy/config_test.go
  • backend/internal/caddy/manager_additional_test.go

Estimate: 1 hour

  • Test ruleset selection priority
  • Test handler validation
  • Test empty ruleset handling

🎨 Phase 2: Frontend (No Changes Required)

The frontend WAF configuration UI is working correctly. No changes needed.


🛠️ Phase 3: DevOps/CI Fixes

Task 3.1: Improve Integration Test Robustness

File: scripts/coraza_integration.sh Estimate: 45 minutes

#!/usr/bin/env bash
set -euo pipefail

# ... existing setup ...

# IMPROVEMENT 1: Add config verification step
verify_waf_config() {
    local retries=5
    local wait=2

    for i in $(seq 1 $retries); do
        CADDY_CONFIG=$(curl -s http://localhost:2019/config)

        if echo "$CADDY_CONFIG" | grep -q '"handler":"waf"'; then
            echo "✓ WAF handler verified in Caddy config"

            # Also verify the directives include our ruleset
            if echo "$CADDY_CONFIG" | grep -q "integration-xss"; then
                echo "✓ Ruleset 'integration-xss' found in directives"
                return 0
            fi
        fi

        echo "Waiting for config to propagate (attempt $i/$retries)..."
        sleep $wait
    done

    echo "✗ WAF handler verification failed after $retries attempts"
    echo "Caddy config dump:"
    curl -s http://localhost:2019/config | head -200
    return 1
}

# IMPROVEMENT 2: Add container log dump on failure
on_failure() {
    echo ""
    echo "=== FAILURE DEBUG INFO ==="
    echo ""
    echo "=== Charon API Logs ==="
    docker logs charon-debug 2>&1 | tail -100
    echo ""
    echo "=== Caddy Config ==="
    curl -s http://localhost:2019/config | head -200
    echo ""
    echo "=== Ruleset Files ==="
    docker exec charon-debug sh -c 'ls -la /app/data/caddy/coraza/rulesets/ 2>/dev/null' || echo "No rulesets found"
    docker exec charon-debug sh -c 'cat /app/data/caddy/coraza/rulesets/*.conf 2>/dev/null' || echo "No ruleset content"
}
trap on_failure ERR

# ... rest of test with verify_waf_config calls ...

Task 3.2: Add CI Debug Output

File: .github/workflows/waf-integration.yml Estimate: 20 minutes

Add step to dump Caddy config on failure for easier debugging.


🕵️ Phase 4: QA & Testing

Manual Test Checklist

  1. Block Mode Test

    • Create ruleset with XSS rule
    • Set WAF mode to block
    • Send <script> payload → Expect 403
  2. Monitor Mode Test

    • Set WAF mode to monitor
    • Send <script> payload → Expect 200 (logged only)
    • Verify log entry shows WAF detection
  3. Ruleset Priority Test

    • Create two rulesets: test-rules and owasp-crs
    • Set waf_rules_source to test-rules
    • Verify test-rules is used (not owasp-crs)
  4. Empty Ruleset Test

    • Enable WAF with no rulesets created
    • Verify no WAF handler is added (not a broken one)

CI Verification

After fixes are merged:

  • WAF Integration workflow passes
  • No regressions in main CI pipeline

📚 Phase 5: Documentation

Task 5.1: Update Cerberus Docs

File: docs/cerberus.md

  • Update WAF status from "Prototype" to "Functional"
  • Document proper ruleset creation flow
  • Add troubleshooting section

Task 5.2: Add Debug Guide

File: docs/debugging-waf.md (new)

Document how to:

  • Inspect Caddy config via admin API
  • Check ruleset file contents
  • Read WAF logs

⏱️ Timeline Estimate

Phase Task Estimate
1 Backend fixes 2.5 hours
2 Frontend 0 hours
3 CI/DevOps 1 hour
4 QA Testing 1 hour
5 Documentation 1 hour
Total ~5.5 hours

📎 Appendix: Technical Reference

Coraza-Caddy Plugin Source

ModSecurity Directive Reference

  • SecRuleEngine On|Off|DetectionOnly
  • SecRequestBodyAccess On|Off
  • SecRule VARIABLE OPERATOR "ACTIONS"
  • Include /path/to/file.conf

Charon WAF Flow

User creates ruleset → DB insert → ApplyConfig triggered
  ↓
manager.go writes ruleset file with hash
  ↓
config.go buildWAFHandler() creates handler with Include directive
  ↓
Handler added to securityHandlers slice
  ↓
JSON config sent to Caddy admin API
  ↓
Caddy loads coraza-caddy plugin, parses directives, creates WAF instance