test #25

Merged
brian merged 2 commits from test into main 2026-08-30 07:45:15 +00:00
Owner
No description provided.
feat: back the whole server up to a remote borg repository
All checks were successful
Build and Deploy / test (push) Successful in 39s
Build and Deploy / build (push) Successful in 1m23s
Build and Deploy / deploy-main (push) Has been skipped
Build and Deploy / deploy (push) Successful in 27s
a4f25a75aa
The per-user Drive/Nextcloud backups export one account's notebooks as JSON,
which is the right shape for "I want my journal somewhere else" and the wrong
shape for "the host is gone". This adds the other one: an admin points the
server at a Borg repository and each run writes one archive holding a pg_dump
of the database and the whole upload volume — everything needed to rebuild the
deployment.

Configured in ⚙ Settings → Admin, stored in app_config: repository, passphrase,
SSH key, schedule and retention. The scheduler runs it alongside the per-user
backups; the panel shows the last run, failures included, because a backup
nobody can see the state of is a backup nobody knows is broken. Restore is
offered per archive behind a typed confirmation.

The SSH private key never touches the data volume — it lives in the database
and is written 0600 into a temp directory for one command, since anything under
DATA_DIR is inside the archive that key is authenticating. Borg's own cache and
the restore staging area are excluded for the same family of reason.

Two things were found by running it rather than by reading the docs:

- borg 1.4 takes --glob-archives, not --match-archives (that is a borg 2
  rename). Prune failed outright. An unrecognised flag at least fails loudly; a
  silently ignored pattern would have pruned every archive in the repository,
  including ones this app didn't write, so the scoping is now pinned by a test.
- Debian trixie ships postgresql-client-17, and pg_restore 17 opens a restore
  with SET transaction_timeout, which does not exist before 17 — a Postgres 16
  server rejected the transaction and every restore aborted. The image now
  installs postgresql-client-16 from PGDG, with a build-time check that fails
  the build if client and server majors ever drift.

Verified end to end in the built image against live borg 1.4.0 and a live
Postgres 16: archive layout and exclusions, retention leaving a foreign archive
alone, restore rolling files back and dropping ones written since, and a
corrupt dump refused with the database left intact by --single-transaction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123mShaQCyiSPN46SwrWRVF
feat: let the server generate its own borg SSH key
All checks were successful
Build and Deploy / test (push) Successful in 27s
Build and Deploy / build (push) Successful in 27s
Build and Deploy / deploy-main (push) Has been skipped
Build and Deploy / deploy (push) Successful in 17s
8c4106522d
Setting up a backup meant pasting a private key into a browser form, which means
that key had already been through a clipboard, a terminal, and whatever was used
to open it. The server can now make its own Ed25519 pair instead: the private
half goes straight from memory into app_config and never leaves, and the only
thing anyone carries to the backup host is the public line, which the panel
shows with a Copy button.

Generation is done in-process with `cryptography` rather than by shelling out to
ssh-keygen, so the private key is never a file on the way to the database.

Pasting an existing key still works and is now checked rather than taken on
trust. A passphrase-protected key is refused with the reason — backups run under
BatchMode=yes and nothing can answer a prompt, so that key would have failed at
the next unattended run with nobody watching. Whether the key was pasted or
generated, the public half is derived and stored, so the line to install on the
backup host is always to hand; a key stored before this change has its public
half derived on the first read of the panel.

Verified against a real sshd with borg on the far side: the generated key
authenticates over ssh://, the host key is learned on first connection and
reused rather than re-learned on the second, and a key absent from
authorized_keys is refused.

That last check found a bad error message. An unauthorised key reported borg's
own summary — "Connection closed by remote host. Is borg working on the
server?" — which sends an admin to check borg on a host where borg is fine. The
line that names the cause arrives prefixed with "Remote:", which the previous
filter skipped on purpose. It now looks for the line that explains the failure
and, for a rejected key, says where to go and fix it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123mShaQCyiSPN46SwrWRVF
brian merged commit 1f518d76d4 into main 2026-08-30 07:45:15 +00:00
Sign in to join this conversation.
No reviewers
No labels
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
brian/Journal!25
No description provided.