Security

Deployment posture

Open-FDD is local-first for LAN, VPN, or OT networks. Central binds the API on :8080 and the openfdd-web React container serves the engineering UI on :3000.

Open-FDD is not internet-ready. LAN / VPN / OT only until the checklist below is complete and independently reviewed.

Not internet-ready until

  • Fail-closed when Central binds non-loopback without a ≥32-char OPENFDD_JWT_SECRET and OPENFDD_ADMIN_PASSWORD
  • Open mode (unset JWT secret) is loopback-only
  • Startup logs auth_enabled without secrets
  • Dedicated reverse proxy / TLS on every deployment (expose web proxy only; do not publish :8080 to the internet)
  • Per-building tenancy (JWT role is not multi-tenant isolation)
  • Viewer is read-only; mutations require operator/admin
  • Login throttle + generic credential errors
  • Package zip-slip / bomb caps on archive ingest
  • No wildcard CORS in the SPA nginx config
  • SPA CSP without unsafe-eval (Unity /twins may use wasm-unsafe-eval only)
  • Production secret rotation, SSO, and WAF as required by the site

OT writes stay off unless an operator explicitly enables them.

Deployment posture

Caddy edge (optional)

Optional compose overlay docker/compose.caddy.react.yml puts Caddy on :80 so http://<machine-ip>/ serves the React SPA (and /api* → central). Enable with react / react-ot / csv recipes (default ON for react/react-ot):

OPENFDD_CADDY=1 ./scripts/openfdd_stack_up.sh react
# or: docker compose -f docker/compose.react.yml -f docker/compose.caddy.react.yml up -d

Security defaults in the Caddyfiles: admin API off, security headers, probe-path 404s, no-new-privileges, dropped capabilities. When Caddy fronts the LAN, bind central to loopback: OPENFDD_CENTRAL_BIND=127.0.0.1. Use a TLS Caddyfile (+ certs) for HTTPS / HSTS when you terminate TLS at the edge.

Authentication

  • JWT on protected REST routes
  • Credentials in workspace/auth.env.local (mode 600, never commit)
  • Integrator role for commissioning; rotate with openfdd_auth_init.sh

TLS

The openfdd-web React app talks to central’s REST API (:8080). For HTTPS on the LAN edge, use the Caddy TLS Caddyfile (above) or terminate TLS on your ingress. MQTT between fieldbus edges and central is always MQTTS (8883) using the per-site provisioning kits.

Secrets

  • Never log or commit tokens, passwords, or auth.env.local
  • MCP agents receive JWT via environment — not embedded in docs

Audit and application logging (pen-test ready)

Industry-typical dual sink — enough for auditors / pen testers, not a SIEM product:

Channel What Where
Security audit Login success/fail/throttle, command authz, command issue, telemetry suspend/resume, fieldbus API-key rejects JSONL file + stdout target security_audit
App / error logs tracing at info/warn/error with request_id stdout (Railway / AWS / docker logs)

File audit (local + volume mounts):

$OPENFDD_WORKSPACE/logs/security_audit.jsonl
# also mirrored to auth_audit.jsonl for older tooling

Rotation (defaults): OPENFDD_AUDIT_LOG_MAX_BYTES=10485760 (10 MiB), OPENFDD_AUDIT_LOG_KEEP=5. Override path with OPENFDD_AUDIT_LOG_PATH. Secrets / passwords / tokens / API keys are redacted.

Container stdout rotation (GHCR compose / local Docker):

logging:
  driver: json-file
  options:
    max-size: "10m"
    max-file: "5"

Hosted (Railway / AWS): set OPENFDD_LOG_FORMAT=json (compose default). Platform log retention is the volume cap; app JSON lines stay queryable (event, request_id, channel=security_audit).

For pen tests, ask operators for: docker logs (or Railway/AWS log export) filtered on security_audit, plus workspace/logs/security_audit.jsonl*.

BACnet write safety

  • POST /api/bacnet/write-dry-run before live writes
  • Human approval required for production BACnet writes
  • Agents must not write without explicit authorization

Backup before change

Always back up workspace/ before image updates or historian purges — see Backup, update, restore.

Dependency scanning

Repository CI runs Rust audit, npm audit, Trivy, and Gitleaks on pull requests.