Security Hardening
Dashboard Protections
The dashboard includes several baseline protections:
- Authentication is required before dashboard data is visible.
- Admin-only routes protect settings, users, diagnostics, connection tests, and exports.
- Login attempts are rate-limited per username and client address. The client address ignores
X-Forwarded-Forunless you tell the dashboard how many proxies to trust (see Reverse Proxy Settings), so a forged header cannot be used to dodge the limit. Unknown usernames take as long to reject as real ones, so response time does not reveal which accounts exist. - Browser writes and WebSocket connections are rejected when their
Origindoes not match the dashboard's own host, which closes cross-site request forgery from other sites on your domain. - A Content-Security-Policy restricts where scripts, styles, images and connections can load from.
- Malformed URLs and cookies can no longer crash the server, and errors are caught per request.
- Webhook URLs and SNMP community strings are never echoed in error messages or stored history, and webhook delivery has a timeout.
- The container runs as an unprivileged user and ships only production dependencies, with a healthcheck.
- Session cookies are
HttpOnly,SameSite=Lax, and markedSecurewhen the dashboard is reached through HTTPS. - JSON and static responses include security headers for content type sniffing, framing, referrer behavior, and browser permissions.
- Admin audit events are recorded for login, registration, settings save, restore, user changes, notification tests, security setting changes, and logout.
- Admins can change their password from the
Security Auditpanel. A successful password change rotates the session secret and signs active sessions out. - Admins can set the session timeout from the
Security Auditpanel.
Reverse Proxy Settings
Set these on the dashboard container, not in nginx:
| Variable | Value | Why |
|---|---|---|
TRUST_PROXY_HOPS |
1 behind one proxy, 0 otherwise |
Trusts the last X-Forwarded-For entry, the address your proxy appended. With the default 0 the header is ignored and every client appears to be the proxy, so the login rate limit is shared by everyone. Never set it when clients can reach port 3000 directly, because they could forge the header. |
DASHBOARD_ALLOWED_ORIGINS |
only if needed | A proxy that passes the public Host header through (proxy_set_header Host $host) needs nothing. If yours rewrites Host, list the public origin here, for example https://dashboard.example.com. A rejected request is logged as Rejected POST ... from origin .... |
If you bind the container to the same host as nginx, publish the port on loopback only (127.0.0.1:3000:3000) so nothing can bypass the proxy.
First-Run Setup Token
By default whoever opens /admin first on a fresh install creates the admin user. Set DASHBOARD_SETUP_TOKEN to require a secret for that first registration, so nobody else can claim a new install while it is reachable.
Wallboard Display Key
A wall display cannot type a password. DASHBOARD_DISPLAY_KEY (24 or more characters; shorter values are ignored) lets a screen open /wall?key=... without logging in. The key only authorizes the live data connection. It cannot read or change settings, users, backups or any admin endpoint, guessing is rate limited, and it is compared in constant time. Anyone holding the key can see your lab status, so keep the dashboard on your LAN and treat the key like a password. To revoke it, change the variable and restart. See Wallboard.
Demo Mode
DASHBOARD_DEMO=ok|warn replaces all data with synthetic samples and never contacts a real service. It is for previews and screenshots. Never set it on a real deployment; the dense view shows a red DEMO DATA badge and the server logs a warning when it is on.
Reverse Proxy Checklist
- Serve the dashboard over HTTPS.
- Forward
X-Forwarded-Proto: httpsfrom the proxy so secure cookies are enabled. - Keep WebSocket support enabled for live dashboard updates.
- Restrict direct container access to trusted networks.
- Keep the dashboard behind your normal authentication and firewall layers when exposing it outside the lab.
Settings Backup
Dashboard state is stored in /data/dashboard.db. On upgrade, existing settings.json, notification-history.json, and audit-log.json files are imported automatically into the database. Keep the whole Docker data volume in your normal backup plan.
The admin recovery area includes:
Export settings: redacted export for troubleshooting.Export full backup: full settings backup including secrets. It asks for your admin password again before it downloads.Restore full backup: replace saved dashboard settings from a trusted full backup JSON file.
Treat full backups like password vault exports. Store them encrypted, avoid sending them through chat or email, and rotate any exposed API keys.
Restoring a backup can replace users and session secrets. If the restored user data does not match your current session, sign in again after the restore completes.
Audit Log
The Security Audit page, shown with demo data. The admin Security Audit panel shows recent security-relevant events:
- Actor.
- Action.
- Target.
- Status.
- Source IP as seen by the dashboard.
- Timestamp and short detail.
Audit history is saved in the dashboard database at /data/dashboard.db. On upgrade, an existing audit-log.json file is imported automatically. The dashboard keeps the latest 100 events so routine admin history survives container restarts while staying compact.
Session Timeout
The Security Audit panel includes a Session timeout field in hours. The accepted range is 0.25 to 168 hours.
Changing this value affects new sign-ins. Existing sessions keep the expiry that was issued when the user logged in.
You can also set the default with:
DASHBOARD_SESSION_MAX_AGE_HOURS=8
Iteration Checks
Run these checks before publishing a new image:
npm run build
npm run security:check
npm run test:hardening
npm run test:hardening starts throwaway servers and checks the crash fixes, rate limiting, origin checks, the display key, backup re-authentication, notification safety, the download clients and demo mode.
For backend changes, also run the production server locally with a temporary DATA_DIR and check /api/health plus the app shell.
