Purpose
A tested configuration for backing up to XNS with restic so that the
backups cannot be deleted by whatever deleted your data — plus the restore drill that
proves they come back. Every command below was run against a live gateway on
2026-09-09 and the output shown is the output we got. Where something does not work,
it says so.
| Tested | 2026-09-09 |
|---|---|
| Tested by | Kenneth Bell, SCP, Corp |
| Gateway | XNS S3 Gateway 1.1.0-2350-gf3a94e00, release channel, built 2026-08-18 |
| Client | restic 0.19.1, aws-cli 2.36.25, Ubuntu 24.04 |
| Data set | 551 files / 391.8 MiB — a source tree plus incompressible binary blobs |
Before you start
You need an XNS endpoint, an access key and secret, and the aws CLI in
addition to restic — restic can create buckets but it cannot create one with Object
Lock enabled, so the bucket is made once by hand.
Object Lock must be enabled when the bucket is created. It cannot be
turned on afterwards. If you already have a restic repository in a normal bucket, you
will be creating a second one and re-seeding it, not converting the first.
Minimum gateway version. Object Lock bucket creation returns an
internal error on gateway builds older than 2026-08-08. If
create-bucket --object-lock-enabled-for-bucket returns
500 InternalError, your gateway predates the fix — everything else on this
page still applies once it is updated.
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export XNS=https://your-gateway:9000Step 1 — create the bucket with Object Lock
aws --endpoint-url $XNS s3api create-bucket \
--bucket backups --object-lock-enabled-for-bucketEnabling Object Lock enables versioning at the same time; you do not need to ask for it
separately. Confirm both:
$ aws --endpoint-url $XNS s3api get-bucket-versioning --bucket backups
{
"Status": "Enabled"
}Step 2 — set the retention rule
A bucket-level default retention rule applies to every object written afterwards, which
is what makes this work with restic: restic knows nothing about Object Lock and never
sets retention itself.
aws --endpoint-url $XNS s3api put-object-lock-configuration \
--bucket backups \
--object-lock-configuration '{"ObjectLockEnabled":"Enabled","Rule":{"DefaultRetention":{"Mode":"GOVERNANCE","Days":30}}}'Choose the mode deliberately. GOVERNANCE can be overridden
by a credential holding the s3:BypassGovernanceRetention permission, which
is what lets you clean up later. COMPLIANCE cannot be overridden by anyone,
including the account owner, until the period expires — there is no bypass and no
support request that shortens it. Use COMPLIANCE when that is the point,
and understand that you are committing to store and pay for those objects for the full
period.
Step 3 — initialize and back up
export RESTIC_REPOSITORY="s3:$XNS/backups"
export RESTIC_PASSWORD_FILE=/etc/restic/password
restic init
restic backup /srv/data --tag nightlyWhat that produced on the tested run:
Files: 551 new, 0 changed, 0 unmodified
Dirs: 25 new, 0 changed, 0 unmodified
Added to the repository: 392.009 MiB (386.460 MiB stored)
processed 551 files, 391.786 MiB in 0:09
snapshot a485bd8c savedStep 4 — confirm the objects are actually locked
Do not take the bucket configuration on trust. Check that retention landed on the pack
files restic wrote:
$ aws --endpoint-url $XNS s3api head-object --bucket backups \
--key data/03/033f35c7... \
--query '{Mode:ObjectLockMode,Until:ObjectLockRetainUntilDate}'
{
"Mode": "GOVERNANCE",
"Until": "2026-10-10T18:36:43.662000+00:00"
}Then confirm the lock is enforced, not merely recorded. Deleting a specific version of
a locked object is refused:
$ aws --endpoint-url $XNS s3api delete-object --bucket backups \
--key data/03/033f35c7... --version-id 4bf7714c-...
An error occurred (AccessDenied) when calling the DeleteObject operation: Access DeniedStep 5 — the restore drill
A backup nobody has restored is a claim, not a control. Run this on a schedule, not
once.
restic restore latest --target /var/tmp/drillTiming from the tested run, with the local restic cache cleared first so nothing was
served from disk:
| Backup | 391.786 MiB, 551 files — 9.4 seconds |
|---|---|
| Restore, cold cache | 391.786 MiB — 12.7 seconds |
These are local-network figures, measured between two hosts on the same
rack. They are the right numbers for judging whether the gateway is the bottleneck and
the wrong numbers for predicting a restore over the internet, where your own link
decides the answer. Measure your own and write the result down.
Verify the restore rather than eyeballing it. Take a checksum manifest before the backup
and compare after:
cd /srv/data && find . -type f -print0 | sort -z | xargs -0 sha256sum > /tmp/before.txt
cd /var/tmp/drill/srv/data && find . -type f -print0 | sort -z | xargs -0 sha256sum > /tmp/after.txt
diff /tmp/before.txt /tmp/after.txt && echo "restore verified"On the tested run the two manifests were identical — same 551 lines, same SHA-256 of the
manifest file itself, zero differences.
What does not work, and what to watch for
Object Lock does not stop a delete marker, and a delete marker breaks the repository
This is the most important limitation on this page. Object Lock protects versions.
An ordinary delete — no version ID — is still permitted: it leaves every locked version
intact and places a delete marker on top. The bytes are safe. But restic reads the
current version, and the current version is now a delete marker:
$ restic check
pack 033f35c7...: does not exist
The repository is damaged and must be repaired.
Fatal: repository contains errorsNothing has been lost. Delete the marker version and the repository is whole again:
$ aws --endpoint-url $XNS s3api delete-object --bucket backups \
--key data/03/033f35c7... --version-id <the delete marker's version id>
$ restic check
no errors were foundList markers with
aws s3api list-object-versions --bucket backups --query 'DeleteMarkers[]'.
The practical lesson: Object Lock buys you recoverability, not immunity from disruption.
Something with your credentials can still make the repository unreadable; it cannot make
it unrecoverable.
Pruning does not free space while retention holds
restic forget --prune reports success and reports how much it removed. Under
Object Lock, it has not removed it. Prune deletes the current version of each pack it
retires; the locked versions stay, and they stay billable until retention expires.
Measured on the tested run immediately after a successful prune:
| What restic reported | 354.4 MiB |
|---|---|
| What the bucket held | 468.2 MiB across 48 versions, plus 20 delete markers |
| Locked, non-current, still charged | 100.6 MiB |
That is 32% more stored than restic believes. Any retention period longer than your
backup rotation means paying for both the live repository and everything the rotation
has retired but retention still holds. Budget for it: size your retention period against
your rotation, not against a compliance number picked in isolation.
Where not to use this
Object Lock plus restic is a good fit for a repository you write to and read back whole.
It is a poor fit if you need to reclaim space promptly, if your retention requirement is
much longer than your snapshot rotation, or if you are relying on prune to control cost.
For those, keep the retention window short and let lifecycle rules do the expiring.
Rollback — how to take a locked repository apart
You will need this eventually, and it is better to know before you need it.
Under GOVERNANCE, a credential holding
s3:BypassGovernanceRetention can delete locked versions. Without it every
delete is refused:
aws --endpoint-url $XNS s3api delete-object --bucket backups \
--key <key> --version-id <version> --bypass-governance-retentionEvery version and every delete marker must go before the bucket can be deleted — a
lock-enabled bucket is versioned, so an ordinary aws s3 rm --recursive
leaves the versions behind and the bucket refuses to delete. Iterate
list-object-versions until it returns nothing, then
delete-bucket. Under COMPLIANCE, none of this works until the
retention period expires.
What it costs
$6.00 per TB per month. $0 egress, uncapped. 30-day minimum retention, no early-delete
fee. That is the whole price, per stored TB — a restore, including a full restore, adds
nothing to the bill. Current terms are published as machine-readable JSON at
xns.tech/pricing.json.
Two things to fold into your own estimate: the locked non-current versions described
above are stored data and are charged as such, and the 30-day minimum applies to objects
you delete before 30 days — which, with a retention period of 30 days or more, you
cannot do anyway.
Related
Object Lock’s full scope, including how retention and legal hold are keyed per tenant, is
in Versioning and Object Lock. Which parts of
the S3 API a backup workflow touches and how each behaves here is in
Backup capabilities and limits. The conformance
results behind “S3-compatible” — 79.1% of the counted scope, 478 of 604 tests, scope v3.0,
run 2026-08-14 — and the list of what is intentionally unsupported are at
S3 compatibility.
