audit 3: backup CLI, envelope budget, directory sync, docs; findings of the reviews
Steps 3-6 of stage 3 and what the two reviews found in steps 1-6, in one commit: the review fixes sit in the same hunks. Step 3, CLI backup (M3, M5, L1, L2; L4 of the stage's list): - backup.snapshotDB refuses a database that is not there and opens the source mode=ro (measured: mode=ro creates no file). - selfpost-backup holds an encrypting password to validate.MinSecretFilePasswordLen; -o of both directions writes a temporary file beside the name and renames it whole; -decrypt peeks the first chunk (password and type authenticated) before the output exists. - what a backup leaves out (a link under data/, a link under certs/ to a directory, a socket...) is told to OnWarn; certs/ with no file is backup.ErrNoCerts, refused by Check and Create, named by the panel. - parseTrustedProxies: a bare address takes /128 when written with a colon (::ffff:10.0.0.1/32 was ::/32). Step 4, secretfile (M4, L3): scrypt's V at most 128 MiB, r <= 64, p <= 2; bytes after the last chunk are ErrCorrupt. Step 5 (L7): atomicfile.SyncDir after the rename, used by Write, replaceSASLDB and the CLI; atomicfile_test.go. Step 6: security.md and guide on backup and restore. Reviews (technical, security): - H1: a restore of a server with DMARC on was refused on every start - the archive records no owner, tar as root left the DMARC maps root's, and the entrypoint only chowned the maps it named. It now hands every file beside queue/ in /data/postfix to panel:selfpost 0640; e2e writes an extra map before the backup and finds it panel:selfpost 640 after a root unpack. - the comparison reads regular files only (Lstat, size, LimitReader); a record unpacked again wins over the earlier check's mark; ConsumeRestore removes both; the mark is synced; a journal file beside a freshly unpacked database is refused. - AttachRefusalLine refuses an empty line as the old statement did; skipped entries named in words; -decrypt of an empty payload is no failure; guide wording on the shell's '>' and a killed CLI; the in-place restore's 'mv .' (older than this stage) fixed. Tested on the test server: go vet + go test of 25 packages, make e2e (all pass, 643 s) on this tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+35
-8
@@ -724,7 +724,10 @@ into `/etc/letsencrypt`, the commands of "Reverse proxy" above give certbot's
|
||||
own files the group). A key readable by root alone is enough for Postfix, but
|
||||
then the panel refuses to make a backup and names the file: it does not hand
|
||||
over an archive without the key. Links under `certs/` are followed: the
|
||||
archive holds the files they lead to.
|
||||
archive holds the files they lead to. A `certs/` that is there with no file
|
||||
in it is refused for the same reason; what a backup leaves out - a link to
|
||||
a directory under `certs/`, a link under `data/` - it names in a warning (the
|
||||
CLI's standard error, the panel's log).
|
||||
|
||||
**2. `selfpost-backup` CLI — while the container is running (no downtime).**
|
||||
The panel button and this command call the same code path: a consistent SQLite
|
||||
@@ -737,9 +740,12 @@ docker exec <container> selfpost-backup > selfpost-backup.tar.gz
|
||||
```
|
||||
|
||||
Use `-o /path/inside/container` to write inside the container instead of
|
||||
stdout. Optional encryption: set `SELFPOST_BACKUP_PASSWORD` or pass
|
||||
`-password-file` — see [Encrypting a backup or
|
||||
export](#encrypting-a-backup-or-export).
|
||||
stdout; the file is written beside that name and takes it only when it is
|
||||
whole, so a backup that fails leaves no half-written file and whatever had
|
||||
the name before. A database that is not where `SELFPOST_DB_PATH` points is
|
||||
refused, rather than archived empty. Optional encryption: set
|
||||
`SELFPOST_BACKUP_PASSWORD` or pass `-password-file` — see [Encrypting a
|
||||
backup or export](#encrypting-a-backup-or-export).
|
||||
|
||||
**3. `tar` of the whole project directory — with the service stopped.** Stop
|
||||
the instance so nothing is writing to SQLite, then archive the operator
|
||||
@@ -832,7 +838,7 @@ back after a bad change):
|
||||
docker compose down
|
||||
|
||||
# 2. Move the current project aside rather than deleting it
|
||||
mv . ../selfpost.before-restore
|
||||
cd .. && mv selfpost selfpost.before-restore
|
||||
mkdir selfpost && cd selfpost
|
||||
|
||||
# 3. Unpack the backup into the fresh directory
|
||||
@@ -871,7 +877,15 @@ The last entry of every panel and CLI archive, `data/contents.json`, records
|
||||
each file under `data/` with its size and SHA-256, and the first start
|
||||
compares `/data` with it; a file that differs is named in the message.
|
||||
`docker-compose.yml`, `.env` and `certs/` are not compared — they are yours to
|
||||
edit before that start.
|
||||
edit before that start. A journal file beside the database
|
||||
(`selfpost.db-wal`, `-shm`, `-journal`) is refused too: no archive holds one,
|
||||
so it is left from what the directory held before, and SQLite would take it
|
||||
up. To unpack again, unpack into an **empty** directory.
|
||||
|
||||
The files are compared once: a first start that gets past the comparison
|
||||
and then fails (its Resync, say) leaves the manifest, and the next
|
||||
`docker compose up -d` runs the Resync without comparing again - that start
|
||||
had the database open, so its files are no longer the archive's.
|
||||
|
||||
**Moving to a different host** is the same flow: create an empty project
|
||||
directory, unpack the backup there, edit `.env` (and `docker-compose.yml` if
|
||||
@@ -957,9 +971,22 @@ unpacked into `/data`, which the CLI does with the same password:
|
||||
docker exec -i <container> selfpost-backup -decrypt < backup.spbk > backup.tar.gz
|
||||
```
|
||||
|
||||
The password is checked on the file's first block, before the command
|
||||
writes anything - but the shell's `>` has already emptied `backup.tar.gz`
|
||||
before the command starts, so a wrong password leaves it empty; `-o` keeps
|
||||
a file that was there. A file cut short fails at its end, with the command's
|
||||
exit status non-zero, after what came before has been written: an archive
|
||||
cut so is refused at the restore when it lost any file (see
|
||||
[Restore](#restore) - the record is the archive's last entry). With `-o` the
|
||||
output takes its name only when it is whole; a command killed outright
|
||||
leaves a `.NAME.tmp-*` file beside it, readable by its owner alone, to
|
||||
remove by hand.
|
||||
|
||||
The CLI also *writes* encrypted backups for scripted/cron use. The password
|
||||
comes from `SELFPOST_BACKUP_PASSWORD` or `-password-file <path>` (first line),
|
||||
never from a command-line argument, which would be visible in the process list.
|
||||
must be at least 12 characters, as on the panel's forms (decrypting takes
|
||||
any), and it comes from `SELFPOST_BACKUP_PASSWORD` or `-password-file <path>`
|
||||
(first line), never from a command-line argument, which would be visible in
|
||||
the process list.
|
||||
`-e` with the name only takes the value from your shell's environment, so it is
|
||||
not written on the command line:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user