Backups: refuse an unsigned archive and an unknown download rule #995

Merged
tiagoagueda merged 1 commit from sec2-d1-backups into main 2026-10-18 09:57:58 +00:00
Owner

What and why

#974 (Medium). A restore treated an archive with no authentication object as one "written before archives were signed" and restored its database and media with a warning. Nothing tells that apart from a signed archive whose signature was removed, so altering a copy needed no forged MAC. Now restore_backup refuses any archive that is not authentic unless --unverified (unsigned as well as unverified). The Restore page computes authenticity(), shows the verdict (authentic / unsigned / unverified) and prints --force --unverified when the archive is not authentic. The threat-model sentence is amended.

#978. POSTULO_BACKUP_DOWNLOAD is now in checks.CHOICES (postulo.E001 at start-up) and may_download fails closed: only administrators and operator grant anything, any other word grants nobody.

Deviations from the issue text

  • The old "unsigned archive is restored without plugins/" branch is removed, as it can no longer be reached (an unsigned archive is refused outright); RestoreReport.plugins_withheld and its CLI line went with it. With --unverified plugins are restored, as for an unverified archive.
  • The optional BACKUP_FORMAT 3 bump was not done (the issue marks it as defence in depth).

Tests

Changed deliberately: test_backup.py::test_an_archive_from_before_the_plugins_went_in_still_restores and test_hardening_772.py (pre-signing archive) pinned the permissive path; they now assert the refusal and then restore with unverified=True. New: stripped-signature test (nothing changed on refusal, CLI refuses, --unverified restores with authenticity == unsigned), Restore page test for both verdicts, test_hardening_810.py tests for E001 and a 404 under operators. The new tests fail without the fix (checked by stashing src). Full unit suite: 18869 passed, 122 skipped. No browser tests run locally; the restore template changed but no e2e test covers it as far as I know.

Closes #974, Closes #978

🤖 Generated with Claude Code

## What and why **#974 (Medium).** A restore treated an archive with no `authentication` object as one "written before archives were signed" and restored its database and media with a warning. Nothing tells that apart from a signed archive whose signature was removed, so altering a copy needed no forged MAC. Now `restore_backup` refuses any archive that is not authentic unless `--unverified` (unsigned as well as unverified). The Restore page computes `authenticity()`, shows the verdict (authentic / unsigned / unverified) and prints `--force --unverified` when the archive is not authentic. The threat-model sentence is amended. **#978.** `POSTULO_BACKUP_DOWNLOAD` is now in `checks.CHOICES` (`postulo.E001` at start-up) and `may_download` fails closed: only `administrators` and `operator` grant anything, any other word grants nobody. ## Deviations from the issue text - The old "unsigned archive is restored without `plugins/`" branch is removed, as it can no longer be reached (an unsigned archive is refused outright); `RestoreReport.plugins_withheld` and its CLI line went with it. With `--unverified` plugins are restored, as for an unverified archive. - The optional BACKUP_FORMAT 3 bump was not done (the issue marks it as defence in depth). ## Tests Changed deliberately: `test_backup.py::test_an_archive_from_before_the_plugins_went_in_still_restores` and `test_hardening_772.py` (pre-signing archive) pinned the permissive path; they now assert the refusal and then restore with `unverified=True`. New: stripped-signature test (nothing changed on refusal, CLI refuses, `--unverified` restores with `authenticity == unsigned`), Restore page test for both verdicts, `test_hardening_810.py` tests for E001 and a 404 under `operators`. The new tests fail without the fix (checked by stashing src). Full unit suite: 18869 passed, 122 skipped. No browser tests run locally; the restore template changed but no e2e test covers it as far as I know. Closes #974, Closes #978 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Backups: refuse an unsigned archive and an unknown download rule
Some checks failed
CI / Unit tests and coverage (Python 3.14) (pull_request) Failing after 35m0s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 3m35s
CI / Database tests on PostgreSQL (pull_request) Successful in 4m39s
CI / Security audit (pull_request) Successful in 1m24s
CI / Browser tests (Chromium) (pull_request) Failing after 49m3s
eb5f4888fa
A restore treated an archive with no `authentication` object as one written
before archives were signed, and restored its database and media with only a
warning. Nothing distinguishes that from a signed archive whose signature was
stripped by whoever altered it (BACKUP_FORMAT stayed 2 when signing arrived),
so altering a copy needed no forged MAC. restore_backup now refuses any
archive that is not authentic unless --unverified, for unsigned as for
unverified; the plugins-withheld half-measure is gone, as it can no longer be
reached. The Restore page computes authenticity(), shows the verdict, and
prints --unverified in the commands when it is not authentic. The two tests
that pinned the permissive path (test_backup.py, test_hardening_772.py) now
assert the refusal and pass unverified=True deliberately. The threat model
sentence is amended to match; the wiki already promised this.

POSTULO_BACKUP_DOWNLOAD fell open to every administrator on any word it did
not know. It is now in checks.CHOICES (postulo.E001 at start-up) and
may_download fails closed for a word it does not recognise.

Closes #974
Closes #978

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
tiagoagueda force-pushed sec2-d1-backups from eb5f4888fa
Some checks failed
CI / Unit tests and coverage (Python 3.14) (pull_request) Failing after 35m0s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 3m35s
CI / Database tests on PostgreSQL (pull_request) Successful in 4m39s
CI / Security audit (pull_request) Successful in 1m24s
CI / Browser tests (Chromium) (pull_request) Failing after 49m3s
to fa40a88c73
All checks were successful
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 13m3s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 2m23s
CI / Database tests on PostgreSQL (pull_request) Successful in 3m20s
CI / Security audit (pull_request) Successful in 48s
CI / Browser tests (Chromium) (pull_request) Successful in 28m2s
2026-10-18 08:53:21 +00:00
Compare
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo!995
No description provided.