Skip to content

BorgBackup append-only servers: ransomware-resistant repositories

How to deploy a BorgBackup repository in append-only mode over SSH so a compromised client cannot delete or rewrite history — keys, restricted shells, and the rotation runbook.

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

  1. 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
  2. Generate a per-client SSH key on the client side (no passphrase, dedicated to backups), copy its public key, and craft the authorized_keys entry with the forced command.

    command="borg serve --append-only --restrict-to-repository /srv/borg-repos/acme-prod",restrict ssh-ed25519 AAAA... acme-prod-backup

    restrict is the modern shorthand that disables port forwarding, X11, agent forwarding, PTY allocation, and user RC. Forced command + restrict is the security boundary.

  3. 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-prod

    Export the keyfile material and store it outside the client. Losing the repokey loses the data.

  4. 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
  5. Wire the prune from the trusted operator host. That host has its own key, without the append-only forced command, allowed to run borg serve in 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-prod

    Lock that host down: it is the only system on the network that can erase backup history.

  6. Confirm the protections from the client side — delete and prune should 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=1 in the repo config file but forgetting the per-key forced command. A new admin key with a plain borg serve invocation can still delete. Belt and suspenders — use both.
  • Forgetting that borg compact is 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.