1# nzip security23nzip is a personal, single-owner service. Its short public URLs are convenient4addresses, not secrets. Security comes from explicit authentication, password5protection where needed, bounded public surfaces, owner-approved device6enrollment, and observable abuse controls.78Sites execute at `https://<address>.<site-domain>/`, giving every address a9distinct browser origin. Origin-scoped localStorage, IndexedDB, OPFS,10CacheStorage, service workers, and passkey defaults are isolated between sites.11nzip's unlock cookies are host-only. The control-plane shorthand12`https://<control-origin>/<address>` is redirect-only and never serves artifact13bytes.1415App origins are treated as permanent identity boundaries. `app init` records an16address tombstone before deployment, and neither site deletion nor expiry makes17that address allocatable again. This avoids handing a previous application's18service-worker scope, browser storage namespace, or passkey origin to unrelated19future content. A vault named `public` describes durable retention; it does not20disable password protection or make an address confidential.2122Subdomain isolation is a browser security boundary, not a content-safety review.23A hosted application still has its own origin's ambient authority, can make24cross-origin network requests where a target permits them, and can deceive its25visitors. Artifact responses disable `document.domain` relaxation so sibling26site hostnames cannot opt back into a shared origin.2728Cookies and WebAuthn have additional domain rules. Until `n.zip` is accepted29into the [Public Suffix List](https://publicsuffix.org/), an arbitrary site can30set a `Domain=n.zip` cookie, and it can request the parent RP ID `n.zip`. It31cannot request a sibling RP ID such as `ffff.n.zip`. Applications should use32`__Host-` or otherwise host-only cookies and explicitly set33`rp.id = location.hostname`; on `2a3f.n.zip`, the RP ID is `2a3f.n.zip`. Do not34use passkeys or application-defined cookies on the control origin, or claim35hostile-tenant credential isolation, until that Public Suffix List boundary36ships in target browsers.3738## Reporting a vulnerability3940Do not disclose a vulnerability in a public issue. Use the repository's private41security advisory channel when available, or contact the repository owner42privately. Include affected versions, reproduction steps, impact, and any43suggested mitigation.4445## Trust boundaries4647- Every `/api/*` management route requires the deployment-wide bearer token.48- The bearer token stays in Worker secrets and the CLI's mode-0600 configuration49 file; browsers and installed notification apps never receive it.50- `nzip.app.json` contains a public target and origin but never the bearer token.51- Public site addresses occupy only 16 bits and should be treated as enumerable.52- The wildcard route accepts only exact four-character lowercase hexadecimal53 site hostnames. Unknown wildcard hosts cannot dispatch management or54 notification APIs.55- Artifact hostnames are mutually isolated browser origins. The exact control56 origin alone owns `/api/*`, notification enrollment, and the root notification57 PWA.58- Password-protected sites use PBKDF2 hashes and signed, versioned cookies.59 Changing password policy invalidates existing cookies immediately. Unlock60 cookies use the `__Host-` prefix and are host-only, so one site cannot plant61 or receive another site's unlock state.62- Notification click targets are same-origin paths pinned to an existing site63 manifest. Arbitrary external URLs are rejected.64- Web Push subscription endpoints must match configured exact HTTPS provider65 origins. Wildcards, credentials, IP literals, alternate ports, and the66 deployment's own origin are rejected.6768The deployment runbook includes an optional Free-plan WAF custom rule that69mirrors the hostname shape at the edge. Worker validation remains the source of70truth, so correctness does not depend on the WAF rule or paid regular-expression71support.7273The Worker disables Cloudflare shared-edge caching on every response with74`Cloudflare-CDN-Cache-Control: no-store`. This is a required origin-isolation75control: the exact custom domain and wildcard Worker route can otherwise reuse76one cached public response for the same path on another hostname. Public sites77still advertise short browser caching and support `ETag` revalidation.7879## Notification pairing8081Pairing is closed by default. An authenticated owner opens a fixed 10-minute82window with `nzip notify pair`. The public root reveals the pairing action only83during that window, and the Worker enforces the same deadline when creating an84enrollment.8586Pairing codes are short-lived, normalized, and stored only as keyed hashes. A87code identifies a pending device but cannot approve itself; approval still88requires the owner bearer token. Device claims use `HttpOnly`, `Secure`,89`SameSite=Lax`, host-only cookies and transition through pending, approved, and90active states before notifications can be delivered.9192The root pairing surface cannot be framed. Pairing-state reads are same-origin93and rate-limited, and pending pages do not expose a second enrollment action.9495## Enumeration controls9697The address space is intentionally small, so nzip observes and limits98enumeration rather than claiming addresses are unguessable.99100```mermaid101flowchart TB102 REQ(["request"]) --> SURFACE{"request surface"}103 SURFACE -- "/api/*" --> TOKEN["bearer check"]104 SURFACE -- "GET /xxxx" --> ENUM["enum limiter"]105 SURFACE -- "POST unlock" --> UNLOCK["password limiter"]106 TOKEN --> API["owner API"]107 ENUM --> SERVE["site lookup"]108 UNLOCK --> SERVE109 SERVE --> GATE{"password protected?"}110 GATE -- "yes" --> COOKIE["PBKDF2 + cookie"]111 GATE -- "no" --> PUBLIC["public response"]112 ENUM -. "HMAC identity" .-> PROBES[("D1 probe windows")]113 PROBES --> EVAL["incident evaluator"]114 EVAL --> OUTBOX[("email outbox")]115 OUTBOX --> EMAIL["verified email"]116 EMAIL --> INBOX(["operator inbox"])117```118119Raw client IPs are neither logged nor stored. Scanner identities are120HMAC-derived. Probe rows are deduplicated, capped per scanner and Cloudflare121location, and pruned after seven days.122123The evaluator opens or escalates incidents on these signals:124125| severity | signal |126| --------- | ---------------------------------------------------------------------- |127| warning | one scanner tries 20 distinct addresses in five minutes |128| warning | 128 addresses from 10 scanners with at least 90% misses |129| confirmed | an enumeration request reaches the rate limiter |130| confirmed | a live hit follows sequence evidence or occurs during an open incident |131132Duplicate alert email is suppressed unless severity increases, volume doubles, a133live site is hit for the first time, or a new vault is targeted after 30134minutes. Active incidents summarize at most hourly and resolve after three quiet135windows. Alert payloads are committed before delivery and retried with stable136notification identifiers.137138## Operational monitoring139140- Filter Workers Logs on `event = "security.request"` for sampled request141 context.142- Watch D1 row writes for the enumeration-telemetry budget.143- Keep the Email Routing destination verified and test it through the144 authenticated `/api/security/test-alert` endpoint.145- Treat notification titles and bodies as lock-screen-visible. Never include146 credentials, private URLs, or sensitive personal information.147148Deployment configuration and security-alert setup are documented in149[`worker/setup.md`](worker/setup.md).