Even if we wanted to,
we couldn't read your backups.
Our database leaked tomorrow? Every backup is still unreadable. We published our threat model — read it and poke holes. Zero-knowledge isn't a marketing label here; it's a design constraint that makes the convenient path impossible.
Encryption architecture
We use age (X25519 asymmetric encryption), not passphrase-symmetric encryption. The distinction matters: naive zero-knowledge designs encrypt each backup with a passphrase — but cron can't type a passphrase, so the passphrase ends up stored in plaintext on disk, which defeats the point.
- At setup, the skill generates an age keypair on the user's machine.
- The public key is stored in local config and optionally mirrored to our server for dashboard display. It can only encrypt.
- Every backup is encrypted to the public key. Cron needs no secrets to encrypt — a compromised or unattended machine can create backups but a stolen upload can never be decrypted without the private key.
- The private key (
AGE-SECRET-KEY-1...) is shown to the user exactly once, stored locally at~/.hermes/hermesbackup/age-identity.txt(chmod 600) for convenient same-machine restores. - Recovery kit: setup prints private key + account email + restore one-liner. The user (and the agent, loudly) tells the user to save it off-machine. Losing the machine and the key means the backups are permanently unreadable. That is the deal.
What the server ever sees
✓ What we store
Ciphertext blobs, sizes, SHA-256 of ciphertext, labels, timestamps, device names, public key (for display only).
✗ What we never see
Plaintext backups. Private keys. Passphrases. Decryption capabilities of any kind.
Transport & platform security
- All traffic TLS 1.3 via Cloudflare.
- Device tokens: 256-bit random, stored hashed (SHA-256) in D1, scoped per device, revocable from the dashboard.
- Presigned URLs expire in 15 minutes (PUT) / 5 minutes (GET). Backup bytes never stream through the Worker.
- R2 bucket is private — no public access.
- Dashboard sessions via Clerk with MFA available.
- Webhook endpoints (Stripe, Emailit) verify signatures.
- Rate limiting per device token via Workers KV.
initrequests rate-limited to 30/hour/device. - Object keys are
u/{user_id}/{backup_id}.age— no user-supplied path components, no traversal surface. - Cloudflare infrastructure logs: the Worker runs on Cloudflare's edge network. Cloudflare may retain console output (error messages, operational counters) and request metadata (URL, method, status, IP) in their logs per their standard platform logging. Worker code is written to log only aggregate counts and error types — no request-context metadata (device names, backup labels, user IDs) is emitted to console. R2 access logs include object keys derived from server-generated IDs; no user-supplied data is in key paths.
Abuse controls (the free tier must not become free Dropbox)
Blobs are opaque — we can't inspect content. Caps on count, size, and cadence are the enforcement surface. They're generous for real backup use while making generic file-hosting impractical:
| Limit | Free | Pro |
|---|---|---|
| Max object size | 2 GB | 2 GB |
| Uploads per day | 4 | 24 |
| Stored objects | 60 | 500 |
| Init rate limit | 30/hour/device | 30/hour/device |
Threat model & mitigations
Transparency is the brand. Below is the full set of threats we model and how each is mitigated. The audit snapshot is current as of July 2026 — a full third-party security audit is planned post-launch.
Server database leak Mitigated
D1 contains user metadata, device token hashes, backup metadata, and ciphertext SHA-256s — but no plaintext, no keys.
R2 bucket compromise Mitigated
R2 stores ciphertext blobs. An attacker with direct R2 access gets opaque .age files they cannot decrypt.
Worker compromise Mitigated
The Worker never sees backup bytes (presigned URLs only) and has no decryption capability.
Device token theft Medium
A stolen device_token allows uploads/downloads/deletes on that device's behalf.
Private key loss By design
If the user loses both their machine and their recovery kit, backups become permanently unreadable.
MITM on presigned URL Low
Attacker intercepts a presigned PUT/GET URL within its expiry window.
Tampered ciphertext upload Mitigated
Attacker uploads different ciphertext to a presigned PUT slot.
POST /backups/{id}/complete makes the Worker HEAD the R2 object and verify size + SHA-256 against the value declared at init. Mismatch marks the backup as failed — it never reaches "stored" status.Path traversal via object key Mitigated
Attacker attempts to write to arbitrary R2 paths.
u/{user_id}/{backup_id}.age. No user-supplied path components are accepted.Quota bypass Mitigated
Attacker attempts to exceed storage limits.
init checks quota before issuing presigned URL. 507 returned if exceeded. Server never silently deletes — the client handles rotation per its configured policy.Abuse of free tier for file hosting Mitigated
Attacker uses free tier as arbitrary encrypted storage.
Webhook forgery (Stripe / Emailit) Mitigated
Attacker sends fake webhook to flip plan or suppress bounce.
/webhooks/stripe and /webhooks/emailit verify signatures before acting. Invalid signatures are rejected.Recovery-key email interception Low
Attacker reads the user's email to find the private key.
Audit status
A full internal audit (July 2026) verified the core zero-knowledge invariants against the Worker and client implementations. 18 issues found (2 HIGH, 6 MEDIUM, 10 LOW), 5 remediation cards created for the engineering team. Core invariants hold: no plaintext/keys/passphrases reach the server.
A third-party security audit is planned post-launch, before broader public charging. The audit report will be published here when complete.
Warrant canary
As of July 26, 2026, HermesBackup has received zero government requests for user data, backdoors, or key escrow. This statement will be updated weekly. Its absence is evidence.
Found a vulnerability?
Email [email protected] with details and reproduction steps. We respond within 72 hours and credit responsible disclosure. The client script is open-source and shipped in the skill — start there.