The app is local-first, but it can be bound to a LAN or reverse-proxy interface when the deployment is configured intentionally.
Current controls:
- Local username/password login.
- PBKDF2-HMAC-SHA256 password hashes with per-user salts.
- HTTP-only same-site session cookies.
- Normal sessions are time-limited and held in memory; the in-memory session store prunes expired entries with a hard active-session cap.
- The optional "Stay signed in" login checkbox creates a long-lived persistent session for trusted personal devices. Only a hash of the browser token is stored in the local JSON database, the raw token remains in the HTTP-only cookie, and logout deletes the current persistent session.
- Persistent sessions survive service restarts but validate against the current user credential fingerprint on every use. Changing the user's password or disabling the user invalidates remembered sessions.
- Repeated login failures are rate-limited in memory and return
429withRetry-After. - Login-failure tracking is pruned and capped to avoid unbounded in-memory growth.
- CSRF token checks for mutating auth, diagnostic, notification, Docker-control, manual status refresh, and settings requests. Tokens are compared using constant-time comparison.
- Origin and referer checks for mutating requests. Cross-site POSTs are rejected unless the origin or referer matches the current host,
server.public_url, orserver.allowed_origins. - API responses are marked
Cache-Control: no-store. - Request bodies for mutating endpoints are capped at 1 MiB.
- Browser hardening headers include
X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy: no-referrer,Cross-Origin-Opener-Policy: same-origin,Cross-Origin-Resource-Policy: same-origin, and a restrictive content security policy. - HTTPS deployments emit
Strict-Transport-Securitywhenserver.public_urlis configured with anhttps://URL. - Admin/general-user/custom role separation.
- The detailed admin panel and lightweight compact web app are served on separate ports. The compact router does not register
/api/admin/*endpoints. - Per-role app visibility. Hidden apps are removed from app lists, incidents, facts, and role-scoped LLM context.
- Docker start/stop/restart actions are admin-only, CSRF-protected, audited, and resolved from the server-side app snapshot before calling Unraid. Stop/restart requests also require an explicit confirmation flag and a
confirm_app_idmatching the resolved app ID, so blind disruptive requests fail before the Docker adapter is called. Docker restart prefers Unraid's native restart mutation; stop/start fallback is used only for schema compatibility and reports when recovery may have left the container stopped. Docker control/log adapters use strict Docker object IDs returned by Unraid GraphQL when available, or safecontainer:<name>fallbacks from the server snapshot; arbitrary user-submittedPrefixedIDvalues and friendly display names are not used as remote Docker targets. - Docker-provided app icon labels are sanitized with the same URL rules as admin icon overrides; unsupported schemes and embedded credentials are ignored.
- Optional Unraid SSH Docker fallback is disabled by default, uses argument-array process execution rather than shell-built commands, disables password authentication for batch runs, and requires a configured host and user. It is used for SSH-sourced apps, larger SSH-sourced app lists, and GraphQL transport/schema-unavailable failures; GraphQL permission, validation, and not-found errors fail closed instead of falling through to SSH. Use a restricted SSH key where possible.
- No unauthenticated admin endpoints.
- Remote bind safety: non-loopback bind addresses require an explicit bootstrap admin password unless
auth.allow_insecure_remoteis enabled for development only. - Secrets are read from environment variables, local config, or explicitly configured local API key files only.
.env, local config, state, data, logs, and executables are ignored by git.- Docker logs are admin-only, bounded to a small response, resolved from the current server-side app snapshot, redacted before JSON response, and audited without storing log text.
- Status history is stored locally in
history.jsonlbeside the JSON database. It contains status transitions and display names, not raw logs or secrets, but it is still local operational state and must remain out of git. - Redaction runs before audit entries, log display, notification text, and LLM context. Audit detail redaction walks nested maps and lists.
- LLM agent tools are read-only status fetches (no Docker control, shell, or filesystem access). Admin-requested diagnosis enables these read-only tools by default with a small call budget; they are force-disabled for non-admin policies, restricted to an explicit per-tool allowlist, bounded by a per-policy maximum tool-call count, and fail closed when disabled or when the limit is exceeded. Every tool call is audited (
llm.agent_tool) and operates only on role-filtered, redacted snapshots. - LLM repair execution is server-side and app-action limited, with one narrow storage exception. The model never sends a command; it can only return a schema-allowlisted
recommended_action_idplus a structured target hint. NoobBoard re-resolves that target against a fresh admin snapshot, mapsask_admin_to_restart_containerto Dockerstartfor stopped/exited apps or Dockerrestartfor non-stopped repair targets, and rejects other app/container recommendations as non-executing. If the model misses that recommendation while exactly one repair-eligible app is down/degraded, the server may show a clearly labeled suggested app-fix plan, but execution gates are unchanged. Approval-gated app repair requires CSRF/same-origin checks,agent_control_enabled=true, a short-lived server-signed token bound to actor/action/target, a single-use replay nonce, a target app withagent_repair_allowed=true, a non-blacklisted app, the per-app 1-minute cooldown, and the global 5/hour repair limit. Request-scoped autonomous app repair additionally requires the chat request to includeauto_repair:true,action_auto_review_enabled=true, a non-online opted-in target app, and reviewer approval before Docker is called; reviewer failures or denials fail closed. The reviewer selector supportssame,openai/<model>,chatgpt/<model>, andanthropic/<model>, and reference files are allowlisted todocs/*,README.md, andAGENTS.md, capped by file/total byte limits, and skipped when missing or unsafe so secrets such as local auth files cannot be sent to the reviewer. After Docker accepts the action, NoobBoard refreshes status, writes a repair outcome event to history, and returns recovered/still-down verification to chat without retrying. Proposed, suggested, auto-reviewed, approved, denied, not-enabled, refused, rate-limited, replay-blocked, failed, executed, and verified decisions are audited, with autonomous actions recorded underllm.agent_auto_repair.*plusapp.container.action. The legacy/api/admin/agent/armroute is not part of the current UI gate. - LLM-only array start is intentionally narrower than general Unraid control. Compact chat may surface
ask_admin_to_start_arrayonly when compact LLM is enabled for the role and the current full snapshot reportsunraid_array_stateas stopped/offline; the role's normal NAS/server-status detail visibility is a display setting and does not block this LLM-only recovery path. The response must include admin-first guidance: contact the admin to confirm it was not intentionally stopped, but if the admin is unavailable or asleep and service needs to be restored, starting the array is okay. The compact frontend receives a short-livedexecution_token;POST /api/user/agent/actionaccepts only that token,choice:"start_array", andtarget_id:"unraid_array", re-checks the current array state, prevents token replay, applies the shared repair rate limit, calls only Unraidarray.setState(input:{desiredState:START}), refreshes status for verification, writes infrastructure history, and audits the lifecycle underllm.array_start.*plusinfra.unraid_array.action. No arbitrary Unraid mutation, stop-array action, shell access, or manual compact array button is exposed. - General-user repair requests are non-actuating. A general user can create a pending request only for an app visible to their role and a fixed app-fix recommendation; the request is stored locally, audited, sent to admins through the configured notification backend, and shown in the admin Review Queue. Admin approval of a request still requires an admin session, CSRF/same-origin checks, saved admin app-fix enablement, current target re-resolution, per-app repair opt-in, blacklist checks, optional safety-reviewer approval, shared cooldown/rate limits, and Docker execution from the server. Requesters can read their own request outcomes but cannot approve or execute requested repairs.
- General-user notification records are read-only and scoped to the signed-in user, plus any intentionally global notification records. Notification text is redacted before the compact endpoint returns it.
- Direct general-user app controls are not model actions. They require CSRF/same-origin checks, a visible app,
app_catalog.general_user_restarts_enabled=true, a per-apprestart_allowed_general_useropt-in, a matching confirmation payload, a Docker target, no privacy blacklist match, optional safety-reviewer approval, and the shared per-app/global repair limits before the server calls Docker. The legacy setting names are preserved for compatibility but gate Start, Restart, and Stop in the compact app. Hidden, blacklisted, not-opted-in, no-op state requests, stopped-app restart requests that should use Start, and non-Docker apps are refused before actuation and audited. Successful direct controls are audit/history events only and must not create Review Queue items. - General-user diagnosis auto-fix is a permanent saved setting plus a per-request chat toggle, not a transient gate. It requires
auto_repair:true,app_catalog.general_user_auto_repair_enabled=true, and the same standard-user app-control global/per-app opt-in, visibility, blacklist, reviewer, and rate-limit gates. The model still cannot choose arbitrary commands; it can only return the fixed app-fix recommendation and target hint. The server choosesstartfor an opted-in stopped app andrestartfor an opted-in non-online running app; it never selectsstopautomatically. - The OpenAI ChatGPT connector login (browser and headless/device-code) uses OAuth 2.0 with PKCE (S256) and a single-use, time-limited
state. The auth routes are admin-only and CSRF-protected. The browser-flow callback server binds to loopback (localhost:1455) only and is offered only when the admin page is opened on the host; LAN devices use the device-code flow. The OAuth issuer is a fixed endpoint, not a user-supplied URL. - API keys and ChatGPT tokens are write-only in the settings API: responses report only whether a value is set, never the value. They are never logged and are covered by the redaction blacklist (
*_KEY,*_TOKEN,AUTHORIZATION). They are persisted in plaintext in the git-ignored runtime settings store, so treat that store (and its host) as sensitive. - The login form does not ship a default password value in the HTML.
Development bootstrap users:
admin/change-me-nowviewer/change-me-now
Change these before real LAN or WAN-proxied use by setting NOOBBOARD_BOOTSTRAP_ADMIN_USERNAME and NOOBBOARD_BOOTSTRAP_ADMIN_PASSWORD before the first run, then create named users from Admin -> Settings -> Role Access.
If you seed the admin login during install.ps1, the bootstrap password is written in plaintext to the service config.yaml (ACL-restricted to Administrators/SYSTEM). It is hashed into the database on first run but is not removed from the file; delete the bootstrap_admin_password line after first sign-in if you want it gone.
For reverse-proxy deployment:
- Set
NOOBBOARD_BIND_ADDRESS=0.0.0.0only behind a trusted firewall or reverse proxy. - Set
NOOBBOARD_PUBLIC_URL=https://status.example.com. - Set
NOOBBOARD_COOKIE_SECURE=truewhen served over HTTPS. - Set
NOOBBOARD_ALLOWED_ORIGINS=https://status.example.comif the proxy origin differs from the request host.