Security model
Restura’s security posture is asymmetric between platforms by virtue of capability gaps. The UI surfaces “Desktop only” badges on fields whose underlying capability isn’t available in the browser. Here’s the model.
At rest
Section titled “At rest”Desktop
Section titled “Desktop”- Encryption keys are persisted via Electron’s
safeStorage, which wraps them with the OS keychain — macOS Keychain, Windows Credential Manager, Linux libsecret. - Stored data (collections, history, environments, secrets) is encrypted with AES-256-GCM using a key derived from the keychain-wrapped key.
- Secret-bearing auth fields use the
SecretRefhandle pattern (ADR 0007): the plaintext lives inelectron/main/security/secret-handle-store.tsand the renderer only ever sees a handle ({ kind: 'handle', id, label? }). Plaintext is resolved only at wire-signing time in the main process — never enters the Zustand store, persistence layer, or exported collections.
- Persisted state uses plaintext IndexedDB by default and is protected only by the browser’s same-origin policy.
- Future: an opt-in passphrase prompt can derive an encryption key via PBKDF2; that UI has not shipped and is not advertised as an available capability.
- mTLS, custom CA, SOCKS, and “Verify SSL = off” are not available — the browser sandbox doesn’t expose them.
In transit
Section titled “In transit”SSRF guards on both Worker and Electron paths
Section titled “SSRF guards on both Worker and Electron paths”Every outbound URL passes through shared/protocol/url-validation.ts, which blocks:
- RFC 1918 (private networks).
- RFC 6598 (CGNAT,
100.64.0.0/10). - Link-local
169.254/16— covers the cloud-metadata endpoint. - Loopback and IPv6 loopback.
- IPv6 unique-local (
fc00::/7). - IPv4-mapped IPv6.
The guard is the single source of truth — before the shared-protocol refactor it had drifted between backends.
DNS-rebind guard (desktop)
Section titled “DNS-rebind guard (desktop)”The Electron main process additionally enforces a pre-flight DNS guard: hostnames are resolved before connecting, and assertResolvedAddressAllowed is called against every record returned. Note: this is pre-flight only — it does not mitigate a true DNS-rebind that swaps records at TTL=0 between resolve and connect.
Wire-level auth signing
Section titled “Wire-level auth signing”AWS SigV4, OAuth 1.0a, and WSSE are all signed in the Worker / Electron main process, not the renderer. The signature matches the exact bytes the upstream receives — proxying through a layer that re-encodes the body would silently invalidate it.
Hop-by-hop header sanitisation
Section titled “Hop-by-hop header sanitisation”shared/protocol/header-policy.ts strips hop-by-hop headers (Connection, Keep-Alive, Proxy-Authenticate, Transfer-Encoding, TE, Trailers, Upgrade) from both directions and rejects header names with control characters.
Renderer lockdown (desktop)
Section titled “Renderer lockdown (desktop)”The Electron renderer is sandboxed (sandbox: true, contextIsolation: true, nodeIntegration: false) and pinned down by two policies in electron/main/main.ts:
- Default-deny web permissions —
setPermissionRequestHandler/setPermissionCheckHandleron the default session reject every web-platform permission exceptclipboard-sanitized-write(copy buttons). Everything privileged goes through the validated IPC surface instead. Growing the allowlist is an explicit, test-guarded change (ADR 0026). - CSP — production loads enforce
default-src 'self' file:withobject-src 'none',worker-src 'self' file:,wasm-unsafe-evalonly (QuickJS), and nounsafe-eval. The policy exists twice — a response header inelectron/main/main.tsand a<meta>fallback injected at build time invite.config.mts— and a structural test (electron/main/__tests__/security-hardening.test.ts) fails if the two ever diverge.
Scripts
Section titled “Scripts”Pre-request and test scripts run in a QuickJS WASM sandbox (shared/scripts/script-executor.ts):
- No DOM.
- No filesystem.
- No network escape —
fetchandXMLHttpRequestare absent. - Memory cap (10 MB by default).
- Execution time cap (5 seconds).
This matters because shared collections might carry scripts from anywhere; the sandbox guarantees they can’t exfiltrate or pivot.
AI assistant — prompt redaction
Section titled “AI assistant — prompt redaction”Before any prompt goes to a provider, shared/protocol/ai/redaction.ts scrubs sensitive material from the context snapshot:
- Sensitive header names (
Authorization,Cookie,X-Api-Key). - Absolute URLs (host replaced with
[REDACTED_HOST]; path retained). - Inline secret values where they’re identifiable.
Telemetry and privacy
Section titled “Telemetry and privacy”Error reporting is on by default and can be disabled in Settings → Privacy. Desktop errors and native crashes go to Sentry only while that preference is enabled; tracing is disabled, sendDefaultPii is false, and the scrubber removes request context, hostnames, users, breadcrumb data, stack-frame locals, secrets, and file paths. The same opt-out also controls Sentry Release Health’s anonymous desktop session counts (crash-free rate and version adoption); Restura does not create a per-user or device identifier.
For the hosted web app, /api/telemetry/error is a redacted, rate-limited, allowlist-only log sink and is never stored or forwarded to a third party. Self-hosted deployments collect no server-side usage telemetry. See ADR 0027 — Telemetry and privacy-preserving usage signal for the full decision.
MCP server mode
Section titled “MCP server mode”When Restura acts as an MCP server, every tool response is run through a redactor (electron/main/collection-export-redactor.ts) that strips secrets by name. SecretRef handles are never resolved for the MCP surface — agents see metadata, not plaintext.
Worker auth gate
Section titled “Worker auth gate”The deployed Cloudflare Worker requires one of:
WORKER_PROXY_TOKENset as a secret, sent by the client asX-Worker-Token.REQUIRE_CF_ACCESS=trueand a Cloudflare Access policy in front of the Worker.
Local dev bypasses auth only when Miniflare is detected or DEV_BYPASS_AUTH=true is set in .dev.vars. Never put DEV_BYPASS_AUTH in wrangler.jsonc (the deployed config).
Reporting
Section titled “Reporting”Report security issues privately via GitHub security advisories. See SECURITY.md.