Skip to main content
RunBook Academy

Backup & DRVIII · Backup Repositories: restic, Borg and Repository FailureRepositories

BorgBackup: a different set of trade-offs

Advanced⏱ ~28 minborgrestic

What you'll learn

  • Map Borg repository and archive vocabulary onto the restic model already in use
  • Choose an encryption mode knowing where it leaves the key and where it leaves the passphrase
  • Verify a repository with borg check --verify-data and rehearse a prune before it removes anything
  • Constrain a backup client with borg serve and escrow the key material away from the host

Prerequisites

Verified against restic 0.19.1 · BorgBackup 1.4.5 · rclone 1.75.0 · MinIO (S3-compatible object storage) RELEASE.2025-09-07T16-13-09Z · OpenZFS 2.4.1 · LVM2 2.03.31(2) · btrfs-progs 6.17.1 · PostgreSQL 18.6 · pgBackRest 2.59.1 · Kubernetes (k3s) and etcd k3s v1.36.3+k3s1, etcd 3.7.1 · Velero 1.18.2 · Docker Engine 29.7.2 · Proxmox Backup Server (documentation only) 4.0.10-1 · Ubuntu (host baseline) 26.04 LTS · 2026-08-28

Not yet marked complete on this device.

restic’s repository was one answer to a question that has several. Borg answers the same question — deduplicated, encrypted, incremental-forever storage that a restore can be driven out of — and disagrees with restic about where the key lives, about what a backup client is allowed to do to the repository, and about which command reclaims space. Reading the two side by side is the fastest cure for treating either one’s semantics as the way repositories simply work: they are decisions, each with a bill attached.

A repository holds archives, and under repokey it holds the key too

The vocabulary maps almost cleanly. A Borg repository is what a restic repository is: a chunk store plus an index plus the metadata that reconstructs files from chunks. A Borg archive is what restic calls a snapshot: one recorded state of a set of paths, at a point in time, sharing chunks with every other archive in the repository.

The first divergence is in how you name one. restic generates the snapshot ID; Borg addresses an archive as REPOSITORY::NAME where the name is yours to choose — the capture used day1 and day2 — and records a fingerprint alongside it. An operator-chosen name is a scheduling handle you can predict from a cron entry, and also a string two jobs can collide on. Neither is better; they fail differently.

The second divergence matters more, and Borg prints it during initialisation.

Configuration change`borg init` states where the key goes, and what you now owe it
$ borg init --encryption=repokey-blake2 /work/repo
IMPORTANT: you will need both KEY AND PASSPHRASE to access this repo!

Key storage location depends on the mode:
- repokey modes: key is stored in the repository directory.
- keyfile modes: key is stored in the home directory of this user.

For any mode, you should:
1. Export the borg key and store the result at a safe place:
 borg key export           REPOSITORY encrypted-key-backup
 borg key export --paper   REPOSITORY encrypted-key-backup.txt
 borg key export --qr-html REPOSITORY encrypted-key-backup.html
2. Write down the borg key passphrase and store it at safe place.

>>> exit code: 0

Read the second block as an architecture statement rather than as a note. Under a repokey mode the encrypted key material lives in the repository directory, so every copy of the repository — the offsite one, the tape, the object-storage mirror — carries the key with it. Under a keyfile mode the key lives in the home directory of the user running the backup, which is usually the host being protected, so the repository copies are useless without a file that the disaster is likely to take.

The consequence of choosing repokey is worth stating in one sentence, because the whole of Part IX depends on it: the passphrase is then the only secret that is not co-located with the data. Everything else has been replicated along with the repository. If the passphrase exists only in the memory of the person who typed it, or only in a file on the machine being backed up, the offsite copy is a locked box shipped with its lock and without its key.

--stats prints two answers, and only one of them is this run’s cost

Borg’s per-archive accounting is printed by borg create --stats, and it reports the same quantity three ways for two different scopes. The capture took day1 from a 41.94 MB tree of two files, then took day2 from the same tree.

Configuration changethe second archive of an unchanged tree
$ borg create --stats /work/repo::day2 ./app ./db
Archive fingerprint: 85d3e533e094eb96663fd26a148b14737667bfc4251eea6b7144827ea2d279b8
Time (start): Fri, 2026-08-28 13:58:07
Time (end):   Fri, 2026-08-28 13:58:07
Duration: 0.07 seconds
Number of files: 2
Utilization of max. archive size: 0%
------------------------------------------------------------------------------
                     Original size      Compressed size    Deduplicated size
This archive:               41.94 MB             41.94 MB                613 B
All archives:               83.89 MB             83.89 MB             41.95 MB

                     Unique chunks         Total chunks
Chunk index:                      23                   40
------------------------------------------------------------------------------

>>> exit code: 0

The This archive row describes the run: it walked and hashed 41.94 MB of original data, and it added 613 B to the repository. The All archives row describes the repository: the two archives present 83.89 MB of logical content between them, stored in 41.95 MB. The first archive’s own stats reported 41.94 MB in all three columns, because at that point nothing had been stored before it.

The chunk index says the same thing structurally. After day1 it read 20 unique chunks out of 20 total. After day2 it reads 23 unique out of 40 — forty chunk references across the two archives resolving to twenty-three distinct stored chunks. That ratio, not the file count and not the archive count, is what the repository’s size follows.

The operational point is that the deduplicated column is the only one that belongs in a capacity plan, and it is a function of how much the source changed not of how large the source is. restic reports the same idea under a different name, printing Added to the repository for each run; in its own capture a second backup of a 60 MiB tree with one changed file added 2.062 KiB. The numbers are not comparable across different trees and chunkers, but the planning question is the same.

Segments, transactions, and what append_only = 1 actually forbids

The capture set append_only 1 on a second repository holding three archives, then did what a compromised client would do. All three borg delete commands exited 0 and borg list came back empty. That is not a bug and the upstream documentation says so directly, in the sentence the capture quotes:

Please note that this only affects the low level structure of the repository, and running borg delete or borg prune or reading from the repository will still be allowed.

Append-only does not refuse the delete. What it refuses is compaction, and the evidence is that after the archives had vanished from the listing, the capture measured 41M still occupying /work/aorepo/data, while the repository’s transaction log grew from transactions 5, 9 and 13 to include 17, 21 and 25.

borg extract, borg check --verify-data, and a prune you read first

Three commands decide whether a repository is a recovery capability or a directory of files, and all three appeared in the capture.

Extraction is the one that matters, and Borg’s is quiet.

Read-only / Safeextracting an archive and checking the bytes against the source
$ borg extract /work/repo::day1
>>> exit code: 0

--- verifying the restored tree against the source checksums ---
./app/orders.csv: OK
./db/data.bin: OK
>>> verification exit code: 0

borg extract writes into the current working directory rather than taking a target path the way restic restore --target does, which is a small difference with a sharp edge: the same command run from the wrong directory unpacks an archive over live data. Run it from an empty directory created for the purpose, and the edge never arrives.

The verification lines are the part to keep: two files came back matching the checksums recorded before the backup, which is what makes this an extraction rather than a hope. Repository verification is the other half, and Borg’s counterpart to restic check --read-data is a flag.

Service impact possiblefull data verification, which succeeds by saying nothing
$ borg check --verify-data /work/repo
>>> exit code: 0

That empty output is the whole result, and the flag matters more than the silence. A plain borg check already reads the segment files and checks what it reads by size and CRC, so accidental damage is within its reach, along with the archive metadata and the existence of every chunk an archive references. What it never does is decrypt. --verify-data adds the cryptographic verification — the data is read out of the repository, decrypted and decompressed, which the upstream page calls very time consuming — and that is what makes it the only form of the check that catches deliberate corruption as well as accidental. Borg reports by exception, so success produces an exit status, a timestamp, and nothing a log search will ever find. Monitor the age of the last success, because there will never be an event to alert on.

Retention is the third command, and it is the only one in a backup system whose job is to remove recovery points.

Data-loss risk`prune` rehearsed before it is allowed to act
$ borg prune --list --dry-run --keep-daily=1 /work/repo
Keeping archive (rule: daily #1):            day2                                 Fri, 2026-08-28 13:58:07 [85d3e533e094eb96663fd26a148b14737667bfc4251eea6b7144827ea2d279b8]

>>> exit code: 0

One line, naming one archive, and the significant part is the archive absent from it. Both were taken on 2026-08-28, --keep-daily=1 keeps one archive per day, and so day1 was not kept. The pairing of --dry-run and --list turns a retention policy from an assertion into something a person can read before it takes effect; a retention change whose output nobody read is a deletion plan nobody reviewed.

Two things a backup client should not hold

The append-only result narrows the question rather than answering it. If a client holding backup credentials can remove archives from the index, the remaining defence is to reduce what those credentials reach at all. Borg’s mechanism is borg serve, which runs on the repository host at the far end of an SSH connection and is where the restrictions are enforced.

REPO=/srv/borg/web01

# On the backup server, as the account the client's SSH key authenticates to.
borg serve --append-only --restrict-to-path "$REPO"

In practice that line is pinned as a forced command in the client’s entry in the server’s authorized_keys, so the key can do nothing else. Read the restriction precisely: --restrict-to-path confines the session to a path and grants its sub-directories implicitly, which is what stops a compromised web server from reaching the database server’s repository, while --restrict-to-repository is the stricter form that names one repository and does not carry a subtree with it. --append-only applies the setting the previous section measured, with the scope that section established rather than the scope its name suggests.

restic reaches the same goal from a different starting point. Its client speaks to a backend rather than to a peer process: a local path, SFTP, object storage, or the REST backend implemented by rest-server, whose --append-only mode allows new backups to be created and refuses deletion or modification of what is already stored. That is a different guarantee from Borg’s, and a different actor enforces it. Borg’s restriction is applied by a Borg process on the server that understands repositories; restic’s is applied by whatever the backend is — a server flag, a bucket policy, an object-lock configuration — which means restic inherits every enforcement mechanism the storage provides and depends on none of its own.

The second thing to move off the client is the key material, and Borg exports it in a form that survives the loss of every system that held the repository.

Data-loss risk`borg key export --paper` produces something a safe can hold
$ borg key export --paper /work/brepo
To restore key use borg key import --paper /path/to/repo

BORG PAPER KEY v1
id: 31 / 6ece3c b2eaf0 547454 / ab77e5 a39843 - ce
1: 86a961 6c676f 726974 686da6 736861 323536 - 14
2: a46461 7461da 019ee5 d18e4e 246dfa 8e1bb1 - 59
3: 620691 f2dea2 918fcf ef0e59 4e1923 ca1ffa - 5d
4: 9156ef de13d2 0406ff a3be7b f4edec b27562 - 67
5: 49a3c5 f22540 1b9be8 bfa8a5 5ed425 fab040 - 3f
6: cb83da a857c9 e5bc97 32f394 452c1a b9c48b - e3
7: 72cc14 df7270 948df1 bed610 b7e152 81f4c4 - ed
8: 0e1d11 f617f9 3baa03 8b1189 f111e8 a2bec3 - 06

That printable block is the encrypted key material — an id line, eight numbered rows, a two-character value closing each one — and its own first line says how it comes back: borg key import --paper. It is still useless alone, because it is the key and not the passphrase, which is why the two are escrowed separately and to different custodians; held by one custodian they collapse back into the single object the export existed to split.

The version position, and the comparison worth making

State the release position plainly, because it is the kind of fact that rots. The capture ran on borg 1.4.0, and 1.4.x is the current stable line. Borg 2.0 was still in beta at the time of writing, the newest tags being 2.0.0b23 and 2.0.0.dev0. This lesson therefore does not present 2.x as a production choice: a repository format is something you must still be able to read in three years, and a pre-release has not accumulated the operational record that claim requires.

The comparison against restic is then worth making on three axes, and worth leaving undecided on all three.

On the client/server model, Borg expects a Borg process at both ends and gets a server that understands repository semantics, which is what makes --restrict-to-path possible. restic expects a storage backend and gets portability across local disks, SFTP, every S3-compatible service and rest-server, at the price of having no privileged peer to enforce anything.

On append-only support, the same two words name different guarantees, as the previous section showed: Borg permits the delete and forbids the compaction that would make it final, while rest-server refuses the request before it reaches the repository at all. Neither is the storage-layer version of the property — object lock, or a filesystem the client cannot write to — which is stronger than both because it does not depend on the process the client is talking to.

On what is moving underneath, the honest statement is narrow. The capture recorded 1.4.x as the production line with its successor still tagged beta, which puts a repository-format transition somewhere in the future of anyone standardising on Borg now. Nothing comparable surfaced in the restic 0.19.1 work in this part, and this lesson claims no more than that: a format migration is a plannable cost rather than a defect, and it belongs in the decision beside the other two axes.

The choice between them is a choice about which failure you would rather manage, made against your own estate: who holds credentials, what your storage can enforce, and how you intend to prove a restore.

What to take from this

  • borg create --stats reported 41.94 MB original and 613 B deduplicated for the second archive of an unchanged tree, with 41.95 MB deduplicated across all archives. The deduplicated column tracks change, not size, and it is the only one a capacity plan should use.
  • The chunk index moved from 20 unique / 20 total to 23 unique / 40 total — forty references across two archives resolving to twenty-three stored chunks.
  • borg check --verify-data /work/repo printed no output and exited 0. It decrypts and decompresses the stored data rather than only CRC-checking the segments, and because it reports by exception, the thing to monitor is the age of the last success.
  • borg prune --list --dry-run --keep-daily=1 printed Keeping archive (rule: daily #1) for day2 alone, so day1 would have been removed. The value of the dry run is the archive that is missing from the list.
  • With append_only = 1, three borg delete runs exited 0 and the archive listing came back empty, while 41M remained under /work/aorepo/data and the transaction log grew to 25. Rolling back to transaction 13 returned all three archives and the extract exited 0.
  • borg init --encryption=repokey-blake2 printed that under repokey modes the key is stored in the repository directory, which leaves the passphrase as the only secret not travelling with the data. borg key export --paper produced a printable BORG PAPER KEY v1 block, still useless without that passphrase.

Cross-course references

  • Linux for Production Sysadmins — Part XXVI (SSH) covers the authorized_keys options that pin a forced command to a key, which is the layer this lesson’s borg serve --restrict-to-path depends on entirely: the restriction only holds if the client’s key cannot obtain a shell instead.
  • Secrets, PKI & Certificate Management for Infrastructure Engineers — Part XV (KMS, HSM and Key Protection) supplies the custody models for the two pieces of material this lesson separates, the exported BORG PAPER KEY v1 block and the passphrase that is the only secret repokey does not replicate with the repository.
  • Observability for Production Sysadmins — Part XVIII (Alerting Rules) is where the monitoring problem raised here gets solved: borg check --verify-data succeeds silently, so the rule has to fire on the age of the last successful verification rather than on an event that never occurs.

Quiz

Knowledge check · 5 questions

  1. Q1. A repository created with `--encryption=repokey-blake2` is replicated nightly to a second site. The primary site and every host in it are destroyed. What does the second site hold?

  2. Q2. A compromised client holds the credentials for a repository with `append_only = 1` set and runs `borg delete` against every archive. What is the state of the repository afterwards?

  3. Q3. A repokey repository is replicated to a second site. Which measures change the outcome if the primary site and everyone who knew the passphrase are lost? Select all that apply.

  4. Q4. `borg check --verify-data` finished with no output and exit code 0, which establishes that the repository structure is consistent but leaves the stored chunk data unread.

  5. Q5. A colleague proposes standardising new repositories on the newest Borg release available rather than on 1.4. State what you would establish before agreeing, and what would change your mind.

Passing score: 75%. Answers are checked in this browser.