A BorgBackup repository on its own is not ransomware-resistant: a client that has read/write SSH access can call borg delete or borg prune and erase its own history. The append-only mode flips that — the server records new chunks but refuses any operation that would delete existing data. Pair it with a forced SSH command and a restricted user and you have a repository that a fully compromised client cannot wipe. This article walks through the deployment, the prune-from-a-trusted-host pattern, and the operational fences we add.
How to verify
On the server, confirm the user, shell, key, and append-only flag:
getent passwd borguser
ls -la /home/borguser/.ssh/authorized_keys
sudo cat /home/borguser/.ssh/authorized_keys | head
sudo -u borguser borg info /srv/borg-repos/acme-prod/ 2>&1 | head
grep append_only /srv/borg-repos/acme-prod/config
The config file inside the repo controls per-repo append-only state; the SSH authorized_keys entry controls per-key append-only enforcement at the server. Both layers exist and you can use either or both.
What’s happening
BorgBackup speaks a wire protocol over SSH. When a client connects, the server side runs borg serve and the two ends exchange chunk hashes, encrypted chunk payloads, and manifest updates. borg serve --append-only accepts new chunks and manifest entries but rejects any RPC that would shrink the repo — delete, prune, compact, and rewrites of existing manifests are all refused.
Restricted shells: instead of letting borguser get a real login, the SSH authorized_keys entry pins a forced command — borg serve --append-only --restrict-to-repository /srv/borg-repos/<client>. The client cannot pass arbitrary commands; even an interactive SSH session immediately runs borg serve and dies.
The trade: append-only means the server can’t prune. Old archives accumulate forever. The pattern is to have a separate, trusted operator host (not the client) periodically connect with a key that does not have the append-only restriction and run borg prune from there. The compromise surface narrows to that one host.
The procedure
-
Create the user and key on the backup server.
useradd -m -s /bin/bash -d /home/borguser borguser install -d -m 0700 -o borguser -g borguser /home/borguser/.ssh install -d -m 0700 -o borguser -g borguser /srv/borg-repos apt install -y borgbackup -
Generate a per-client SSH key on the client side (no passphrase, dedicated to backups), copy its public key, and craft the
authorized_keysentry with the forced command.command="borg serve --append-only --restrict-to-repository /srv/borg-repos/acme-prod",restrict ssh-ed25519 AAAA... acme-prod-backuprestrictis the modern shorthand that disables port forwarding, X11, agent forwarding, PTY allocation, and user RC. Forced command + restrict is the security boundary. -
Initialize the repository from the client side. Use repokey-blake2 so the encryption key lives only on the client; the server is opaque.
borg init --encryption repokey-blake2 [email protected]:/srv/borg-repos/acme-prodExport the keyfile material and store it outside the client. Losing the repokey loses the data.
-
Take a backup and verify the wire still works.
borg create --stats --progress \ [email protected]:/srv/borg-repos/acme-prod::"{hostname}-{utcnow:%Y%m%d-%H%M%S}" \ /etc /var/www /home borg list [email protected]:/srv/borg-repos/acme-prod -
Wire the prune from the trusted operator host. That host has its own key, without the append-only forced command, allowed to run
borg servein full mode. The cron there issues:borg prune --list --keep-daily 14 --keep-weekly 8 --keep-monthly 12 \ [email protected]:/srv/borg-repos/acme-prod borg compact [email protected]:/srv/borg-repos/acme-prodLock that host down: it is the only system on the network that can erase backup history.
-
Confirm the protections from the client side —
deleteandpruneshould fail.borg delete --force [email protected]:/srv/borg-repos/acme-prod::some-archive # → "Refusing to delete archive in append-only mode"
Common pitfalls
- Setting
append_only=1in the repoconfigfile but forgetting the per-key forced command. A new admin key with a plainborg serveinvocation can still delete. Belt and suspenders — use both. - Forgetting that
borg compactis the operation that actually frees space after prune. Without it the disk fills despite the pruned-archive count looking healthy. - Storing the repokey beside the data. The repokey lets anyone with the repo read everything; keep it in a separate vault.
- Relying on append-only as the only line of defense. If the attacker compromises the trusted operator host too, they prune and you lose history. Pair with an offsite, immutable second copy — BorgBase or an S3 bucket with object-lock.
- Letting the repo grow without compact for years. The first compact on a multi-TB repo can run for hours and lock writes. Schedule it weekly.
In the engagements we run, BorgBackup is one of the dominant choices for Linux fleets that need encrypted, dedupe-aware, host-pull-style backups. The append-only pattern, paired with a separate offsite target and a quarterly restore drill, is the default operating model. We codify the SSH key files, the prune host, and the restore-test scripts as configuration alongside the rest of the platform. The way that fits into a broader managed environment is in Managed Operations.