Backup & DRVIII · Backup Repositories: restic, Borg and Repository FailureRepositories
BorgBackup: a different set of trade-offs
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
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.
$ borg init --encryption=repokey-blake2 /work/repoIMPORTANT: 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: 0Read 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.
$ borg create --stats /work/repo::day2 ./app ./dbArchive 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: 0The 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.
$ 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: 0borg 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.
$ borg check --verify-data /work/repo>>> exit code: 0That 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.
$ borg prune --list --dry-run --keep-daily=1 /work/repoKeeping archive (rule: daily #1): day2 Fri, 2026-08-28 13:58:07 [85d3e533e094eb96663fd26a148b14737667bfc4251eea6b7144827ea2d279b8]
>>> exit code: 0One 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.
$ borg key export --paper /work/brepoTo 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 - 06That 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 --statsreported 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/repoprinted 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=1printedKeeping archive (rule: daily #1)forday2alone, soday1would have been removed. The value of the dry run is the archive that is missing from the list.- With
append_only = 1, threeborg deleteruns exited 0 and the archive listing came back empty, while 41M remained under/work/aorepo/dataand the transaction log grew to 25. Rolling back to transaction 13 returned all three archives and the extract exited 0. borg init --encryption=repokey-blake2printed that under repokey modes thekey is stored in the repository directory, which leaves the passphrase as the only secret not travelling with the data.borg key export --paperproduced a printableBORG PAPER KEY v1block, still useless without that passphrase.
Cross-course references
- Linux for Production Sysadmins — Part XXVI (SSH) covers the
authorized_keysoptions that pin a forced command to a key, which is the layer this lesson’sborg serve --restrict-to-pathdepends 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 v1block 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-datasucceeds 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
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?
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?
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.
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.
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.