Layer 2 — Granular Borg
Extracted from the master document on 2026-09-04. §N numbering preserved so as not to break cross-references.
39. Layer 2 — Borg (docker-host → backup-manager)
Section titled “39. Layer 2 — Borg (docker-host → backup-manager)”39.1 Why Borg and not Restic
Section titled “39.1 Why Borg and not Restic”Both were evaluated. The decisive reason was append-only mode with
borg serve restricted over SSH: from the destination’s
authorized_keys, a key can be forced to only add data,
never delete it.
Threat model: if
docker-hostis compromised, the attacker must not be able to destroy the backups using the same credentials that the backup system itself uses to write them.
39.2 SSH forced-command + append-only
Section titled “39.2 SSH forced-command + append-only”On VM 101, the borg user has:
command="borg serve --append-only --restrict-to-path /home/borg/repo",restrict ssh-ed25519 AAAA...Key stolen from docker-host │ ├─► interactive shell? NO (forced-command) ├─► other commands? NO ├─► other directories? NO (--restrict-to-path) └─► delete repo data? NO (--append-only)Empirically verified nuance — the one almost nobody explains:
In append-only mode, a `borg delete`/`prune` launched from the restrictedclient DOES make the archive disappear from `borg list`……but it does NOT delete the physical data from the disk.
borg delete → removes the LABEL (reversible) borg compact → frees the REAL SPACE (irreversible) ← local only
That is why `compact` runs ONLY in borg-maintenance.sh, insideVM 101, never through the restricted SSH channel.
Timeline of an attack: t0 attacker with the docker-host key "deletes" the archive t1 borg list no longer shows it ← looks lost t2 the data is STILL in /home/borg/repo ← recoverable t3 only someone with direct access to VM 101 can compact39.3 What borg-backup.sh backs up
Section titled “39.3 What borg-backup.sh backs up”/usr/local/bin/borg-backup.shEnvironment: /etc/borg-backup/borg-backup.envRepository: ssh://borg@192.168.2.10/home/borg/repoSSH: /etc/borg-backup/ssh/borg_backup /etc/borg-backup/ssh/known_hostsLog: /var/log/borg-backup.logTimer: daily 03:00Compression: zstdSources included in each archive:
/opt/stacks ← configuration and persistence of everything/mnt/storage/nextcloud-aio-backup ← native AIO backup (§41)/var/backups/borg-staging ← consistent DB snapshots (§39.4)BORG_RSH forces the use of that specific identity. This explains why
running Borg by hand as root or balsero with the normal SSH identity
returns Permission denied (publickey) even though the automated job
works perfectly.
39.4 Consistency of the Vaultwarden DB
Section titled “39.4 Consistency of the Vaultwarden DB”Vaultwarden uses SQLite in WAL mode. Copying db.sqlite3 while it is live can
produce a corrupt copy.
BEFORE borg create: sqlite3 .backup → /var/backups/borg-staging/vaultwarden-db.sqlite3 (complete copy, live, without stopping the container)
EXCLUDED from the archive: INCLUDED in the archive: db.sqlite3 /var/backups/borg-staging/... db.sqlite3-wal db.sqlite3-shm icon_cacheVerified automatic run of 2026-09-01:
docker-host-20260901-030007.
39.5 Repository passphrase
Section titled “39.5 Repository passphrase”repokey-blake2 encryption: the key lives inside the repo itself, encrypted
with a passphrase generated with openssl rand -base64 32 and stored in
Vaultwarden. Without it the entire repository is inaccessible even though the
data is still physically on the disk.