# 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.*