Correct the security claims in docs and comments, and record the accepted risks (closes #171)
check / check (pull_request) Successful in 2m32s

Docs and comments only; no behaviour change. Corrects ten overclaims the
security review found: snapshot names are hashed but the hash uses no
secret, so a guessed hostname and name can be confirmed; a blob is named
by hex(SHA256(SHA256(uncompressed contents))), stated once in
docs/REPOSTRUCTURE.md and referenced elsewhere; double hashing does not
hide known content (blob packing does); age uses ChaCha20-Poly1305, not
XChaCha20; encryption is required, not optional; a snapshot is marked
complete before its metadata is uploaded; the export comment now matches
its only caller; deep verify detects corruption, not authorship; adding a
recipient does not reach existing data; restore examples target a
user-owned directory.

Adds an Accepted Risks subsection under Security Considerations with the
seven documented risks, cross-referenced from the README.

Model: opus-4-8
This commit is contained in:
2026-09-22 17:04:01 +00:00
parent c3bec7d3aa
commit ae06beb2c4
14 changed files with 108 additions and 67 deletions
+1 -1
View File
@@ -119,7 +119,7 @@ type ScannerConfig struct {
Storage storage.Storer
MaxBlobSize int64
CompressionLevel int
AgeRecipients []string // Optional, empty means no encryption
AgeRecipients []string // required; output is always encrypted
EnableProgress bool // Enable the live progress reporter (ETAs, throughput)
UI *ui.Writer // Where user-facing scanner messages go; nil = discard
Exclude []string // Glob patterns for files/directories to exclude
+15 -14
View File
@@ -24,7 +24,7 @@ package snapshot
// 7. Close the temporary database
// 8. VACUUM the database to remove deleted data and compact (security critical)
// 9. Compress the binary database with zstd
// 10. Encrypt the compressed database with age (if encryption is enabled)
// 10. Encrypt the compressed database with age (always; recipients are required)
// 11. Upload to S3 as: metadata/{snapshot-id}/db.zst.age
// 12. Reopen the main database
//
@@ -238,14 +238,12 @@ func (sm *SnapshotManager) CompleteSnapshot(
// 3. Cleans the copy to contain only current snapshot data
// 4. Dumps the cleaned database to SQL
// 5. Compresses the SQL dump with zstd
// 6. Encrypts the compressed data (if encryption is enabled)
// 6. Encrypts the compressed data with age (always; recipients are required)
// 7. Uploads to S3 at: snapshots/{snapshot-id}.sql.zst[.age]
//
// The caller is responsible for:
// - Ensuring the main database is closed before calling this method
// - Reopening the main database after this method returns
//
// This ensures database consistency during the copy operation.
// The only caller (finalizeSnapshotMetadata) does not close the main database
// before calling this method: the index is copied at dbPath while it is still
// open, and every step here operates on that copy, never on the live index.
func (sm *SnapshotManager) ExportSnapshotMetadata(
ctx context.Context, dbPath string, snapshotID string,
) error {
@@ -415,9 +413,11 @@ func (sm *SnapshotManager) prepareExportDB(
// uploadSnapshotArtifacts uploads the database backup and blob manifest
// to remote storage at metadata/<remote-key>/, where remote-key is the
// double-SHA256 derivation of the snapshot ID (see RemoteSnapshotKey).
// We never write the human-readable snapshot ID into any unencrypted
// part of remote storage so a listing of the destination bucket leaks
// no host, configuration, or scheduling information.
// The human-readable snapshot ID is never written into an unencrypted part
// of remote storage, so a plain listing shows only the hashed key, not the
// hostname or snapshot name. The hash uses no secret, so a guessed hostname
// and snapshot name can still be confirmed against a listing, and the backup
// time is public: the manifest carries a plaintext timestamp.
func (sm *SnapshotManager) uploadSnapshotArtifacts(
ctx context.Context, snapshotID string, dbData, manifestData []byte,
) error {
@@ -814,10 +814,11 @@ func (sm *SnapshotManager) generateBlobManifest(
}
// Create manifest. SnapshotID in the unencrypted manifest is the
// double-SHA256 remote key (see RemoteSnapshotKey), not the human ID,
// so neither this field nor the directory name reveals the hostname or
// snapshot name. Timestamp below is written in the clear, so the backup
// time is observable to anyone who can read the manifest.
// double-SHA256 remote key (see RemoteSnapshotKey), not the human ID, so
// neither this field nor the directory name spells out the hostname or
// snapshot name — but the key uses no secret, so a guessed hostname and
// snapshot name can be confirmed. Timestamp below is written in the clear,
// so the backup time is observable to anyone who can read the manifest.
manifest := &Manifest{
SnapshotID: RemoteSnapshotKey(snapshotID),
Timestamp: time.Now().UTC().Format(time.RFC3339),