Reported symptoms
A restore request arrives for a nightly recovery point eleven days old. The
object is not in rbdr-offsite, and neither are twenty-one of its
neighbours. Only the eight nightlies written since a storage clean-up ten
days ago remain.
The objects are not merely hidden. A listing that shows versions returns
nothing for those keys — no zero-byte DEL marker above an intact PUT
version, which is what an ordinary delete against a versioned bucket leaves
behind. The versions themselves are gone.
The architecture document says, under the heading Offsite copy, that nightly recovery points are held immutably for thirty days, and the last design review signed that sentence off unamended. The backup job reported success every night throughout the window, because it writes and never reads back.
The administrator who ran the clean-up remembers the sequence: the first removal was refused with an error mentioning WORM protection, and a second attempt with one extra flag was accepted silently. There is no intrusion and no stolen credential. The first theory in the incident channel is that immutability was never configured — that somebody wrote the sentence and nobody applied the setting.
Evidence provided
$ 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: 0$ 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$ 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 The bucket configuration for rbdr-offsite reads GOVERNANCE 30DAYS. The
architecture document, the design-review record and the monitoring
configuration each contain the word immutable and none contains a field
naming a mode.
Work the evidence before reading on
- The plain version delete was refused at exit code 1. What does that one line, on its own, settle about the incident channel’s first theory?
- Adding one flag turned exit code 1 into exit code 0 and an empty listing. Which property of the bucket does that isolate, and which properties does it leave untouched?
- The same administrator, the same flag and the same three-day period were refused on a second bucket. What differs between the two buckets?
- The design review approved the sentence “held immutably for thirty days”. Which three facts does that sentence fail to record, and which of them would have caught this?
Root cause
Governance mode defines a door, and the administrator was holding the key
The two modes are written with the same subcommand, one word apart, and they
are not two strengths of one promise. Governance retention refuses deletion
by every identity that does not hold the bypass right, and grants a
documented route to the identity that does. Two things must both be present:
the caller needs the s3:BypassGovernanceRetention permission, and the
request must carry the x-amz-bypass-governance-retention:true header, which
is what mc rm --bypass sets on the wire. The server evaluates that pair
before it consults the retention metadata, so the removal is not defeating
the lock; it is walking through a door the mode defines. That door is a
permission — grantable, auditable, and stealable along with the identity
holding it.
The administrator held it because they were the administrator, and the first
refusal was not a wall but a prompt to add a flag. Measured on one object
with one three-day period, the entire difference is a single exit code:
GOVERNANCE 3DAYS refused the plain delete at exit 1 and returned
Removed ... at exit 0 once --bypass was added, while COMPLIANCE 3DAYS
refused the same administrator with the same flag at exit 1 — compliance mode
defines no bypass permission for anybody to hold and no header a client can
set.
The alternative this rules out. The incident channel’s first theory was
that locking had never been applied — that the sentence in the document
described an intention rather than a configuration. Two pieces of evidence
kill it. First, the plain version delete under governance was refused, at
exit code 1, with is WORM protected and cannot be overwritten. An
unprotected version would have been removed at exit code 0; a refusal is only
possible if retention is in force. Second, the retention is readable per
version as server-recorded metadata —
X-Amz-Object-Lock-Mode : COMPLIANCE and
X-Amz-Object-Lock-Retain-Until-Date: 2026-08-31T13:28:20.402Z on the
measured object — so “was locking applied?” is a question with an answer you
can fetch rather than infer. Locking was applied. It was applied in the mode
that has a door.
The control recorded a word instead of a mechanism, a mode and a window
The second defect is why the first lived long enough to matter. Nothing in the estate distinguished the two modes, because nothing in the estate recorded which mode was in use.
The architecture document says immutable. The design-review checklist asks
whether the offsite copy is immutable, and the answer was yes — truthfully.
The bucket does have object lock enabled, it does refuse ordinary deletes,
and it does carry a thirty-day retain-until date on every version. Every
claim was accurate. A bucket configured GOVERNANCE 30DAYS therefore passed
precisely the review a bucket configured COMPLIANCE 30DAYS would have
passed, and no reviewer was negligent: the form had nowhere to write the
difference down.
An immutability claim is only checkable if it names three things — the mechanism that enforces it, the mode it runs in, and the window it covers. Drop the mode and the claim collapses into a feature name, and the feature is the half that does not carry the promise.
Resolution
Accept first that the twenty-two versions are unrecoverable. A removed version is not a delete marker; there is no undo. The eight surviving nightlies are the recovery estate.
Remove s3:BypassGovernanceRetention from the identities used for routine
storage work. That one permission change takes effect immediately and turns
governance retention into a real control for every operator who is not
deliberately breaking glass.
Then rebuild the destination in compliance mode. Locking is fixed at bucket creation, so this is a new bucket and a re-write, not a setting change:
ALIAS=lab
BUCKET=rbdr-offsite-compliance
mc mb --with-lock "$ALIAS/$BUCKET"
mc retention set --default COMPLIANCE 30d "$ALIAS/$BUCKET"
Repoint the backup job, whose identity needs write access and neither delete nor bypass. Finally, edit the architecture document in the same change: the sentence containing immutable is replaced by the mechanism, the mode and the window. Skip that edit and the second defect is recreated on the new bucket.
Verification
Configuration is not evidence. Create a disposable locked bucket, write one object, capture its version id, and run both deletes as the administrator:
ALIAS=lab
BUCKET=rbdr-verify
OBJECT=backup-0900.tar
VERSION=1425b7eb-5124-4607-a19f-1ced4c55527c
mc rm --versions --version-id "$VERSION" "$ALIAS/$BUCKET/$OBJECT"
mc rm --bypass --version-id "$VERSION" "$ALIAS/$BUCKET/$OBJECT"
Under compliance, both must fail: exit code 1 twice, with is WORM protected and cannot be overwritten twice. If the second returns Removed ... at exit
code 0, the destination is in governance mode whatever the document claims.
Then read the retention back off a written object and confirm the server reports the compliance mode and a retain-until date consistent with the configured window. That read-back separates “wrong mode” from “never applied”, and the two have different fixes. Close by restoring one object end to end: a destination that refuses deletion has proved half of what a recovery point is for.
Prevention
Ban immutable as a design-document term and require three fields wherever a
copy is claimed to resist deletion — mechanism, mode, window. A row reading
object lock / COMPLIANCE / 30DAYS can be checked against an endpoint; a row
reading immutable cannot be checked against anything.
Change the review question from “is this copy protected?” to “name every identity that can delete a version before its retain-until date”. Under compliance the honest answer is none. Under governance it is a list, and the list is the finding.
Hold the bypass right in a break-glass identity rather than in the
administrator role, and alert on WORM refusals instead of suppressing them:
exit code 1 with is WORM protected and cannot be overwritten is the
earliest signal that something is trying to destroy recovery points, and here
it was read as an obstacle to route around.
Finally, re-measure after every endpoint change. “S3-compatible” describes accepted requests and response shapes, not retention semantics, so run the two-command probe whenever the provider, the version or the bucket changes.