The unencrypted manifest publishes backup timestamps and a per-blob size profile #81
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Split out of #67, where I initially got the privacy posture wrong. This is
the underlying design question and it needs an owner decision.
What is published in the clear
Every snapshot writes
metadata/<remote-key>/manifest.json.zst— zstdcompressed, not
.age, so unencrypted at rest. Anyone who can listor read the destination can read it.
internal/snapshot/manifest.go:15-28:snapshot_idhex(SHA256(SHA256("vaultik|" + id))))timestampblob_counttotal_compressed_sizeblobs[].hashblobs[].compressed_sizeHashing the snapshot ID successfully hides the hostname and
snapshot name. It does not hide the timestamp, because the timestamp
is written separately in the clear alongside it.
Why this is worth a decision
Taken together these give an observer, per snapshot: when it ran, how
many blobs it produced, the total size, and the individual size of every
blob. That is a timing-and-size profile. It supports inferences the
hashing was clearly meant to prevent — backup cadence, when a machine was
active or went quiet, roughly how much changed between runs, and
correlation of the same content across destinations via blob hashes.
There is also a false comment in the code,
snapshot.go:842-844:The next statement writes
Timestamp: time.Now().UTC().Format(time.RFC3339)into those exact public bytes.
remotekey.go's doc comment makes the sameoverreaching claim. Whatever is decided here, both comments must stop
asserting something untrue — that is what misled me, and it will mislead
the next reader.
Options
manifest.json.zst.age), leaving only thehashed directory name public. Strongest privacy. Cost:
prune,verify, and remote listing currently read the manifest without aprivate key — check every consumer before choosing this, since it may
force those operations to require the key, which cuts against the
product's core premise. This interacts directly with #64.
timestampand per-blob sizes, keep blob hashes ifpruneneedsthem. Partial improvement, no key required.
threat model exactly what an observer learns. Cheapest, and honest,
but it permanently concedes the timing channel.
My recommendation: option 3 for 1.0, then revisit. Option 1 is the
right long-term answer but it risks requiring the private key for
listing/prune/verify, which would undermine the tool's central claim, and
that trade deserves its own design pass rather than being rushed into a
release. Option 2 is a half-measure that still leaks cadence via object
mtimes on most backends. Documenting the real posture is achievable now
and unblocks #67.
Note the timing channel is not fully closable by encrypting the manifest
anyway — object creation times and per-object sizes remain visible at the
storage layer on S3 and
file://alike. Any claim we make should accountfor that.
Definition of done
snapshot.go:842-844and inremotekey.gocorrected regardless of which option is chosen.(
prune,verify, remote listing,snapshot rm) confirmed stillworking without a private key, or the loss of that property explicitly
accepted and documented.
docs/REPOSTRUCTURE.mdthreat model matches the outcome — this feeds#67, which is blocked on this decision.
script/cibuildexits 0.Assigning to
sneak: this is a threat-model trade-off, not animplementation detail.
3 is the obvious previously made decision. don’t second guess my design decisions already implemented.
Closing per
sneak's ruling: option 3. The unencrypted manifest'sbackup timestamps and per-blob size profile are an accepted, previously
made design decision — not a defect, and not open for revision here.
His words: "3 is the obvious previously made decision. don't second
guess my design decisions already implemented."
I was wrong to file this as an issue. Filing implies a defect and a
remediation obligation, and applying that framing to a deliberate,
already-implemented choice misrepresents it and spends the owner's
attention re-explaining a decision he had already made. The observation
was worth raising; the tracker was the wrong place for it. Chat would
have been correct.
Taking the standing rule from this: do not file the owner's
already-implemented design decisions as defects. Genuine defects still
get filed, and so do choices that are merely undocumented — the
distinction is whether the code reflects an intentional decision or an
oversight.
What carries forward, and where
One item from this issue is a genuine defect and survives its closure —
the code comment at
internal/snapshot/snapshot.go:842-844:The next statement writes
Timestamp: time.Now().UTC().Format(time.RFC3339)into exactly those public bytes. That comment is false regardless of which
option was chosen, and
remotekey.go's doc comment overreaches the sameway. A comment asserting the opposite of the adjacent code is an
oversight, not a design decision — it is what misled me into filing this
in the first place, and it will mislead the next reader.
Moved to #67, which is now unblocked and rescoped: the threat model
should describe the manifest's exposure as intended behaviour, stated
plainly so users can evaluate it, rather than as something to be fixed.
docs/REPOSTRUCTURE.md's existing claim that backup times are observablewas correct all along — it just attributed them to the snapshot ID rather
than to the manifest.