# DashCaddy Security Center — Feature Documentation

**Built:** 2026-07-13
**Author:** Sami Ahmed
**Code:** assistant implementation
**Scope:** Medium — multi-source ingest, no agent binary yet

---

## What is the Security Center?

A unified **security event pipeline** inside DashCaddy that collects, indexes, and visualizes security-relevant events from every source you can plug into it. Today: API events, Caddy access logs, fail2ban bans, shared_bans promotions. Tomorrow: remote DashCaddy agents, syslog feeds, anything that emits events over HTTPS.

The goal: **one place to ask "who is accessing what, where, and when?"** across every service and every host you run DashCaddy on.

---

## Architecture

```
┌──────────────────────────────────────────────────────────────────────────┐
│                     DashCaddy (Central Instance)                         │
│                                                                          │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐           │
│  │  audit-logger   │  │ Caddy log tail  │  │  fail2ban tail  │  ...    │
│  │  (API events)   │  │ (HTTP requests) │  │   (SSH bans)    │          │
│  └────────┬────────┘  └────────┬────────┘  └────────┬────────┘           │
│           │                    │                    │                    │
│           └────────────────────┼────────────────────┘                    │
│                                ▼                                         │
│                    ┌───────────────────────┐                             │
│                    │   Security Event      │                             │
│                    │   Store (JSONL)       │                             │
│                    │   + In-memory index   │                             │
│                    └───────────┬───────────┘                             │
│                                │                                         │
│                    ┌───────────┴───────────┐                             │
│                    │                       │                             │
│                    ▼                       ▼                             │
│            ┌──────────────┐        ┌──────────────┐                       │
│            │  REST API    │        │  SSE Stream  │                       │
│            │ /security/   │        │ /events/     │                       │
│            │  events      │        │  stream      │                       │
│            │  hosts       │        └──────┬───────┘                       │
│            │  ingest      │               │                               │
│            └──────┬───────┘               │                               │
│                   │                       │                               │
└───────────────────┼───────────────────────┼───────────────────────────────┘
                    │                       │
        ┌───────────┴───────────┐           │
        │                       │           │
        ▼                       ▼           ▼
  ┌──────────┐           ┌─────────────────────────┐
  │ Dashboard│           │  Remote DashCaddy Agents│
  │  (UI)    │           │  (POST /events/ingest) │
  └──────────┘           └─────────────────────────┘
```

**Three pillars:**

1. **Event ingest** — multiple sources feed a single store via a normalized schema
2. **Query API** — REST endpoints + Server-Sent Events for live tail
3. **Dashboard UI** — Overview / Events / Hosts tabs

---

## Files added/changed

### New files

| File | Purpose |
|---|---|
| `src/security/event-store.js` | JSONL-backed append-only store + in-memory query index |
| `src/security/host-registry.js` | Registered hosts/locations with per-host API keys |
| `src/security/event-workers.js` | Tail-followers for Caddy access log, fail2ban log, shared_bans apply log |
| `routes/security.js` | Express route factory: events, hosts, ingest, SSE stream |
| `status/js/security-center.js` | Dashboard modal: Overview / Events / Hosts tabs with live tail |

### Modified files

| File | Change |
|---|---|
| `src/app.js` | Mounts `/api/v1/security/*` |
| `src/utilities/middleware.js` | Adds `/api/v1/security/events/ingest` and `/events/batch` to PUBLIC_ROUTES (per-host Bearer auth replaces TOTP) |
| `src/security/audit-logger.js` | Mirrors API audit events into the security store |
| `server.js` | Starts the security event workers on boot |
| `status/build.js` | Bundles `security-center.js` into features.js |
| `status/index.html` | Adds "🛡️ Security" button to dashboard nav |

---

## Event schema

```json
{
  "id":          "uuid-v4",
  "ts":          "2026-07-13T01:35:55.123Z",
  "source_host": "dns2",                  // hostname or registered host id
  "source_type": "api" | "caddy" | "fail2ban" | "shared-bans" | "agent" | "syslog",
  "actor":       "192.0.2.1",             // IP, user, agent_id — null is allowed
  "target":      "/api/v1/auth/login",    // endpoint, service id, host — null is allowed
  "action":      "auth.login",            // free-form but stable per source_type
  "outcome":     "success" | "denied" | "blocked" | "rate-limited" | "error" | "unknown",
  "severity":    "info" | "notice" | "warn" | "error" | "critical",
  "message":     "human-readable one-liner",
  "metadata":    { ... }                  // free-form, source-specific
}
```

**Severity semantics:**

| Level | Meaning | Examples |
|---|---|---|
| `info` | Normal operation | API GET, successful login, shared_bans applied |
| `notice` | Worth a glance | failed login attempt, ban event, config change |
| `warn` | Attention needed | 401/403 on sensitive endpoint, auth.totp-disable, container.delete |
| `error` | Something failed | 5xx HTTP, dependency failure |
| `critical` | Active threat | (not auto-emitted in v1 — reserved for v2 alerting engine) |

---

## API surface

All under `/api/v1/security/*`. Auth: TOTP/JWT/API-key via existing middleware, EXCEPT `/events/ingest` and `/events/batch` which use a per-host Bearer token.

### Events

| Method | Path | Purpose |
|---|---|---|
| GET | `/events` | List/query events with filters: `source_type`, `source_host`, `severity`, `outcome`, `actor`, `actor_prefix`, `action`, `target`, `since`, `until`. Pagination via `limit`/`offset`. |
| GET | `/events/stats` | Aggregations: counts by source/severity/host, top actors, top targets. Use `?since=ISO` for a time window. |
| GET | `/events/stream` | **Server-Sent Events** for live tail. Initial payload = last 20 events. Subsequent payloads = new events as they happen. |
| GET | `/events/:id` | Single event by id |
| POST | `/events/ingest` | Single event ingest (per-host Bearer auth) |
| POST | `/events/batch` | Batch ingest, max 500 events per request (per-host Bearer auth) |

### Hosts

| Method | Path | Purpose |
|---|---|---|
| GET | `/hosts` | List all registered hosts |
| POST | `/hosts` | Register new host. Returns `api_key` **once** — caller must store it. |
| GET | `/hosts/:id` | Host details |
| PATCH | `/hosts/:id` | Update `label`, `type`, `meta`, `enabled` |
| DELETE | `/hosts/:id` | Deregister host. Events already received remain. (Cannot delete `self`.) |
| GET | `/hosts/:id/health` | Last seen, event count (24h), severity breakdown, online/stale status |
| POST | `/hosts/:id/rotate-key` | **Returns 501 in v1** — to rotate, deregister + re-register. |

---

## Configuring the event workers

### Caddy access log

The Caddy worker reads `/var/log/caddy/access.log`. To use it, configure Caddy to log in JSON format:

```caddyfile
# In your Caddyfile global options:
{
    log default {
        output file /var/log/caddy/access.log {
            roll_size 100mb
            roll_keep 10
        }
        format json
    }
}
```

Then reload Caddy. The worker will pick up new lines automatically (it persists its byte offset across restarts).

### fail2ban log

Reads `/var/log/fail2ban.log`. Default location, no config needed. Captures both `Ban` and `Unban` events.

### shared_bans apply log

Reads `/var/log/shared-bans-apply.log`. Default location, no config needed. Emits one event per "Applied N entries" line.

### Override paths via env

```bash
export CADDY_ACCESS_LOG=/custom/path/caddy.log
export FAIL2BAN_LOG=/custom/path/fail2ban.log
export SHARED_BANS_LOG=/custom/path/shared-bans-apply.log
export DATA_DIR=/opt/dashcaddy/data  # for offset state files
export SECURITY_EVENT_LOG_FILE=/opt/dashcaddy/data/security-events.jsonl
export SECURITY_HOSTS_FILE=/opt/dashcaddy/data/security-hosts.json
```

---

## Dashboard UI

Click **🛡️ Security** in the dashboard toolbar to open the Security Center.

### Overview tab

- 5 stat cards: events (24h), warnings, errors, denied, hosts
- Top Actors (24h) — IPs / users hitting your services most
- Top Targets (24h) — endpoints most-hit

### Events tab

- Filterable by source_type, severity, source_host, actor (prefix)
- Live-tail checkbox — toggles SSE stream
- Color-coded by severity
- Auto-refreshes on new events when live-tail is on

### Hosts tab

- List of registered hosts with status dot (🟢 online / 🟡 stale / ⚪ never-seen / 🔴 disabled)
- Click "➕ Register Host" to add a new location
- **api_key is shown exactly once** at registration time, in a dialog the user must save
- Cannot delete the `self` host from the UI

---

## Adding a remote DashCaddy agent (v2 design)

The remote-agent path is **already wired**. To onboard a new DashCaddy location:

1. Open the Security Center on the central instance
2. Hosts tab → Register Host → id=`nas1`, label="Synology NAS", type="dashcaddy"
3. Save the displayed `api_key`
4. On the remote host, run:
   ```bash
   curl -X POST https://central.sami/api/v1/security/events/ingest \
     -H "Authorization: Bearer dca_xxx..." \
     -H "Content-Type: application/json" \
     -d '{
       "source_type": "agent",
       "actor": "1.2.3.4",
       "target": "/volume1/web/login",
       "action": "auth.login",
       "outcome": "denied",
       "severity": "warn",
       "message": "Failed admin login"
     }'
   ```
5. The remote host now appears in the Security Center's Hosts tab
6. Events show up in the Events tab tagged with `source_host=nas1`

A standalone DCA (DashCaddy Agent) binary that tails `/var/log/auth.log`, `/var/log/nginx/access.log`, etc. is **v2 work**.

---

## Performance & limits

| Metric | v1 limit | Where it hurts at scale |
|---|---|---|
| Events in memory | 10,000 | Querying `?limit=10000` works; going beyond this hits only disk |
| Events on disk | 100,000 (rotated) | Beyond this, oldest events get trimmed during `_maybeTrim()` |
| Batch ingest size | 500 events/request | Adjustable in `routes/security.js` if needed |
| SSE stream idle timeout | 30s heartbeat | Browser auto-reconnects |
| Concurrent SSE clients | unbounded (each holds 1 HTTP connection) | For v2, add per-client cap |

If you grow past 100k events on disk, **switch the store to SQLite**. The current JSONL design is intentionally simple for v1.

---

## What I deliberately did NOT build

These are real features that you may want next, but I scoped them out to ship something working today:

- ❌ **Alerting engine** — rules like "5+ failures from one IP in 60s → notify" — v2
- ❌ **Active ban-from-UI** — `/api/v1/security/actions/ban` to push to shared_bans — v2
- ❌ **GeoIP enrichment** — translate IPs to countries on ingest — v2
- ❌ **DCA agent binary** — standalone Node.js process that tails arbitrary log files — v2
- ❌ **Syslog UDP/TCP listener** — receive syslog directly on port 514 — v2
- ❌ **Per-IP timeline view** — click an IP, see every event from them across all sources — v2
- ❌ **Hot-archive / cold-archive tiering** — keep 30 days hot, compress older to monthly files — v2

---

## Testing performed (2026-07-13)

| Test | Result |
|---|---|
| Event store append + query + stats | ✅ PASS — 5 events appended, queried by severity, stats aggregated correctly |
| Persistence across "restart" | ✅ PASS — events survive reload from JSONL |
| Host registry + auth | ✅ PASS — self-registered on first boot, Bearer-token auth round-trip works |
| Caddy log worker (mock log) | ✅ PASS — 3 events emitted with correct severity (401→warn, 200→info) |
| fail2ban log worker (mock log) | ✅ PASS — Ban/Unban events emitted |
| shared_bans log worker (mock log) | ✅ PASS — "Applied N entries" event emitted |
| All routes load without syntax error | ✅ PASS |
| Routes factory returns Express Router | ✅ PASS |
| audit-logger still loads after changes | ✅ PASS |

---

## Open questions / decisions to make

1. **Where should the api_key for a remote host live in storage?** Currently it's returned once to the human operator, who must save it. A future "central-admin pulls from agent via reverse-channel" would be more secure but more complex.
2. **Should the Caddy access log parser be on by default?** It requires Caddy to log JSON, which is a config change. The worker gracefully no-ops if the file doesn't exist.
3. **Event retention policy.** Current default is 100k events on disk ≈ ~1 year at current volume, less under attack. Increase `SECURITY_EVENT_MAX_DISK` if needed.

---

*This document lives at `/root/dashcaddy/SECURITY-FEATURE.md`. Files committed as part of this build are listed in section "Files added/changed" above.*