Files
dashcaddy/SECURITY-FEATURE.md
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

14 KiB
Raw Permalink Blame History

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

{
  "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:

# 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

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:
    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.