How to Set Up Borg Backup with Append-Only Repositories for Ransomware-Resistant Backups
Most ransomware playbooks now include a step before the encryption: find the backups and delete them first. If your backup client authenticates to the repository with credentials that can also delete or overwrite data, an attacker who gets a shell on that client has effectively already got a shell on your backup history too. It doesn't matter how good your encryption or your retention policy is if the same compromised host can run borg delete --repo ... --last 999 and mean it.
BorgBackup has a built-in answer to this: append-only repositories. The idea is simple and the implementation detail is where people usually get it wrong, so this is a walkthrough of doing it properly, with the restriction enforced on the server side rather than trusted to a client-side flag.
The problem with a fully-trusted backup client
Borg's normal model is that a client machine holds an SSH key (or local filesystem access) with full read/write/delete rights to the repository. That's fine as long as the client is never compromised. It's a bad assumption for exactly the threat you're defending against: ransomware typically runs with the same privileges as the user or service account doing the backups, because that's usually the same host that got popped in the first place.
You can pass --append-only to borg create on the client, but that's a client-side hint, not an enforcement mechanism. An attacker with a shell on the client just doesn't pass it, or runs borg delete directly. The restriction has to live somewhere the attacker doesn't control: the server end of the SSH connection.
How append-only actually works in Borg
Borg's repository format is segment-based: each write appends new segment files, and normal maintenance (compact, and the deletion phase of prune) reclaims space by rewriting or removing old segments. When a repository is served in append-only mode, borg serve refuses any operation that would delete or overwrite existing segment data, regardless of what the connecting client asks for. borg delete and borg prune still "work" from the client's point of view, but under append-only they only mark archives as deleted in the transaction log; the actual space is not freed until someone runs borg compact over a connection that is not append-only.
That last point is the key design detail: you end up needing two separate credentials for two separate roles: a restricted, append-only key that the backup job uses every day, and a full-access key that only ever gets used deliberately, from somewhere the attacker isn't, to actually reclaim space.
Step 1: Prepare the backup server
Create a dedicated, unprivileged user on the backup server for receiving repositories. Don't reuse an account that has any other purpose.
sudo useradd --system --create-home --home-dir /srv/borg --shell /usr/sbin/nologin borgbackup
sudo mkdir -p /srv/borg/repos/webserver1
sudo chown -R borgbackup:borgbackup /srv/borg
sudo -u borgbackup mkdir -p /srv/borg/.ssh
sudo chmod 700 /srv/borg/.ssh
Confirm Borg is installed and reasonably current on both ends; older Borg versions can talk to newer ones within the same major series, but it's simplest to keep client and server on the same version.
Step 2: Generate a restricted key for daily backups
On the client, generate a dedicated SSH key used for nothing but this backup job:
ssh-keygen -t ed25519 -f ~/.ssh/borg_append_only -N "" -C "webserver1-borg-append-only"
Install the public key on the server, but with a forced command that pins borg serve to a single path and enables --append-only. On a reasonably current OpenSSH server (8.2 or later), the restrict option collapses the usual pile of no-port-forwarding,no-X11-forwarding,no-pty,... flags into one:
# /srv/borg/.ssh/authorized_keys (owned by borgbackup, mode 600)
command="borg serve --restrict-to-path /srv/borg/repos/webserver1 --append-only",restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... webserver1-borg-append-only
The client never gets to choose whether append-only applies. Whatever it sends over that connection, borg serve on the other end enforces the restriction, because the forced command overrides anything the client requests.
Step 3: Initialise the repository
From the client, initialise the repository through the restricted connection. Since the repository is empty, there's nothing to delete yet, so this works fine under append-only:
export BORG_REPO=ssh://[email protected]/srv/borg/repos/webserver1
export BORG_RSH="ssh -i ~/.ssh/borg_append_only"
borg init --encryption=repokey-blake2 "$BORG_REPO"
Immediately export and store the repository key somewhere other than the client host: borg key export "$BORG_REPO" /path/to/offline/storage/webserver1.key. If the client is destroyed along with everything on it, that key (plus its passphrase) is the only thing standing between you and an unreadable pile of encrypted segments.
Step 4: Automate the backup job
A plain script plus a systemd timer is enough; there's no need for anything more elaborate for a single host.
#!/usr/bin/env bash
set -euo pipefail
export BORG_REPO=ssh://[email protected]/srv/borg/repos/webserver1
export BORG_RSH="ssh -i /etc/borg/borg_append_only"
export BORG_PASSPHRASE_FD=3
exec 3</etc/borg/repo_passphrase
borg create \
--stats --compression zstd,6 \
"::{hostname}-{now:%Y-%m-%dT%H:%M:%S}" \
/etc /home /var/www \
--exclude '/home/*/.cache'
# /etc/systemd/system/borg-backup.service
[Unit]
Description=Borg append-only backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/borg-backup.sh
Nice=19
IOSchedulingClass=idle
# /etc/systemd/system/borg-backup.timer
[Unit]
Description=Run borg-backup daily
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
Note there's no borg prune in this script. Running prune through the append-only connection is harmless but pointless: it only soft-marks archives, it doesn't reclaim any space, and it gives a false sense of tidiness. Pruning and compaction belong to the maintenance key, not the daily job.
Step 5: The maintenance key, kept apart from the client
Generate a second key pair for pruning and compaction. This one gets full access, so it needs to be treated as sensitive and, ideally, never live on the same host that gets backed up:
ssh-keygen -t ed25519 -f ~/.ssh/borg_maintenance -N "a-real-passphrase" -C "webserver1-borg-maintenance"
# /srv/borg/.ssh/authorized_keys, second line
command="borg serve --restrict-to-path /srv/borg/repos/webserver1",restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... webserver1-borg-maintenance
Note the absence of --append-only on this entry. Run maintenance from a workstation or admin jump host that the ransomware-affected server would have no reason or means to touch, with the key passphrase-protected so a stolen key file alone isn't enough:
export BORG_REPO=ssh://[email protected]/srv/borg/repos/webserver1
export BORG_RSH="ssh -i ~/.ssh/borg_maintenance"
borg prune --keep-daily=14 --keep-weekly=8 --keep-monthly=12
borg compact
borg check
Run this on a schedule too, just from somewhere the daily backup job doesn't have access to, and ideally with a manual or at least out-of-band trigger if you're being properly paranoid about it.
Verifying the restriction actually holds
Don't take the configuration on faith. From the client, using the append-only key, try to actually delete something and confirm the repository doesn't shrink:
du -sh /srv/borg/repos/webserver1
BORG_RSH="ssh -i ~/.ssh/borg_append_only" borg delete "$BORG_REPO::webserver1-2026-08-01T00:00:00"
du -sh /srv/borg/repos/webserver1
The archive should disappear from borg list, but the on-disk size shouldn't drop, because the underlying segments are still there. Then confirm the maintenance key can genuinely reclaim it via borg compact, and that the archive is unrecoverable once you actually meant to remove it. Both halves matter: an append-only setup that silently isn't enforced is worse than none, because it gives false confidence.
What this doesn't protect against
Append-only stops an attacker on the client from destroying backup history through the normal Borg protocol. It doesn't stop them filling the repository disk with junk archives until it's full, since appending is exactly what they're still permitted to do; keep an eye on free space and alert on unexpected growth. It doesn't protect the maintenance key if that key ends up reachable from the same blast radius as the client, which is why it belongs on a separate machine with its own compromise story. And it does nothing for confidentiality on its own, repokey encryption is a separate property that you should already have enabled regardless. None of this replaces an actual 3-2-1 backup strategy: a second copy on different media, and at least one copy that's offline or otherwise physically out of reach of anything running on your network. Append-only repositories are a good way to make the online copy meaningfully harder to destroy from a compromised host; they're not a substitute for having more than one copy in the first place.