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_SECRETandOPENFDD_ADMIN_PASSWORD - Open mode (unset JWT secret) is loopback-only
- Startup logs
auth_enabledwithout secrets - Dedicated reverse proxy / TLS on every deployment (expose web proxy only; do not publish
:8080to 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/twinsmay usewasm-unsafe-evalonly) - 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(mode600, 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-runbefore 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.