SPB Git
5.9 KB

# Security

# Threat model

Assets: the public catalogue (integrity), the owner's private inventory (confidentiality: use states, notes, subscriptions, deployment URLs, projects), credentials (owner account, DB roles, AI keys, backup passphrase), the node itself (SSRF pivot into the cluster LAN / WireGuard overlay). Adversaries: anonymous web users, automated scanners, a compromised vendor page processed by the link checker or AI suggestions, a stolen session cookie.

# Identity and admin isolation

  • Single owner account (optional editor role). Password: argon2id (64 MiB, t=3), policy ≥ 14 chars, mixed case + digit, numeric-only rejected. Initial password must be changed at first login (must_change_password).
  • MFA: TOTP (otplib) and/or passkeys/WebAuthn (@simplewebauthn, userVerification: required). A passkey login satisfies MFA; TOTP-enabled accounts cannot use admin APIs until mfa_passed.
  • Sessions: 32-byte random token, SHA-256 stored, idle 14 d / absolute 30 d, rotated after MFA, after password change and every 12 h; cookie __Host-tools_session (HttpOnly, Secure, SameSite=Strict, Path=/). Revocation list in Settings; password change and TOTP enable revoke other sessions.
  • Brute force: DB-backed limiter (5 failures / 15 min per IP or e-mail, exponential lockout to 60 min), constant-time dummy verify for unknown e-mails, in-memory limiters on TOTP/passkey/report endpoints.
  • CSRF: Sec-Fetch-Site/Origin check on every mutating request and a double-submit token (HMAC of a per-session secret, delivered as <meta name="csrf-token">, required in x-csrf-token). Login itself is protected by the origin check.
  • Authorization is enforced server-side on every /api/admin/* handler (requireUser) and every admin page (requireUserPage); owner-only operations (owner_notes, projects, full export) check role. Client-side hiding is never relied upon.

# Public/private segregation (defence in depth)

  1. Database role tools_reader (used by all public routes) cannot read tools, owner_notes, projects, users, sessions, audit_log, jobs; it only sees the public_tools view (published + public + not merged) and reference tables. Writes are impossible by privilege except INSERT on error_reports and search_events; 5 s statement timeout.
  2. Application: public query module imports only the public client; admin modules are server-only.
  3. HTTP: /admin, /login, /api/admin send X-Robots-Tag: noindex and Cache-Control: no-store; robots.txt disallows them; sitemap lists public pages only.
  4. Tests: tests/integration/db.test.ts asserts the role boundary; scripts/security-audit.ts asserts unauthenticated access, IDOR by slug/id, cross-site login, payload field sets and leakage patterns.

# Web security headers (next.config.ts)

CSP default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests (no unsafe-eval in production; no third-party origins at all), HSTS 2 years incl. subdomains, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin, Permissions-Policy (camera/mic/geo/topics off), COOP same-origin, poweredByHeader: false. CORS: no cross-origin API access is granted (same-origin only).

# Input handling

  • Zod validation on every API body; search input sanitized and length-capped; all SQL through parameterized tagged templates (postgres-js) or Drizzle; Markdown rendered through sanitize-html allow-list; React escapes output; JSON-LD serialized with < escaped.
  • Error report form: honeypot field, size limits, 5/h/IP, IP stored only as a salted hash.

# Outbound requests (SSRF)

safeFetch resolves the hostname and refuses RFC1918/loopback/link-local/CGNAT/multicast/ULA/IPv4-mapped addresses, localhost, *.local, *.internal, *.maclustr.io, bare hostnames and the cloud metadata address; every redirect hop is re-validated; https→http downgrades are rejected; 15 s timeout, 1 MiB body cap, credentials omitted. The worker is the only component that makes outbound requests (plus the admin AI-suggest endpoint, owner-triggered).

# AI features

Optional, off by default (AI_PROVIDER=none). Keys are read from the node .env only (never shipped to the browser); monthly budget (AI_MONTHLY_BUDGET_USD) enforced before every call; usage telemetry in settings.ai_usage:<month>. Fetched text is passed as <untrusted_document> with explicit instructions to ignore embedded instructions; output must parse as JSON matching a zod schema; evidence quotes are verified against the page; nothing is persisted without the editor clicking Save (which writes a revision and audit entry).

# Data protection and operations

  • Secrets only in ~/apps/tools-spboucher-ai/.env (mode 600) on the node and an offline copy under ~/Desktop/Cluster/secrets/tools-spboucher-ai/; .env* is git-ignored; the deploy hook refuses to run without it; the manifest contains no secrets.
  • PostgreSQL listens on localhost; the app listens on 0.0.0.0 only so that the WireGuard peer (10.67.0.x) can reach it — the LAN is private and the gateway is the only public entry.
  • Backups: pg_dump custom format encrypted with AES-256-CBC (PBKDF2 200k) → local dir + rsync to M3U96b; restore drill script creates a temporary database and compares counts/hashes.
  • Audit log and revisions are append-only (DB trigger). Dependency scanning: pnpm audit in CI (scripts/ci.sh).

# Known limitations / follow-ups

  • In-memory rate limiters are per process (single PM2 instance — acceptable; move to Postgres if the web tier is scaled).
  • CSP keeps 'unsafe-inline' for scripts because pages are statically cached (no per-request nonce); no third-party scripts exist, which limits the exposure.
  • No WAF/fail2ban on the gateway for this host (cluster-wide item in the MacLustr backlog).
Files · ba7bd272caa00f8811bdea5814fbf1150547c055
Menu

Theme