# CrowdSec Full Implementation Plan

**Status:** Planning
**Created:** December 12, 2025
**Priority:** Critical - CrowdSec is completely non-functional
**Issue:** `FATAL crowdsec init: while loading acquisition config: no datasource enabled`

---

## Table of Contents

1. [Executive Summary](#executive-summary)
2. [Root Cause Analysis](#root-cause-analysis)
3. [Architecture Overview](#architecture-overview)
4. [Implementation Phases](#implementation-phases)
   - [Phase 1: Core CrowdSec Configuration](#phase-1-core-crowdsec-configuration)
   - [Phase 2: Caddy Integration](#phase-2-caddy-integration)
   - [Phase 2.5: Unified Logging & Live Viewer Integration](#phase-25-unified-logging--live-viewer-integration)
   - [Phase 3: API Integration](#phase-3-api-integration)
   - [Phase 4: Testing](#phase-4-testing)
5. [File Changes Summary](#file-changes-summary)
6. [Configuration Options](#configuration-options)
7. [Ignore Files Review](#ignore-files-review)
8. [Rollout & Verification](#rollout--verification)

---

## Executive Summary

CrowdSec is currently **completely broken** in Charon. When starting the container with `CERBERUS_SECURITY_CROWDSEC_MODE=local`, CrowdSec crashes immediately with:

```
FATAL crowdsec init: while loading acquisition config: no datasource enabled
```

This indicates that while CrowdSec binaries are installed and configuration files are copied, the **acquisition configuration** (which tells CrowdSec what logs to parse) is missing or empty.

### What Works Today

- ✅ CrowdSec binaries installed (`crowdsec`, `cscli`)
- ✅ Base config files copied from release tarball to `/etc/crowdsec.dist/`
- ✅ Config copied to `/etc/crowdsec/` at container startup
- ✅ LAPI port changed to 8085 to avoid conflict with Charon
- ✅ Machine registration via `cscli machines add -a --force`
- ✅ Hub index update via `cscli hub update`
- ✅ Backend API handlers for decisions, ban/unban, import/export
- ✅ caddy-crowdsec-bouncer compiled into Caddy binary

### What's Broken/Missing

- ❌ **No `acquis.yaml`** - CrowdSec doesn't know what logs to parse
- ❌ **No parsers installed** - No way to understand Caddy log format
- ❌ **No scenarios installed** - No detection rules
- ❌ **No collections installed** - No pre-packaged security configurations
- ❌ **Caddy not logging in parseable format** - Default JSON logging may not match parsers
- ❌ **Bouncer not registered** - caddy-crowdsec-bouncer needs API key
- ❌ **No automated bouncer API key generation**

---

## Root Cause Analysis

### The Fatal Error Explained

CrowdSec requires **datasources** to function. A datasource tells CrowdSec:

1. Where to find logs (file path, journald, etc.)
2. What parser to use for those logs
3. Optional labels for categorization

Without datasources configured in `acquis.yaml`, CrowdSec has nothing to monitor and refuses to start.

### Missing Acquisition Configuration

The CrowdSec release tarball includes default config files, but the `acquis.yaml` in the tarball is either:

1. Empty
2. Contains example datasources that don't exist in the container (like syslog)
3. Not present at all

**Current entrypoint flow:**

```bash
# Step 1: Copy base config (MISSING acquis.yaml or empty)
cp -r /etc/crowdsec.dist/* /etc/crowdsec/

# Step 2: Variable substitution (doesn't create acquis.yaml)
envsubst < config.yaml > config.yaml.tmp && mv config.yaml.tmp config.yaml

# Step 3: Port changes (doesn't affect acquisition)
sed -i 's|listen_uri: 127.0.0.1:8080|listen_uri: 127.0.0.1:8085|g' config.yaml

# Step 4: Hub update (doesn't install parsers/scenarios by default)
cscli hub update

# Step 5: Machine registration (succeeds)
cscli machines add -a --force

# Step 6: START CROWDSEC → CRASH (no datasources!)
crowdsec &
```

### Missing Components

1. **Parsers**: CrowdSec needs parsers to understand log formats
   - Caddy uses JSON logging by default
   - Need `crowdsecurity/caddy-logs` parser or custom parser

2. **Scenarios**: Detection rules that identify attacks
   - HTTP flood detection
   - HTTP probing
   - HTTP path traversal
   - HTTP SQLi/XSS patterns

3. **Collections**: Bundled parsers + scenarios for specific applications
   - `crowdsecurity/caddy` collection (may not exist)
   - `crowdsecurity/http-cve` for known vulnerabilities
   - `crowdsecurity/base-http-scenarios` for generic HTTP attacks

4. **Acquisition Config**: Tells CrowdSec where to read logs

   ```yaml
   # /etc/crowdsec/acquis.yaml
   source: file
   filenames:
     - /var/log/caddy/access.log
   labels:
     type: caddy
   ```

---

## Architecture Overview

### How CrowdSec Should Integrate with Charon

```
┌─────────────────────────────────────────────────────────────────────┐
│                         Docker Container                             │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  ┌─────────────┐         ┌─────────────────────────────────────┐    │
│  │   Caddy     │──logs──▶│  /var/log/caddy/access.log (JSON)   │    │
│  │ (Reverse    │         └──────────────┬──────────────────────┘    │
│  │  Proxy)     │                        │                           │
│  │             │                        ▼                           │
│  │ ┌─────────┐ │         ┌─────────────────────────────────────┐    │
│  │ │Bouncer  │◀├─LAPI───│  CrowdSec Agent (LAPI :8085)         │    │
│  │ │Plugin   │ │         │  ┌─────────────────────────────────┐│    │
│  │ └─────────┘ │         │  │ acquis.yaml → file acquisition  ││    │
│  └─────────────┘         │  │ parsers → crowdsecurity/caddy   ││    │
│        │                 │  │ scenarios → http-flood, etc.    ││    │
│        │                 │  └─────────────────────────────────┘│    │
│        │                 └──────────────┬──────────────────────┘    │
│        │                                │                           │
│        ▼                                ▼                           │
│  ┌─────────────┐         ┌─────────────────────────────────────┐    │
│  │   Charon    │◀─API───│  cscli (CLI management)              │    │
│  │ (Management │         │  - decisions list/add/delete        │    │
│  │    API)     │         │  - hub install parsers/scenarios   │    │
│  └─────────────┘         └─────────────────────────────────────┘    │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘
```

### Data Flow

1. **Request arrives** at Caddy reverse proxy
2. **Caddy bouncer plugin** checks CrowdSec LAPI for decision on client IP
   - If banned → Return 403
   - If allowed → Continue processing
3. **Caddy logs request** to `/var/log/caddy/access.log` in JSON format
4. **CrowdSec agent** reads log file via acquisition config
5. **Parser** converts JSON log to normalized event
6. **Scenarios** analyze events for attack patterns
7. **Decisions** are created (ban IP for X duration)
8. **Bouncer plugin** receives decision via streaming/polling
9. **Future requests** from banned IP are blocked

### Required CrowdSec Components

| Component | Source | Purpose |
|-----------|--------|---------|
| `crowdsecurity/caddy-logs` | Hub | Parse Caddy JSON access logs |
| `crowdsecurity/base-http-scenarios` | Hub | Generic HTTP attack detection |
| `crowdsecurity/http-cve` | Hub | Known HTTP CVE detection |
| Custom `acquis.yaml` | Charon | Point CrowdSec at Caddy logs |
| Bouncer API key | Auto-generated | Authenticate bouncer with LAPI |

---

## Implementation Phases

### Phase 1: Core CrowdSec Configuration

**Goal:** Make CrowdSec agent start successfully and process logs.

#### 1.1 Create Acquisition Template

Create a default acquisition configuration that reads Caddy logs:

**New file: `configs/crowdsec/acquis.yaml`**

```yaml
# Charon/Caddy Log Acquisition Configuration
# This file tells CrowdSec what logs to monitor

# Caddy access logs (JSON format)
source: file
filenames:
  - /var/log/caddy/access.log
  - /var/log/caddy/*.log
labels:
  type: caddy
---
# Alternative: If using syslog output from Caddy
# source: journalctl
# journalctl_filter:
#   - "_SYSTEMD_UNIT=caddy.service"
# labels:
#   type: caddy
```

#### 1.2 Create Default Config Template

**New file: `configs/crowdsec/config.yaml.template`**

```yaml
# CrowdSec Configuration for Charon
# Generated at container startup

common:
  daemonize: false
  log_media: stdout
  log_level: info
  log_dir: /var/log/crowdsec/
  working_dir: .

config_paths:
  config_dir: /etc/crowdsec/
  data_dir: /var/lib/crowdsec/data/
  simulation_path: /etc/crowdsec/simulation.yaml
  hub_dir: /etc/crowdsec/hub/
  index_path: /etc/crowdsec/hub/.index.json
  notification_dir: /etc/crowdsec/notifications/
  plugin_dir: /usr/lib/crowdsec/plugins/

crowdsec_service:
  enable: true
  acquisition_path: /etc/crowdsec/acquis.yaml
  acquisition_dir: /etc/crowdsec/acquis.d/
  parser_routines: 1
  buckets_routines: 1
  output_routines: 1

cscli:
  output: human
  color: auto
  # hub_branch: master

db_config:
  log_level: info
  type: sqlite
  db_path: /var/lib/crowdsec/data/crowdsec.db
  flush:
    max_items: 5000
    max_age: 7d

plugin_config:
  user: root
  group: root

api:
  client:
    insecure_skip_verify: false
    credentials_path: /etc/crowdsec/local_api_credentials.yaml
  server:
    log_level: info
    listen_uri: 127.0.0.1:8085
    profiles_path: /etc/crowdsec/profiles.yaml
    console_path: /etc/crowdsec/console.yaml
    online_client:
      credentials_path: /etc/crowdsec/online_api_credentials.yaml
    tls:
      # TLS disabled for local-only LAPI
      # Enable when exposing LAPI externally
prometheus:
  enabled: true
  level: full
  listen_addr: 127.0.0.1
  listen_port: 6060
```

#### 1.3 Create Local API Credentials Template

**New file: `configs/crowdsec/local_api_credentials.yaml.template`**

```yaml
# CrowdSec Local API Credentials
# This file is auto-generated - do not edit manually

url: http://127.0.0.1:8085
login: ${CROWDSEC_MACHINE_ID}
password: ${CROWDSEC_MACHINE_PASSWORD}
```

#### 1.4 Create Bouncer Registration Script

**New file: `configs/crowdsec/register_bouncer.sh`**

```bash
#!/bin/sh
# Register the Caddy bouncer with CrowdSec LAPI
# This script is idempotent - safe to run multiple times

BOUNCER_NAME="${CROWDSEC_BOUNCER_NAME:-caddy-bouncer}"
API_KEY_FILE="/etc/crowdsec/bouncers/${BOUNCER_NAME}.key"

# Ensure bouncer directory exists
mkdir -p /etc/crowdsec/bouncers

# Check if bouncer already registered
if cscli bouncers list -o json 2>/dev/null | grep -q "\"name\":\"${BOUNCER_NAME}\""; then
    echo "Bouncer '${BOUNCER_NAME}' already registered"

    # If key file doesn't exist, we need to re-register
    if [ ! -f "$API_KEY_FILE" ]; then
        echo "API key file missing, re-registering bouncer..."
        cscli bouncers delete "${BOUNCER_NAME}" 2>/dev/null || true
    else
        echo "Using existing API key from ${API_KEY_FILE}"
        cat "$API_KEY_FILE"
        exit 0
    fi
fi

# Register new bouncer and save API key
echo "Registering bouncer '${BOUNCER_NAME}'..."
API_KEY=$(cscli bouncers add "${BOUNCER_NAME}" -o raw)

if [ -z "$API_KEY" ]; then
    echo "ERROR: Failed to register bouncer" >&2
    exit 1
fi

# Save API key to file
echo "$API_KEY" > "$API_KEY_FILE"
chmod 600 "$API_KEY_FILE"

echo "Bouncer registered successfully"
echo "API Key: $API_KEY"
```

#### 1.5 Create Hub Setup Script

**New file: `configs/crowdsec/install_hub_items.sh`**

```bash
#!/bin/sh
# Install required CrowdSec hub items (parsers, scenarios, collections)
# This script runs during container startup

set -e

echo "Installing CrowdSec hub items for Charon..."

# Update hub index first
echo "Updating hub index..."
cscli hub update

# Install Caddy log parser (if available)
# Note: crowdsecurity/caddy-logs may not exist yet - check hub
if cscli parsers inspect crowdsecurity/caddy-logs >/dev/null 2>&1; then
    echo "Installing Caddy log parser..."
    cscli parsers install crowdsecurity/caddy-logs --force
else
    echo "Caddy-specific parser not available, using HTTP parser..."
    cscli parsers install crowdsecurity/http-logs --force 2>/dev/null || true
fi

# Install base HTTP parsers (always needed)
echo "Installing base parsers..."
cscli parsers install crowdsecurity/syslog-logs --force 2>/dev/null || true
cscli parsers install crowdsecurity/geoip-enrich --force 2>/dev/null || true
cscli parsers install crowdsecurity/http-logs --force 2>/dev/null || true

# Install HTTP scenarios for attack detection
echo "Installing HTTP scenarios..."
cscli scenarios install crowdsecurity/http-probing --force 2>/dev/null || true
cscli scenarios install crowdsecurity/http-sensitive-files --force 2>/dev/null || true
cscli scenarios install crowdsecurity/http-backdoors-attempts --force 2>/dev/null || true
cscli scenarios install crowdsecurity/http-path-traversal-probing --force 2>/dev/null || true
cscli scenarios install crowdsecurity/http-xss-probing --force 2>/dev/null || true
cscli scenarios install crowdsecurity/http-sqli-probing --force 2>/dev/null || true
cscli scenarios install crowdsecurity/http-generic-bf --force 2>/dev/null || true

# Install CVE collection for known vulnerabilities
echo "Installing CVE collection..."
cscli collections install crowdsecurity/http-cve --force 2>/dev/null || true

# Install base HTTP collection (bundles common scenarios)
echo "Installing base HTTP collection..."
cscli collections install crowdsecurity/base-http-scenarios --force 2>/dev/null || true

# Verify installation
echo ""
echo "=== Installed Components ==="
echo "Parsers:"
cscli parsers list -o json 2>/dev/null | grep -o '"name":"[^"]*"' | head -10 || echo "  (none or error)"

echo ""
echo "Scenarios:"
cscli scenarios list -o json 2>/dev/null | grep -o '"name":"[^"]*"' | head -10 || echo "  (none or error)"

echo ""
echo "Collections:"
cscli collections list -o json 2>/dev/null | grep -o '"name":"[^"]*"' | head -10 || echo "  (none or error)"

echo ""
echo "Hub installation complete!"
```

#### 1.6 Update Dockerfile

**File: `Dockerfile`**

Add CrowdSec configuration files to the image:

```dockerfile
# After the CrowdSec installer stage, add:

# Copy CrowdSec configuration templates
COPY configs/crowdsec/acquis.yaml /etc/crowdsec.dist/acquis.yaml
COPY configs/crowdsec/config.yaml.template /etc/crowdsec.dist/config.yaml.template
COPY configs/crowdsec/local_api_credentials.yaml.template /etc/crowdsec.dist/local_api_credentials.yaml.template
COPY configs/crowdsec/register_bouncer.sh /usr/local/bin/register_bouncer.sh
COPY configs/crowdsec/install_hub_items.sh /usr/local/bin/install_hub_items.sh

# Make scripts executable
RUN chmod +x /usr/local/bin/register_bouncer.sh /usr/local/bin/install_hub_items.sh
```

#### 1.7 Update docker-entrypoint.sh

**File: `docker-entrypoint.sh`**

Replace the CrowdSec initialization section with a more robust implementation:

```bash
#!/bin/sh
set -e

echo "Starting Charon with integrated Caddy..."

# ============================================================================
# CrowdSec Initialization
# ============================================================================
CROWDSEC_PID=""
SECURITY_CROWDSEC_MODE=${CERBERUS_SECURITY_CROWDSEC_MODE:-${CHARON_SECURITY_CROWDSEC_MODE:-$CPM_SECURITY_CROWDSEC_MODE}}

# Initialize CrowdSec configuration if cscli is present
if command -v cscli >/dev/null; then
    echo "Initializing CrowdSec configuration..."

    # Create required directories
    mkdir -p /etc/crowdsec
    mkdir -p /etc/crowdsec/hub
    mkdir -p /etc/crowdsec/acquis.d
    mkdir -p /etc/crowdsec/bouncers
    mkdir -p /etc/crowdsec/notifications
    mkdir -p /var/lib/crowdsec/data
    mkdir -p /var/log/crowdsec
    mkdir -p /var/log/caddy

    # Copy base configuration if not exists
    if [ ! -f "/etc/crowdsec/config.yaml" ]; then
        echo "Copying base CrowdSec configuration..."
        if [ -d "/etc/crowdsec.dist" ]; then
            cp -r /etc/crowdsec.dist/* /etc/crowdsec/ 2>/dev/null || true
        fi
    fi

    # Create/update acquisition config for Caddy logs
    # This is CRITICAL - CrowdSec won't start without datasources
    if [ ! -f "/etc/crowdsec/acquis.yaml" ] || [ ! -s "/etc/crowdsec/acquis.yaml" ]; then
        echo "Creating acquisition configuration for Caddy logs..."
        cat > /etc/crowdsec/acquis.yaml << 'EOF'
# Caddy access logs acquisition
# CrowdSec will monitor these files for security events
source: file
filenames:
  - /var/log/caddy/access.log
  - /var/log/caddy/*.log
labels:
  type: caddy
EOF
    fi

    # Ensure config.yaml has correct LAPI port (8085 to avoid conflict with Charon)
    if [ -f "/etc/crowdsec/config.yaml" ]; then
        sed -i 's|listen_uri: 127.0.0.1:8080|listen_uri: 127.0.0.1:8085|g' /etc/crowdsec/config.yaml
        sed -i 's|listen_uri: 0.0.0.0:8080|listen_uri: 127.0.0.1:8085|g' /etc/crowdsec/config.yaml
    fi

    # Update local_api_credentials.yaml to use correct port
    if [ -f "/etc/crowdsec/local_api_credentials.yaml" ]; then
        sed -i 's|url: http://127.0.0.1:8080|url: http://127.0.0.1:8085|g' /etc/crowdsec/local_api_credentials.yaml
        sed -i 's|url: http://localhost:8080|url: http://127.0.0.1:8085|g' /etc/crowdsec/local_api_credentials.yaml
    fi

    # Update hub index
    if [ ! -f "/etc/crowdsec/hub/.index.json" ]; then
        echo "Updating CrowdSec hub index..."
        cscli hub update || echo "Warning: Failed to update hub index (network issue?)"
    fi

    # Register machine with LAPI (required for cscli commands)
    echo "Registering machine with CrowdSec LAPI..."
    cscli machines add -a --force 2>/dev/null || echo "Warning: Machine registration may have failed"

    # Install hub items (parsers, scenarios, collections)
    if [ "$SECURITY_CROWDSEC_MODE" = "local" ]; then
        echo "Installing CrowdSec hub items..."
        /usr/local/bin/install_hub_items.sh 2>/dev/null || echo "Warning: Some hub items may not have installed"
    fi
fi

# Start CrowdSec agent if local mode is enabled
if [ "$SECURITY_CROWDSEC_MODE" = "local" ]; then
    echo "CrowdSec Local Mode enabled."

    if command -v crowdsec >/dev/null; then
        # Create an empty access log so CrowdSec doesn't fail on missing file
        touch /var/log/caddy/access.log

        echo "Starting CrowdSec agent..."
        crowdsec -c /etc/crowdsec/config.yaml &
        CROWDSEC_PID=$!
        echo "CrowdSec started (PID: $CROWDSEC_PID)"

        # Wait for LAPI to be ready
        echo "Waiting for CrowdSec LAPI..."
        for i in $(seq 1 30); do
            if wget -q -O- http://127.0.0.1:8085/health >/dev/null 2>&1; then
                echo "CrowdSec LAPI is ready!"
                break
            fi
            sleep 1
        done

        # Register bouncer for Caddy
        if [ -x /usr/local/bin/register_bouncer.sh ]; then
            echo "Registering Caddy bouncer..."
            BOUNCER_API_KEY=$(/usr/local/bin/register_bouncer.sh 2>/dev/null | tail -1)
            if [ -n "$BOUNCER_API_KEY" ]; then
                export CROWDSEC_BOUNCER_API_KEY="$BOUNCER_API_KEY"
                echo "Bouncer registered with API key"
            fi
        fi
    else
        echo "CrowdSec binary not found - skipping agent startup"
    fi
fi

# ... rest of entrypoint (Caddy startup, Charon startup, etc.)
```

### Phase 2: Caddy Integration

**Goal:** Configure Caddy to log in a format CrowdSec can parse, and enable the bouncer plugin.

#### 2.1 Configure Caddy Access Logging

Charon generates Caddy configuration dynamically. We need to ensure access logs are written in a format CrowdSec can parse.

**Update: `backend/internal/caddy/config.go`**

Add logging configuration to the Caddy JSON config:

```go
// In the buildCaddyConfig function or where global config is built

// Add logging configuration for CrowdSec
func buildLoggingConfig() map[string]interface{} {
    return map[string]interface{}{
        "logs": map[string]interface{}{
            "default": map[string]interface{}{
                "writer": map[string]interface{}{
                    "output":   "file",
                    "filename": "/var/log/caddy/access.log",
                },
                "encoder": map[string]interface{}{
                    "format": "json",
                },
                "level": "INFO",
            },
        },
    }
}
```

#### 2.2 Update CrowdSec Bouncer Handler

The existing `buildCrowdSecHandler` function already generates the correct format, but we need to ensure the API key is available.

**File: `backend/internal/caddy/config.go`**

The function at line 752 is mostly correct. Verify it includes:

- `api_url`: Points to `http://127.0.0.1:8085` (already done)
- `api_key`: From environment variable (already done)
- `enable_streaming`: For real-time updates (already done)

#### 2.3 Create Custom Caddy Parser for CrowdSec

Since there may not be an official `crowdsecurity/caddy-logs` parser, we need to create a custom parser or use the generic HTTP parser with appropriate normalization.

**New file: `configs/crowdsec/parsers/caddy-json-logs.yaml`**

```yaml
# Custom parser for Caddy JSON access logs
# Install with: cscli parsers install ./caddy-json-logs.yaml --force

name: charon/caddy-json-logs
description: Parse Caddy JSON access logs for Charon
filter: evt.Meta.log_type == 'caddy'

# Caddy JSON log format example:
# {"level":"info","ts":1702406400.123,"logger":"http.log.access","msg":"handled request","request":{"remote_ip":"192.168.1.100","method":"GET","uri":"/api/v1/test","host":"example.com"},"status":200,"duration":0.001}

pattern: '%{DATA:message}'
grok:
  apply_on: message
  pattern: '%{GREEDYDATA:json_data}'

statics:
  - parsed: json_data
    target: evt.Unmarshaled

# Extract fields from JSON
jsondecoder:
  - apply_on: json_data
    target: evt.Parsed

# Map Caddy fields to CrowdSec standard fields
statics:
  - target: evt.Meta.source_ip
    expression: evt.Parsed.request.remote_ip

  - target: evt.Meta.http_method
    expression: evt.Parsed.request.method

  - target: evt.Meta.http_path
    expression: evt.Parsed.request.uri

  - target: evt.Meta.http_host
    expression: evt.Parsed.request.host

  - target: evt.Meta.http_status
    expression: evt.Parsed.status

  - target: evt.Meta.http_user_agent
    expression: evt.Parsed.request.headers["User-Agent"][0]

  - target: evt.StrTime
    expression: string(evt.Parsed.ts)
```

### Phase 2.5: Unified Logging & Live Viewer Integration

**Goal:** Make Caddy access logs accessible to ALL Cerberus security modules (CrowdSec, WAF/Coraza, Rate Limiting, ACL) and integrate with the existing Live Log Viewer on the Cerberus dashboard.

#### 2.5.1 Architecture: Unified Security Log Pipeline

The current architecture has separate logging paths that need to be unified:

```
┌─────────────────────────────────────────────────────────────────────────────────┐
│                         Unified Logging Architecture                              │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                   │
│  ┌───────────────────────────────────────────────────────────────────────────┐   │
│  │                              CADDY REVERSE PROXY                          │   │
│  │  ┌───────────┐  ┌───────────┐  ┌───────────┐  ┌───────────────────────┐   │   │
│  │  │ CrowdSec  │  │  Coraza   │  │   Rate    │  │ Request Logging       │   │   │
│  │  │  Bouncer  │  │   WAF     │  │  Limiter  │  │ (access.log → JSON)   │   │   │
│  │  └─────┬─────┘  └─────┬─────┘  └─────┬─────┘  └───────────┬───────────┘   │   │
│  │        │              │              │                    │               │   │
│  └────────┼──────────────┼──────────────┼────────────────────┼───────────────┘   │
│           │              │              │                    │                   │
│           ▼              ▼              ▼                    ▼                   │
│  ┌─────────────────────────────────────────────────────────────────────────────┐ │
│  │                    UNIFIED LOG FILE: /var/log/caddy/access.log              │ │
│  │                    (JSON format with security event annotations)            │ │
│  └─────────────────────────────────────────────────────┬───────────────────────┘ │
│                                                        │                         │
│                    ┌───────────────────────────────────┼───────────────────┐     │
│                    │                                   │                   │     │
│                    ▼                                   ▼                   ▼     │
│  ┌─────────────────────────┐  ┌──────────────────────────────────────────────┐   │
│  │     CrowdSec Agent      │  │          Charon Backend                      │   │
│  │   (acquis.yaml reads    │  │  ┌──────────────────────────────────────┐    │   │
│  │    /var/log/caddy/*.log)│  │  │ LogWatcher Service (NEW)             │    │   │
│  │                         │  │  │ - Tail log file in real-time         │    │   │
│  │  Parses → Scenarios →   │  │  │ - Parse JSON entries                 │    │   │
│  │  Decisions → LAPI       │  │  │ - Broadcast to WebSocket listeners   │    │   │
│  └─────────────────────────┘  │  │ - Tag entries by security module     │    │   │
│                               │  └──────────────────┬───────────────────┘    │   │
│                               │                     │                        │   │
│                               │                     ▼                        │   │
│                               │  ┌──────────────────────────────────────┐    │   │
│                               │  │ Live Logs WebSocket Handler          │    │   │
│                               │  │ /api/v1/logs/live                    │    │   │
│                               │  │ /api/v1/cerberus/logs/live (NEW)     │    │   │
│                               │  └──────────────────┬───────────────────┘    │   │
│                               └──────────────────────┼───────────────────────┘   │
│                                                      │                           │
│                                                      ▼                           │
│  ┌─────────────────────────────────────────────────────────────────────────────┐ │
│  │                          FRONTEND                                           │ │
│  │  ┌─────────────────────────────────────────────────────────────────────┐    │ │
│  │  │ LiveLogViewer Component (Enhanced)                                   │    │ │
│  │  │ - Subscribe to cerberus log stream                                   │    │ │
│  │  │ - Filter by: source (waf, crowdsec, ratelimit, acl), level, IP      │    │ │
│  │  │ - Color-coded security events                                        │    │ │
│  │  │ - Click to expand request details                                    │    │ │
│  │  └─────────────────────────────────────────────────────────────────────┘    │ │
│  └─────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
```

#### 2.5.2 Create Log Watcher Service

A new service that tails the Caddy access log and broadcasts entries to WebSocket subscribers. This replaces the current approach that only broadcasts internal Go application logs.

**New file: `backend/internal/services/log_watcher.go`**

```go
package services

import (
    "bufio"
    "context"
    "encoding/json"
    "io"
    "os"
    "sync"
    "time"

    "github.com/Wikid82/charon/backend/internal/logger"
    "github.com/Wikid82/charon/backend/internal/models"
)

// LogWatcher provides real-time tailing of Caddy access logs
type LogWatcher struct {
    mu           sync.RWMutex
    subscribers  map[string]chan models.SecurityLogEntry
    logPath      string
    ctx          context.Context
    cancel       context.CancelFunc
}

// SecurityLogEntry represents a security-relevant log entry broadcast to clients
type SecurityLogEntry struct {
    Timestamp   string                 `json:"timestamp"`
    Level       string                 `json:"level"`
    Source      string                 `json:"source"`      // "caddy", "waf", "crowdsec", "ratelimit", "acl"
    ClientIP    string                 `json:"client_ip"`
    Method      string                 `json:"method"`
    Host        string                 `json:"host"`
    Path        string                 `json:"path"`
    Status      int                    `json:"status"`
    Duration    float64                `json:"duration"`
    Message     string                 `json:"message"`
    Blocked     bool                   `json:"blocked"`      // True if request was blocked
    BlockReason string                 `json:"block_reason"` // "waf", "crowdsec", "ratelimit", "acl"
    Details     map[string]interface{} `json:"details,omitempty"`
}

// NewLogWatcher creates a new log watcher instance
func NewLogWatcher(logPath string) *LogWatcher {
    ctx, cancel := context.WithCancel(context.Background())
    return &LogWatcher{
        subscribers: make(map[string]chan models.SecurityLogEntry),
        logPath:     logPath,
        ctx:         ctx,
        cancel:      cancel,
    }
}

// Start begins tailing the log file
func (w *LogWatcher) Start() error {
    go w.tailFile()
    return nil
}

// Stop halts the log watcher
func (w *LogWatcher) Stop() {
    w.cancel()
}

// Subscribe adds a new subscriber for log entries
func (w *LogWatcher) Subscribe(id string) <-chan models.SecurityLogEntry {
    w.mu.Lock()
    defer w.mu.Unlock()

    ch := make(chan models.SecurityLogEntry, 100)
    w.subscribers[id] = ch
    return ch
}

// Unsubscribe removes a subscriber
func (w *LogWatcher) Unsubscribe(id string) {
    w.mu.Lock()
    defer w.mu.Unlock()

    if ch, ok := w.subscribers[id]; ok {
        close(ch)
        delete(w.subscribers, id)
    }
}

// broadcast sends a log entry to all subscribers
func (w *LogWatcher) broadcast(entry models.SecurityLogEntry) {
    w.mu.RLock()
    defer w.mu.RUnlock()

    for _, ch := range w.subscribers {
        select {
        case ch <- entry:
        default:
            // Skip if channel is full (prevents blocking)
        }
    }
}

// tailFile continuously reads new entries from the log file
func (w *LogWatcher) tailFile() {
    for {
        select {
        case <-w.ctx.Done():
            return
        default:
        }

        // Wait for file to exist
        if _, err := os.Stat(w.logPath); os.IsNotExist(err) {
            time.Sleep(time.Second)
            continue
        }

        file, err := os.Open(w.logPath)
        if err != nil {
            logger.Log().WithError(err).Error("Failed to open log file for tailing")
            time.Sleep(time.Second)
            continue
        }

        // Seek to end of file
        file.Seek(0, io.SeekEnd)

        reader := bufio.NewReader(file)
        for {
            select {
            case <-w.ctx.Done():
                file.Close()
                return
            default:
            }

            line, err := reader.ReadString('\n')
            if err != nil {
                if err == io.EOF {
                    // No new data, wait and retry
                    time.Sleep(100 * time.Millisecond)
                    continue
                }
                logger.Log().WithError(err).Warn("Error reading log file")
                break // Reopen file
            }

            if line == "" || line == "\n" {
                continue
            }

            entry := w.parseLogEntry(line)
            if entry != nil {
                w.broadcast(*entry)
            }
        }

        file.Close()
        time.Sleep(time.Second) // Brief pause before reopening
    }
}

// parseLogEntry converts a Caddy JSON log line into a SecurityLogEntry
func (w *LogWatcher) parseLogEntry(line string) *models.SecurityLogEntry {
    var caddyLog models.CaddyAccessLog
    if err := json.Unmarshal([]byte(line), &caddyLog); err != nil {
        return nil
    }

    entry := &models.SecurityLogEntry{
        Timestamp: time.Unix(int64(caddyLog.Ts), 0).Format(time.RFC3339),
        Level:     caddyLog.Level,
        Source:    "caddy",
        ClientIP:  caddyLog.Request.RemoteIP,
        Method:    caddyLog.Request.Method,
        Host:      caddyLog.Request.Host,
        Path:      caddyLog.Request.URI,
        Status:    caddyLog.Status,
        Duration:  caddyLog.Duration,
        Message:   caddyLog.Msg,
        Details:   make(map[string]interface{}),
    }

    // Detect security events from status codes and log metadata
    if caddyLog.Status == 403 {
        entry.Blocked = true
        entry.Level = "warn"

        // Determine block reason from response headers or log fields
        // WAF blocks typically include "waf" or "coraza" in the response
        // CrowdSec blocks come from the bouncer
        // Rate limit blocks have specific status patterns
        if caddyLog.Logger == "http.handlers.waf" ||
           containsKey(caddyLog.Request.Headers, "X-Coraza-Id") {
            entry.Source = "waf"
            entry.BlockReason = "WAF rule triggered"
        } else {
            entry.Source = "cerberus"
            entry.BlockReason = "Access denied"
        }
    }

    if caddyLog.Status == 429 {
        entry.Blocked = true
        entry.Source = "ratelimit"
        entry.Level = "warn"
        entry.BlockReason = "Rate limit exceeded"
    }

    return entry
}

// containsKey checks if a header map contains a specific key
func containsKey(headers map[string][]string, key string) bool {
    _, ok := headers[key]
    return ok
}
```

#### 2.5.3 Add Security Log Entry Model

**Update file: `backend/internal/models/logs.go`**

Add the SecurityLogEntry type alongside existing log models:

```go
// SecurityLogEntry represents a security-relevant log entry for live streaming
type SecurityLogEntry struct {
    Timestamp   string                 `json:"timestamp"`
    Level       string                 `json:"level"`
    Source      string                 `json:"source"`      // "caddy", "waf", "crowdsec", "ratelimit", "acl"
    ClientIP    string                 `json:"client_ip"`
    Method      string                 `json:"method"`
    Host        string                 `json:"host"`
    Path        string                 `json:"path"`
    Status      int                    `json:"status"`
    Duration    float64                `json:"duration"`
    Message     string                 `json:"message"`
    Blocked     bool                   `json:"blocked"`
    BlockReason string                 `json:"block_reason,omitempty"`
    Details     map[string]interface{} `json:"details,omitempty"`
}
```

#### 2.5.4 Create Cerberus Live Logs WebSocket Handler

A new WebSocket endpoint specifically for Cerberus security logs that streams parsed Caddy access logs.

**New file: `backend/internal/api/handlers/cerberus_logs_ws.go`**

```go
package handlers

import (
    "net/http"
    "strings"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/google/uuid"
    "github.com/gorilla/websocket"

    "github.com/Wikid82/charon/backend/internal/logger"
    "github.com/Wikid82/charon/backend/internal/services"
)

// CerberusLogsHandler handles Cerberus security log streaming
type CerberusLogsHandler struct {
    watcher *services.LogWatcher
}

// NewCerberusLogsHandler creates a new Cerberus logs handler
func NewCerberusLogsHandler(watcher *services.LogWatcher) *CerberusLogsHandler {
    return &CerberusLogsHandler{watcher: watcher}
}

// LiveLogs handles WebSocket connections for Cerberus security log streaming
func (h *CerberusLogsHandler) LiveLogs(c *gin.Context) {
    logger.Log().Info("Cerberus logs WebSocket connection attempt")

    // Upgrade HTTP connection to WebSocket
    conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
    if err != nil {
        logger.Log().WithError(err).Error("Failed to upgrade Cerberus logs WebSocket")
        return
    }
    defer conn.Close()

    subscriberID := uuid.New().String()
    logger.Log().WithField("subscriber_id", subscriberID).Info("Cerberus logs WebSocket connected")

    // Parse query filters
    sourceFilter := strings.ToLower(c.Query("source")) // waf, crowdsec, ratelimit, acl
    levelFilter := strings.ToLower(c.Query("level"))
    ipFilter := c.Query("ip")
    hostFilter := strings.ToLower(c.Query("host"))
    blockedOnly := c.Query("blocked") == "true"

    // Subscribe to log watcher
    logChan := h.watcher.Subscribe(subscriberID)
    defer h.watcher.Unsubscribe(subscriberID)

    // Channel to detect client disconnect
    done := make(chan struct{})
    go func() {
        defer close(done)
        for {
            if _, _, err := conn.ReadMessage(); err != nil {
                return
            }
        }
    }()

    ticker := time.NewTicker(30 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case entry, ok := <-logChan:
            if !ok {
                return
            }

            // Apply filters
            if sourceFilter != "" && !strings.EqualFold(entry.Source, sourceFilter) {
                continue
            }
            if levelFilter != "" && !strings.EqualFold(entry.Level, levelFilter) {
                continue
            }
            if ipFilter != "" && !strings.Contains(entry.ClientIP, ipFilter) {
                continue
            }
            if hostFilter != "" && !strings.Contains(strings.ToLower(entry.Host), hostFilter) {
                continue
            }
            if blockedOnly && !entry.Blocked {
                continue
            }

            // Send to WebSocket client
            if err := conn.WriteJSON(entry); err != nil {
                logger.Log().WithError(err).Debug("Failed to write Cerberus log to WebSocket")
                return
            }

        case <-ticker.C:
            if err := conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
                return
            }

        case <-done:
            return
        }
    }
}
```

#### 2.5.5 Register New Routes

**Update file: `backend/internal/api/routes/routes.go`**

Add the new Cerberus logs endpoint:

```go
// In SetupRoutes function, add:

// Initialize log watcher for Caddy access logs
logPath := filepath.Join(cfg.DataDir, "logs", "access.log")
logWatcher := services.NewLogWatcher(logPath)
if err := logWatcher.Start(); err != nil {
    logger.Log().WithError(err).Error("Failed to start log watcher")
}

// Cerberus security logs WebSocket
cerberusLogsHandler := handlers.NewCerberusLogsHandler(logWatcher)
api.GET("/cerberus/logs/live", cerberusLogsHandler.LiveLogs)

// Alternative: Also expose under /logs/security/live for clarity
api.GET("/logs/security/live", cerberusLogsHandler.LiveLogs)
```

#### 2.5.6 Update Frontend API Client

**Update file: `frontend/src/api/logs.ts`**

Add the new security logs connection function:

```typescript
export interface SecurityLogEntry {
  timestamp: string;
  level: string;
  source: string;      // "caddy", "waf", "crowdsec", "ratelimit", "acl"
  client_ip: string;
  method: string;
  host: string;
  path: string;
  status: number;
  duration: number;
  message: string;
  blocked: boolean;
  block_reason?: string;
  details?: Record<string, unknown>;
}

export interface SecurityLogFilter {
  source?: string;     // Filter by security module
  level?: string;
  ip?: string;
  host?: string;
  blocked?: boolean;   // Only show blocked requests
}

/**
 * Connects to the Cerberus security logs WebSocket endpoint.
 * This streams Caddy access logs with security event annotations.
 */
export const connectSecurityLogs = (
  filters: SecurityLogFilter,
  onMessage: (log: SecurityLogEntry) => void,
  onOpen?: () => void,
  onError?: (error: Event) => void,
  onClose?: () => void
): (() => void) => {
  const params = new URLSearchParams();
  if (filters.source) params.append('source', filters.source);
  if (filters.level) params.append('level', filters.level);
  if (filters.ip) params.append('ip', filters.ip);
  if (filters.host) params.append('host', filters.host);
  if (filters.blocked) params.append('blocked', 'true');

  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
  const wsUrl = `${protocol}//${window.location.host}/api/v1/cerberus/logs/live?${params.toString()}`;

  console.log('Connecting to Cerberus logs WebSocket:', wsUrl);
  const ws = new WebSocket(wsUrl);

  ws.onopen = () => {
    console.log('Cerberus logs WebSocket connected');
    onOpen?.();
  };

  ws.onmessage = (event: MessageEvent) => {
    try {
      const log = JSON.parse(event.data) as SecurityLogEntry;
      onMessage(log);
    } catch (err) {
      console.error('Failed to parse security log:', err);
    }
  };

  ws.onerror = (error: Event) => {
    console.error('Cerberus logs WebSocket error:', error);
    onError?.(error);
  };

  ws.onclose = (event: CloseEvent) => {
    console.log('Cerberus logs WebSocket closed', event);
    onClose?.();
  };

  return () => {
    if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
      ws.close();
    }
  };
};
```

#### 2.5.7 Update LiveLogViewer Component

**Update file: `frontend/src/components/LiveLogViewer.tsx`**

Enhance the component to support both application logs and security access logs:

```tsx
import { useEffect, useRef, useState } from 'react';
import {
  connectLiveLogs,
  connectSecurityLogs,
  LiveLogEntry,
  LiveLogFilter,
  SecurityLogEntry,
  SecurityLogFilter
} from '../api/logs';
import { Button } from './ui/Button';
import { Pause, Play, Trash2, Filter, Shield, Globe } from 'lucide-react';

type LogMode = 'application' | 'security';

interface LiveLogViewerProps {
  filters?: LiveLogFilter;
  securityFilters?: SecurityLogFilter;
  mode?: LogMode;
  maxLogs?: number;
  className?: string;
}

// Unified log entry for display
interface DisplayLogEntry {
  timestamp: string;
  level: string;
  source: string;
  message: string;
  blocked?: boolean;
  blockReason?: string;
  clientIP?: string;
  method?: string;
  host?: string;
  path?: string;
  status?: number;
  details?: Record<string, unknown>;
}

export function LiveLogViewer({
  filters = {},
  securityFilters = {},
  mode = 'application',
  maxLogs = 500,
  className = ''
}: LiveLogViewerProps) {
  const [logs, setLogs] = useState<DisplayLogEntry[]>([]);
  const [isPaused, setIsPaused] = useState(false);
  const [isConnected, setIsConnected] = useState(false);
  const [currentMode, setCurrentMode] = useState<LogMode>(mode);
  const [textFilter, setTextFilter] = useState('');
  const [levelFilter, setLevelFilter] = useState('');
  const [sourceFilter, setSourceFilter] = useState('');
  const [showBlockedOnly, setShowBlockedOnly] = useState(false);
  const logContainerRef = useRef<HTMLDivElement>(null);
  const closeConnectionRef = useRef<(() => void) | null>(null);
  const shouldAutoScroll = useRef(true);

  // Convert entries to unified format
  const toDisplayEntry = (entry: LiveLogEntry | SecurityLogEntry): DisplayLogEntry => {
    if ('client_ip' in entry) {
      // SecurityLogEntry
      const secEntry = entry as SecurityLogEntry;
      return {
        timestamp: secEntry.timestamp,
        level: secEntry.level,
        source: secEntry.source,
        message: secEntry.blocked
          ? `🚫 BLOCKED: ${secEntry.block_reason} - ${secEntry.method} ${secEntry.path}`
          : `${secEntry.method} ${secEntry.path} → ${secEntry.status}`,
        blocked: secEntry.blocked,
        blockReason: secEntry.block_reason,
        clientIP: secEntry.client_ip,
        method: secEntry.method,
        host: secEntry.host,
        path: secEntry.path,
        status: secEntry.status,
        details: secEntry.details,
      };
    }
    // LiveLogEntry (application logs)
    return {
      timestamp: entry.timestamp,
      level: entry.level,
      source: entry.source || 'app',
      message: entry.message,
      details: entry.data,
    };
  };

  useEffect(() => {
    // Cleanup previous connection
    if (closeConnectionRef.current) {
      closeConnectionRef.current();
    }

    const handleMessage = (entry: LiveLogEntry | SecurityLogEntry) => {
      if (!isPaused) {
        const displayEntry = toDisplayEntry(entry);
        setLogs((prev) => {
          const updated = [...prev, displayEntry];
          return updated.length > maxLogs ? updated.slice(-maxLogs) : updated;
        });
      }
    };

    const handleOpen = () => {
      console.log(`${currentMode} log viewer connected`);
      setIsConnected(true);
    };

    const handleError = (error: Event) => {
      console.error('WebSocket error:', error);
      setIsConnected(false);
    };

    const handleClose = () => {
      console.log(`${currentMode} log viewer disconnected`);
      setIsConnected(false);
    };

    // Connect based on mode
    if (currentMode === 'security') {
      closeConnectionRef.current = connectSecurityLogs(
        { ...securityFilters, blocked: showBlockedOnly },
        handleMessage as (log: SecurityLogEntry) => void,
        handleOpen,
        handleError,
        handleClose
      );
    } else {
      closeConnectionRef.current = connectLiveLogs(
        filters,
        handleMessage as (log: LiveLogEntry) => void,
        handleOpen,
        handleError,
        handleClose
      );
    }

    return () => {
      if (closeConnectionRef.current) {
        closeConnectionRef.current();
      }
      setIsConnected(false);
    };
  }, [currentMode, filters, securityFilters, isPaused, maxLogs, showBlockedOnly]);

  // ... rest of component (auto-scroll, handleScroll, filtering logic)

  // Color coding for security events
  const getEntryStyle = (log: DisplayLogEntry) => {
    if (log.blocked) {
      return 'bg-red-900/30 border-l-2 border-red-500';
    }
    const level = log.level.toLowerCase();
    if (level.includes('error') || level.includes('fatal')) return 'text-red-400';
    if (level.includes('warn')) return 'text-yellow-400';
    return '';
  };

  const getSourceBadge = (source: string) => {
    const colors: Record<string, string> = {
      waf: 'bg-orange-600',
      crowdsec: 'bg-purple-600',
      ratelimit: 'bg-blue-600',
      acl: 'bg-green-600',
      caddy: 'bg-gray-600',
      cerberus: 'bg-indigo-600',
    };
    return colors[source.toLowerCase()] || 'bg-gray-500';
  };

  return (
    <div className={`bg-gray-900 rounded-lg border border-gray-700 ${className}`}>
      {/* Header with mode toggle */}
      <div className="flex items-center justify-between p-3 border-b border-gray-700">
        <div className="flex items-center gap-2">
          <h3 className="text-sm font-semibold text-white">
            {currentMode === 'security' ? 'Security Access Logs' : 'Live Security Logs'}
          </h3>
          <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${
            isConnected ? 'bg-green-900 text-green-300' : 'bg-red-900 text-red-300'
          }`}>
            {isConnected ? 'Connected' : 'Disconnected'}
          </span>
        </div>
        <div className="flex items-center gap-2">
          {/* Mode toggle */}
          <div className="flex bg-gray-800 rounded-md p-0.5">
            <button
              onClick={() => setCurrentMode('application')}
              className={`px-2 py-1 text-xs rounded ${
                currentMode === 'application' ? 'bg-blue-600 text-white' : 'text-gray-400'
              }`}
              title="Application logs"
            >
              <Globe className="w-4 h-4" />
            </button>
            <button
              onClick={() => setCurrentMode('security')}
              className={`px-2 py-1 text-xs rounded ${
                currentMode === 'security' ? 'bg-blue-600 text-white' : 'text-gray-400'
              }`}
              title="Security access logs"
            >
              <Shield className="w-4 h-4" />
            </button>
          </div>
          {/* Existing controls */}
          <Button variant="ghost" size="sm" onClick={() => setIsPaused(!isPaused)}>
            {isPaused ? <Play className="w-4 h-4" /> : <Pause className="w-4 h-4" />}
          </Button>
          <Button variant="ghost" size="sm" onClick={() => setLogs([])}>
            <Trash2 className="w-4 h-4" />
          </Button>
        </div>
      </div>

      {/* Enhanced filters for security mode */}
      <div className="flex items-center gap-2 p-2 border-b border-gray-700 bg-gray-800">
        <Filter className="w-4 h-4 text-gray-400" />
        <input
          type="text"
          placeholder="Filter by text..."
          value={textFilter}
          onChange={(e) => setTextFilter(e.target.value)}
          className="flex-1 px-2 py-1 text-sm bg-gray-700 border border-gray-600 rounded text-white"
        />
        {currentMode === 'security' && (
          <>
            <select
              value={sourceFilter}
              onChange={(e) => setSourceFilter(e.target.value)}
              className="px-2 py-1 text-sm bg-gray-700 border border-gray-600 rounded text-white"
            >
              <option value="">All Sources</option>
              <option value="waf">WAF</option>
              <option value="crowdsec">CrowdSec</option>
              <option value="ratelimit">Rate Limit</option>
              <option value="acl">ACL</option>
            </select>
            <label className="flex items-center gap-1 text-xs text-gray-400">
              <input
                type="checkbox"
                checked={showBlockedOnly}
                onChange={(e) => setShowBlockedOnly(e.target.checked)}
                className="rounded border-gray-600"
              />
              Blocked only
            </label>
          </>
        )}
      </div>

      {/* Log display with security styling */}
      <div
        ref={logContainerRef}
        className="h-96 overflow-y-auto p-3 font-mono text-xs bg-black"
      >
        {logs.length === 0 && (
          <div className="text-gray-500 text-center py-8">
            No logs yet. Waiting for events...
          </div>
        )}
        {logs.map((log, index) => (
          <div
            key={index}
            className={`mb-1 px-1 -mx-1 rounded ${getEntryStyle(log)}`}
          >
            <span className="text-gray-500">{log.timestamp}</span>
            <span className={`ml-2 px-1 rounded text-xs ${getSourceBadge(log.source)}`}>
              {log.source.toUpperCase()}
            </span>
            {log.clientIP && (
              <span className="ml-2 text-cyan-400">{log.clientIP}</span>
            )}
            <span className="ml-2 text-gray-200">{log.message}</span>
            {log.blocked && log.blockReason && (
              <span className="ml-2 text-red-400">[{log.blockReason}]</span>
            )}
          </div>
        ))}
      </div>

      {/* Footer */}
      <div className="p-2 border-t border-gray-700 bg-gray-800 text-xs text-gray-400">
        Showing {logs.length} logs {isPaused && <span className="text-yellow-400">⏸ Paused</span>}
      </div>
    </div>
  );
}
```

#### 2.5.8 Update Security Page to Use Enhanced Viewer

**Update file: `frontend/src/pages/Security.tsx`**

Change the LiveLogViewer invocation to use security mode:

```tsx
{/* Live Activity Section */}
{status.cerberus?.enabled && (
  <div className="mt-6">
    <LiveLogViewer
      mode="security"
      securityFilters={{ source: 'cerberus' }}
      className="w-full"
    />
  </div>
)}
```

#### 2.5.9 Update Caddy Logging to Include Security Metadata

**Update file: `backend/internal/caddy/config.go`**

Enhance the logging configuration to include security-relevant fields:

```go
// In GenerateConfig, update the logging configuration:

Logging: &LoggingConfig{
    Logs: map[string]*LogConfig{
        "access": {
            Level: "INFO",
            Writer: &WriterConfig{
                Output:       "file",
                Filename:     logFile,
                Roll:         true,
                RollSize:     10,
                RollKeep:     5,
                RollKeepDays: 7,
            },
            Encoder: &EncoderConfig{
                Format: "json",
                // Include all relevant fields for security analysis
                Fields: map[string]interface{}{
                    "request>remote_ip":    "",
                    "request>method":       "",
                    "request>host":         "",
                    "request>uri":          "",
                    "request>proto":        "",
                    "request>headers>User-Agent": "",
                    "request>headers>X-Forwarded-For": "",
                    "status":               "",
                    "size":                 "",
                    "duration":             "",
                    "resp_headers>X-Coraza-Id": "",  // WAF tracking
                    "resp_headers>X-RateLimit-Remaining": "", // Rate limit tracking
                },
            },
            Include: []string{"http.log.access.access_log"},
        },
    },
},
```

#### 2.5.10 Summary of File Changes for Phase 2.5

| Path | Type | Purpose |
|------|------|---------|
| `backend/internal/services/log_watcher.go` | New | Tail Caddy logs and broadcast to WebSocket |
| `backend/internal/models/logs.go` | Update | Add SecurityLogEntry type |
| `backend/internal/api/handlers/cerberus_logs_ws.go` | New | Cerberus security logs WebSocket handler |
| `backend/internal/api/routes/routes.go` | Update | Register new /cerberus/logs/live endpoint |
| `frontend/src/api/logs.ts` | Update | Add SecurityLogEntry types and connectSecurityLogs |
| `frontend/src/components/LiveLogViewer.tsx` | Update | Support security mode with enhanced filtering |
| `frontend/src/pages/Security.tsx` | Update | Use enhanced LiveLogViewer with security mode |
| `backend/internal/caddy/config.go` | Update | Include security metadata in access logs |

### Phase 3: API Integration

**Goal:** Ensure existing handlers work correctly and add any missing functionality.

#### 3.1 Update CrowdSec Handler Initialization

**File: `backend/internal/api/handlers/crowdsec_handler.go`**

The existing handler is comprehensive. Key areas to verify/update:

1. **LAPI Health Check**: Already implemented at `CheckLAPIHealth`
2. **Decision Management**: Already implemented via `ListDecisions`, `BanIP`, `UnbanIP`
3. **Process Management**: Already implemented via `Start`, `Stop`, `Status`

#### 3.2 Add Bouncer Registration Endpoint

**New endpoint in `crowdsec_handler.go`:**

```go
// RegisterBouncer registers a new bouncer or returns existing bouncer API key
func (h *CrowdsecHandler) RegisterBouncer(c *gin.Context) {
    ctx := c.Request.Context()

    // Use the registration helper from internal/crowdsec package
    apiKey, err := crowdsec.EnsureBouncerRegistered(ctx, "http://127.0.0.1:8085")
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    // Don't expose the full API key - just confirm registration
    c.JSON(http.StatusOK, gin.H{
        "status": "registered",
        "bouncer_name": "caddy-bouncer",
        "api_key_preview": apiKey[:8] + "...",
    })
}

// Add to RegisterRoutes:
// rg.POST("/admin/crowdsec/bouncer/register", h.RegisterBouncer)
```

#### 3.3 Add Acquisition Config Endpoint

```go
// GetAcquisitionConfig returns the current acquisition configuration
func (h *CrowdsecHandler) GetAcquisitionConfig(c *gin.Context) {
    acquis, err := os.ReadFile("/etc/crowdsec/acquis.yaml")
    if err != nil {
        if os.IsNotExist(err) {
            c.JSON(http.StatusNotFound, gin.H{"error": "acquisition config not found"})
            return
        }
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "content": string(acquis),
        "path": "/etc/crowdsec/acquis.yaml",
    })
}

// UpdateAcquisitionConfig updates the acquisition configuration
func (h *CrowdsecHandler) UpdateAcquisitionConfig(c *gin.Context) {
    var payload struct {
        Content string `json:"content" binding:"required"`
    }
    if err := c.ShouldBindJSON(&payload); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "content required"})
        return
    }

    // Backup existing config
    backupPath := fmt.Sprintf("/etc/crowdsec/acquis.yaml.backup.%s", time.Now().Format("20060102-150405"))
    if _, err := os.Stat("/etc/crowdsec/acquis.yaml"); err == nil {
        _ = os.Rename("/etc/crowdsec/acquis.yaml", backupPath)
    }

    // Write new config
    if err := os.WriteFile("/etc/crowdsec/acquis.yaml", []byte(payload.Content), 0644); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to write config"})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "status": "updated",
        "backup": backupPath,
        "reload_hint": true,
    })
}
```

### Phase 4: Testing

**Goal:** Update existing test scripts and create comprehensive integration tests.

#### 4.1 Update Integration Test Script

**File: `scripts/crowdsec_decision_integration.sh`**

Add pre-flight checks for CrowdSec readiness:

```bash
# Add after container start, before other tests:

# TC-0: Verify CrowdSec agent started successfully
log_test "TC-0: Verify CrowdSec agent started"

# Check container logs for CrowdSec startup
CROWDSEC_STARTED=$(docker logs ${CONTAINER_NAME} 2>&1 | grep -c "CrowdSec LAPI is ready" || echo "0")
if [ "$CROWDSEC_STARTED" -ge 1 ]; then
    log_info "  CrowdSec agent started successfully"
    pass_test
else
    # Check for the fatal error
    FATAL_ERROR=$(docker logs ${CONTAINER_NAME} 2>&1 | grep -c "no datasource enabled" || echo "0")
    if [ "$FATAL_ERROR" -ge 1 ]; then
        fail_test "CrowdSec failed to start: no datasource enabled (acquis.yaml missing)"
    else
        log_warn "  CrowdSec may not have started properly"
        pass_test
    fi
fi

# TC-0b: Verify acquisition config exists
log_test "TC-0b: Verify acquisition config exists"
ACQUIS_EXISTS=$(docker exec ${CONTAINER_NAME} cat /etc/crowdsec/acquis.yaml 2>/dev/null | grep -c "source:" || echo "0")
if [ "$ACQUIS_EXISTS" -ge 1 ]; then
    log_info "  Acquisition config found"
    pass_test
else
    fail_test "Acquisition config missing or empty"
fi
```

#### 4.2 Create CrowdSec Startup Test

**New file: `scripts/crowdsec_startup_test.sh`**

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

# Brief: Test that CrowdSec starts correctly in Charon container
# This is a focused test for the startup issue

PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$PROJECT_ROOT"

CONTAINER_NAME="charon-crowdsec-startup-test"

echo "=== CrowdSec Startup Test ==="

# Build if needed
if ! docker image inspect charon:local >/dev/null 2>&1; then
    echo "Building charon:local image..."
    docker build -t charon:local .
fi

# Clean up any existing container
docker rm -f ${CONTAINER_NAME} 2>/dev/null || true

# Start container with CrowdSec enabled
echo "Starting container with CERBERUS_SECURITY_CROWDSEC_MODE=local..."
docker run -d --name ${CONTAINER_NAME} \
    -p 8580:8080 \
    -e CHARON_ENV=development \
    -e CERBERUS_SECURITY_CROWDSEC_MODE=local \
    -e FEATURE_CERBERUS_ENABLED=true \
    charon:local

echo "Waiting 30 seconds for CrowdSec to initialize..."
sleep 30

# Check logs for errors
echo ""
echo "=== Container Logs (last 50 lines) ==="
docker logs ${CONTAINER_NAME} 2>&1 | tail -50

echo ""
echo "=== Checking for CrowdSec Status ==="

# Check for fatal error
if docker logs ${CONTAINER_NAME} 2>&1 | grep -q "no datasource enabled"; then
    echo "❌ FAIL: CrowdSec failed with 'no datasource enabled'"
    echo "   The acquis.yaml file is missing or empty"
    docker rm -f ${CONTAINER_NAME}
    exit 1
fi

# Check if LAPI is healthy
LAPI_HEALTH=$(docker exec ${CONTAINER_NAME} wget -q -O- http://127.0.0.1:8085/health 2>/dev/null || echo "failed")
if [ "$LAPI_HEALTH" != "failed" ]; then
    echo "✅ PASS: CrowdSec LAPI is healthy"
else
    echo "⚠️  WARN: CrowdSec LAPI not responding (may still be starting)"
fi

# Check acquisition config
echo ""
echo "=== Acquisition Config ==="
docker exec ${CONTAINER_NAME} cat /etc/crowdsec/acquis.yaml 2>/dev/null || echo "(not found)"

# Check installed items
echo ""
echo "=== Installed Parsers ==="
docker exec ${CONTAINER_NAME} cscli parsers list 2>/dev/null || echo "(cscli not available)"

echo ""
echo "=== Installed Scenarios ==="
docker exec ${CONTAINER_NAME} cscli scenarios list 2>/dev/null || echo "(cscli not available)"

# Cleanup
docker rm -f ${CONTAINER_NAME}

echo ""
echo "=== Test Complete ==="
```

#### 4.3 Update Go Integration Test

**File: `backend/integration/crowdsec_decisions_integration_test.go`**

Add a specific test for CrowdSec startup:

```go
func TestCrowdsecStartup(t *testing.T) {
    t.Parallel()

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
    defer cancel()

    cmd := exec.CommandContext(ctx, "bash", "./scripts/crowdsec_startup_test.sh")
    cmd.Dir = "../../"

    out, err := cmd.CombinedOutput()
    t.Logf("crowdsec startup test output:\n%s", string(out))

    if err != nil {
        t.Fatalf("crowdsec startup test failed: %v", err)
    }

    // Check for fatal errors in output
    if strings.Contains(string(out), "no datasource enabled") {
        t.Fatal("CrowdSec failed to start: no datasource enabled")
    }
}
```

---

## File Changes Summary

### New Files to Create

| Path | Purpose |
|------|---------|
| `configs/crowdsec/acquis.yaml` | Default acquisition config for Caddy logs |
| `configs/crowdsec/config.yaml.template` | CrowdSec main config template |
| `configs/crowdsec/local_api_credentials.yaml.template` | LAPI credentials template |
| `configs/crowdsec/register_bouncer.sh` | Script to register Caddy bouncer |
| `configs/crowdsec/install_hub_items.sh` | Script to install parsers/scenarios |
| `configs/crowdsec/parsers/caddy-json-logs.yaml` | Custom parser for Caddy JSON logs |
| `scripts/crowdsec_startup_test.sh` | Focused startup test script |
| `backend/internal/services/log_watcher.go` | Tail Caddy access logs and broadcast to WebSocket subscribers |
| `backend/internal/api/handlers/cerberus_logs_ws.go` | Cerberus security logs WebSocket handler |

### Files to Modify

| Path | Changes |
|------|---------|
| `Dockerfile` | Copy CrowdSec config files, make scripts executable |
| `docker-entrypoint.sh` | Complete rewrite of CrowdSec initialization section |
| `backend/internal/caddy/config.go` | Add logging configuration for Caddy with security metadata |
| `backend/internal/api/handlers/crowdsec_handler.go` | Add bouncer registration, acquisition endpoints |
| `backend/internal/models/logs.go` | Add SecurityLogEntry type for live streaming |
| `backend/internal/api/routes/routes.go` | Register `/cerberus/logs/live` WebSocket endpoint |
| `frontend/src/api/logs.ts` | Add SecurityLogEntry types and connectSecurityLogs function |
| `frontend/src/components/LiveLogViewer.tsx` | Support security mode with enhanced filtering |
| `frontend/src/pages/Security.tsx` | Use enhanced LiveLogViewer with security mode |
| `scripts/crowdsec_decision_integration.sh` | Add CrowdSec startup verification |

### File Structure

```
Charon/
├── configs/
│   └── crowdsec/
│       ├── acquis.yaml
│       ├── config.yaml.template
│       ├── local_api_credentials.yaml.template
│       ├── register_bouncer.sh
│       ├── install_hub_items.sh
│       └── parsers/
│           └── caddy-json-logs.yaml
├── backend/
│   └── internal/
│       ├── api/
│       │   └── handlers/
│       │       └── cerberus_logs_ws.go (new)
│       ├── models/
│       │   └── logs.go (updated)
│       └── services/
│           └── log_watcher.go (new)
├── frontend/
│   └── src/
│       ├── api/
│       │   └── logs.ts (updated)
│       ├── components/
│       │   └── LiveLogViewer.tsx (updated)
│       └── pages/
│           └── Security.tsx (updated)
├── docker-entrypoint.sh (modified)
├── Dockerfile (modified)
└── scripts/
    ├── crowdsec_decision_integration.sh (modified)
    └── crowdsec_startup_test.sh (new)
```

---

## Configuration Options

### Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `CERBERUS_SECURITY_CROWDSEC_MODE` | `disabled` | `disabled`, `local`, or `external` |
| `CHARON_SECURITY_CROWDSEC_MODE` | (fallback) | Alternative name for mode |
| `CROWDSEC_API_KEY` | (auto) | Bouncer API key (auto-generated if local) |
| `CROWDSEC_BOUNCER_API_KEY` | (auto) | Alternative name for API key |
| `CERBERUS_SECURITY_CROWDSEC_API_URL` | `http://127.0.0.1:8085` | LAPI URL for external mode |
| `CROWDSEC_BOUNCER_NAME` | `caddy-bouncer` | Name for registered bouncer |

### CrowdSec Paths in Container

| Path | Purpose |
|------|---------|
| `/etc/crowdsec/` | Main config directory |
| `/etc/crowdsec/acquis.yaml` | Acquisition configuration |
| `/etc/crowdsec/hub/` | Hub index and downloaded items |
| `/etc/crowdsec/bouncers/` | Bouncer API keys |
| `/var/lib/crowdsec/data/` | SQLite database, GeoIP data |
| `/var/log/crowdsec/` | CrowdSec logs |
| `/var/log/caddy/` | Caddy access logs (monitored by CrowdSec) |

---

## Ignore Files Review

### `.gitignore` Updates

Add the following entries:

```gitignore
# -----------------------------------------------------------------------------
# CrowdSec Runtime Data
# -----------------------------------------------------------------------------
/etc/crowdsec/
/var/lib/crowdsec/
/var/log/crowdsec/
*.key
```

### `.dockerignore` Updates

Add the following entries:

```dockerignore
# CrowdSec configs are copied explicitly in Dockerfile
# No changes needed - configs/crowdsec/ is included by default
```

### `.codecov.yml` Updates

Add CrowdSec config files to ignore:

```yaml
ignore:
  # ... existing entries ...

  # CrowdSec configuration (not source code)
  - "configs/crowdsec/**"
```

### `Dockerfile` Updates

Add directory creation and COPY statements:

```dockerfile
# Create CrowdSec directories
RUN mkdir -p /etc/crowdsec /etc/crowdsec/acquis.d /etc/crowdsec/bouncers \
             /etc/crowdsec/hub /etc/crowdsec/notifications \
             /var/lib/crowdsec/data /var/log/crowdsec /var/log/caddy

# Copy CrowdSec configuration templates
COPY configs/crowdsec/ /etc/crowdsec.dist/
COPY configs/crowdsec/register_bouncer.sh /usr/local/bin/
COPY configs/crowdsec/install_hub_items.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/register_bouncer.sh /usr/local/bin/install_hub_items.sh
```

---

## Rollout & Verification

### Pre-Implementation Checklist

- [ ] Create `configs/crowdsec/` directory structure
- [ ] Write acquisition config template
- [ ] Write bouncer registration script
- [ ] Write hub items installation script
- [ ] Test scripts locally with Docker

### Implementation Checklist

- [ ] Update Dockerfile with new COPY statements
- [ ] Update docker-entrypoint.sh with new initialization
- [ ] Build and test image locally
- [ ] Verify CrowdSec starts without "no datasource enabled" error
- [ ] Verify LAPI responds on port 8085
- [ ] Verify bouncer registration works
- [ ] Verify Caddy logs are being written
- [ ] Verify CrowdSec parses Caddy logs

### Post-Implementation Testing

1. **Build Test:**

   ```bash
   docker build -t charon:local .
   ```

2. **Startup Test:**

   ```bash
   docker run --rm -d --name charon-test \
       -p 8080:8080 \
       -e CERBERUS_SECURITY_CROWDSEC_MODE=local \
       charon:local
   sleep 30
   docker logs charon-test 2>&1 | grep -i crowdsec
   ```

3. **LAPI Health Test:**

   ```bash
   docker exec charon-test wget -q -O- http://127.0.0.1:8085/health
   ```

4. **Integration Test:**

   ```bash
   bash scripts/crowdsec_decision_integration.sh
   ```

5. **Full Workflow Test:**
   - Enable CrowdSec in UI
   - Ban a test IP
   - Verify IP appears in banned list
   - Unban the IP
   - Verify removal

6. **Unified Logging Test:**

   ```bash
   # Verify log watcher connects to Caddy logs
   curl -s http://localhost:8080/api/v1/status | jq '.log_watcher'

   # Test WebSocket connection (using websocat if available)
   websocat ws://localhost:8080/api/v1/cerberus/logs/live

   # Generate some traffic and verify logs appear
   curl -s http://localhost:80/nonexistent 2>/dev/null
   ```

7. **Live Log Viewer UI Test:**
   - Open Cerberus dashboard in browser
   - Verify "Security Access Logs" panel appears
   - Toggle between Application and Security modes
   - Verify blocked requests show with red highlighting
   - Test source filters (WAF, CrowdSec, Rate Limit, ACL)

### Success Criteria

- [ ] CrowdSec agent starts without errors
- [ ] LAPI responds on port 8085
- [ ] `cscli decisions list` works
- [ ] `cscli decisions add -i <IP>` works
- [ ] Caddy access logs are written to `/var/log/caddy/access.log`
- [ ] Bouncer plugin can query LAPI for decisions
- [ ] Integration tests pass
- [ ] **NEW:** LogWatcher service starts and tails Caddy logs
- [ ] **NEW:** WebSocket endpoint `/api/v1/cerberus/logs/live` streams logs
- [ ] **NEW:** LiveLogViewer displays security events in real-time
- [ ] **NEW:** Security events (403, 429) are highlighted with source tags
- [ ] **NEW:** Filters by source (waf, crowdsec, ratelimit, acl) work correctly

---

## References

- [CrowdSec Documentation](https://doc.crowdsec.net/)
- [CrowdSec Acquisition Configuration](https://doc.crowdsec.net/docs/data_sources/intro)
- [caddy-crowdsec-bouncer Plugin](https://github.com/hslatman/caddy-crowdsec-bouncer)
- [CrowdSec Hub](https://hub.crowdsec.net/)
- [Caddy Logging Documentation](https://caddyserver.com/docs/json/apps/http/servers/logs/)
- [Charon Security Documentation](../security.md)
- [Cerberus Technical Documentation](../cerberus.md)
- [Gorilla WebSocket](https://github.com/gorilla/websocket) - WebSocket implementation used
