Files
versioning/research_notes/Backup encryption security choices/untrusted-remote.md
T

24 KiB
Raw Blame History

Untrusted-Server Threat Model: restic, BorgBackup, Kopia, rclone crypt on dumb backends

What plaintext metadata does each system leave on the backend (repo layout, config files, sizes, timestamps)?

Takeaway

All four encrypt file content client-side but leak different amounts of structural metadata: restic leaks backend object sizes/counts/timestamps and config filenames; Borg leaks chunk sizes/proximity unless obfuscation is enabled; Kopia leaks only prefixed blob names (p/q/x) and approximate pack sizes; rclone crypt leaks file sizes (±16B) and mtimes and optionally directory structure.

Cited Findings

  • restic design: “Apart from the files stored within the keys directory, all files are encrypted with AES-256 in counter mode (CTR)” with Poly1305-AES MAC, IV in first 16 bytes - keys directory itself is the exception and contains KDF parameters in plaintext JSON (hostname, username, kdf=scrypt, N=65536,r=8,p=1, salt, created) - Source; same guarantee restated as “Everything except the metadata included for informational purposes in the key files is encrypted and authenticated” - Source
  • restic repo layout on dumb backend is content-defined: config, keys/, locks/, snapshots/, index/, data/ pack files named by hex SHA-256 of plaintext content; filenames of packs/snapshots/indexes are plaintext hashes, sizes and mtimes visible to server - Source
  • restic explicitly warns attacker with write access “can determine which files belong to what snapshot (e.g. based on the timestamps of the stored files)” - Source
  • restic local cache “is also encrypted to prevent metadata leaks” - Source
  • Borg attack model: “environment of the client process (e.g. borg create) is trusted and the repository (server) is not. The attacker has any and all access to the repository, including interactive manipulation (man-in-the-middle)” - Source
  • Borg guarantees vs untrusted server: attacker cannot (1) modify archive data undetected, (2) rename/remove/add archive undetected, (3) recover plaintext, (4) recover definite structural information such as object graph - but “heuristics based on access patterns are possible” - Source
  • Borg multi-client caveat: “When the above attack model is extended to include multiple clients independently updating the same repository, then Borg fails to provide confidentiality (i.e. guarantees 3) and 4) do not apply any more)” - Source
  • Borg chunk-ID is MAC of plaintext (id = AUTHENTICATOR(id_key, data)), encryption is Encrypt-then-MAC AES-256-CTR + HMAC-SHA256 or BLAKE2b-256; IV counter “added in plaintext” and tracked via client security DB + repo reservation - Source
  • Borg 2.0 changes to AEAD (AES-256-OCB or chacha20-poly1305) with session keys, chunk ID via MAC over plaintext, object ID bound as AAD so attacker cannot “change the type of the object or move content to a different object ID” - Source
  • Borg fingerprinting: “A borg repository does not hide the size of the chunks it stores”; small files <512KiB yield single chunk; buzhash chunker uses secret per-repo chunker seed; optional obfuscate pseudo-compressor pads with 0x00 bytes (only adds size) to hinder size fingerprinting - Source
  • Borg proximity leak: “Borg does not try to obfuscate order / proximity of files”; sorts by inode order not name, but “when new files are close to each other [in recursion order], the resulting chunks will be also stored close to each other in segment file(s)” - Source
  • Kopia envelope encryption: repo has plaintext JSON formatBlob with tool, buildVersion, uniqueID, keyAlgo=scrypt-65536-8-1, version, encryption (e.g. AES256_GCM), plus encryptedBlockFormat ciphertext; UniqueID doubles as PBKDF salt and HKDF info - Source
  • Kopia passphrase → 32B master key Km = PBKDF(passphrase, UniqueID) (scrypt N=65536,r=8,p=1), then Ke = HKDF(SHA256,Km,UniqueID,"AES",32) and AD = HKDF(SHA256,Km,UniqueID,"CHECKSUM",32) for format-blob AES-GCM - Source
  • Kopia content encryption options are AES256-GCM-HMAC-SHA256 (default) or CHACHA20-POLY1305-HMAC-SHA256, chosen at repo creation, cannot be changed after - Source
  • Kopia blob layer: pack blobs have random names, “don’t reveal anything about their contents or structure”; prefixes p (data packs), q (metadata packs), x (indices); object-ID single-letter prefixes k (directory), m (manifest block), x (indirect JSON) route to q vs p packs - Source
  • Kopia packs are 20–40MB merged blobs; “sizes are also generally unrelated to their content due to splitting and merging”; per-pack trailing local index enables recovery if global index lost - Source
  • Kopia manifests (snapshots/policies) are small JSON stored as encrypted CABS blocks, addressed by key=value labels (e.g. type:policy, hostname:… path:… username:… visible via kopia manifest list client-side, not plaintext on backend) - Source
  • Kopia: “encrypts these snapshots before they leave your computer”, “password never leaves your machine”, single password per repo, “currently no access control mechanism when sharing a repository” - Source
  • rclone crypt: wraps any backend; “automatically encrypt (before uploading) and decrypt (after downloading) on your local system … leaving the data encrypted at rest”; direct access to wrapped remote bypasses crypto - Source
  • rclone crypt file format: 8B magic RCLONE\x00\x00 + 24B nonce header, then 64KiB chunks each with 16B Poly1305 tag (XSalsa20+Poly1305 SecretBox); 1B file → 49B total, 1MiB → 1048864B (+0.03%) - Source
  • rclone crypt name encryption: path split on /, PKCS#7 padded to 16B, EME-AES-256 deterministic, base32-lowercase-no-pad; identical names → identical ciphertexts; common prefixes hidden - Source
  • rclone crypt explicitly “does not encrypt file length - this can be calculated within 16 bytes” nor “modification time - used for syncing”; versions suffix -vYYYY-MM-DD… left plaintext; directory names optionally unencrypted - Source
  • rclone crypt filename modes: standard (encrypted, ~143 char limit, dir structure visible), obfuscate (“Very simple filename obfuscation … cannot be relied upon for strong protection”), off (adds .bin only) - Source
  • rclone crypt: “Any metadata supported by the underlying remote is read and written” - i.e. WebDAV/SFTP xattrs/mtimes pass through unencrypted - Source
  • rclone WebDAV backend: “Plain WebDAV does not support modified times” nor hashes except via Fastmail/ownCloud/Nextcloud vendor extensions - Source

Inferences

  • For size-hiding, Kopia pack merging (>20MB) is the strongest default; restic pack (~4-8MB default, content-defined) is intermediate; Borg chunk-level sizes leak most without obfuscate compressor.
  • Deterministic name encryption (rclone crypt standard, restic hash-named packs are content-derived not name-derived) still leaks equality + directory shape; only Kopia random pack names hide shape by default.
  • Plaintext keys/ (restic) and formatBlob (Kopia) both expose KDF parameters and repo unique IDs - useful for offline dictionary attack cost estimation but not plaintext.

Gaps

  • No reliable primary-doc figure found for exact restic snapshot/index plaintext filename entropy or padding policy; restic docs do not claim filename obfuscation or padding.
  • Borg obfuscate compressor size-overhead curve (“few percent [cheap] to ridiculously larger [expensive]”) has no numeric table in fetched docs.

How do they handle an actively malicious or compromised server (authentication of manifests, rollback protection, replay of old snapshots)?

Takeaway

Borg has the strongest formal malicious-server story (Horton-anchored DAG + TAM-signed manifest + nonce tracking); restic authenticates all objects but explicitly does not detect deletion/rollback or timestamp-grouping attacks; Kopia relies on content-addressed HMAC + maintenance/verify with server-side immutability as add-on; rclone crypt has per-chunk authentication but no manifest/rollback concept.

Cited Findings

  • restic guarantees: “Modifications to data … can be detected” and “Data that has been tampered will not be decrypted” (MAC checked before decrypt) - Source
  • restic non-goals: “not designed to protect against attackers deleting files”; attacker deleting timestamp-correlated packs makes “particular snapshot vanish … Restic is not designed to detect this attack” - Source
  • restic compromised-host cases: attacker with repo write can “Create snapshots (containing garbage data) which cover all modified files and wait until a trusted host has used forget often enough to remove all correct snapshots” and “Create a garbage snapshot for every existing snapshot with slightly different timestamp” to trick rotation - Source
  • restic append-only caveat: attacker with append-only access can still “Capture the password and decrypt past and future backups” (no forward secrecy); safe forget requires separate doc procedure - Source
  • restic key-leak: “impossible to securely revoke a leaked key without re-encrypting the whole repository” - Source
  • restic open issue #5041 proposes extending model with append-only enforcement + management/data key separation so “adversary who compromises the host cannot retroactively publish a snapshot which will trick forget into deletion” - still open/discussion as of 2026 - Source
  • Borg structural auth follows “Horton principle”: every object referenced by parent via plaintext-MAC object ID up to manifest, forming authenticated DAG - Source
  • Borg manifest has fixed ID 000…000 so cannot be DAG-authenticated; protected since 1.0.9 by TAM: tam_key = HKDF-SHA-512(id_key||enc_key||enc_hmac_key, RANDOM(64), "borg-metadata-authentication-manifest"), HMAC(tam_key, packed_manifest) stored in manifest - Source
  • Borg nonce/counter anti-reuse: client commits +4GiB reservation to security DB + repo before encrypting; crash-safe via SaveFile; but “in a multiple-client scenario a repository can trick a client into reusing counter values by ignoring counter reservations and replaying the manifest (which will fail if the client has seen a more recent manifest or has a more recent nonce reservation)” - Source
  • Borg RPC: over system SSH (no own network code), server can only send responses not requests; msgpack limited Unpacker; worst-case server can impose repo DoS; log-channel confusion limited to in-progress requests - Source
  • Borg CVE-2023-36811: “flaw in the cryptographic authentication scheme … allowed an attacker to fake archives and potentially indirectly cause backup data loss”; requires inserting files without headers + repo write; “does not disclose plaintext … nor affect authenticity of existing archives”; fixed in 1.2.5 + upgrade procedure; mitigate by reviewing archives after check --repair before prune - Source
  • Borg append-only: borg config repo append_only 1 or borg serve --append-only; “never overwrite or delete committed data” at segment level, but delete/prune still allowed (appear to succeed, only tag as deleted in new transaction); compact becomes no-op with no warning - Source
  • Borg append-only rollback: transaction log (transactions file with UTC timestamps) allows manual rollback by deleting segment files from attack point onward (e.g. rm data/**/{6..13}), provided compact has not run; must clear client cache after (borg delete --cache-only) - Source
  • Borg append-only limits: “Append-only mode is not respected by tools other than Borg. rm still works”; clients must only access via borg serve; any non-append-only write (admin prune/create) permanently compacts away “deleted” data; SSH authorized_keys split (--append-only key for untrusted clients, full key for admin) is recommended pattern - Source
  • Kopia consistency: kopia snapshot verify walks snapshot roots, checks index structures + blob existence; runs automatically during daily full maintenance; --verify-files-percent=N samples downloads/decrypts (100% ≈ test restore, discarded after check); e.g. 1% daily → ~98% coverage over a year - Source
  • Kopia corruption causes listed as non-POSIX/networked filesystems, silent bit-rot, large clock skew (few minutes tolerated, larger can cause self-deletion); recommends mature POSIX FS or cloud storage + NTP - Source
  • Kopia ransomware model: malware may exfiltrate cloud keys and delete snapshots; mitigation is provider-side restricted keys (no delete) + object-lock/retention (COMPLIANCE), not client crypto alone - Source
  • Kopia object-lock: --retention-mode COMPLIANCE --retention-period <e.g.30d> at repo create s3, plus maintenance set --extend-object-locks true with full-interval ≥1 day shorter than retention; supports S3 (+B2-via-S3), Azure version-level immutability, GCS versioning+retention; --point-in-time reconnect for S3 restores - Source
  • rclone crypt integrity: per-chunk Poly1305 via SecretBox; “data integrity is protected by an extremely strong crypto authenticator”; cryptcheck (not check) required since hashes not stored - Source
  • rclone crypt has no manifest/snapshot ledger: replay of old encrypted files, rollback, or server-side deletion is undetectable by crypt layer; --pass-bad-blocks (zero-fill corrupt chunks) is opt-in recovery only - Source

Inferences

  • Only Borg attempts to bind manifest meaning (TAM) against a fully MITM server with persistent client state; restic/Kopia assume server may be read/write-malicious for confidentiality/integrity of objects but push rollback/availability to out-of-band controls (append-only buckets, object-lock, separate admin host).
  • For WebDAV-class backends with no compute, Borg-style serve --append-only enforcement is unavailable; closest analogues are restic append-only-capable REST backends (not WebDAV) or Kopia S3 object-lock - WebDAV must rely on server ACLs/versioning.

Gaps

  • No primary-source evidence found that Kopia authenticates manifest labels/meaning against malicious-server manifest substitution beyond content HMAC; treat as gap.
  • No restic primary doc found describing rollback detection (e.g. monotonic snapshot counter); restic docs explicitly disclaim it.

How do they rate-limit, retry, and verify uploads (idempotent PUTs, integrity re-checks)?

Takeaway

All target dumb blob stores with idempotent content-addressed PUTs and client-side re-verification (check/verify/cryptcheck); retry/backoff is client-configured and backend-specific; none documents server-side rate-limiting - throttling is via client concurrency limits and provider quotas.

Cited Findings

  • restic repo design “allows parallel access of multiple instances … even parallel writes”; locks with --retry-lock retry until timeout; restic check verifies pack hash vs pack ID, cat pack <id> warns on mismatch - Source
  • Kopia architecture: content-addressed blocks (Block ID = hash(data)); identical blocks dedup naturally; uploads merged into packs; index maps block→(blob,offset,len) - enabling idempotent re-PUT (same ID = same bytes) - Source
  • Kopia verification tiers: metadata-only snapshot verify every full maintenance + opt-in content download sample --verify-files-percent + --file-parallelism for throughput control - Source
  • Kopia storage requirements imply retry need: “strong read-after-write consistency and eventual list-after-write consistency”; “can compensate for such inconsistent behaviors for up to several minutes, but larger inconsistencies can lead to data loss” - Source
  • Borg RPC worst-case throttling noted only as server-imposed “denial of repository service”; client uses limited msgpack Unpacker to avoid memory-DoS from large messages - Source
  • rclone crypt backup guidance: use sync on encrypted paths with same passwords so “will check the checksums while copying”; check between two encrypted remotes works, cryptcheck needed vs plaintext - Source
  • rclone crypt chunking (64KiB + 16B tag) bounds retry unit; header nonce incremented per chunk, reuse probability ~2e-32 per exabyte - Source

Inferences

  • Content-addressing (restic pack ID, Borg chunk ID, Kopia block ID) makes PUTs naturally idempotent and safe to retry; verification is always client-driven (check/verify/cryptcheck) since dumb backends cannot attest.
  • Parallelism knobs (--file-parallelism, rclone --transfers/--checkers, Borg client concurrency) are the de facto rate-limiters; no evidence of server-push backpressure handling in fetched docs.

Gaps

  • No primary-source values found in fetched docs for restic --retry-lock defaults, backend backoff schedule, or WebDAV-specific retry/idempotency; mark as gap - need restic backend docs + rclone WebDAV backend options.
  • No Kopia primary-doc retry/backoff parameters or blob Put atomicity guarantees surfaced in fetched pages; need repo/blob Go API docs.
  • No Borg primary-doc upload retry/rate-limit parameters surfaced; Borg docs focus on correctness not throttling.

Any guidance specific to WebDAV-class dumb backends (no server compute) that applies to a local daemon syncing to WebDAV?

Takeaway

Treat WebDAV as untrusted byte store: do all crypto/index/manifest work client-side, never depend on server mtime/hash, use atomic PUT + verify-after-write, and add out-of-band append-only/versioning since WebDAV has no compute to enforce it.

Cited Findings

  • Kopia repository model: “All Repository features are implemented client-side, without any need for a custom server, thus encryption keys never leave the client”; layers are Object/Manifest/Block/Raw-BLOB over simple blob API - directly applicable to WebDAV - Source
  • Kopia supports “Any remote server or cloud storage that supports WebDAV” and SFTP as first-class repo storage, plus Rclone-wrapped Dropbox/OneDrive/Google-Drive (experimental) - Source
  • Kopia caveat for dumb/networked FS: requires “POSIX semantics, atomic writes (either native or emulated), strong read-after-write … eventual list-after-write”; “avoid emulated, layered, or networked filesystems which may not be fully compliant. Alternatively, use cloud storage”; WebDAV falls in risky class - Source
  • Kopia clock rule: “clock skews of few minutes are tolerated, but bigger clock skews can lead to major inconsistencies, including Kopia deleting its own data”; run NTP on clients and servers - Source
  • rclone WebDAV limits: “Plain WebDAV does not support modified times … does not support hashes” (except Fastmail/ownCloud/Nextcloud extensions) - so sync daemons must not trust mtime/hash for change detection - Source
  • rclone crypt on WebDAV pattern: point crypt at remote:path subdirectory, access exclusively via crypt remote; .bin suffix added when names unencrypted to prevent provider interpreting content; strict_names errors on mixed encrypted/unencrypted dirs - Source
  • rclone crypt password rotation requires full re-upload (decrypt-with-old → encrypt-with-new), either from local source or crypt-to-crypt move; “half the bandwidth and charged twice” on metered backends - Source
  • restic threat-model implication for WebDAV: server sees object sizes/counts/timestamps; timestamp-grouping enables targeted snapshot deletion - mitigation is to avoid leaking timing (jitter uploads, shared pack sizes) though not prescribed in docs - Source
  • Borg inode-order (not alpha) directory traversal + secret chunker seed + optional size obfuscation are concrete dumb-backend-hardening techniques portable to any daemon: randomize scan order, per-repo secret for chunking, pad to size classes - Source
  • Borg SSH authorized_keys split (borg serve --append-only for untrusted clients vs full for admin) has no WebDAV equivalent; WebDAV equivalent is provider ACL/versioning or Kopia-style object-lock where available (S3/Azure/GCS only - not WebDAV) - Source; Kopia object-lock “currently only supports object locks when using an S3 repo” (native B2 excluded, use S3 mode) - Source
  • Filippo Valsorda review quoted in restic docs: “The design might not be perfect, but it’s good. Encryption is a first-class feature, the implementation looks sane and … deduplication trade-off is worth it” - informal audit signal, not formal audit - Source

Inferences

  • For a local daemon syncing to WebDAV: (1) encrypt+MAC everything client-side with random pack names (Kopia-style) or hash-named packs (restic-style); (2) keep manifest/index signed with local key (Borg TAM-style) and verify on every sync; (3) never use server mtime/ETag as source of truth; (4) verify-after-write via GET+MAC before deleting local staging; (5) mitigate rollback by keeping local monotonic snapshot counter + out-of-band copy; (6) pad/obfuscate sizes and jitter upload times to reduce grouping leaks.
  • Since WebDAV cannot enforce append-only, ransomware safety must come from server-side versioning/quotas + separate prune identity, mirroring Borg append-only and Kopia restricted-keys guidance.

Gaps

  • No primary WebDAV-server hardening guide (append-only ACLs, versioning) surfaced for restic/Borg/Kopia on plain WebDAV; restic WebDAV backend doc and rclone WebDAV option reference not fetched (tool-call budget) - need follow-up.
  • No formal third-party audit reports (e.g. Cure53/Quarkslab) confirmed for restic/Borg/Kopia in fetched sources; only Valsorda informal review and CVE record found - need dedicated audit search.