StratoFusion uses layered controls across the Next.js app, the VM-hosted rclone
worker, and the PostgreSQL data layer. The previous Fly.io deployment is
retained as a stopped recovery resource and shares the worker security
contract. This document reflects controls currently implemented in the repo.
Authentication
User authentication is handled by Clerk in src/middleware.ts.
Public API matchers in middleware are backed by route-level auth checks for sensitive provider and rclone routes.
Rclone endpoints enforce service-to-service authentication in fly-rclone/src/middleware/auth.js in both current VM and legacy recovery deployments.
Cron routes validate Authorization: Bearer <CRON_SECRET> and use guardProductionCronRequest() where applicable.
Authorization
Operation polling and cancellation routes require an authenticated user and verify persisted ownership before returning data.
Service-account and token queries are scoped by userId in src/lib/database/service-accounts.ts.
Two-user negative tests cover service-account/token access, scheduled jobs,
transfer operations, and AI indexing jobs. Foreign identifiers do not cause
provider, worker, or mutation calls.
Admin-only audit-log access is enforced in src/app/api/logs/audit/route.ts.
Non-production dev, debug, demo, rclone-test, and popup-test paths are
disabled in production and loopback-only in other runtimes. Other requests
receive a private, non-cacheable 404 before authentication; route-level
dev/admin checks remain defense in depth locally.
Public documentation renders under /docs; /dev/docs is available only in
a non-production runtime on a loopback host.
Request Integrity
src/lib/auth/request-integrity.ts is the centralized CSRF boundary for all
mutating requests. The security scan currently inventories 71 route
modules exporting , , , or .
/api/*
POST
PUT
PATCH
DELETE
Exact same-origin Origin or Sec-Fetch-Site: same-origin evidence is
required for browser mutations. Cross-site, sibling-origin (same-site),
opaque/malformed origins, and cookie-bearing requests without browser origin
evidence fail closed with a non-cacheable 403.
Credentialless Stripe webhooks, cron invocations, and internal service calls
can reach their handlers without browser headers; their existing signature,
bearer, or service-authentication checks remain mandatory.
Token Protection
OAuth tokens are encrypted at rest through src/lib/security/token-encryption.ts.
Service-token writes in src/lib/database/service-accounts.ts use encryptTokenValue().
Token reads decrypt through the shared helper rather than exposing raw encrypted values.
Operational logging has been scrubbed to record token presence and length instead of token prefixes in the main transfer and refresh flows.
Rclone Service
helmet is enabled in fly-rclone/server.js.
Service authentication is enforced before route handlers.
Current production TLS terminates at Caddy. The retained Fly production
config also enforces HTTPS for approved recovery.
rclone execution paths use argument arrays with spawn(...); no shell: true execution path is allowed by the security audit.
Worker paths are decoded repeatedly, Unicode-normalized, and canonicalized
across slash variants before traversal, drive-path, UNC-path, and system-path
checks. Encoded and mixed-separator traversal attempts fail before rclone is
invoked.
Untrusted Files and URLs
DOCX, XLSX, and PPTX extraction validates ZIP central-directory metadata
before parser dispatch. Entry count, individual and aggregate expanded size,
compression ratio, encryption, ZIP64 size markers, and unsafe entry paths are
bounded or rejected.
The configured rclone service URL permits the fixed loopback development
endpoint or a pathless public HTTPS origin. Credentials, private/metadata
authorities, URL paths, query strings, fragments, and remote HTTP are denied.
The web download proxy validates the token shape and size before making an
upstream request, refuses redirects, and returns a generic upstream failure
instead of disclosing raw network details.
Provider-supplied SharePoint links render only after validation as an HTTPS
sharepoint.com origin without credentials or a custom port.
VM Administrative Access
The self-hosted VM remains headless; no desktop, RDP, VNC, browser IDE, or
public container-management UI is part of the supported deployment.
VS Code Remote SSH reuses the key-only deploy account. Password SSH and root
SSH remain disabled.
Tailscale provides the private management network without replacing the
existing CI SSH path during the current migration phase.
Optional Cockpit binds only to 127.0.0.1:9090 and is published to approved
tailnet administrators through Tailscale Serve on the private tailnet.
Cockpit uses a separate human operator account with password-protected sudo;
the account is not a Docker-group member and the deploy account remains
passwordless.
No Cockpit route is added to public Caddy and no public UFW rule is added for
its backend or tailnet HTTPS port.
next.config.js and vercel.json configure HSTS, CSP frame/object/base restrictions, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy, and Permissions-Policy.
Next.js framework disclosure through X-Powered-By is disabled. A
production-mode loopback Playwright regression verifies the header policy,
non-production route isolation, and cross-site mutation rejection.
Cross-Origin-Opener-Policy remains unsafe-none for OAuth popup compatibility. OAuth state and same-origin BroadcastChannel handling are the compensating controls.
Direct Google, OneDrive, and Dropbox authorization-code flows use an opaque
HMAC-SHA256-signed state value with a cryptographically random nonce and a
ten-minute expiry. Callback routes reject missing, expired, malformed,
legacy unsigned, or tampered state before exchanging the authorization code.
Popup state is issued only for an authenticated Clerk user. The signing key
uses OAUTH_STATE_SECRET when configured and otherwise the server-only token
key or Clerk secret hierarchy. Provider-token encryption itself requires an
independent 32+ byte TOKEN_ENCRYPTION_KEY, identifies ciphertext keys in
authenticated v2 envelopes, and accepts earlier keys only through the
explicit server-side rotation ring. Plaintext token reads fail closed.
Database and Auditability
src/lib/database/client.ts normalizes remote database URLs to require TLS via sslmode=require.
Transfer operations persist ownership metadata in transfer_operations.
Structured system audit logs already exist in system_audit_logs.
Admin audit-log reads are served through src/app/api/logs/audit/route.ts.
Administrative MFA
Every /admin page is guarded by the server layout, every /api/admin
handler applies the same resource-level policy, and middleware provides an
additional early boundary.
An administrator is allowed only when the signed Clerk v2 session token has
a finite, non-negative second-factor age in fva[1]. Missing or malformed
claims and Clerk's -1 no-second-factor marker fail closed.
Code enforcement does not configure the Clerk instance. An authorized Clerk
administrator must enable supported MFA strategies, require enrollment, and
retain redacted production evidence for every configured StratoFusion admin.
Current Follow-up Items
Add explicit request rate limiting for the rclone worker if service exposure expands.
Capture mandatory Clerk MFA configuration and successful second-factor
evidence for every exposed production admin before assessment.
Capture production session-cookie, expiry, rotation, and revocation evidence.
Capture authenticated browser traces proving same-origin mutations succeed
and assessor-generated cross-origin mutation attempts are rejected.
Session Timeout Safety
Auto-logout is operation-aware for foreground copy, move, upload, and download work. Active rclone operation IDs registered by the transfer dialog, direct uploads, and browser downloads keep the inactivity timer alive until every tracked operation reaches a terminal state.
Logout clears in-memory account/search state plus all localStorage and
sessionStorage data before Clerk sign-out. The explicit app logout and
reauthentication flows first attempt authenticated server-side provider-token
cleanup; Clerk sign-out still proceeds if that cleanup is unavailable.
AuthWrapper also clears both browser stores when Clerk transitions from a
signed-in to a signed-out state, covering Clerk-hosted sign-out controls.
Reporting
The repo-aware audit scripts live in scripts/security-audit/.
In PowerShell, run the audit scripts with pnpm exec tsx scripts/security-audit/<script>.ts.
If using scripts/security-audit/run-all.sh, invoke it explicitly with Git Bash: C:\Program Files\Git\bin\bash.exe scripts/security-audit/run-all.sh.
Run pnpm casa:security for the deterministic local security gate. It checks
both production dependency graphs, invokes all six scans, generates the
current report and CycloneDX SBOMs, verifies immutable workflow/action/image
and non-production-route policy, validates the 48-control assessor handoff,
and checks changed files for high-confidence secret signatures, diff errors,
and the 500 LOC limit.
pnpm exec tsx scripts/security-audit/generate-report.ts writes current
scanner results to the private audits/casa/evidence/ tree. It intentionally
does not infer historical fixes or test results.
pnpm casa:sbom refreshes the web and worker CycloneDX inventories under
audits/casa/evidence/phase-1/.
CASA readiness evidence is tracked in public/docs/security/CASA_TIER_2_READINESS.md; private engineering evidence remains in the repository and is not a public production route.
Active Security Test Safety
Playwright defaults to a self-started production build on loopback. The
credential-free anonymous security suite uses a narrow two-flag test runtime
(SKIP_ENV_VALIDATION plus E2E_ANONYMOUS_RUNTIME) that is set only by the
local Playwright server command and is not a supported deployment mode.
pnpm casa:e2e:preflight reports each capability without printing secret
values. Authenticated suites require an explicit storage-state file;
identity, billing, and provider mutations also require separate opt-ins and
isolated test credentials.
Remote origins require an explicit read-only opt-in. Anonymous active tests
and mutation suites fail closed for every remote application origin,
including production, so a test command cannot probe mutations, create,
delete, bill, or change identity state there.