Backup & DRX · Object Storage, Versioning and RetentionObject storage
Retention modes: governance, compliance, and what each promises
What you'll learn
- Choose between governance and compliance retention from the adversary each one is designed to stop
- Plan bucket creation around the measured fact that object lock cannot be added afterwards
- Predict which identities a retained object version resists, and which it does not
- Verify retention semantics against a specific endpoint instead of assuming they match AWS
Prerequisites
Practice
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
Lifecycle rules quietly remove recovery points because somebody wrote a rule and nobody read it again. Retention is the instrument pointed the other way: it exists to refuse a removal that somebody very much wants. Both are a few words of bucket configuration, both are written in the same command, and the resemblance is where the trouble starts — because object lock offers two retention modes, they are configured almost identically, and the difference between them is not a matter of degree. One stops mistakes. The other stops an attacker who is holding your administrative credentials, and only that second promise is an answer to the question ransomware asks.
--with-lock is a decision the bucket only gets to make once
Before either mode can be discussed there is an architectural constraint that decides whether the discussion is available at all. Object lock is not a setting you turn on; it is a property the bucket either has from birth or never has. The capture below asked an ordinary bucket to accept a default COMPLIANCE retention, and then created a second bucket with the flag that enables locking at creation time.
$ mc mb lab/rbdr-plain Bucket created successfully `lab/rbdr-plain`.
>>> exit code: 0
$ mc retention set --default COMPLIANCE 7d lab/rbdr-plain
...attempting to add locking to a bucket that was created without it:
mc: <ERROR> Unable to apply bucket lock configuration. Object Lock configuration cannot be enabled on existing buckets.
>>> exit code: 1
$ mc mb --with-lock lab/rbdr-immutable (locking enabled at creation)
Bucket created successfully `lab/rbdr-immutable`.
>>> exit code: 0Nothing was wrong with the first bucket. It was healthy, it was writable, and it simply lacked a property that is fixed at creation. The error is precise about it: the configuration cannot be enabled on existing buckets, exit code
- The identical command against a bucket created with
--with-lockwas accepted.
The consequence is not operational, it is architectural, and it is the kind that arrives at the worst time. A destination that has been collecting nightly backups for a year cannot be converted into one that resists deletion. There is no upgrade, no migration flag, and no administrative override. The only route from an ordinary bucket to a locked one is to create a new bucket and re-write the data into it, paying the full transfer and the full storage cost of both copies while the move is in flight. Teams discover this on the day after an incident, when the sentence “we should make the backup bucket resist deletion” turns out to describe a project rather than a configuration change.
So the sequence matters more than the setting:
ALIAS=lab
BUCKET=rbdr-backups
mc mb --with-lock "$ALIAS/$BUCKET"
mc retention set --default COMPLIANCE 7d "$ALIAS/$BUCKET"
The order is not stylistic. Reverse it and the second command has nothing to
apply to; run the first without the flag and the second command is the one that
fails. There is also a dependency underneath both: object lock is defined only
for versioned buckets, and AWS documents that it works only in buckets that
have versioning enabled. In the capture the locked bucket reported
lab/rbdr-immutable versioning is enabled before any object was written.
What COMPLIANCE refused, and whom it refused
With the locked bucket in place the capture applied a default COMPLIANCE retention of three days and wrote a nightly backup object into it. The retention that ends up on the object is worth looking at directly, because it is per-version metadata rather than a bucket-level flag someone can flip.
$ mc retention set --default COMPLIANCE 3d lab/rbdr-immutable Object locking 'COMPLIANCE' is configured for 3DAYS.
>>> exit code: 0
lab/rbdr-immutable versioning is enabled
--- retention actually recorded on the object ---
Name : backup-0900.tar
Size : 38 B
VersionID : 133fd99f-1f98-41c0-9d08-95e6e2944157
X-Amz-Object-Lock-Retain-Until-Date: 2026-08-31T13:28:20.402Z
X-Amz-Object-Lock-Mode : COMPLIANCE Two things were stamped onto the version at write time: a mode and a date. The
object was written at 13:28:20 on 28 August and carries a retain-until date of
2026-08-31T13:28:20.402Z, three days later to the second. Nothing about that
lives in a policy document or a job configuration; it is metadata on one
version, and every version gets its own.
The interesting question is not whether that metadata exists but what it
survives. The capture created a second identity called production, holding
the built-in readwrite policy on the bucket — full read, write and delete.
These are the credentials an attacker takes off a compromised application host,
and they are the credentials your backup job legitimately needs. That identity
issued an ordinary mc rm and got exit code 0, because an ordinary delete
against a versioned bucket writes a zero-byte delete marker rather than
destroying anything: the 38 B v1 PUT version was still listed underneath. So
far the object could be made to look gone. Then the attempts moved to the
version itself.
$ mc rm --versions --version-id 133fd99f-1f98-41c0-9d08-95e6e2944157 prod/rbdr-immutable/backup-0900.tar mc: <ERROR> Failed to remove `prod/rbdr-immutable/backup-0900.tar`. Object, 'backup-0900.tar (Version ID=133fd99f-1f98-41c0-9d08-95e6e2944157)' is WORM protected and cannot be overwritten
>>> exit code: 1
--- can the production identity force it? ---
$ mc rm --bypass --version-id ... prod/rbdr-immutable/backup-0900.tar
mc: <ERROR> Failed to remove `prod/rbdr-immutable/backup-0900.tar`. Object, 'backup-0900.tar (Version ID=133fd99f-1f98-41c0-9d08-95e6e2944157)' is WORM protected and cannot be overwritten
>>> exit code: 1
--- can the FULL ADMIN force it? ---
mc: <ERROR> Failed to remove `lab/rbdr-immutable/backup-0900.tar`. Object, 'backup-0900.tar (Version ID=133fd99f-1f98-41c0-9d08-95e6e2944157)' is WORM protected and cannot be overwritten
>>> exit code: 1
--- final state of the backup object ---
[2026-08-28 13:28:22 UTC] 0B STANDARD 4b3c593c-e8ad-444d-aa87-89e380a1fbae v2 DEL backup-0900.tar
[2026-08-28 13:28:20 UTC] 38B STANDARD 133fd99f-1f98-41c0-9d08-95e6e2944157 v1 PUT backup-0900.tar
>>> exit code: 0Three refusals, three exit codes of 1, and the same sentence each time: is
WORM protected and cannot be overwritten. The production identity could not
remove the version. The production identity could not force it with
--bypass. And the full administrator — the alias that created the bucket,
created the users and wrote the retention policy — could not remove it either.
The final listing shows both versions still present: the zero-byte v2 DEL
marker on top, and the 38 B v1 PUT version intact beneath it.
That third refusal is the entire point. Every access-control mechanism most sysadmins carry in their heads has an administrator at the top who can override it, and the reflex when reading “protected” is to ask which role holds the override. Here there is no such role. The refusal was not “you lack a permission”; it was “this operation does not exist for this object until 2026-08-31T13:28:20.402Z”.
GOVERNANCE, the same three days, and one flag
The second mode is configured through the same subcommand, with one word changed, and against a bucket that also had locking enabled at creation. Same retention period, same object, same administrator.
$ mc retention set --default GOVERNANCE 3d lab/rbdr-governance Object locking 'GOVERNANCE' is configured for 3DAYS.
>>> exit code: 0
governance-protected version id: 1425b7eb-5124-4607-a19f-1ced4c55527c
$ mc rm --version-id ... (no bypass flag)
mc: <ERROR> Failed to remove `lab/rbdr-governance/backup-0900.tar`. Object, 'backup-0900.tar (Version ID=1425b7eb-5124-4607-a19f-1ced4c55527c)' is WORM protected and cannot be overwritten
>>> exit code: 1
$ mc rm --bypass --version-id ... (admin holding the bypass right)
Removed `lab/rbdr-governance/backup-0900.tar` (versionId=1425b7eb-5124-4607-a19f-1ced4c55527c).
>>> exit code: 0
--- did the governance-protected object survive? ---
>>> exit code: 0The plain version delete was refused with the identical message and the
identical exit code 1. Add one flag and the same command against the same
version returned Removed ... and exit code 0, and the listing that followed
was empty. The recovery point is gone — not marked deleted, not shadowed by a
marker, gone — two days and twenty-three hours before its retain-until date.
Read side by side, the two captures say something a configuration diff never
would. A reviewer comparing the two buckets sees GOVERNANCE 3DAYS against
COMPLIANCE 3DAYS, concludes that both buckets are protected for three days,
and is right about the words and wrong about the property. Under one of them a
single administrative credential ends the protection at will. Under the other,
nothing does.
Two promises, two threat models
Now the modes can be stated as what they actually are, which is two different security claims rather than two strengths of one claim.
Governance retention protects a recovery point against everyone who does not hold the bypass right. That is a real and useful control, and the population it defends against is large: the operator clearing space under pressure, the cleanup script with a wrong prefix, the runaway lifecycle rule, the colleague who meant to delete the staging bucket. Every one of those is an accident, and governance mode stops all of them. It also stays reversible, which is exactly what you want when the retention period itself might have been a mistake.
Compliance retention protects a recovery point against an adversary who holds
administrative credentials. That is a different population with one member, and
it is the member that matters when the incident is not a mistake. An attacker
who has taken the administrator identity has, by definition, the bypass right;
they have it precisely because they took the account that grants rights. A
control that an administrator can lift is a control the intruder has already
lifted. The measured result is the argument in one line: under governance the
administrator’s --bypass returned exit code 0 and the object was gone, and
under compliance the same administrator got exit code 1.
So the ransomware question — can an intruder with our credentials destroy our backups before we notice? — has exactly one of these two as an answer. Governance mode answers a different question honestly and well. It does not answer this one.
“S3-compatible” is a claim about the API, not about the semantics
Everything above was executed against MinIO RELEASE.2025-09-07T16-13-09Z with mc RELEASE.2025-08-13T08-35-41Z, and that provenance is part of the finding rather than a footnote to it. MinIO documents its object locking as feature and API compatible with AWS S3, and on the behaviours measured here it matched the documented AWS semantics: locking fixed at bucket creation, per-version retention metadata, a governance bypass gated by a permission, and no compliance override for anybody.
That agreement is not something to generalise from. “S3-compatible” is a
statement about accepted requests and response shapes, and the things that
matter for a backup destination live underneath it: whether the implementation
offers compliance mode at all, whether it enforces the retain-until date on
every path including lifecycle expiry, whether a retention period can be
shortened by an operator, and what happens at the moment the date passes. The
error strings differ even where the outcome agrees — MinIO refused with is WORM protected and cannot be overwritten and exit code 1, where AWS documents
a 403 Forbidden on the same class of request — so a monitoring rule written
against one endpoint’s text will not fire on another’s.
The remedy is to stop reading and start measuring. The capture reproduced in
this lesson is five steps long: create a bucket without the flag and try to add
locking, create one with the flag, write an object and read back its recorded
mode and date, attempt the version delete as the writing identity and as the
administrator with and without --bypass, then repeat the last step under the
other mode. Run it against the endpoint you are actually going to trust, record
the exit codes, and keep the transcript next to the design document. A backup
destination whose retention semantics were assumed from a vendor’s
compatibility page has not been verified; it has been hoped for.
What to take from this
mc retention set --default COMPLIANCE 7dagainst an ordinary bucket failed withObject Lock configuration cannot be enabled on existing bucketsand exit code 1, while a bucket created withmc mb --with-lockaccepted the same configuration. Locking is fixed at bucket creation.- Under a COMPLIANCE retention of 3DAYS, the object version carried
X-Amz-Object-Lock-Mode : COMPLIANCEandX-Amz-Object-Lock-Retain-Until-Date: 2026-08-31T13:28:20.402Z. Retention is metadata on one version, not a bucket-wide switch. - The production identity holding the built-in
readwritepolicy could not remove that version (exit 1) and could not force it with--bypass(exit 1), and the full administrator got the same refusal at exit code 1. - Under GOVERNANCE with the same three-day period, the plain version delete was
refused at exit code 1, and the administrator adding
--bypassreturnedRemoved ...at exit code 0 with nothing left in the listing. - Governance protects against mistakes; compliance protects against an adversary holding administrative credentials. Only the second is an answer to the ransomware question, because the intruder who took the administrator account took the bypass right with it.
- These behaviours were measured on MinIO RELEASE.2025-09-07T16-13-09Z with mc RELEASE.2025-08-13T08-35-41Z. S3-compatible implementations do not all share identical semantics, and the five-step probe above is cheap to run against the endpoint you intend to trust.
Cross-course references
- Secrets, PKI & Certificate Management for Infrastructure Engineers — Part XVIII (Incidents and Recovery) covers what an organisation does once a credential is known to be compromised, which is the moment this lesson’s distinction pays out: a governance-retained copy is worth exactly as much as the identities holding the bypass right are trustworthy, so the credential-compromise runbook and the retention mode have to be designed as one decision.
- Ceph & Distributed Storage for Production Sysadmins — Part XLV (RADOS Gateway) is the other S3-compatible endpoint most readers of this course will operate, and it is the concrete reason for the closing warning here: the exit codes above were measured on MinIO, so an RGW backup destination has to be put through the same probe before the same promise is claimed for it.
- Observability for Production Sysadmins — Part XVIII (Alerting Rules) supplies the mechanism for turning the refusals measured here into a signal. A WORM refusal at exit code 1 is not noise to suppress but the earliest evidence that something is trying to destroy recovery points, and it deserves an alert rule of its own.
Quiz
Knowledge check · 5 questions
Q1. A bucket has held nightly backups for a year. The team now needs those backups to resist deletion by an attacker holding administrative credentials. What does the measured MinIO behaviour say about the next step?
Q2. A COMPLIANCE retention still has three days to run. The full administrator runs `mc rm --bypass --version-id ...` against the protected version. What was measured?
Q3. In the capture, the production identity ran an ordinary `mc rm` against the COMPLIANCE-protected object and got exit code 0, even though no recovery point was destroyed.
Q4. Which of these statements are supported by what the MinIO capture actually measured? Select all that apply.
Q5. A team proposes GOVERNANCE retention on the backup bucket and describes it as ransomware protection. State what governance retention achieves and what it does not, against an attacker who has taken the administrator credential.
Passing score: 75%. Answers are checked in this browser.