Files
dashcaddy/SECURITY-FEATURE.md
T
hermes c9d067c2f0
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
Add Security Center — multi-source event pipeline with dashboard UI
Introduces a unified security event store and HTTP API that ingests events
from any of the configured sources (API audit, Caddy access log, fail2ban,
shared_bans, future remote agents) and surfaces them in the dashboard.

New files:
  src/security/event-store.js      JSONL-backed store + in-memory query index
  src/security/host-registry.js    Registered hosts with per-host API keys
  src/security/event-workers.js    Tail-followers for Caddy/fail2ban/shared_bans logs
  routes/security.js               Events, hosts, ingest, SSE stream endpoints
  status/js/security-center.js     Dashboard modal with Overview/Events/Hosts tabs
  SECURITY-FEATURE.md              Full feature documentation
  DEAD-CODE.md, DUP-CODE.md, HARDENING.md   Prior audits

Modified:
  src/app.js                       Mount /api/v1/security/*
  src/utilities/middleware.js      Add ingest endpoints to PUBLIC_ROUTES
  src/security/audit-logger.js     Mirror audit events into security store
  server.js                        Start security workers on boot
  status/build.js                  Bundle security-center.js
  status/index.html                Add Security button to nav
2026-07-13 02:28:56 -07:00

300 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.*