Skip to content

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)”

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-host is compromised, the attacker must not be able to destroy the backups using the same credentials that the backup system itself uses to write them.

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 restricted
client 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, inside
VM 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 compact
/usr/local/bin/borg-backup.sh
Environment: /etc/borg-backup/borg-backup.env
Repository: ssh://borg@192.168.2.10/home/borg/repo
SSH: /etc/borg-backup/ssh/borg_backup
/etc/borg-backup/ssh/known_hosts
Log: /var/log/borg-backup.log
Timer: daily 03:00
Compression: zstd

Sources 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.

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_cache

Verified automatic run of 2026-09-01: docker-host-20260901-030007.

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.